• v0.0.18
    Gates / frontend (push) Successful in 2m34s
    Gates / test (push) Successful in 2m39s
    Gates / test-aarch64 (push) Successful in 8m16s
    Gates / package (push) Successful in 4m21s
    Gates / container (push) Successful in 16s
    CI / gates (push) Successful in 15m39s
    Release / guard (push) Successful in 35s
    Gates / frontend (push) Successful in 2m1s
    Gates / test (push) Successful in 2m28s
    Gates / test-aarch64 (push) Successful in 7m24s
    Gates / package (push) Successful in 52s
    Gates / container (push) Successful in 11s
    Release / gates (push) Successful in 10m59s
    Release / publish (push) Successful in 5m11s
    Stable

    mokhtar released this 2026-09-09 17:57:40 +00:00 | 66 commits to master since this release

    Fixed

    • The Overview scope pickers no longer sit apart, and a long device name no longer pushes the period picker off a phone screen. The Device and Period dropdowns fill their wrappers and stand side by side; a long name truncates with an ellipsis in the trigger and stays fully readable in the list.

    Artifacts

    b815a397c16f3f85603e2b0983274d6b3c8fbb8d238d06bbfa2eb8f8cfc1e740  nxdns-0.0.18-x86_64-linux-musl.tar.gz
    ed39ae26be9392045d3e462f36210e1557e4ce4a7e7638d64a697ebc051e7bbf  nxdns-0.0.18-aarch64-linux-musl.tar.gz
    26775f1fd8642a6b90ef34016559a917dc459f9cb980d4f2f83e87aeefa802f2  IMAGE-DIGEST.txt
    
    git.mial.net/mokhtar/nxdns:0.0.18@sha256:2af3899f65c886120267806310c63e319c8bb891d1ab8b72145c43e3a6f5b488
    

    Compare v0.0.17...v0.0.18

    Commits since v0.0.17
    bcd9bce build: bump version to 0.0.18
    2b790c4 changelog: 0.0.18 section
    1860ff5 admin: the overview pickers fill their wrappers and share a narrow row
    
    Downloads
  • v0.0.17
    Gates / frontend (push) Successful in 2m23s
    Gates / test (push) Successful in 3m15s
    Gates / test-aarch64 (push) Successful in 8m38s
    Gates / package (push) Successful in 5m0s
    Gates / container (push) Successful in 19s
    CI / gates (push) Successful in 17m17s
    Release / guard (push) Successful in 37s
    Gates / frontend (push) Successful in 2m15s
    Gates / test (push) Successful in 2m34s
    Gates / test-aarch64 (push) Successful in 7m33s
    Gates / package (push) Successful in 51s
    Gates / container (push) Successful in 10s
    Release / gates (push) Successful in 11m14s
    Release / publish (push) Successful in 8m35s
    Stable

    mokhtar released this 2026-09-08 22:35:07 +00:00 | 69 commits to master since this release

    Added

    • A Nix flake with tag-pinned hashes. flake.nix at the repository root builds nxdns for aarch64-linux and x86_64-linux from the release tarballs, and carries their hashes in a generated block. A consumer pins the flake to a release tag and gets the exact bytes that tag published; docs/how-to/install-with-nix.md covers the input, the nixpkgs follows line, and Renovate.

    Changed

    • The release archive is written by the project's own tool. zig build dist no longer shells out to the runner's tar and gzip; it writes the tar stream and the gzip container itself, with sorted entries, fixed modes, and zero timestamps. The tarball bytes now depend on the source tree, the compiler, and the admin bundle, and on nothing the host supplies — which is what lets a hash be pinned before CI rebuilds it.
    • The cut pins the release hashes before it commits. zig build cut asserts the local Node, npm, and Zig match the versions CI pins, builds the release from the bumped manifest in a normalized environment, writes the resulting hashes into flake.nix, and commits that file alongside build.zig.zon as one commit. CI reverifies the pins on the bump commit and again on the tag, before anything is uploaded.
    • nxdns version no longer reports a git commit. The commit is gone from the command's output, from GET /api/version, and from the admin footer, which now shows nxdns v<version>. A release identifies itself by version, and the bytes are reproducible from the tag, so a commit embedded in the binary told a reader nothing the tag did not.

    Artifacts

    8ee0b1f30b1ee43bcb0a0c8e789469fac794523e31468b54c199d0b571c8ab6b  nxdns-0.0.17-x86_64-linux-musl.tar.gz
    b439ca554968b31e8d2b9a8ba9400fbc89dc27e0bb259d5d3526bc4221333c77  nxdns-0.0.17-aarch64-linux-musl.tar.gz
    35ac38080eaf72f39fa6a97bd18bcfa3cc62df44b3228e52d35dc49682a24c52  IMAGE-DIGEST.txt
    
    git.mial.net/mokhtar/nxdns:0.0.17@sha256:dde9833df05ce227a13618f3469cccd1be2ad0fcaf03f526c076a89bf7ce5c55
    

    Compare v0.0.16...v0.0.17

    Commits since v0.0.16
    f168247 cut: build the release with the official zig tarball, skip hidden bundle files; re-pin 0.0.17
    d596fd7 build: bump version to 0.0.17
    f067742 cut: the pin stage compiles, and `zig build test` builds the cut binary
    a4eb749 changelog: 0.0.17
    22abcd9 release: nix flake with tag-pinned hashes, reproducible tarballs (milestone 40)
    3e57f43 cut: rerun a flaky release run once, resume after the tag
    299d7af upstream: overlap attribution test builds its schedule with gates
    
    Downloads
  • v0.0.16
    Gates / frontend (push) Successful in 1m57s
    Gates / test (push) Successful in 2m34s
    Gates / test-aarch64 (push) Successful in 8m9s
    Gates / package (push) Successful in 7m14s
    Gates / container (push) Successful in 17s
    CI / gates (push) Successful in 18m17s
    Release / guard (push) Successful in 33s
    Gates / test-aarch64 (push) Successful in 7m22s
    Gates / container (push) Successful in 11s
    Release / gates (push) Successful in 10m35s
    Gates / frontend (push) Successful in 2m8s
    Gates / test (push) Successful in 2m16s
    Gates / package (push) Successful in 44s
    Release / publish (push) Successful in 10m4s
    Stable

    mokhtar released this 2026-09-07 22:05:41 +00:00 | 76 commits to master since this release

    The Overview page is redesigned around the two dashboards people already know — Pi-hole's layout, NextDNS's charts — and every number in the admin is spelled one way.

    Changed

    • Overview reads like an analytics dashboard. Four centred totals (queries, blocked, active clients, and the blocked share), a smooth area chart of total and blocked queries where hovering marks the point and shows its figures beside it, a stacked client-activity chart in eight distinct hues with everything else as one gray band, and a row of cards under them: the cache hit rate as a progress bar, the query types as a single-hue ring, and the upstream breakdown.
    • Overview can be scoped to one device. A device selector sits beside the period selector; both are URL state, so /overview?period=7d&client=192.0.2.30 is a link to exactly that view. The API takes the same client parameter on GET /api/overview, and the tile links carry the scope into Activity.
    • One formatting contract. Counts are thousands-grouped, percentages always show two decimals (15.44%, never 15.4%), rates one decimal, and durations their two largest nonzero units ("6d 4h", "12m 5s", "6d 5s"). A test sweeps the sources for any number formatted outside the one module.
    • A quieter, more consistent chrome. One 4px corner radius everywhere, cards with a prominent title and a one-line description, the sidebar grouped into Monitoring and System with a status block at its foot (protection, queries per minute on Overview, uptime), and a top bar in place of the sidebar on narrow screens. The active navigation item and the accent blue are darkened to clear WCAG AA on their washes.

    Artifacts

    c682f6801aace3e1529b9a5e181abfc24fa3f421395bcc03aee19f9bcc6c908e  nxdns-0.0.16-x86_64-linux-musl.tar.gz
    0bfff7496e26bd10cd175a7e1e93e2ce94c1a2c3efa23348776b6258f44abb7f  nxdns-0.0.16-aarch64-linux-musl.tar.gz
    5ed07ca8e33c299669788e238e260c7f2a3db659f59e23f48c1ecf2331fb7569  IMAGE-DIGEST.txt
    
    git.mial.net/mokhtar/nxdns:0.0.16@sha256:e5b7360d4cc6d6f32e77094c554103978ce875893613d84aad25935a9e940966
    

    Compare v0.0.15...v0.0.16

    Commits since v0.0.15
    85b8be5 admin: overview redesign, device scope, one formatting contract (milestone 39)
    e656670 admin: ui polish pass, thirty findings from the emil audit
    
    Downloads
  • v0.0.15
    Gates / frontend (push) Successful in 1m51s
    Gates / test (push) Successful in 2m29s
    Gates / test-aarch64 (push) Successful in 7m24s
    Release / guard (push) Successful in 32s
    Gates / frontend (push) Successful in 2m14s
    Gates / package (push) Successful in 4m56s
    Gates / test (push) Successful in 2m20s
    Gates / test-aarch64 (push) Successful in 7m25s
    Gates / package (push) Successful in 1m3s
    Gates / container (push) Successful in 15s
    CI / gates (push) Successful in 26m26s
    Gates / container (push) Successful in 11s
    Release / gates (push) Successful in 16m6s
    Release / publish (push) Successful in 6m43s
    Stable

    mokhtar released this 2026-08-31 17:24:54 +00:00 | 78 commits to master since this release

    The Activity page's history filters become a toolbar you can actually use. Filters apply as you set them, the client field stops asking you to remember IP addresses, and the whole admin picks up one coherent icon set.

    Changed

    • History filters apply live from a toolbar. The form-and-Apply-button row is gone. Domain text filters as you type (debounced), result and time are one-click controls, and a custom date range applies as one change. Every filter state is a bookmarkable URL.
    • The client filter is a picker, not a text field. It lists the clients the server knows — named, sorted, multi-select — instead of asking for an exact address. Selected clients show as removable chips (first three, then a count), and a query can filter on up to 32 clients at once; the API accepts the same list.
    • One icon set. Interface glyphs (dropdown carets, checkbox ticks, the search magnifier, status marks, back arrows) are now Phosphor icons instead of a mix of text characters and hand-drawn shapes.
    • The admin bundle budget rises from 800,000 to 900,000 bytes. The client picker, the accessible menu and dialog primitives behind it, and the Phosphor icon components are the arrivals that spend it; the built assets sit at about 840,000 bytes.

    Added

    • Accessibility pass over the admin. Focus-visible rings on every control, labels wired to their inputs, keyboard-reachable menus and dialogs, and visible text where information previously lived only in hover titles.

    Artifacts

    c0f12429cc4755e8690388ef43d76b28a5a088e4737a294ae89e60a6932a034d  nxdns-0.0.15-x86_64-linux-musl.tar.gz
    739857763e84eb849bbbf4594a6f87f46e1834a4e9870d19f20cdf12fe7901d2  nxdns-0.0.15-aarch64-linux-musl.tar.gz
    71f2261dbc3f073948370683b93768fc92fb1367c6610d86f5f0327a3b052c32  IMAGE-DIGEST.txt
    
    git.mial.net/mokhtar/nxdns:0.0.15@sha256:b9053a0660b70639572e11697e75b21a3cc47e1fae09d1eeb92e8466ef0c58a3
    

    Compare v0.0.14...v0.0.15

    Commits since v0.0.14
    2ae0c97 admin: per-icon phosphor imports, the barrel loads every icon module under vitest
    2525b01 admin: phosphor icons replace hand-drawn svgs and text-character glyphs
    b774b05 activity: select-only multi-client filter
    d79dd0b admin: prettier formatting for the toolbar-era files, missed before ci ever ran them
    039eeed admin: activity history filters become a live-apply toolbar
    c65d92d admin: title-only information becomes visible text
    207252a admin: enable toggles become rac switches
    f1de804 admin: group sources and safe search become rac checkboxes
    3c67496 admin: the mobile drawer becomes a rac disclosure, asset budget raised to 850,000 bytes
    59d6be9 admin: associate inline field errors with their inputs
    317d5dd admin: live query detail becomes a modal, live/history and period pickers become tabs and radios, client delete confirms in a dialog
    79578e1 admin: reclaim the desktop header, move pause and log out to the sidebar
    d5613ee admin: pointer cursor on every button
    
    Downloads
  • v0.0.14
    Release / guard (push) Successful in 34s
    Gates / frontend (push) Successful in 1m47s
    Gates / test (push) Successful in 2m43s
    Gates / test-aarch64 (push) Successful in 7m29s
    Gates / package (push) Successful in 58s
    Gates / container (push) Successful in 14s
    Release / gates (push) Successful in 11m27s
    Release / publish (push) Successful in 1m30s
    Stable

    mokhtar released this 2026-08-30 10:34:06 +00:00 | 91 commits to master since this release

    Schema changes stop costing you your query history. querylog.db is now version-stamped and migrated in place; the server refuses to start rather than ever reset a healthy file, and the release tooling refuses to ship a schema change that is neither migratable nor explicitly disclosed with recovery steps. Three releases (0.0.6, 0.0.9, 0.0.12) each discarded the log on upgrade; this ends that.

    Changed

    • querylog.db is migrated in place. The file now carries a schema version, and a release that changes the schema ships a migration that runs at startup: one consistent backup (querylog.db.pre-migrate-<timestamp>, mode 0600, only the most recent kept), then every step and the version stamp in a single transaction. A failure before the commit rolls back and leaves your file exactly as it was.
    • The server refuses instead of resetting. A querylog.db it cannot use — newer than the binary, older than 0.0.12, or mid-migration failure — is left untouched and the server exits with a clear message instead of setting the file aside and starting an empty log. The exit code (2) tells systemd not to restart-loop a deliberate refusal. Corruption is the only case that still sets a file aside automatically.
    • The release gate now enforces the contract. A schema change cannot be tagged unless it either ships a working migration (proven in CI against a frozen fixture of the previous schema, with shipped migration files locked byte-for-byte once released) or explicitly declares a break — which requires a version bump the server refuses on, a reset disclosure, and step-by-step restore instructions in this file.

    One hazard to know when downgrading. The first start under this release restamps querylog.db from the old fingerprint to version 1 (contents untouched). If you later downgrade to 0.0.13 or older, that binary treats the new stamp as a schema mismatch, moves your file aside as querylog.db.schema-changed-<timestamp>, and starts an empty log. To recover: return to 0.0.14 or newer, stop the server, move the empty querylog.db away and delete its querylog.db-wal and querylog.db-shm files (leaving them would corrupt the restored file), rename the .schema-changed-<timestamp> file back to querylog.db, and start.

    Artifacts

    bd75dbc880109880f5d9e1ef56862637d771f3dda29fca396371612c48d3e869  nxdns-0.0.14-x86_64-linux-musl.tar.gz
    71e9102d536b6e87c8de892b538521c08e6ad26baf2e7a602755636243edac40  nxdns-0.0.14-aarch64-linux-musl.tar.gz
    776d7b8392db091e0de3f24e17fff89bbd086588ecb2623b4893f2a10ef642f8  IMAGE-DIGEST.txt
    
    git.mial.net/mokhtar/nxdns:0.0.14@sha256:c729fb4aa37054c5dbafa3a0d668898222e1c1e0364eeb955ef861a04707ebe9
    

    Compare v0.0.8...v0.0.14

    Commits since v0.0.8
    09932b2 build: bump version to 0.0.14
    272655f storage: version querylog.db and migrate it in place, never reset a healthy file
    dd5a9f6 build: bump version to 0.0.13
    72cdbbd upstream: one absolute per-query budget across queueing and failover
    6170571 build: bump version to 0.0.12
    c847072 overview: one endpoint, live projections and a response cache (m36)
    7cdb74e build: bump version to 0.0.11
    4367003 changelog: 0.0.11
    728b8d6 admin: draw the overview charts with visx
    091962a build: bump version to 0.0.10
    4fcbd0a changelog: 0.0.10
    d961b15 db-mode config changes apply live in-process
    17e6e93 admin: only the content region scrolls on wide screens
    8a44d90 cut: schema gate — refuse to release an undisclosed querylog schema change
    03149b5 ci: drop the one-insn-per-tb workaround, the qemu panic is an 11.1.0-only regression
    a972c8d logger tests: join every writer future before its database closes, fix the gate-open race
    78d688a ci: run the aarch64 suite with one qemu insn per tb, tcg optimization falsely trips the ubsan pointer check
    fe5bd96 admin: live ring capacity is injectable, eviction test no longer timing-bound
    1f215ac build: bump version to 0.0.9
    2f86cd7 changelog: 0.0.9 releases today
    51cda8b milestone 33: contract closure — samples, file-authority enumeration, dead code, bundle ceiling
    e5f23d5 querylog: return to the default checkpoint cadence
    24521ab milestone 32: task-shaped configuration, file mode as a rendering, config status api
    c99a37d milestone 31: concurrent upstream exchanges, dot session reuse, queue metrics
    623667e milestone 30: overview as a dashboard, explicit health contract, period aggregations
    0e83477 agents: move opus-coder to the global scope
    fa323c7 milestone 29: activity — history, live and policy simulation on one surface
    0fd6bbd milestone 28: query provenance — every logged query is exactly explainable
    7e6cb50 release cut: bump-kind justfile recipe and a compiled, tested cut tool
    8a17e9e admin: fix dashboard phantom scroll, drop last-failure column from upstream table
    0107df5 spec: measurement window is the deployment side's call
    
    Downloads
  • v0.0.13
    Release / guard (push) Successful in 34s
    Gates / frontend (push) Successful in 1m39s
    Gates / test (push) Successful in 2m27s
    Gates / test-aarch64 (push) Successful in 8m9s
    Gates / package (push) Successful in 4m24s
    Gates / container (push) Successful in 16s
    Release / gates (push) Successful in 15m18s
    Release / publish (push) Failing after 4m55s
    Stable

    mokhtar released this 2026-08-30 10:20:09 +00:00 | 93 commits to master since this release

    The upstream query budget becomes one honest deadline. A busy network no longer blames a healthy standby for running out of time, and a query burst no longer queues invisibly until everything answers SERVFAIL at once.

    Fixed

    • The per-query upstream budget is now one absolute deadline, spent by everything that blocks. Waiting for a free slot on a saturated upstream now spends the query's upstream.total_timeout_ms budget just like the exchange itself, instead of being invisible to it — under a burst, queries used to wait out their whole budget in the queue and then start attempts they could never finish. An attempt near the end of the budget runs truncated, and when a truncated attempt runs out of time that is evidence about the budget, not the upstream: it no longer counts against that upstream's health or success rate, and the query log no longer names an upstream that was given no fair chance. A field incident produced 279 rows blaming a standby whose health counters read zero for zero; those rows now attribute nothing.
    • A query no longer blocks behind a saturated upstream while another has capacity. Admission sweeps the upstreams in priority order and takes the first free slot; priority now means the order among upstreams that can be admitted right now, and a query blocks only when nothing has capacity — on the highest-priority eligible upstream, bounded by the remaining budget.
    • Conditional forward zones spend upstream.read_timeout_ms once per query. A UDP attempt, a truncated answer and the TCP retry now share the one budget instead of taking a fresh one each, so a slow zone resolver can no longer stretch a single query to several times the configured timeout.

    Added

    • nxdns_upstream_budget_exhausted_total. A pool-wide counter of queries whose budget ran out — in the queue or mid-attempt — before any upstream answered. It carries no per-upstream label on purpose: running out of budget is a fact about the pool.
    • A configuration with more than 64 enabled upstreams is rejected at validation with a clear message, instead of tripping an internal limit at startup.

    Artifacts

    ef3d01e57e5a07c729aae2fa1c9ed5973984605fd4aefaf96c40960f1f0c946a  nxdns-0.0.13-x86_64-linux-musl.tar.gz
    daf75bb747e953a498473ce0e6035326f8be4d5a2f66047667c8b83320b6793a  nxdns-0.0.13-aarch64-linux-musl.tar.gz
    bed65338d9ef9dcc72475342ff99735262ffb93e2e15c8805d353f43853a52ab  IMAGE-DIGEST.txt
    
    git.mial.net/mokhtar/nxdns:0.0.13@sha256:75ee3fa99c48cb2302671cfb2062d01bbc303edc18d526c4f9be0887a472a1d6
    

    Compare v0.0.8...v0.0.13

    Commits since v0.0.8
    dd5a9f6 build: bump version to 0.0.13
    72cdbbd upstream: one absolute per-query budget across queueing and failover
    6170571 build: bump version to 0.0.12
    c847072 overview: one endpoint, live projections and a response cache (m36)
    7cdb74e build: bump version to 0.0.11
    4367003 changelog: 0.0.11
    728b8d6 admin: draw the overview charts with visx
    091962a build: bump version to 0.0.10
    4fcbd0a changelog: 0.0.10
    d961b15 db-mode config changes apply live in-process
    17e6e93 admin: only the content region scrolls on wide screens
    8a44d90 cut: schema gate — refuse to release an undisclosed querylog schema change
    03149b5 ci: drop the one-insn-per-tb workaround, the qemu panic is an 11.1.0-only regression
    a972c8d logger tests: join every writer future before its database closes, fix the gate-open race
    78d688a ci: run the aarch64 suite with one qemu insn per tb, tcg optimization falsely trips the ubsan pointer check
    fe5bd96 admin: live ring capacity is injectable, eviction test no longer timing-bound
    1f215ac build: bump version to 0.0.9
    2f86cd7 changelog: 0.0.9 releases today
    51cda8b milestone 33: contract closure — samples, file-authority enumeration, dead code, bundle ceiling
    e5f23d5 querylog: return to the default checkpoint cadence
    24521ab milestone 32: task-shaped configuration, file mode as a rendering, config status api
    c99a37d milestone 31: concurrent upstream exchanges, dot session reuse, queue metrics
    623667e milestone 30: overview as a dashboard, explicit health contract, period aggregations
    0e83477 agents: move opus-coder to the global scope
    fa323c7 milestone 29: activity — history, live and policy simulation on one surface
    0fd6bbd milestone 28: query provenance — every logged query is exactly explainable
    7e6cb50 release cut: bump-kind justfile recipe and a compiled, tested cut tool
    8a17e9e admin: fix dashboard phantom scroll, drop last-failure column from upstream table
    0107df5 spec: measurement window is the deployment side's call
    
    Downloads
  • v0.0.12
    Release / guard (push) Successful in 35s
    Gates / frontend (push) Successful in 1m40s
    Gates / test (push) Successful in 2m23s
    Gates / test-aarch64 (push) Successful in 7m12s
    Gates / package (push) Successful in 4m18s
    Gates / container (push) Successful in 16s
    Release / gates (push) Successful in 14m12s
    Release / publish (push) Failing after 4m57s
    Stable

    mokhtar released this 2026-08-30 09:58:51 +00:00 | 95 commits to master since this release

    Overview stops re-reading the whole query log. One endpoint, one snapshot, pre-aggregated buckets — a 30-day view now costs the same on a month of history as on a day of it. Read the upgrade note first: it resets your query history.

    Changed

    • Upgrading resets your query history. The query-log schema gains the aggregate tables described below, and querylog.db is never migrated: the first start after the upgrade sets the old file aside (kept on disk next to the new one, named with the reason) and begins a fresh log. Settings, groups, blocklists and every other configuration are untouched.
    • The Overview is served by one endpoint, GET /api/overview. It replaces GET /api/stats, /api/stats/timeseries, /api/stats/types, /api/stats/routes and /api/stats/clients, which are gone. The five panels now come from a single database snapshot, so they can no longer disagree with each other, and the page shows one loading and one error state instead of five.
    • Query statistics are pre-aggregated as they are written. The query log now maintains 30-minute aggregate tables in the same transaction that stores the rows, and the 24-hour, 7-day and 30-day views read those instead of scanning every logged query. The cost of opening the Overview no longer grows with the size of the log: measured at three million rows, the 30-day view went from roughly eight-tenths of a second of scanning to under fifty milliseconds, at the price of about ten percent on each background write batch and ~1.5 MB of disk. The server also keeps the most recent response per period in memory and serves repeat polls from it while nothing has changed — until new queries land, retention prunes, or the period's time window rolls forward — so on a quiet network most of the steady 30-second refreshes do no database work at all.

    The Overview charts move to visx and grow up: one hover treatment across all four, honest labels, and maintained d3 math under the app's own rendering.

    Changed

    • The Overview charts are drawn with visx. The hand-written chart layout code is replaced by visx 4.0.0 primitives — maintained d3 math for the scales, ticks, stacking and arcs — while the rendering, colours and themes stay the app's own. The charts read as before, with four behaviour improvements: all four charts now share the same hover treatment (the per-client chart and both donuts gain the tooltip and dimming the query timeline already had, so pointing at a ring segment names it, its count and its share), an open tooltip follows a data refresh instead of showing stale counts, and it retires cleanly when the time window rolls. The admin bundle grows by about 68 KB and stays under its size budget.
    • The query timeline's third series is called "Allowed". What the chart called "Other" is every query that was neither blocked nor served from cache — answered upstream, from a local record or a forward zone — so it is now named for what it is rather than for the subtraction that produces it. It stays on the chart even in a window where nothing was allowed, alongside Blocked and Cached: all three name a kind of answer a query can get, and a period where every query was blocked or cached is worth seeing.
    • The client chart drops "Other" in a window where it counted nothing. That series aggregates the clients outside the top eight, so when it counts nothing there is nothing being aggregated, and a legend entry, a tooltip row and a table column that exist only to say "zero" are noise. The named clients stay even at zero, because a client that went quiet is a fact about the window.

    Artifacts

    f31e83a49ca21f6d97cc6c0223c6d9ca05f0f1b2432653a4b2f9a93ca0e78d29  nxdns-0.0.12-x86_64-linux-musl.tar.gz
    45fe8337866989b9404f5e67945dfa989d72dc207bed3f95ea3c51dfa284b32e  nxdns-0.0.12-aarch64-linux-musl.tar.gz
    4cbfc00d7ca2db57a1b78587de185c2e77148eba70a2b0b4eefc97d52e53ab39  IMAGE-DIGEST.txt
    
    git.mial.net/mokhtar/nxdns:0.0.12@sha256:a9a60ba0e64701d0252d433a794dc5a035e3122f07e9fab5b1e2e6a77924f33c
    

    Compare v0.0.8...v0.0.12

    Commits since v0.0.8
    6170571 build: bump version to 0.0.12
    c847072 overview: one endpoint, live projections and a response cache (m36)
    7cdb74e build: bump version to 0.0.11
    4367003 changelog: 0.0.11
    728b8d6 admin: draw the overview charts with visx
    091962a build: bump version to 0.0.10
    4fcbd0a changelog: 0.0.10
    d961b15 db-mode config changes apply live in-process
    17e6e93 admin: only the content region scrolls on wide screens
    8a44d90 cut: schema gate — refuse to release an undisclosed querylog schema change
    03149b5 ci: drop the one-insn-per-tb workaround, the qemu panic is an 11.1.0-only regression
    a972c8d logger tests: join every writer future before its database closes, fix the gate-open race
    78d688a ci: run the aarch64 suite with one qemu insn per tb, tcg optimization falsely trips the ubsan pointer check
    fe5bd96 admin: live ring capacity is injectable, eviction test no longer timing-bound
    1f215ac build: bump version to 0.0.9
    2f86cd7 changelog: 0.0.9 releases today
    51cda8b milestone 33: contract closure — samples, file-authority enumeration, dead code, bundle ceiling
    e5f23d5 querylog: return to the default checkpoint cadence
    24521ab milestone 32: task-shaped configuration, file mode as a rendering, config status api
    c99a37d milestone 31: concurrent upstream exchanges, dot session reuse, queue metrics
    623667e milestone 30: overview as a dashboard, explicit health contract, period aggregations
    0e83477 agents: move opus-coder to the global scope
    fa323c7 milestone 29: activity — history, live and policy simulation on one surface
    0fd6bbd milestone 28: query provenance — every logged query is exactly explainable
    7e6cb50 release cut: bump-kind justfile recipe and a compiled, tested cut tool
    8a17e9e admin: fix dashboard phantom scroll, drop last-failure column from upstream table
    0107df5 spec: measurement window is the deployment side's call
    
    Downloads
  • v0.0.11
    Release / guard (push) Successful in 35s
    Gates / frontend (push) Successful in 1m51s
    Gates / test (push) Successful in 3m20s
    Gates / test-aarch64 (push) Successful in 7m30s
    Gates / package (push) Successful in 5m46s
    Gates / container (push) Successful in 15s
    Release / gates (push) Successful in 16m54s
    Release / publish (push) Failing after 4m48s
    Stable

    mokhtar released this 2026-08-30 09:38:12 +00:00 | 97 commits to master since this release

    The Overview charts move to visx and grow up: one hover treatment across all four, honest labels, and maintained d3 math under the app's own rendering.

    Changed

    • The Overview charts are drawn with visx. The hand-written chart layout code is replaced by visx 4.0.0 primitives — maintained d3 math for the scales, ticks, stacking and arcs — while the rendering, colours and themes stay the app's own. The charts read as before, with four behaviour improvements: all four charts now share the same hover treatment (the per-client chart and both donuts gain the tooltip and dimming the query timeline already had, so pointing at a ring segment names it, its count and its share), an open tooltip follows a data refresh instead of showing stale counts, and it retires cleanly when the time window rolls. The admin bundle grows by about 68 KB and stays under its size budget.
    • The query timeline's third series is called "Allowed". What the chart called "Other" is every query that was neither blocked nor served from cache — answered upstream, from a local record or a forward zone — so it is now named for what it is rather than for the subtraction that produces it. It stays on the chart even in a window where nothing was allowed, alongside Blocked and Cached: all three name a kind of answer a query can get, and a period where every query was blocked or cached is worth seeing.
    • The client chart drops "Other" in a window where it counted nothing. That series aggregates the clients outside the top eight, so when it counts nothing there is nothing being aggregated, and a legend entry, a tooltip row and a table column that exist only to say "zero" are noise. The named clients stay even at zero, because a client that went quiet is a fact about the window.

    Artifacts

    b1546aa7906c9824deebcb31e9725212cee005925de97ed84c84c7a6dbb7be42  nxdns-0.0.11-x86_64-linux-musl.tar.gz
    0a25078204e64ca46fb8036ba49ca87e306e44af0fdcc9dc3f98d6c79a981068  nxdns-0.0.11-aarch64-linux-musl.tar.gz
    98af29c08b61bd5ab7cdf78b87c38a24a48161392bbb8c74f6b010fcef9a2a49  IMAGE-DIGEST.txt
    
    git.mial.net/mokhtar/nxdns:0.0.11@sha256:a671dd8e3ed4f593ac2bb6911bee2a264e7087a543a89af9d986d5843ac0ab3a
    

    Compare v0.0.8...v0.0.11

    Commits since v0.0.8
    7cdb74e build: bump version to 0.0.11
    4367003 changelog: 0.0.11
    728b8d6 admin: draw the overview charts with visx
    091962a build: bump version to 0.0.10
    4fcbd0a changelog: 0.0.10
    d961b15 db-mode config changes apply live in-process
    17e6e93 admin: only the content region scrolls on wide screens
    8a44d90 cut: schema gate — refuse to release an undisclosed querylog schema change
    03149b5 ci: drop the one-insn-per-tb workaround, the qemu panic is an 11.1.0-only regression
    a972c8d logger tests: join every writer future before its database closes, fix the gate-open race
    78d688a ci: run the aarch64 suite with one qemu insn per tb, tcg optimization falsely trips the ubsan pointer check
    fe5bd96 admin: live ring capacity is injectable, eviction test no longer timing-bound
    1f215ac build: bump version to 0.0.9
    2f86cd7 changelog: 0.0.9 releases today
    51cda8b milestone 33: contract closure — samples, file-authority enumeration, dead code, bundle ceiling
    e5f23d5 querylog: return to the default checkpoint cadence
    24521ab milestone 32: task-shaped configuration, file mode as a rendering, config status api
    c99a37d milestone 31: concurrent upstream exchanges, dot session reuse, queue metrics
    623667e milestone 30: overview as a dashboard, explicit health contract, period aggregations
    0e83477 agents: move opus-coder to the global scope
    fa323c7 milestone 29: activity — history, live and policy simulation on one surface
    0fd6bbd milestone 28: query provenance — every logged query is exactly explainable
    7e6cb50 release cut: bump-kind justfile recipe and a compiled, tested cut tool
    8a17e9e admin: fix dashboard phantom scroll, drop last-failure column from upstream table
    0107df5 spec: measurement window is the deployment side's call
    
    Downloads
  • v0.0.10
    Release / guard (push) Successful in 38s
    Gates / frontend (push) Successful in 1m40s
    Gates / test (push) Successful in 2m29s
    Gates / test-aarch64 (push) Successful in 7m53s
    Gates / package (push) Successful in 4m17s
    Gates / container (push) Successful in 15s
    Release / gates (push) Successful in 14m58s
    Release / publish (push) Failing after 6m21s
    Stable

    mokhtar released this 2026-08-30 09:15:42 +00:00 | 100 commits to master since this release

    Configuration goes live: when the database owns the configuration, saving a setting reconfigures the running process instead of asking for a restart. The restart-required set shrinks to the listen sockets and the admin interface switch.

    Changed

    • Almost every settings change now applies while the server runs. When the database owns the configuration, saving a setting takes effect immediately — the blocking response, upstream timeouts, cache size, rate limits, session lifetime, log level and destination, blocklist update schedule, privacy flags, disk thresholds and the query-log buffer all reconfigure the running process, exactly as Pi-hole and AdGuard Home do. Nothing is written to the database unless the running server already accepted it, so the API can never report a value the process refused. The restart-required set shrinks from every scalar key to the twelve that genuinely need one: listen addresses and ports, and turning the admin interface itself on or off. The admin pages drop their restart notices for everything else, and an upstream edit — the loudest offender — now applies to the next query. A configuration file still works the way it always has: edit the file, restart the process.
    • The release cut refuses to ship an undisclosed query-log schema change. zig build cut now compares the querylog.db schema fingerprint of the previous release tag against this tree's, and when they differ it requires the changelog section for the version being cut to state that the upgrade discards the stored query history. 0.0.9 changed the schema and its announcement did not mention it; the file is never migrated, so that upgrade silently threw every logged query away.

    Artifacts

    b206d7af478ad4e2329c58673e4d3eb59b97451b81a673c625f6b323451006be  nxdns-0.0.10-x86_64-linux-musl.tar.gz
    097ad1c7a486afdf6857b8f79e2e6e46e07f3195c2e18795234054bc5d594731  nxdns-0.0.10-aarch64-linux-musl.tar.gz
    c05828c26c0fd2ed90bb4770a8bef632a9c75e8cbeb335b9e14ba512eba5f0a1  IMAGE-DIGEST.txt
    
    git.mial.net/mokhtar/nxdns:0.0.10@sha256:15d120f48e4a856f62a7a7235d9365f38de144474be59df8b2e5620fdd3f3327
    

    Compare v0.0.8...v0.0.10

    Commits since v0.0.8
    091962a build: bump version to 0.0.10
    4fcbd0a changelog: 0.0.10
    d961b15 db-mode config changes apply live in-process
    17e6e93 admin: only the content region scrolls on wide screens
    8a44d90 cut: schema gate — refuse to release an undisclosed querylog schema change
    03149b5 ci: drop the one-insn-per-tb workaround, the qemu panic is an 11.1.0-only regression
    a972c8d logger tests: join every writer future before its database closes, fix the gate-open race
    78d688a ci: run the aarch64 suite with one qemu insn per tb, tcg optimization falsely trips the ubsan pointer check
    fe5bd96 admin: live ring capacity is injectable, eviction test no longer timing-bound
    1f215ac build: bump version to 0.0.9
    2f86cd7 changelog: 0.0.9 releases today
    51cda8b milestone 33: contract closure — samples, file-authority enumeration, dead code, bundle ceiling
    e5f23d5 querylog: return to the default checkpoint cadence
    24521ab milestone 32: task-shaped configuration, file mode as a rendering, config status api
    c99a37d milestone 31: concurrent upstream exchanges, dot session reuse, queue metrics
    623667e milestone 30: overview as a dashboard, explicit health contract, period aggregations
    0e83477 agents: move opus-coder to the global scope
    fa323c7 milestone 29: activity — history, live and policy simulation on one surface
    0fd6bbd milestone 28: query provenance — every logged query is exactly explainable
    7e6cb50 release cut: bump-kind justfile recipe and a compiled, tested cut tool
    8a17e9e admin: fix dashboard phantom scroll, drop last-failure column from upstream table
    0107df5 spec: measurement window is the deployment side's call
    
    Downloads
  • v0.0.9 03149b5d65

    v0.0.9
    Release / guard (push) Successful in 36s
    Gates / frontend (push) Successful in 2m3s
    Gates / test (push) Successful in 2m18s
    Gates / test-aarch64 (push) Successful in 7m41s
    Gates / package (push) Successful in 4m20s
    Gates / container (push) Successful in 10s
    Release / gates (push) Successful in 19m21s
    Release / publish (push) Failing after 4m49s
    Stable

    mokhtar released this 2026-08-30 08:52:51 +00:00 | 105 commits to master since this release

    Query provenance: every logged query becomes exactly explainable — what the policy decided, what matched, where the answer came from and what the client saw. The handler records all of it as the reply goes out, query_log stores it, and a detail page reads one query back in the order the pipeline decided it. Read the upgrade note below first: it resets your query history.

    Added

    • Every logged query has a detail page. A row in Activity now links to /activity/queries/{id}, which explains that one query in the order it was decided: the request, the group it was matched under, the policy verdict with the rule that produced it and the blocklist source that rule came from, any CNAME uncloaking or safe-search rewrite, the route the answer took — blocked, local, forward zone, upstream or cache — and what the client got back, RCODE and duration included. GET /api/queries/{id} serves the same object; an id that retention has already deleted is a 404. The live view carries the same provenance for the queries it streams, so a query is explainable as it happens as well as afterwards.
    • Query Log, Live and Lookup are one Activity page. /activity is the single surface for what nxdns answered: History reads the stored log, Live reads the stream, and both show the same seven columns — Time, Domain, Client, Type, Result, Route, Duration. The mode and every filter live in the URL, so an investigation is one link that shows the recipient exactly what you were looking at, and an absolute time range stays that range instead of drifting as the day goes on. A new Result column says what the client actually got — Blocked, NOERROR, SERVFAIL and the rest — with the Route column beside it saying how the answer was produced, which is the pair the old Status column could not show: a blocked name is answered with NOERROR, and reading only the code made a block look like a success. Both unhappy cases are marked by weight and shape as well as colour. Switching between History and Live keeps your filters, and leaving Live closes the stream instead of holding a viewer slot open. A live row that the log has not written yet opens its own provenance in place — no invented row id — and the open detail stays put while the 500-row buffer scrolls past underneath it. Domain testing moves to /activity/test as Current policy simulation, worded so it can never be misread as an account of a query that already happened.
    • Diagnostics can be scoped to an absolute window. /diagnostics?since=…&until=… now validates and applies both bounds to the active and resolved lists, and the page states the window it is showing with a way to clear it. A query's detail page links here with the five minutes either side of that query, which is where the underlying failure text for a SERVFAIL lives.
    • The query log and the stats endpoints say how far back the history goes. GET /api/queries and all five /api/stats* endpoints each carry a coverage object: available_since, the first second the file can answer for, and complete, whether the window you asked for begins inside it. A period that starts before the query log does now says so instead of charting the missing part as zero — which is what a recreate, a retention pass or a fresh install would otherwise look like.
    • Three new period breakdowns: GET /api/stats/types, /api/stats/routes and /api/stats/clients. They take the same period parameter as /api/stats and report over the same UTC-aligned window, so every panel of one page describes the same span. types counts queries per DNS type, with the queries that recorded no type kept as their own row instead of dropped — the numeric type only, since naming types is the admin's job and a second table in the server would drift out of agreement with it. routes counts queries by how they were answered, grouping upstream rows by the answering resolver and forward-zone rows by the zone, with blocked, cache, local and rejected answers carrying no source. clients returns one bucketed series per client, aligned exactly like /api/stats/timeseries so the two charts share an x-axis: the eight busiest clients in the window are named and everything else sums into an other series, which is always present and always the same length as the named ones.
    • /metrics says whether an upstream is queueing. Five new per-upstream series, labelled by index and redacted url like the existing ones: nxdns_upstream_in_flight and nxdns_upstream_slots are the exchanges in flight against an upstream and the ceiling they cannot cross, nxdns_upstream_queued_total and nxdns_upstream_queued_seconds_total count the exchanges that had to wait for a slot and the time they spent waiting — including the ones that were cancelled while waiting, which is exactly the query that ends in SERVFAIL — and nxdns_upstream_reuse_recoveries_total counts the stale DoT connections that were redialled, so connection churn is a number instead of log noise. The two queue counters are approximate: they are sampled when a query is admitted, not measured as a queue length. /api/health is unchanged.
    • Each of those responses is read atomically. Every window-bounded read — the five stats endpoints and GET /api/queries — now takes its rows and its coverage watermark inside one SQLite read transaction. A retention pass that runs mid-response can no longer hand back rows from before the prune tagged with an available_since from after it, and the clients breakdown ranks and buckets from one database state rather than two. The transaction is a deferred read, so it never blocks the query logger or retention.

    Removed

    • The upstream-history subsystem and GET /api/upstream/health are gone. nxdns recorded every upstream exchange into per-minute aggregates in querylog.db so the dashboard could show each upstream's counts, success rate and last failure over the selected period. The Overview replacement drops that table, which left a writer whose only reader was its own failure signal, so the whole subsystem goes: the accumulator and its flush task, the upstream_targets and upstream_minute tables, the /api/upstream/health endpoint, the retention pass over the minute rows, and the four nxdns_upstream_history_* and one nxdns_retention_upstream_rows_pruned_total Prometheus metrics. What replaces it: /api/health says how many upstreams are available of how many enabled, /metrics keeps the live per-upstream nxdns_upstream_up and nxdns_upstream_success_rate series, and a failing upstream is a Diagnostics episode (upstream.exchange) with its own error text and duration. Ranged per-upstream counts are not replaced. Existing upstream_history.write diagnostics entries stay readable; nothing writes new ones, and any that were still open when you upgrade are closed at the first start.
    • /queries, /queries/{id}, /live and /lookup are gone, and bookmarks to them break. There is no redirect and no alias: the paths simply stop resolving, and the app shows its not-found page. Everything those pages did is on /activity, /activity/queries/{id} and /activity/test. Three navigation entries collapse into one, "Activity". The API is untouched — /api/queries, /api/queries/{id}, /api/queries/live and /api/lookup all answer exactly as before.
    • The Status column, and the block reason on every row. The reason a query was blocked was repeated on each of a hundred rows and pushed the answer the client saw off the table. Result and Route replace it; the exact rule, the blocklist source and the historical group stay one click away on the query's detail page, which is the only place they were ever readable.

    Changed

    • Configuration is three task-shaped pages, and a file-managed server says so once. Groups, Blocklists, Rules, Local DNS, Upstreams and Settings — six navigation entries and six routes — become /configuration/protection, /configuration/resolution and /configuration/system under one labelled "Configuration" group. Protection is group-centred: pick a group and read its safe search, its blocklist sources, its rules and how many clients it filters, with the shared blocklist catalogue and "Update now" on a second tab. Resolution holds the upstream pool, local records and forward zones; System holds the settings sections, the restart-required annotations and "Reload certificates". The tab and the selected group are URL state, so the view you are reading is a link, and the six old paths stop resolving — no redirect, no alias. When a configuration file owns the server, these pages are no longer forms with everything greyed out: they render as definition lists and tables with the exact ZON key beside each value, a note saying which file to edit, and no input, checkbox, Add, Edit, Delete or Save anywhere — while runtime actions that do not touch the file (update blocklists now, reload certificates, pause and resume, deleting an observed client) stay ordinary working buttons. A new GET /api/config/status is the one place that answers who owns the configuration and whether a restart is pending; the admin never guesses from a 403, and until the endpoint answers nothing is editable. restart_pending is now the server's own state rather than a banner one browser tab remembered: it is set by the upstream and settings changes that need a restart, it survives a page reload, another tab sees it within a minute, and only restarting the process clears it. GET /api/settings loses its authority object, which said the same thing twice. Clients keeps its own top-level page, because naming an unknown device is operations, not configuration: the list is address, name, group and first/last seen, ?group= filters it to one group, and each row opens /clients/{id} — the client's identity and where its name came from, the group whose policy it follows with a link to that group, and a link into Activity for its last 24 hours.
    • The Dashboard is now Overview, and it takes Pi-hole's layout. / redirects to /overview, and the page answers one question — what the resolver did over a period you choose — instead of laying out six widgets. The 1h/24h/7d/30d period is URL state (/overview?period=1h), so the view you are reading is a link you can send. Top to bottom: four neutral stat tiles — queries, blocked with its share, distinct clients, average response time — each linking into the rows behind its number; the query-volume timeline split blocked, cached and other; a new per-client chart on the same axis, the busiest clients named — by their registered or reverse-DNS name where they have one, exactly as the query tables name them — and the rest summed as "other"; and two donuts, query types and how queries were answered, with each upstream and forward zone named separately. Colours follow the identity of a client, a type or a route rather than its rank, so one client overtaking another between refreshes does not repaint the page. Each donut is drawn as decoration with a visible legend beside it and a table a screen reader reads instead of the graphic. All five panels describe one window — matched on the period, both bounds and the coverage watermark together — so a refresh that straddles a bucket boundary, or a retention pass mid-page, can never put a headline count above a chart of a different span. Panels load, fail and retry on their own: a failing donut leaves the charts standing. A period with nothing in it says "No queries in this period." rather than drawing an empty frame.
    • The five health conditions moved to Diagnostics, and the nav item says when to look. Protection, Upstreams, Query history, Diagnostics and Storage are now a compact strip at the top of /diagnostics, above the episodes that explain them, instead of a status list on the landing page. Each states its state in words and an icon as well as colour. A healthy condition is quiet; a degraded one is highlighted and links to what can fix it: protection to Blocklists, no reachable upstream to Upstreams, and a losing or failed query log or a low or critical disk to this same page filtered to the component that failed, with any time window cleared so the filter cannot hide the episodes it points at. Dropped rows are reported with the time of the newest drop, so a loss stays visible after the box recovers. When a health poll fails, the conditions on screen are labelled as the last reading that arrived rather than passing for the current state, and a Retry sits beside them. The Diagnostics navigation item carries a badge with the number of open episodes; it shows a plain "!" when the rollup is degraded with nothing open, and also when the last health poll failed, because an unknown must not look like good news. The badge is absent only when health answered and there was nothing to report.
    • Pause moved to the sidebar, and the header indicator is gone. The header carries nothing but the menu button and Log out. Pause and Resume sit at the foot of the navigation sidebar, above the version label, in both the desktop rail and the phone drawer — one global runtime action in the one place that belongs to the resolver rather than to whichever page you are on. The control still appears beside the detail of a query that was blocked, which is the other place the action answers what you are looking at. The control says what it is doing as well as what it offers: Pause while filtering is on, and while it is off, "Paused until 14:05" — or plain "Paused" when the pause has no end — above the Resume button, on every page. "Resume" on its own would name an action without naming the state it ends, and with the header indicator gone no other page could tell you filtering was off. Both controls read the same protection condition, so they cannot disagree, and a pause or resume is reflected immediately rather than at the next poll. Nothing offers to pause while protection is unavailable, since pausing a resolver with no filter snapshot changes nothing.
    • GET /api/health changed shape completely. The body is now status plus five condition objects — protection, upstreams, query_history, diagnostics and disk — and status is degraded when, and only when, one of them is in a degrading state: protection unavailable, upstreams unavailable, query history losing or failed, diagnostics unavailable, or disk low or critical. Nothing can degrade the rollup without appearing in the response any more; the old hidden upstream-history contribution was the reason for the rewrite. A paused protection is reported and does not degrade, because it is a choice you made rather than a fault. queries_dropped and writer_failed fold into query_history, which also carries last_drop_s, the time of the newest dropped row; refreshes_gated and snapshot_generation leave the body and stay in /metrics, as do the disk db_bytes, log_bytes and sample_failures fields. The disk monitor's warn state is reported as low, because warn reads as a log level rather than as a quantity of disk. This is a breaking change to a documented endpoint, taken pre-v0.1 rather than carried.
    • Upgrading resets your query history a second time. Dropping the upstream_targets and upstream_minute tables changes the querylog.db schema fingerprint, and that file is never migrated, so the first start after this release sets the old one aside as querylog.db.schema-changed-<unix seconds> and creates a fresh one — exactly as the provenance change above does, and in the same start. config.db is untouched.
    • GET /api/stats no longer reports cached. The standalone cache card is gone from Overview, so the totals field behind it has no consumer. Cache hits stay visible in the query-volume timeline's blocked/cached/other split, in GET /api/stats/timeseries, and in nxdns_cache_hits_total.
    • GET /api/queries rows changed shape. Each row gains qclass, rcode, policy_action, policy_reason and route_kind, and block_reason is gone: the reason a query was blocked is now one of a closed set of values rather than a formatted string. No table column shows it — the reason is read on the query's detail page, and by an API client from policy_reason on the row. blocked, cache_hit, upstream and every other existing field are unchanged.
    • Upgrading resets your query history. The query_log table gains the provenance columns below, and querylog.db is never migrated (it holds expendable log rows, so a schema change replaces the file instead of upgrading it). On the first start after the upgrade the old file is set aside as querylog.db.schema-changed-<unix seconds> and a fresh one is created. Nothing else is touched: config.db keeps your configuration and your diagnostics history. The recreate files a resolved query_log.recreated diagnostics entry naming the file that was kept and the timestamp the new history begins at, and a new querylog_meta table records that coverage start, so the dashboard can say "history is available from ..." instead of charting an empty range as zero. The set-aside file is a working SQLite database and can be deleted once you have decided you do not want it.
    • logging.query_log_buffer_max now accepts 1 to 37449, down from 1 to 1000000. The queued entry carries every new provenance field by value and is about four times as wide as before — 1792 bytes against 432 — so the meaningful bound is bytes rather than entries. The ceiling is computed at compile time from the width of the entry so that the queue's worst case stays within 64 MiB, and it moves whenever that width does. The default of 10000 is unchanged and costs about 17 MiB. A configuration above the new ceiling is rejected at startup with the ceiling in the message.
    • Group and blocklist source names are now capped at 64 bytes. Both are copied into every query-log row that mentions them, so an unbounded name was an unbounded cost per row. A longer name is rejected as GroupNameTooLong or SourceNameTooLong.
    • The query log returns to SQLite's default checkpoint cadence. 0.0.8 stretched wal_autocheckpoint on every read-write querylog.db connection from the 1000-page default to 8192 pages, on the expectation that it would cut about 130 MiB a day of checkpoint writeback on the deployed Pi. Field measurement on that Pi showed no measurable effect on daily disk writes, so all it bought was a roughly five-hour power-loss durability window in place of the default's ~40 minutes. No pragma is issued any more: the cadence is SQLite's 1000 pages, about 4 MiB, and the ~40-minute boundary is back.
    • The admin bundle now has a ceiling the build enforces. npm run build fails if admin/dist/assets totals more than 800,000 bytes — it is 708,352 today — and prints the largest chunks when it does. The bundle is embedded in the server binary and served to your LAN, so an accidental dependency arriving in it is a regression every other check would have passed. Alongside it the redesign's closure sweep removed the last code the new pages left behind: an unused API client call and type, and the features/queries directory renamed to features/provenance now that no page lives there. The investigation links that carry a time window out of a query detail are pinned by their own tests, including one that a link's emitted bounds survive the Activity page's validation unchanged. Nothing an operator uses changed.

    Fixed

    • A burst of concurrent queries no longer resolves one at a time, and no longer ends in SERVFAIL. The pool held an upstream for the whole of an exchange, so every query against one upstream waited for the one before it, and the DoT client dialled a fresh TCP connection and ran a full TLS handshake for each query on top of that. Thirty concurrent names against one DoT upstream resolved as a staircase at about 92 ms per query on a Pi, and the queries at the back of the queue burned the five-second total budget waiting and were answered SERVFAIL — with nothing on any surface saying a queue existed. Two changes fix it: each upstream now runs up to eight exchanges at once, each on its own leaf client behind a semaphore, so a query waits for a free slot rather than for the whole upstream; and a DoT client keeps its TLS session open across exchanges instead of handshaking per query. A reused connection that the resolver closed while it was idle is detected at use — never by a keepalive timer — and redialled once, and that redial is invisible to health and to Diagnostics because an idle close is normal, not a fault. The slot count is compiled, not configured: there is no new knob.
    • A UDP reply that has to be truncated keeps the answer's RCODE. When an answer does not fit the client's UDP buffer, nxdns replaces it with an empty reply carrying the TC bit, which tells the client to retry over TCP. That replacement was always built as NOERROR, whatever the answer said — so an oversized NXDOMAIN reached the client as a success, and an EDNS extended RCODE above 15 lost the eight upper bits it needs an OPT record to carry. The truncated reply now carries the full twelve-bit code the answer had, split across the header and the reply's OPT record where the code needs it, and the query-log row records the code the client actually saw. The retry over TCP always returned the right RCODE; this was the UDP answer that preceded it.

    Artifacts

    6baeb1b9695453d2b4cdebde6ab372f69793a65ad68644d2f344b698c6e12990  nxdns-0.0.9-x86_64-linux-musl.tar.gz
    60be07dc0129a59ed6cee73dbaafaebec90083e16ff682fbd038250eb6f24594  nxdns-0.0.9-aarch64-linux-musl.tar.gz
    cfc2d1d015b40f91bf032ad68c6e188e390a73802e307bdc2f0d44c42675b7b4  IMAGE-DIGEST.txt
    
    git.mial.net/mokhtar/nxdns:0.0.9@sha256:64af1d3655e8514dd5d212ae80bad35cf1f16ec0e859c38e3071341aecc4ec38
    

    Compare v0.0.8...v0.0.9

    Commits since v0.0.8
    03149b5 ci: drop the one-insn-per-tb workaround, the qemu panic is an 11.1.0-only regression
    a972c8d logger tests: join every writer future before its database closes, fix the gate-open race
    78d688a ci: run the aarch64 suite with one qemu insn per tb, tcg optimization falsely trips the ubsan pointer check
    fe5bd96 admin: live ring capacity is injectable, eviction test no longer timing-bound
    1f215ac build: bump version to 0.0.9
    2f86cd7 changelog: 0.0.9 releases today
    51cda8b milestone 33: contract closure — samples, file-authority enumeration, dead code, bundle ceiling
    e5f23d5 querylog: return to the default checkpoint cadence
    24521ab milestone 32: task-shaped configuration, file mode as a rendering, config status api
    c99a37d milestone 31: concurrent upstream exchanges, dot session reuse, queue metrics
    623667e milestone 30: overview as a dashboard, explicit health contract, period aggregations
    0e83477 agents: move opus-coder to the global scope
    fa323c7 milestone 29: activity — history, live and policy simulation on one surface
    0fd6bbd milestone 28: query provenance — every logged query is exactly explainable
    7e6cb50 release cut: bump-kind justfile recipe and a compiled, tested cut tool
    8a17e9e admin: fix dashboard phantom scroll, drop last-failure column from upstream table
    0107df5 spec: measurement window is the deployment side's call
    
    Downloads