-
v0.0.18
StableGates / frontend (push) Successful in 2m34sGates / test (push) Successful in 2m39sGates / test-aarch64 (push) Successful in 8m16sGates / package (push) Successful in 4m21sGates / container (push) Successful in 16sCI / gates (push) Successful in 15m39sRelease / guard (push) Successful in 35sGates / frontend (push) Successful in 2m1sGates / test (push) Successful in 2m28sGates / test-aarch64 (push) Successful in 7m24sGates / package (push) Successful in 52sGates / container (push) Successful in 11sRelease / gates (push) Successful in 10m59sRelease / publish (push) Successful in 5m11sreleased this
2026-09-09 17:57:40 +00:00 | 66 commits to master since this releaseFixed
- 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.txtgit.mial.net/mokhtar/nxdns:0.0.18@sha256:2af3899f65c886120267806310c63e319c8bb891d1ab8b72145c43e3a6f5b488Commits 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 rowDownloads
-
v0.0.17
StableGates / frontend (push) Successful in 2m23sGates / test (push) Successful in 3m15sGates / test-aarch64 (push) Successful in 8m38sGates / package (push) Successful in 5m0sGates / container (push) Successful in 19sCI / gates (push) Successful in 17m17sRelease / guard (push) Successful in 37sGates / frontend (push) Successful in 2m15sGates / test (push) Successful in 2m34sGates / test-aarch64 (push) Successful in 7m33sGates / package (push) Successful in 51sGates / container (push) Successful in 10sRelease / gates (push) Successful in 11m14sRelease / publish (push) Successful in 8m35sreleased this
2026-09-08 22:35:07 +00:00 | 69 commits to master since this releaseAdded
- A Nix flake with tag-pinned hashes.
flake.nixat the repository root buildsnxdnsforaarch64-linuxandx86_64-linuxfrom 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.mdcovers the input, thenixpkgsfollows line, and Renovate.
Changed
- The release archive is written by the project's own tool.
zig build distno longer shells out to the runner'starandgzip; 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 cutasserts 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 intoflake.nix, and commits that file alongsidebuild.zig.zonas one commit. CI reverifies the pins on the bump commit and again on the tag, before anything is uploaded. nxdns versionno longer reports a git commit. The commit is gone from the command's output, fromGET /api/version, and from the admin footer, which now showsnxdns 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.txtgit.mial.net/mokhtar/nxdns:0.0.17@sha256:dde9833df05ce227a13618f3469cccd1be2ad0fcaf03f526c076a89bf7ce5c55Commits 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 gatesDownloads
- A Nix flake with tag-pinned hashes.
-
v0.0.16
StableGates / frontend (push) Successful in 1m57sGates / test (push) Successful in 2m34sGates / test-aarch64 (push) Successful in 8m9sGates / package (push) Successful in 7m14sGates / container (push) Successful in 17sCI / gates (push) Successful in 18m17sRelease / guard (push) Successful in 33sGates / test-aarch64 (push) Successful in 7m22sGates / container (push) Successful in 11sRelease / gates (push) Successful in 10m35sGates / frontend (push) Successful in 2m8sGates / test (push) Successful in 2m16sGates / package (push) Successful in 44sRelease / publish (push) Successful in 10m4sreleased this
2026-09-07 22:05:41 +00:00 | 76 commits to master since this releaseThe 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.30is a link to exactly that view. The API takes the sameclientparameter onGET /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.txtgit.mial.net/mokhtar/nxdns:0.0.16@sha256:e5b7360d4cc6d6f32e77094c554103978ce875893613d84aad25935a9e940966Commits 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 auditDownloads
-
v0.0.15
StableGates / frontend (push) Successful in 1m51sGates / test (push) Successful in 2m29sGates / test-aarch64 (push) Successful in 7m24sRelease / guard (push) Successful in 32sGates / frontend (push) Successful in 2m14sGates / package (push) Successful in 4m56sGates / test (push) Successful in 2m20sGates / test-aarch64 (push) Successful in 7m25sGates / package (push) Successful in 1m3sGates / container (push) Successful in 15sCI / gates (push) Successful in 26m26sGates / container (push) Successful in 11sRelease / gates (push) Successful in 16m6sRelease / publish (push) Successful in 6m43sreleased this
2026-08-31 17:24:54 +00:00 | 78 commits to master since this releaseThe 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.txtgit.mial.net/mokhtar/nxdns:0.0.15@sha256:b9053a0660b70639572e11697e75b21a3cc47e1fae09d1eeb92e8466ef0c58a3Commits 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 buttonDownloads
-
v0.0.14
StableRelease / guard (push) Successful in 34sGates / frontend (push) Successful in 1m47sGates / test (push) Successful in 2m43sGates / test-aarch64 (push) Successful in 7m29sGates / package (push) Successful in 58sGates / container (push) Successful in 14sRelease / gates (push) Successful in 11m27sRelease / publish (push) Successful in 1m30sreleased this
2026-08-30 10:34:06 +00:00 | 91 commits to master since this releaseSchema 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 emptyquerylog.dbaway and delete itsquerylog.db-walandquerylog.db-shmfiles (leaving them would corrupt the restored file), rename the.schema-changed-<timestamp>file back toquerylog.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.txtgit.mial.net/mokhtar/nxdns:0.0.14@sha256:c729fb4aa37054c5dbafa3a0d668898222e1c1e0364eeb955ef861a04707ebe9Commits 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 callDownloads
- 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 (
-
v0.0.13
StableRelease / guard (push) Successful in 34sGates / frontend (push) Successful in 1m39sGates / test (push) Successful in 2m27sGates / test-aarch64 (push) Successful in 8m9sGates / package (push) Successful in 4m24sGates / container (push) Successful in 16sRelease / gates (push) Successful in 15m18sRelease / publish (push) Failing after 4m55sreleased this
2026-08-30 10:20:09 +00:00 | 93 commits to master since this releaseThe 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_msbudget 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_msonce 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.txtgit.mial.net/mokhtar/nxdns:0.0.13@sha256:75ee3fa99c48cb2302671cfb2062d01bbc303edc18d526c4f9be0887a472a1d6Commits 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 callDownloads
- 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
-
v0.0.12
StableRelease / guard (push) Successful in 35sGates / frontend (push) Successful in 1m40sGates / test (push) Successful in 2m23sGates / test-aarch64 (push) Successful in 7m12sGates / package (push) Successful in 4m18sGates / container (push) Successful in 16sRelease / gates (push) Successful in 14m12sRelease / publish (push) Failing after 4m57sreleased this
2026-08-30 09:58:51 +00:00 | 95 commits to master since this releaseOverview 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.dbis 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 replacesGET /api/stats,/api/stats/timeseries,/api/stats/types,/api/stats/routesand/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.txtgit.mial.net/mokhtar/nxdns:0.0.12@sha256:a9a60ba0e64701d0252d433a794dc5a035e3122f07e9fab5b1e2e6a77924f33cCommits 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 callDownloads
- Upgrading resets your query history. The query-log schema gains the aggregate tables described below, and
-
v0.0.11
StableRelease / guard (push) Successful in 35sGates / frontend (push) Successful in 1m51sGates / test (push) Successful in 3m20sGates / test-aarch64 (push) Successful in 7m30sGates / package (push) Successful in 5m46sGates / container (push) Successful in 15sRelease / gates (push) Successful in 16m54sRelease / publish (push) Failing after 4m48sreleased this
2026-08-30 09:38:12 +00:00 | 97 commits to master since this releaseThe 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.txtgit.mial.net/mokhtar/nxdns:0.0.11@sha256:a671dd8e3ed4f593ac2bb6911bee2a264e7087a543a89af9d986d5843ac0ab3aCommits 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 callDownloads
-
v0.0.10
StableRelease / guard (push) Successful in 38sGates / frontend (push) Successful in 1m40sGates / test (push) Successful in 2m29sGates / test-aarch64 (push) Successful in 7m53sGates / package (push) Successful in 4m17sGates / container (push) Successful in 15sRelease / gates (push) Successful in 14m58sRelease / publish (push) Failing after 6m21sreleased this
2026-08-30 09:15:42 +00:00 | 100 commits to master since this releaseConfiguration 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 cutnow compares thequerylog.dbschema 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.txtgit.mial.net/mokhtar/nxdns:0.0.10@sha256:15d120f48e4a856f62a7a7235d9365f38de144474be59df8b2e5620fdd3f3327Commits 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 callDownloads
-
v0.0.9
StableRelease / guard (push) Successful in 36sGates / frontend (push) Successful in 2m3sGates / test (push) Successful in 2m18sGates / test-aarch64 (push) Successful in 7m41sGates / package (push) Successful in 4m20sGates / container (push) Successful in 10sRelease / gates (push) Successful in 19m21sRelease / publish (push) Failing after 4m49sreleased this
2026-08-30 08:52:51 +00:00 | 105 commits to master since this releaseQuery 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_logstores 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.
/activityis 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,SERVFAILand 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/testas 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/queriesand all five/api/stats*endpoints each carry acoverageobject:available_since, the first second the file can answer for, andcomplete, 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/routesand/api/stats/clients. They take the sameperiodparameter as/api/statsand report over the same UTC-aligned window, so every panel of one page describes the same span.typescounts 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.routescounts 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.clientsreturns one bucketed series per client, aligned exactly like/api/stats/timeseriesso the two charts share an x-axis: the eight busiest clients in the window are named and everything else sums into anotherseries, which is always present and always the same length as the named ones. /metricssays whether an upstream is queueing. Five new per-upstream series, labelled by index and redacted url like the existing ones:nxdns_upstream_in_flightandnxdns_upstream_slotsare the exchanges in flight against an upstream and the ceiling they cannot cross,nxdns_upstream_queued_totalandnxdns_upstream_queued_seconds_totalcount 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 — andnxdns_upstream_reuse_recoveries_totalcounts 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/healthis 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 anavailable_sincefrom 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/healthare gone. nxdns recorded every upstream exchange into per-minute aggregates inquerylog.dbso 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, theupstream_targetsandupstream_minutetables, the/api/upstream/healthendpoint, the retention pass over the minute rows, and the fournxdns_upstream_history_*and onenxdns_retention_upstream_rows_pruned_totalPrometheus metrics. What replaces it:/api/healthsays how many upstreams are available of how many enabled,/metricskeeps the live per-upstreamnxdns_upstream_upandnxdns_upstream_success_rateseries, and a failing upstream is a Diagnostics episode (upstream.exchange) with its own error text and duration. Ranged per-upstream counts are not replaced. Existingupstream_history.writediagnostics 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},/liveand/lookupare 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/liveand/api/lookupall 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/resolutionand/configuration/systemunder 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 newGET /api/config/statusis 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_pendingis 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/settingsloses itsauthorityobject, 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
protectioncondition, 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/healthchanged shape completely. The body is nowstatusplus five condition objects —protection,upstreams,query_history,diagnosticsanddisk— andstatusisdegradedwhen, and only when, one of them is in a degrading state: protectionunavailable, upstreamsunavailable, query historylosingorfailed, diagnosticsunavailable, or diskloworcritical. 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_droppedandwriter_failedfold intoquery_history, which also carrieslast_drop_s, the time of the newest dropped row;refreshes_gatedandsnapshot_generationleave the body and stay in/metrics, as do the diskdb_bytes,log_bytesandsample_failuresfields. The disk monitor'swarnstate is reported aslow, becausewarnreads 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_targetsandupstream_minutetables changes thequerylog.dbschema fingerprint, and that file is never migrated, so the first start after this release sets the old one aside asquerylog.db.schema-changed-<unix seconds>and creates a fresh one — exactly as the provenance change above does, and in the same start.config.dbis untouched. GET /api/statsno longer reportscached. 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, inGET /api/stats/timeseries, and innxdns_cache_hits_total.GET /api/queriesrows changed shape. Each row gainsqclass,rcode,policy_action,policy_reasonandroute_kind, andblock_reasonis 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 frompolicy_reasonon the row.blocked,cache_hit,upstreamand every other existing field are unchanged.- Upgrading resets your query history. The
query_logtable gains the provenance columns below, andquerylog.dbis 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 asquerylog.db.schema-changed-<unix seconds>and a fresh one is created. Nothing else is touched:config.dbkeeps your configuration and your diagnostics history. The recreate files a resolvedquery_log.recreateddiagnostics entry naming the file that was kept and the timestamp the new history begins at, and a newquerylog_metatable 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_maxnow 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
GroupNameTooLongorSourceNameTooLong. - The query log returns to SQLite's default checkpoint cadence. 0.0.8 stretched
wal_autocheckpointon every read-writequerylog.dbconnection 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 buildfails ifadmin/dist/assetstotals 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 thefeatures/queriesdirectory renamed tofeatures/provenancenow 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.txtgit.mial.net/mokhtar/nxdns:0.0.9@sha256:64af1d3655e8514dd5d212ae80bad35cf1f16ec0e859c38e3071341aecc4ec38Commits 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 callDownloads
- Every logged query has a detail page. A row in Activity now links to