feat: add analytics dashboard with traffic monitoring
- 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>
This commit is contained in:
@@ -185,3 +185,30 @@ export const linkingTokens = sqliteTable("linking_tokens", {
|
||||
createdAt: text("created_at").notNull(),
|
||||
expiresAt: text("expires_at").notNull()
|
||||
});
|
||||
|
||||
export const trafficEvents = sqliteTable(
|
||||
'traffic_events',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
ts: integer('ts').notNull(),
|
||||
clientIp: text('client_ip').notNull(),
|
||||
countryCode: text('country_code'),
|
||||
host: text('host').notNull().default(''),
|
||||
method: text('method').notNull().default(''),
|
||||
uri: text('uri').notNull().default(''),
|
||||
status: integer('status').notNull().default(0),
|
||||
proto: text('proto').notNull().default(''),
|
||||
bytesSent: integer('bytes_sent').notNull().default(0),
|
||||
userAgent: text('user_agent').notNull().default(''),
|
||||
isBlocked: integer('is_blocked', { mode: 'boolean' }).notNull().default(false),
|
||||
},
|
||||
(table) => ({
|
||||
tsIdx: index('idx_traffic_events_ts').on(table.ts),
|
||||
hostTsIdx: index('idx_traffic_events_host_ts').on(table.host, table.ts),
|
||||
})
|
||||
);
|
||||
|
||||
export const logParseState = sqliteTable('log_parse_state', {
|
||||
key: text('key').primaryKey(),
|
||||
value: text('value').notNull(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user