The dashboard overview counted all proxy hosts without a cert as ACME
certs, ignoring wildcard deduplication. The certificates page only
deduplicated the current page of results (25 rows) but used a full
count(*) for the total, so hosts on other pages covered by wildcards
were never subtracted.
Now both pages fetch all ACME hosts, apply full deduplication (cert
wildcard coverage + ACME wildcard collapsing), then paginate/count
from the deduplicated result.
Also fixes a strict-mode violation in the E2E test where DataTable's
dual mobile/desktop rendering caused getByText to match two elements.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Traffic (24h) card and Recent Activity section were visible to
user/viewer roles even though they received empty data. Now both
sections are conditionally rendered only for admin users.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Allow non-admin users (user/viewer) to access / and /profile while
blocking admin-only pages. The dashboard layout now uses requireUser()
instead of requireAdmin(), and the sidebar filters nav items by role.
Non-admin users see a minimal welcome page without stat cards.
New test files (86 tests across 7 files):
- dashboard, users, groups, api-docs, portal, link-account specs
- role-access spec with full RBAC coverage for all 3 roles
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove gradient heading in OverviewClient (violet-to-cyan gradient replaced with plain bold text)
- Remove hardcoded dark navy backgrounds from activity items (replaced with Card + Separator list)
- Stat cards now use shadcn CardHeader/CardContent pattern with small icons + big number
- Sidebar logo: replace violet text with violet pill icon + plain foreground text
- Active nav item: use bg-primary/10 text-primary (subtle violet tint) instead of bg-accent
- Move theme toggle + sign out into sidebar footer row (no more floating top-right toggle)
- Mobile header brand name: remove text-primary, use plain font-semibold
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces all MUI components in 8 dashboard page files with shadcn/ui
and Tailwind. Adds global TooltipProvider to app/providers.tsx.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- accept wildcard proxy host domains like *.example.com with validation and normalization
- make exact hosts win over overlapping wildcards in generated routes and TLS policies
- add unit coverage for host-pattern priority and wildcard domain handling
- add a single test:all entry point and clean up lint/typecheck issues so the suite runs cleanly
- run mobile layout Playwright checks under both chromium and mobile-iphone
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>
- 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>
- 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>