-
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 | 40 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 | 43 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 | 48 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
-
v0.0.8
StableGates / frontend (push) Successful in 1m34sGates / test (push) Successful in 1m53sGates / test-aarch64 (push) Successful in 6m34sGates / package (push) Successful in 43sGates / test-aarch64 (push) Successful in 7m23sGates / package (push) Successful in 4m11sGates / container (push) Successful in 18sCI / gates (push) Successful in 27m37sRelease / guard (push) Successful in 18sGates / frontend (push) Successful in 1m9sGates / test (push) Successful in 1m36sGates / container (push) Successful in 10sRelease / gates (push) Successful in 9m6sRelease / publish (push) Successful in 4m33sreleased this
2026-08-21 17:25:32 +00:00 | 65 commits to master since this releaseOne constant, chosen from the 0.0.7 field numbers: the checkpoint cadence was the last first-order write cost on the Pi's SD card.
Changed
- The query log checkpoints its write-ahead log every 32 MiB instead of every 4 MiB. Batching the writer in 0.0.7 took the deployed Pi from about 0.5 to 0.281 GiB of writes a day, and about 130 MiB of what is left is checkpoint writeback: SQLite's 1000-page default trips roughly every 40 minutes and rewrites the same hot index and interior pages into
querylog.dbeach time. Every read-write connection toquerylog.dbnow setswal_autocheckpointto 8192 pages, which stretches that to roughly five hours and cuts those in-place rewrites about eightfold, for an expected total near 190 MiB a day. The price is durability under power loss or a kernel panic. Atsynchronous = NORMALa commit does not fsync, so the checkpoint is the only guaranteed durability boundary, and it now sits about five hours of query rows and upstream-history minutes back rather than 40 minutes. Kernel writeback normally makes the real loss far smaller than that, but nothing guarantees it. A process crash or a clean stop still loses nothing that was committed, and the database is never left inconsistent: recovery replays the longest valid prefix of the log. Thequerylog.db-walfile is expected to sit near 32 MiB rather than capped there, since a long-running reader can hold a checkpoint off and let it overshoot, and the daily retention pass still truncates it.config.dbis unchanged.
Artifacts
aaf2b72be2fdbeb2052e0f9dbc23c52aa2b6dea538dc720f001c69cbe6569c75 nxdns-0.0.8-x86_64-linux-musl.tar.gz 0670430fad5b07d45e1d4248deba03ac72420cdcbb1b3c7c25d160c3dbb22f65 nxdns-0.0.8-aarch64-linux-musl.tar.gz 687feb04186aba42ff3ce8d0b8921b37e9ec875c080840dde79a49dc7d69cda6 IMAGE-DIGEST.txtgit.mial.net/mokhtar/nxdns:0.0.8@sha256:f5ec2a8cdae6225d8207e98e305a20a1fb8cba7ab007f6b3113a80ee754e25dfCommits since v0.0.7
b6cea3f build: bump version to 0.0.8 9b0b7c1 querylog: checkpoint every 8192 wal pages instead of 1000Downloads
- The query log checkpoints its write-ahead log every 32 MiB instead of every 4 MiB. Batching the writer in 0.0.7 took the deployed Pi from about 0.5 to 0.281 GiB of writes a day, and about 130 MiB of what is left is checkpoint writeback: SQLite's 1000-page default trips roughly every 40 minutes and rewrites the same hot index and interior pages into
-
v0.0.7
StableGates / package (push) Successful in 41sGates / test (push) Successful in 1m35sGates / test-aarch64 (push) Successful in 6m48sGates / frontend (push) Successful in 1m22sGates / container (push) Successful in 9sCI / gates (push) Successful in 23m18sGates / frontend (push) Successful in 1m8sGates / package (push) Successful in 40sRelease / guard (push) Successful in 17sGates / test (push) Successful in 1m35sGates / test-aarch64 (push) Successful in 6m55sGates / container (push) Successful in 9sRelease / gates (push) Successful in 9m23sRelease / publish (push) Successful in 4m26sreleased this
2026-08-20 21:24:12 +00:00 | 67 commits to master since this releaseOperational failures get a page of their own, and the query log stops wearing out the disk it lives on: the deployed Pi was writing half a gigabyte a day to store two megabytes of query rows, one transaction per query. Both came out of running 0.0.6 on real hardware.
Added
- A diagnostics page. Operational failures now land in one curated log instead of only journald: blocklist download failures, certificate reload failures, disk pressure, query-log writer and maintenance failures, upstream exchange and history failures, client tracking failures, listener and configuration problems at boot, and the query-log recreation an upgrade causes. One entry per failing subject — an entry opens on the first failure, counts repeats, and closes itself when the subject recovers; nothing needs dismissing. Each entry says what it means for the service and what to do about it.
GET /api/diagnosticsserves the log,GET /api/healthreports the active counts and degrades while the diagnostics store itself cannot write, and/metricsgainsnxdns_diagnostics_active_warnings,nxdns_diagnostics_active_errorsandnxdns_diagnostics_write_failures_total. Resolved entries can be purged when you decide the history has served its purpose — one entry from its row or its detail page, or the whole resolved history at once with "Purge all resolved" (DELETE /api/diagnostics/{id}andDELETE /api/diagnostics). An entry that is still failing is the current state of the box, not history, so it has no purge action and the API answers 409.
Changed
- The query log commits once a minute instead of once a query. The writer batched for 100 milliseconds, which at a household's query rate means almost every query got a transaction of its own — and a transaction costs the disk far more than the row it carries. On the deployed Pi that came to roughly 0.5 GiB of writes a day to store 2.3 MB of query rows, the kind of write volume that kills an SD card. The batch window is now
logging.query_log_flush_interval_s: 60 seconds by default (the same minute Pi-hole'sDBintervaldefaults to, for the same reason), anything from 0 to 3600, editable on the settings page. Batches are still capped at 100 rows, so a burst is committed as soon as it fills one rather than waiting out the window, and the in-memory queue, its drop-oldest backpressure and retention are untouched. The price is two kinds of lag: a crash costs about one interval of query history — more if the writer was held back by a full disk or a slow write — and every query-log-backed view — the query-log page, the dashboard totals, the timeseries — is about one interval behind. The live page is not affected; it is fed before the queue. Set the key to0for the old write-immediately behavior.
Fixed
- Shutdown no longer races the last query rows to the disk. The query-log writer was stopped by the same cancellation that stopped the DNS listeners, so whether the batch it was holding reached the database depended on which happened to land first, the cancellation or the queue closing. Shutdown now stops and joins the listeners and every other query producer first, then closes the queue, then waits for the writer to finish emptying it — the held batch and everything still queued get written. If free space is below the critical threshold and the disk monitor will not let that final write through, the rows are counted as dropped instead of holding the exit open indefinitely.
- An upstream success rate no longer rounds up to 100.0% while failures stand. One decimal place cannot hold 12,696 successes out of 12,698 attempts: it rounded to
100.0%, so the row claimed perfect reliability next to a failure count of 2. Neither end of the scale is reachable by rounding any more —100.0%needs an actual absence of failures and0.0%an actual absence of successes, and a rate a hair off either end shows99.9%or0.1%instead. - A query log set aside by a schema change is no longer named
corrupt. Every recreate wrote the old file toquerylog.db.corrupt-<unix seconds>, whatever sent it there — including the fingerprint mismatch an upgrade causes, where the file is a healthy database this build simply cannot read. The name is the only account of the reason that outlives the log line, so it read as an accusation and invited operators to delete an intact file. The name now says which of the four cases it hit:querylog.db.corrupt-…,.not-a-database-…,.quick-check-failed-…or.schema-changed-…. The 0.0.6 upgrade producesschema-changed. Nothing else about the recreate changed, and no existing aside file is renamed.
Artifacts
0df1353252a817e929438b5e6c1bf8c58be7862c0b46dc7ef167752177f6a2c0 nxdns-0.0.7-x86_64-linux-musl.tar.gz 368d51d30ea2ec3721786c7ea973440827b1dadc54e16d01612e36670dfec3db nxdns-0.0.7-aarch64-linux-musl.tar.gz 7b2527ab6fcc0d16fe7cb9d5168961058e7fc4656f262d3adc17506f862166dc IMAGE-DIGEST.txtgit.mial.net/mokhtar/nxdns:0.0.7@sha256:28363f6a9a7add846cb21d5739c037d20be3df396d4c9b6a3e9f28f4c87e9ec3Commits since v0.0.6
324704b build: bump version to 0.0.7 addf24f query log batching: one transaction per flush interval, not per query 037f209 milestone 27: diagnostics — operational failures land in one curated log, resolved history purgeable 3dd8214 dashboard: a success rate never rounds up to 100.0% while failures stand 64c0d72 querylog: the aside file's name says why, so a schema change is not called corruptDownloads
- A diagnostics page. Operational failures now land in one curated log instead of only journald: blocklist download failures, certificate reload failures, disk pressure, query-log writer and maintenance failures, upstream exchange and history failures, client tracking failures, listener and configuration problems at boot, and the query-log recreation an upgrade causes. One entry per failing subject — an entry opens on the first failure, counts repeats, and closes itself when the subject recovers; nothing needs dismissing. Each entry says what it means for the service and what to do about it.
-
v0.0.6
StableRelease / guard (push) Successful in 22sGates / frontend (push) Successful in 1m30sGates / test (push) Successful in 1m50sGates / frontend (push) Successful in 1m9sGates / test-aarch64 (push) Successful in 6m55sGates / test (push) Successful in 1m34sGates / test-aarch64 (push) Successful in 6m9sGates / package (push) Successful in 3m49sGates / package (push) Successful in 32sGates / container (push) Successful in 15sCI / gates (push) Successful in 21m28sGates / container (push) Successful in 9sRelease / gates (push) Successful in 20m7sRelease / publish (push) Successful in 4m26sreleased this
2026-08-17 16:49:01 +00:00 | 72 commits to master since this releaseThe period picker now scopes the whole dashboard. The upstream table was the last widget that ignored it, and fixing that meant recording upstream outcomes over time instead of counting them since boot. Read the query-log note below before you upgrade.
Added
- Four metrics for the new upstream-history recorder:
nxdns_upstream_history_flushes_total,nxdns_upstream_history_flush_failures_total,nxdns_upstream_history_rows_dropped_totaland thenxdns_upstream_history_pendinggauge. While a flush to the database keeps failing,GET /api/healthreportsdegraded; it recovers on the next flush that succeeds.
Changed
- Upstream health answers for the selected period. The dashboard's upstream table used to print counters accumulated since process start beside a success rate taken over the last 32 exchanges, which is how "63 failures" and "100.0% success rate" ended up in the same row under a period picker that scoped nothing there. Every upstream outcome is now aggregated into its wall-clock minute and written to
querylog.db, andGET /api/upstream/health?period=…serves the selected window: attempts, failures, success rate, and the last failure with its error name, all inside the period, from 31 days of history. A window with no attempts reports no success rate at all instead of a perfect one, and the table shows an em-dash. The in-memory health state that drives failover and backoff is unchanged, as are its/metricsseries. GET /api/upstream/healthchanged shape. Gone from each upstream:consecutive_failures,total_successes,total_failures, the last-32success_rate,last_errorandlast_error_age_s. Each upstream keepsurl,enabledandavailableand gains aperiodobject with the ranged numbers; the body gainsperiod,since,untiland acompleteflag that says whether any outcome was known to be dropped inside the window. The removed counters are still exported by/metricsunder their existing names. On the dashboard the "Right now" section is gone with them: the upstream table rejoined the ranged part of the page, and the disk card, the one live widget left, is titled "Storage now".- The query log is recreated on upgrade. Recording upstream history added two tables to the
querylog.dbschema, and its fingerprint check refuses a database that does not match the shipped definition. On first start this version renames the existingquerylog.dbaside asquerylog.db.corrupt-<unix seconds>in the data directory and creates a fresh one, so query history and stats restart empty. The renamed file is left in place rather than deleted, so removing it is your call.config.dbis untouched: no configuration is lost.
Artifacts
9688a59a5307429e893bb6cde2cf965cf44721d609971fe67b7e73f91003a7e1 nxdns-0.0.6-x86_64-linux-musl.tar.gz 95a4d702da2eec905673205102d6787275d0ff7dc3031aa23304659bca89ee9f nxdns-0.0.6-aarch64-linux-musl.tar.gz 6dade19c7693084ab7ddd0af2e5643fb64ed2597b452c3b6e5ee089aeb2e3ce3 IMAGE-DIGEST.txtgit.mial.net/mokhtar/nxdns:0.0.6@sha256:20076be836bc13a56e0a14cf3b9f5ca829ba41f9051ad2c66b2afdf95ce2cca4Commits since v0.0.5
377f00a changelog: 0.0.6 releases today e0a7cd8 milestone 26: upstream health answers for the selected periodDownloads
- Four metrics for the new upstream-history recorder:
-
v0.0.5
StableGates / test (push) Successful in 1m44sGates / frontend (push) Successful in 1m2sGates / test-aarch64 (push) Successful in 5m55sGates / package (push) Successful in 28sGates / container (push) Successful in 16sCI / gates (push) Successful in 20m25sRelease / guard (push) Successful in 18sGates / frontend (push) Successful in 1m5sGates / test-aarch64 (push) Successful in 6m33sGates / test (push) Successful in 1m31sGates / package (push) Successful in 3m41sGates / container (push) Successful in 9sRelease / gates (push) Successful in 19m27sRelease / publish (push) Successful in 5m43sreleased this
2026-08-16 22:21:39 +00:00 | 74 commits to master since this releaseOne rendering fix on the 0.0.4 feature, caught the day it shipped.
Changed
- The query tables no longer repeat the learned tag on every row: in the live page and the query log a learned name is just muted, with the address still in the row's tooltip. The clients page keeps the tag, where it appears once per client and says something.
Artifacts
e525f0a46824ed75d89c2cc46e9423ab64b49710d143c88e7f09ff412a8b6a67 nxdns-0.0.5-x86_64-linux-musl.tar.gz 17728416c329a053a8f291b0aef272d564a7ec78b770e63725e5bae14f86ce4f nxdns-0.0.5-aarch64-linux-musl.tar.gz 85a8893c993dc92d59edff3980f1bee9ded1afe42723ab3359fa61d1e5354f3e IMAGE-DIGEST.txtgit.mial.net/mokhtar/nxdns:0.0.5@sha256:14da178b9c751f458c2ef1088adb901fe9fd39176124c5c8a66069566c120a68Commits since v0.0.4
3ed9a57 changelog: 0.0.5 releases today 0601098 admin: drop the learned tag from the query tablesDownloads
-
v0.0.4
StableRelease / guard (push) Successful in 26sGates / frontend (push) Successful in 1m26sGates / test (push) Successful in 1m44sGates / frontend (push) Successful in 1m6sGates / test-aarch64 (push) Successful in 6m36sGates / test (push) Successful in 1m33sGates / test-aarch64 (push) Successful in 5m50sGates / package (push) Successful in 5m5sGates / package (push) Successful in 30sGates / container (push) Successful in 15sCI / gates (push) Successful in 21m58sGates / container (push) Successful in 10sRelease / gates (push) Successful in 20m42sRelease / publish (push) Successful in 4m41sreleased this
2026-08-16 21:27:33 +00:00 | 76 commits to master since this releaseThe names learned in 0.0.3 now show up where queries do: the live page and the query log name each client instead of printing its address.
Added
- Client names in the query tables. The live page and the query log show each query's client by name, with the same precedence as the clients page: a hand-typed name wins, else the learned name (muted, tagged learned), else the bare address. When a name replaces the address, the address stays readable as the row's tooltip. Devices that appear mid-stream show their address first and pick up their name within half a minute.
Artifacts
46a0193d0b212090ccea86e4f61184361ecaf5c6da3552d91a76e26f71b69c46 nxdns-0.0.4-x86_64-linux-musl.tar.gz d9c4eaa1941dd728524f1a33e7a0d38bf72189bdca661de3139d7f13eaa7de9a nxdns-0.0.4-aarch64-linux-musl.tar.gz 33c08361bf81113297da0f4a18c83812db8578db328fa3125650b68e8310c8dc IMAGE-DIGEST.txtgit.mial.net/mokhtar/nxdns:0.0.4@sha256:6d0e9e7971ef7ca78e7878be4ef428c3c0b5f309061efca2abea72fc70e21886Commits since v0.0.3
49c7da2 changelog: 0.0.4 releases today 794ea65 admin: live and query log pages show client names ba037c5 website: plan for the nxdns.sh marketing and docs site 1e97c80 rename web/ to admin/, along with the web-named build and cli identifiers 5b3d1cd docs: unwrap hand-wrapped prose repo-wide 50b8fd5 changelog: unwrap hand-wrapped lines, release bodies render newlines as breaksDownloads
-
v0.0.3
StableGates / frontend (push) Successful in 1m3sGates / test (push) Successful in 1m39sGates / test-aarch64 (push) Successful in 6m28sGates / container (push) Successful in 18sRelease / guard (push) Successful in 1m30sGates / test-aarch64 (push) Successful in 5m45sGates / package (push) Successful in 24sRelease / gates (push) Successful in 7m54sGates / package (push) Successful in 5m34sCI / gates (push) Successful in 14m5sGates / frontend (push) Successful in 58sGates / test (push) Successful in 1m29sGates / container (push) Successful in 10sRelease / publish (push) Successful in 9m12sreleased this
2026-08-15 10:57:14 +00:00 | 82 commits to master since this releaseDevices name themselves: the clients table asks the router over reverse DNS instead of waiting for the operator to type every name. The CI container gate also moved from workflow shell into a compiled, tested tool, which fixed a latent temp-directory bug shared with the release tool.
Added
- Client names learned over reverse DNS. A client row that carries no hand-typed name gets one from the network: each tracker flush pass takes up to 16 unnamed rows, builds each address's reverse name, matches it against the declared
forward_zones, and on a match sends one PTR query to that zone's resolver, storing the answer as a learned name. This requires a conditional forward zone covering the LAN's reverse space — for example168.192.in-addr.arpapointed at the router; without one, nothing is sent anywhere. A hand-typed name always wins, learned names never appear innxdns exportand are never set bynxdns import, and each row refreshes once a day (an hour after a failure), so a rename can show stale for up to 24 hours. The API'sClientobject gains alearned_namefield and the clients page shows it.
Changed
- The container CI gate — image build, image-contents assertion against the packaged artifacts, and the startup/shutdown smoke test — moved from workflow shell into
tools/container_check.zig, compiled and unit-tested byzig build testand runnable on a laptop against a local docker daemon. The health probe now runs under a real 60-second deadline (the shell loop's "30 seconds" could stretch past three minutes), and the gate's docker objects carry an ownership label so anything a dead runner leaks is discoverable. The version in CI is parsed frombuild.zig.zonthrough the zon grammar, once, instead of by two copies of asedregex.
Fixed
- The release tool's temporary-directory claim was not exclusive: the "create" it relied on succeeds on a directory that already exists, so a stale or concurrent directory could be silently adopted, written into, and deleted on exit. Both the release tool and the new container gate now claim their directories exclusively and retry on collision.
Downloads
- Client names learned over reverse DNS. A client row that carries no hand-typed name gets one from the network: each tracker flush pass takes up to 16 unnamed rows, builds each address's reverse name, matches it against the declared
-
v0.0.2
StableGates / frontend (push) Successful in 1m18sGates / test (push) Successful in 2m27sGates / test-aarch64 (push) Successful in 5m39sGates / package (push) Successful in 3m34sGates / container (push) Successful in 13sCI / gates (push) Successful in 11m57sRelease / guard (push) Successful in 1m30sGates / frontend (push) Successful in 1m6sGates / test (push) Successful in 1m28sGates / test-aarch64 (push) Successful in 5m41sGates / package (push) Successful in 28sGates / container (push) Successful in 16sRelease / gates (push) Successful in 7m55sRelease / publish (push) Successful in 10m37sreleased this
2026-08-14 01:06:58 +00:00 | 85 commits to master since this releaseConfiguration can now be a file that every boot converges to, filtering gains regex rules and honors blocklist exception lines, and two refresh bugs that silently kept stale state are fixed. Note the three breaking changes below if you script against
nxdns importor run with--config.Added
- Declarative configuration for IaC.
nxdns run --config=<file>makes the file the sole source of configuration: every boot converges the database to it in one transaction, preserving blocklist downloads, compiled lists and client history, so an unchanged file costs zero downloads and zero writes. Barenxdns runkeeps the database (and the web UI) in charge, exactly as before. In file mode the web UI is read-only for configuration and says so; runtime actions (pause, blocklist refresh, certificate reload) stay live.GET /api/settingsreports which authority governs the process. nxdns importnow refuses a file whose application would delete configuration rows, names the tables and counts, and applies it only with the new--allow-deleteflag. Additive and edit-in-place imports need no flag.- Regex rules. Rules gain a third kind,
regex, besideexactandwildcard, for per-group allow and block patterns such as^ad[0-9]+-. The engine is homegrown and linear-time by construction, so no pattern can make matching blow up; backreferences and lookaround do not exist, and a bad pattern is refused at insert time with the limit it hit. Matches appear in/api/lookupand the query log asrule_allow_regex/rule_block_regex. Regex still comes only from you: regex lines in downloaded lists stay counted and skipped. - Blocklist exception lines are honored. An Adblock-Plus
@@||name^line in a downloaded list now lifts that name — and its subdomains — out of what the attached lists block. Exceptions sit below every rule you wrote: a downloaded list can reopen only a hole another downloaded list dug, never override an operator decision. Each source reports how many it carried. - Browser-only lines are counted where you can see them. Every source now reports how many of its lines nxdns skipped as syntax with no DNS meaning — cosmetic filters,
$-modifier rules — beside the existing skipped-regex count. Both blocklist tables show the number and the UI explains the difference: a list whose skipped-unsupported count dwarfs its domain count is written for browser extensions, and its DNS or hosts variant will block more. Previously such a list compiled to almost nothing and looked clean.
Changed
- Breaking:
nxdns run --config <file>changed meaning. It used to seed the database once and then ignore the file; it now makes the file the authority on every boot, which deletes any configuration the file does not declare — including edits made through the web UI since the seed. Before upgrading a unit that carries--config: either drop the flag to keep the database in charge, or adopt file mode with the sequence in the upgrade guide. Order matters there: export the file with the NEW binary (stopped). - Breaking: 0.0.1 exports are refused by this version. A 0.0.1
nxdns exportwrites both.password = ""and the stored.password_hash, and this version refuses a file that carries both. This bites any old export — an adoption file or a configuration backup fed tonxdns importalike. Fix an existing export by deleting its.password = ""line (keep the.password_hashline). Take fresh backups with the new binary. - Breaking: the offline password-change recipe changed. Setting
.password = "new"together with.password_hash = ""is now refused (emptypassword_hashis an explicit "disable authentication", and the two fields cannot both be present). To change the password in the file: set.passwordand delete the.password_hashline entirely. nxdns import --forceis renamed--allow-delete.- A fresh install no longer seeds from
/etc/nxdns/config.zonby presence. Usenxdns importonce, or run in file mode with--config. - The admin UI's internals moved to TypeScript 7 and replaced Tailwind with StyleX and React Aria. The visible change is small: selects are real widgets with working keyboard focus; everything else renders as before.
- The
config.dbschema is a single baseline definition again; numbered migration steps start accumulating at v0.1.
Fixed
- A list switching a name between its exact and wildcard forms never took effect. The compiled-list checksum hashed the exact and wildcard bodies as one unseparated byte stream, so a list carrying
a.exampleand the same list carrying*.a.exampleproduced the same digest, and the refresh kept the old compiled files. The checksum now separates the bodies. Every source recompiles once on its first refresh after the upgrade; no re-download of unchanged content is forced beyond the refresh's normal fetch. - A refresh could store stale skip counts. When a refresh found the list content unchanged, it wrote the previously stored skip counters back to the database while showing the fresh ones in the UI, and the next restart reverted the numbers to the stale copy. All counters now persist from the fresh compile.
- An Adblock-Plus entry with embedded whitespace (
||good.example bad.example^) compiled into an entry no query could ever match. Such lines are now counted as unsupported instead.
Downloads
- Declarative configuration for IaC.