Fetching from cdn.jsdelivr.net was blocked by connect-src 'self'.
Copy countries-110m.json from world-atlas npm package into public/geo/
and reference it as /geo/countries-110m.json instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Parse Caddy access logs every 30s into traffic_events SQLite table
- GeoIP country lookup via maxmind (GeoLite2-Country.mmdb)
- 90-day retention with automatic purge
- Analytics page with interval (24h/7d/30d) and per-host filtering:
- Stats cards: total requests, unique IPs, blocked count, block rate
- Requests-over-time area chart (ApexCharts)
- SVG world choropleth map (d3-geo + topojson-client, React 19 compatible)
- Top countries table with flag emojis
- HTTP protocol donut chart
- Top user agents horizontal bar chart
- Recent blocked requests table with pagination
- Traffic (24h) summary card on Overview page linking to analytics
- 7 authenticated API routes under /api/analytics/
- Share caddy-logs volume with web container (read-only)
- group_add caddy GID to web container for log file read access
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Countries:
- Searchable chip grid (flag emoji + name + code) with 249 countries
- Instant search by name or ISO code prefix
- Select matching / Clear matching when search is active
- Select all / Clear all when no search
- Selected-count indicator in toolbar
- Summary strip showing all selected when search is active
- Custom thin scrollbar, 220px viewport
Continents:
- 7 clickable tiles with emoji + name + code
- Select all / Clear all toolbar
- Warning/success color theming per block/allow tab
Both pickers:
- accentColor prop (warning=orange for block, success=green for allow)
- Hidden form input for server compatibility
- Smooth 120ms transitions
Also simplified TagInput to a plain TextField with inline chips
(removes Autocomplete dependency for freeform fields like ASNs/CIDRs/IPs)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The blocker plugin only accepts literal IP/CIDR strings; Caddy's built-in
'private_ranges' shorthand is not understood by third-party modules.
Expand it to the equivalent CIDR list at config-build time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drizzle's better-sqlite3 migrator splits on '--> statement-breakpoint'.
Without it, the entire file is passed to db.prepare() as a single
statement, which better-sqlite3 rejects with 'more than one statement'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CSP script-src 'unsafe-eval' is now dev-only; Next.js HMR needs it in
development but the production standalone build does not
- Remove X-Frame-Options: DENY since frame-ancestors 'none' in CSP supersedes
it in all modern browsers; keeping both creates a maintenance hazard
- Add comment explaining why state check is added alongside PKCE default
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace empty string "" salt with Buffer.alloc(0) for explicit intent
in security-critical HKDF call
- Add console.warn when legacy SHA-256 decryption path is taken so
operators can track when all secrets have been re-encrypted
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Change providerSubjectIdx from index to uniqueIndex in schema.ts to
prevent multiple users sharing the same (provider, subject) pair,
which caused non-deterministic sign-in resolution via findFirst.
- Add migration 0008_unique_provider_subject.sql: DROP the existing
non-unique index and CREATE UNIQUE INDEX in its place.
- Validate INSTANCE_SYNC_MAX_BYTES env var in sync route: fall back to
10 MB default when the value is non-numeric (e.g. 'off') or
non-positive, preventing NaN comparisons that silently disabled the
size limit.
- Return a generic error message to callers on applySyncPayload /
applyCaddyConfig failure instead of leaking the raw error string;
the original message is still stored internally via setSlaveLastSync.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New field from upstream plugin: when the real client IP is
indeterminate (trusted proxy present but no usable XFF entry),
fail_closed=true blocks the request instead of passing it through.
- Add fail_closed to GeoBlockSettings type
- Include in mergeGeoBlockSettings (OR semantics: either global or host enables it)
- Emit fail_closed in buildBlockerHandler (only when true)
- Parse geoblock_fail_closed from form in both settings and proxy-host actions
- Add Checkbox UI in the Advanced accordion of GeoBlockFields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Geo Blocking section to README with rule types and GeoIP setup
- Add Geo Blocking card to landing page (site/index.html)
- Refresh all 4 screenshots from current UI
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Returns whether GeoLite2-Country and GeoLite2-ASN databases are loaded,
used by the UI to show the GeoIP ready indicator in GeoBlockFields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove /api/geoip-status from the middleware public routes allowlist so
unauthenticated requests are rejected before reaching the route handler.
The route handler already has requireUser() for defense-in-depth.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>