Previously only counted rows in the certificates table (0 for most setups).
Now counts: proxy hosts with certificate_id=NULL (each gets one Caddy ACME cert
via HTTP-01/DNS-01) + imported certs with actual PEM data.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SwapHorizIcon→Proxy Hosts, VpnKeyIcon→Access Lists, SecurityIcon→Certificates,
HistoryIcon→Audit Log. Nav now uses the exact same icons as the overview stat
cards so identical sections share identical icons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DnsIcon→RouterIcon, SecurityIcon→LockIcon, ShieldIcon→WorkspacePremiumIcon,
HistoryIcon→AssignmentIcon — all now match the visual weight of Dashboard,
BarChart, and Settings icons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace DeleteOutline→Delete, CheckCircleOutline→CheckCircle, ErrorOutline→Error,
RemoveCircleOutline→RemoveCircle, InfoOutlined→Info across all dashboard components.
Replace custom SVG bar chart in OverviewClient with BarChartIcon.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Revert 2-column squircle grid back to original list layout —
same space usage as before, just with squircle-style border radius
on each ListItemButton instead of sharp rectangle edges.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace full-width ListItemButton rows with a 2-column grid of
square tiles (borderRadius: 22%, aspectRatio: 1) containing a
centred icon and small label. Active tile uses primary.main fill.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace minWidth/maxWidth with a fixed width:260 + flexShrink:0 so the
Autocomplete never resizes the header row. Chip labels also truncate
with ellipsis instead of wrapping.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
renderTags now shows individual chips for 1-2 selections, and a single
'N hosts' chip (with clear-all × button) for 3+. Prevents the input
from growing vertically when many hosts are selected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PaperComponent header with "Select all" / "Clear" buttons; onMouseDown
preventDefault keeps the popup open when clicking them
- ListboxProps overscrollBehavior: contain prevents the dropdown list's
scroll from propagating to the page when reaching top/bottom
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Only show configured domain names, not raw IPs (e.g. 127.0.0.1:80,
46.225.8.152) that appear in traffic events from direct access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace native datetime-local inputs with @mui/x-date-pickers DateTimePicker
(proper dark-themed calendar popover with time picker, DD/MM/YYYY HH:mm format,
min/max constraints between pickers, 24h clock)
- Replace single-host Select with Autocomplete (multiple, disableCloseOnSelect):
checkbox per option, chip display with limitTags=2, built-in search/filter
- getAnalyticsHosts() now unions traffic event hosts WITH all configured proxy host
domains (parsed from proxyHosts.domains JSON), so every proxy appears in the list
- analytics-db: buildWhere accepts hosts: string[] (empty = all); uses inArray for
multi-host filtering via drizzle-orm
- All 6 API routes updated: accept hosts param (comma-separated) instead of host
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Analytics default interval changed to 1h
- Add 'Custom' toggle option with datetime-local pickers (pre-filled to last 24h)
- Refactor analytics-db: buildWhere now takes from/to unix timestamps instead of Interval
- Export INTERVAL_SECONDS from analytics-db for route reuse
- All 6 API routes accept from/to params (fallback to interval if absent)
- Timeline bucket size computed from duration rather than hardcoded per interval
- Fix map country click highlight: bake isSelected into GeoJSON features (data-driven)
instead of relying on Layer filter prop updates (unreliable in react-map-gl v8)
- Split highlight into countries-selected (data-driven) and countries-hover (filter-driven)
- Show tooltip at country centroid when selected via table, hover takes precedence
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use Popup component for map-anchored tooltips (moves with lat/lng)
- Use filter+highlight layer for hover instead of feature state
- fitBounds to [-168,-56,168,74] to trim polar dead zones
- Country no-data color #1e293b vs ocean #0a1628 — clear contrast
- Visible borders rgba(148,163,184,0.18)
- Add 1h and 12h interval buttons to analytics UI
- formatTs handles 1h/12h with time-only format
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace D3/SVG choropleth with react-map-gl MapGL component
- Use Natural Earth projection for proper world view
- Embed traffic data (norm, total, blocked, alpha2) as GeoJSON properties
- Use feature state only for hover highlighting
- Add 1h and 12h interval options to analytics
- Add worker-src blob: to CSP for MapLibre web workers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dark ocean background (#0b1628) with country borders
- Three-stop color gradient (dark blue → blue → sky) with power curve
distribution so low-traffic countries are still visible
- Hover highlights country in sky-300 with smooth CSS transition
- Floating tooltip on hover: flag emoji, country name, requests, blocked
- Color legend (Low → High) below the map
- Country names lookup table for all ~180 countries
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>