Commit b71d2a8
* feat(models): add RequestLog model and AutoMigrate registration
Adds RequestLog struct to record proxied HTTP requests for the enhanced
dashboard statistics feature (issue #25). Includes BeforeCreate hook for
UUID generation, compound (host_id, timestamp) indexes, and GDPR-safe
pseudonymised client IP hashing. Registers model in AutoMigrate.
* feat(services): add StatsIngester for log fan-out and batch DB writes
- Add stats_types.go with StatsPushData, HostStat, StatusStat,
StatsPushMessage, and BroadcastHub interface (avoids import cycles)
- Add StatsIngester: channel buffer=1000, batch flush at 100 entries
or 500ms interval via CreateInBatches; atomic dropped-count tracking
- Hash client IPs with SHA-256 (first 16 bytes, hex) for GDPR safety
- Add LogWatcher.RegisterIngester() + fan-out in broadcast()
- TDD: 6 tests covering count-flush, timer-flush, back-pressure,
graceful Stop drain, IP hashing determinism, and fan-out wiring
* feat(services): add StatsService with aggregation queries and TTL cache
Adds StatsService providing GetSummary (30s TTL cache), GetTopHosts,
GetStatusDistribution, GetTrafficVolume, and GetCertExpiry with input
validation allowlists. Extends stats_types.go with StatsSummary,
TrafficBucket, and CertExpiry types.
* feat(frontend/api): add stats API client and TypeScript type definitions
Add typed API functions and interfaces for all 8 stats endpoints (summary,
top-hosts, status-distribution, traffic-volume, cert-expiry, requests, health,
and WebSocket hub) with full Vitest test coverage (33 tests).
* feat(frontend/hooks): add useStats and useStatsWebSocket hooks
Add six TanStack Query hooks (useStatsSummary, useTopHosts,
useStatusDistribution, useTrafficVolume, useCertExpiry, useStatsHealth)
with stable query keys and appropriate polling intervals. Add
useStatsWebSocket hook that tracks live summary updates via the stats
WebSocket and disables REST polling when connected. Full Vitest coverage
for all hooks (22 tests).
Also remove unicorn/no-array-for-each ESLint rule removed in unicorn v66.
* feat(frontend/components): add stats chart and widget components
Add 8 pure presentational components under frontend/src/components/stats/:
- RequestCountWidget: 3-stat card for 24h/7d/30d request counts
- TopHostsChart: horizontal bar chart (recharts BarChart)
- StatusDistributionChart: donut chart with accessible HTML summary list
- TrafficVolumeChart: line chart with KB/MB Y-axis formatting
- CertExpiryList: accessible table with red/amber/green day-based color coding
- ServiceHealthWidget: WebSocket live/offline indicator + dropped-event warning
- PeriodSelector: controlled radio button group for 24h/7d/30d
- BucketSelector: controlled radio button group for 1h/6h/1d
All components are pure (no data fetching), strictly typed with no `any` types,
and keyboard accessible. Includes 58 Vitest unit tests covering loading states,
data rendering, color coding, and interaction callbacks.
* feat(frontend/dashboard): integrate stats sections into Dashboard page
Add a responsive Statistics section to the Dashboard page below the
existing content. Uses useStatsWebSocket for live updates, useState for
period/bucket controls, and the six stats hooks + eight stats components
(RequestCountWidget, ServiceHealthWidget, CertExpiryList,
TrafficVolumeChart, TopHostsChart, StatusDistributionChart,
PeriodSelector, BucketSelector). Layout is mobile-first with single
column on small screens, 2-col on sm/md, 3-col top row on lg.
Adds dashboard.statistics and dashboard.trafficVolume i18n keys to all
five locale files. Expands Dashboard tests from 3 to 12 cases.
* test(e2e): add Playwright tests for enhanced dashboard statistics
- tests/stats.spec.ts: 12 E2E tests covering all 9 required scenarios
(stats heading, period selector, bucket selector, request count widget,
service health widget, cert expiry section, traffic/top-hosts/status
distribution chart containers) plus accessibility radio-count assertion
- backend/internal/api/handlers/stats_api_integration_test.go: adds
TestStatsAPI_CertExpiry_366Days_Returns400 to cover the upper-bound
validation (within_days > 365 returns HTTP 400); simplifies function
signature to remove unused return value
- backend/internal/services/stats_ingester_test.go: adds
TestStatsIngester_RegisterHub and TestStatsIngester_ToRequestLog_InvalidTimestamp
to cover the RegisterHub wiring path and the timestamp parse-error fallback
All 12 E2E tests pass against the running E2E container at :8080.
Backend unit tests pass (88.4% coverage, above 87% minimum).
Frontend tests pass (87.86% statement coverage, above 85% minimum).
GORM scan: 0 CRITICAL/HIGH findings.
* docs: update ARCHITECTURE.md and features.md for stats subsystem
* feat(api): add stats handler and WebSocket hub for dashboard stats
* test: fix patch coverage for stats subsystem
Adds targeted tests to cover all previously uncovered patch lines:
Backend:
- stats_ws_hub_test.go (new): full hub coverage — constructor, non-blocking
broadcast, ctx cancel exit, client broadcast, slow-client drop, client
unregister, StatsWS upgrade-error path, StatsWS nil-hub close
- stats_handler_test.go: error-path 500s for all six handlers, non-integer
within_days → 400, invalid limit param silently ignored
- stats_ingester_test.go: Stop flushes batches > batchSize; Run drains big
batch on ctx cancel (covers batchSize branch in drain loop)
- stats_service_test.go: GetTrafficVolume 6h and 1d buckets; GetSummary DB error
Frontend:
- StatusDistributionChart: extended recharts mock calls Pie label/Tooltip
content; adds 1xx test to cover statusClass "other" return
- TrafficVolumeChart: mock calls YAxis tickFormatter with MB/KB/B values
and Tooltip content to cover formatBytes branches
- TopHostsChart: mock calls Tooltip content including hostname ?? label fallback
- CertExpiryList: adds undefined-data test to cover (data ?? []) branch
- useStatsWebSocket: adds non-stats_update message test for the else branch
* feat(frontend): add widget tooltips, top-hosts color coding, and hide/show controls
Adds ELI5 info tooltips to all 6 dashboard stats widgets, a color-coded
legend for the Top Hosts chart, and a per-widget visibility toggle
persisted in localStorage so users can hide widgets they don't need.
* fix(frontend): add aria-expanded to sidebar accordion buttons
Adding the Dashboard "Customize" button (which also carries
aria-expanded) shifted DOM order and caused the WebKit navigation E2E
test to target it instead of the sidebar, since the sidebar's
collapsible accordion buttons never actually exposed aria-expanded.
Add the missing attribute to the real sidebar toggles and scope the
test to the sidebar so it tests what it claims to.
* fix(api): join proxy_hosts to populate Top Hosts hostname
GetTopHosts only selected host_id and a count, never the hostname, so
every entry in the Top Hosts legend and tooltip rendered with a blank
or duplicate label. With every chart category collapsing to the same
empty string, Recharts merged the bars and hovering any bar showed the
same (highest) data point. Join proxy_hosts by UUID to populate the
real hostname, falling back to host_id if the host has since been
deleted.
* fix(database): run SQLite integrity check in background to avoid blocking startup
PRAGMA quick_check was running synchronously in Connect() and could take
well over a minute on larger databases (observed 93.5s), despite being
intended as a non-blocking, warn-only check.
* fix(database): give startup integrity check its own connection
The previous fix moved PRAGMA quick_check to a goroutine, but the main
pool is capped at one connection (SQLite single-writer constraint), so
the background check still held that connection for the full scan and
blocked AutoMigrate behind it. Open a dedicated connection for the
check so it no longer serializes against the rest of startup.
* fix(api): use proxy host Name instead of domain names for Top Hosts
GetTopHosts joined proxy_hosts.domain_names, showing raw domains in
the legend/tooltip instead of the user-assigned host Name. Switch the
join to proxy_hosts.name to match what users configured.
* fix(database): silence record-not-found noise in GORM query logs
Optional lookups (e.g. caddy.keepalive_idle/keepalive_count settings)
return ErrRecordNotFound when unset, which call sites already handle
via `if err == nil` fallbacks. GORM's default logger still logged
these as errors on every startup. Configure IgnoreRecordNotFoundError
so only real query errors and slow queries are logged.
* fix(api): match Top Hosts by domain instead of ProxyHost UUID
RequestLog.HostID stores the raw Host header seen by the proxy (a
domain), not the ProxyHost UUID, so the previous join on
proxy_hosts.uuid = request_logs.host_id never matched and silently
fell back to showing the domain. Build a domain -> name lookup from
proxy_hosts.domain_names (which can hold several comma-separated
domains per host) and resolve hostnames against that instead.
* feat(stats): add warmup guide and improve empty state UX
- Log successful LogWatcher startup with path info
- Add deployment/warmup guide for stats feature (docs)
- Improve TrafficVolumeChart empty state with helpful messaging
- Add data collection info to tooltip when no data available
* test(TrafficVolumeChart): update empty state text assertions
* fix(stats): replace CSS variable with hex literal in TrafficVolumeChart
SVG presentation attributes cannot resolve CSS custom properties defined
as space-separated RGB triplets (Tailwind v4 token format). Replace
`var(--color-brand-500)` with `LINE_COLOR = '#3b82f6'` so Recharts
renders a visible line stroke.
Add unit tests asserting stroke is a valid hex value and tooltip renders
correctly. Add Playwright step verifying recharts-line-curve path exists
when data is present.
* fix(stats): add LineChart mock and update QA report for feature/stats
* test: fix recharts mock to drop importActual for Vitest ESM compat
* chore(deps): bump go-toml to v2.4.0 and prometheus/common to v0.69.0
---------
Co-authored-by: GitHub Actions <actions@github.com>
1 parent 70af480 commit b71d2a8
58 files changed
Lines changed: 7049 additions & 204 deletions
File tree
- backend
- internal
- api
- handlers
- routes
- database
- models
- services
- docs
- plans
- reports
- frontend
- src
- api
- components
- stats
- __tests__
- hooks
- __tests__
- locales
- de
- en
- es
- fr
- zh
- pages
- __tests__
- tests
- core
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
329 | 329 | | |
330 | 330 | | |
331 | 331 | | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
332 | 366 | | |
333 | 367 | | |
334 | 368 | | |
| |||
373 | 407 | | |
374 | 408 | | |
375 | 409 | | |
| 410 | + | |
376 | 411 | | |
377 | 412 | | |
378 | 413 | | |
| |||
823 | 858 | | |
824 | 859 | | |
825 | 860 | | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
| 871 | + | |
| 872 | + | |
| 873 | + | |
| 874 | + | |
| 875 | + | |
| 876 | + | |
| 877 | + | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
| 883 | + | |
| 884 | + | |
826 | 885 | | |
827 | 886 | | |
828 | 887 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
70 | 70 | | |
71 | 71 | | |
72 | 72 | | |
73 | | - | |
| 73 | + | |
74 | 74 | | |
75 | 75 | | |
76 | | - | |
| 76 | + | |
77 | 77 | | |
78 | 78 | | |
79 | 79 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
118 | 118 | | |
119 | 119 | | |
120 | 120 | | |
121 | | - | |
122 | | - | |
| 121 | + | |
| 122 | + | |
123 | 123 | | |
124 | 124 | | |
125 | 125 | | |
126 | 126 | | |
127 | 127 | | |
128 | 128 | | |
129 | | - | |
130 | | - | |
| 129 | + | |
| 130 | + | |
131 | 131 | | |
132 | 132 | | |
133 | 133 | | |
| |||
Lines changed: 283 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
0 commit comments