- Remove disruptive alert() on error pages, rely on role="alert" banner
- Add skip navigation link for keyboard users (WCAG 2.4.1)
- Fix heading hierarchy on error page (sr-only status text in h1)
- Add "opens in a new tab" screen reader hint on external links
- Wrap language selector in form with noscript submit button (WCAG 3.2.2)
- Add aria-label to copy-to-clipboard buttons and footer landmarks
- Use h2 for alt-IP section to maintain single h1 per page
- Add persistent underline on links for non-color identification (WCAG 1.4.1)
- Fix footer attribution contrast ratio to ~5.5:1 (WCAG 1.4.3)
- Add aria-busy/role=status loading announcements for skeleton state
- Add translations for new accessibility strings across all 14 locales
- Update integration tests to assert role="alert" instead of alert()
Rewrite script.js from ES5 (IIFEs, var, .then() chains) to modern ES2022+:
const/let, arrow functions, template literals, async/await, optional
chaining, for...of, .includes(), AbortSignal.timeout(), and DOM APIs
for dynamic HTML construction. Fix unhandled clipboard rejection, abort
timeout leak, and global scope pollution. Update script tag to
type="module" and fix CSS: top -> inset-block-start, flex-start -> start,
and indentation in media query block.
Replace all {{ t_field_name }} template variables with {{ "msgid"|t(locale) }}
filter calls across all 6 template files. This works with the #[askama::filter_fn]
defined in handlers/html.rs, which calls i18n::translate() directly.
Templates updated: index.html, error_alert.html, error_page.html,
_footer.html, _footer_lang.html, _search_form.html
Split the 865-line models.rs into 3 focused files:
- models/mod.rs: conversion logic, helpers, re-exports, tests
- models/mmdb.rs: MMDB deserialization types (internal, pub(crate))
- models/api.rs: API serialization types (public) with From<MmdbProxy> impl
Mmdb types are now pub(crate) since they are internal deserialization details
that should not be part of the public API surface.
Replace DB_UPDATE_INTERVAL_HOURS with DB_UPDATE_TIME_UTC (HH:MM format,
default 01:20) so the database update runs at a predictable time each day.
This ensures the update happens 20 minutes after the upstream Merged IP
Data release at 01:00 UTC.
The JS used fragile numeric indices to look up localized labels from the
server-rendered table. When optional rows (ASN, Org, Country, City) were
missing for the primary IP, the indices shifted, causing 'Proxy' to appear
as the City label. Add data-label attributes to each <tr> in the template
and key the label map by name instead of position.
Show a shimmer skeleton placeholder while fetching the local IPv4
address via the IPv4-only domain. When the primary IP is already IPv4,
no loading state is displayed and the alt section fades in directly.
The detectAltIp() function was unconditionally fetching the viewer's own
IPv6/IPv4 address and displaying it alongside queried IP results. Pass
the is_query flag to the client via a data attribute and guard the
alt-IP detection so it only runs when viewing your own IP.
Docker file-level bind mounts track inodes, not paths. Ansible template
writes create new inodes, leaving the container reading orphaned files.
Replacing SIGHUP with force-recreate ensures fresh mounts every time,
eliminating the admin socket dependency entirely.