Entries were stored in UTC without a zone marker and the Audit and
Diagnostic Log pages read them as local time, so every entry showed
shifted by the viewer's UTC offset (two hours early in Sweden). A shared
parseDbTimestamp() now reads them as UTC, and replaces the inline
workaround the Consistency page had.
The Audit Log never displayed an entry's details at all, so adding an
admin link showed only "server #1". Link entries now carry the server
name and the label/URL, and a new Details column shows them, along with
things like a port scan's address and range. Entries made within the
same second are now ordered by id instead of arbitrarily.
A domain's "Check now" was the one user-triggered action that wasn't
audited; it is now.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
New Domains page listing when each domain registration expires, read from
the registry. Domains behind the DNS zones already synced are picked up
automatically; others can be added by hand. You're reminded daily from N
days before expiry (Settings > Notifications, default 30) until it's
renewed, and told when an expiry date hasn't been refreshable for several
days so a stale date isn't trusted silently.
RDAP alone would not have covered this homelab: .se, .nu, .io, .eu and .de
are not in IANA's RDAP bootstrap. Lookups therefore try RDAP where the TLD
publishes a server and fall back to WHOIS on port 43, found via IANA's
own referral, parsing the expiry line out of the free-text answer. Only
the expiry date and registrar are read or stored. Verified live against
the real registries: .se and .nu via WHOIS, .com/.org/.dev via RDAP.
Behaviour worth knowing:
- A DNS zone that is a subdomain (lab.example.se) resolves to the
registration that actually expires by trying the name and then its
parents, so no public-suffix list is needed. Zones already covered by a
tracked domain are not looked up again.
- "Couldn't ask" is never confused with "not registered": network errors,
rate limits and garbled answers are errors, and a transient error at any
level stops the walk from concluding the domain doesn't exist.
- A failed refresh keeps the last known expiry and records why, rather
than blanking a date that's still relied on.
- Zones that don't resolve to a real registration (.lan, .local, unregistered
names) simply get no row. Zone-derived rows disappear when their zone
does; manual rows stay. Zone-derived rows can't be deleted by hand.
- Registries that don't publish an expiry (.de, .eu) are tracked with a
note instead of a date.
- Input like "example.com/path" is refused rather than silently reduced
to its host.
- Runs on the daily secret-expiry schedule and reminder time, on demand
(Check all now / per domain), and once at startup if nothing has been
read in a day. Never blocks startup, one lookup at a time with a pause.
The warning window lives with the other thresholds in settings.
New table domains (migration 0011); two settings fields (toggle and
warning days).
Verified with 90 checks against fake RDAP/WHOIS backends (name
normalization, date formats, WHOIS parsing including rate-limit and
no-expiry answers, bootstrap and referral caching, stale-cache fallback,
parent walking, add/sync/check/refresh, concurrency guard, alert
selection and stale detection, the daily notification and its toggle,
role rules) plus a live smoke test against real registries and a browser
check of the page against the real router. Real dev database mtime
untouched. Not checked: a screenshot of the finished page (the capture
timed out); structure, sorting, errors and the viewer view were verified.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>