web-agent-bridge 3.9.1 → 3.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,93 +1,93 @@
1
- {
2
- "name": "web-agent-bridge",
3
- "version": "3.9.1",
4
- "description": "Agent Transaction Bridge — the trust + transaction layer for agentic commerce. Signed intent contracts, idempotent transactions, Ed25519-verifiable receipts, explicit compensation. Plus the original WAB stack: sovereign browser, ShieldQR, SSL health, DNS discovery, agent mesh, and unified gateway for safe AI–website interaction.",
5
- "author": "Web Agent Bridge <dev@webagentbridge.com>",
6
- "main": "server/index.js",
7
- "bin": {
8
- "web-agent-bridge": "./bin/cli.js",
9
- "wab": "./bin/cli.js",
10
- "wab-agent": "./bin/cli.js",
11
- "wab-init": "./bin/wab-init.js"
12
- },
13
- "scripts": {
14
- "start": "node server/index.js",
15
- "dev": "node server/index.js",
16
- "test": "jest --forceExit --detectOpenHandles",
17
- "build:script": "node scripts/build.js",
18
- "prepublishOnly": "npm test"
19
- },
20
- "keywords": [
21
- "ai",
22
- "agent",
23
- "bridge",
24
- "protocol",
25
- "platform",
26
- "automation",
27
- "web",
28
- "ai-agent",
29
- "agent-mesh",
30
- "sovereign-browser",
31
- "phone-shield",
32
- "dns-discovery",
33
- "api-gateway",
34
- "browser-automation",
35
- "webdriver-bidi"
36
- ],
37
- "repository": {
38
- "type": "git",
39
- "url": "git+https://github.com/abokenan444/web-agent-bridge.git"
40
- },
41
- "homepage": "https://github.com/abokenan444/web-agent-bridge#readme",
42
- "bugs": {
43
- "url": "https://github.com/abokenan444/web-agent-bridge/issues"
44
- },
45
- "files": [
46
- "bin/",
47
- "server/",
48
- "public/*.html",
49
- "public/*.txt",
50
- "public/*.xml",
51
- "public/*.json",
52
- "public/css/",
53
- "public/js/",
54
- "public/script/",
55
- "public/assets/",
56
- "public/.well-known/",
57
- "script/",
58
- "sdk/",
59
- "templates/",
60
- "examples/",
61
- "README.md",
62
- "README.ar.md",
63
- "LICENSE"
64
- ],
65
- "engines": {
66
- "node": ">=18.0.0"
67
- },
68
- "license": "MIT",
69
- "dependencies": {
70
- "bcryptjs": "^3.0.3",
71
- "better-sqlite3": "^11.6.0",
72
- "cors": "^2.8.5",
73
- "dotenv": "^16.4.5",
74
- "express": "^4.21.0",
75
- "express-rate-limit": "^7.4.1",
76
- "helmet": "^8.0.0",
77
- "jsonwebtoken": "^9.0.2",
78
- "nodemailer": "^8.0.7",
79
- "stripe": "^20.4.1",
80
- "ws": "^8.20.0"
81
- },
82
- "devDependencies": {
83
- "all-contributors-cli": "^6.26.1",
84
- "jest": "^30.3.0",
85
- "supertest": "^7.2.2"
86
- },
87
- "jest": {
88
- "testPathIgnorePatterns": [
89
- "/node_modules/",
90
- "/packages/"
91
- ]
92
- }
93
- }
1
+ {
2
+ "name": "web-agent-bridge",
3
+ "version": "3.9.2",
4
+ "description": "Agent Transaction Bridge — the trust + transaction layer for agentic commerce. Signed intent contracts, idempotent transactions, Ed25519-verifiable receipts, explicit compensation. Plus the original WAB stack: sovereign browser, ShieldQR, SSL health, DNS discovery, agent mesh, and unified gateway for safe AI–website interaction.",
5
+ "author": "Web Agent Bridge <dev@webagentbridge.com>",
6
+ "main": "server/index.js",
7
+ "bin": {
8
+ "web-agent-bridge": "./bin/cli.js",
9
+ "wab": "./bin/cli.js",
10
+ "wab-agent": "./bin/cli.js",
11
+ "wab-init": "./bin/wab-init.js"
12
+ },
13
+ "scripts": {
14
+ "start": "node server/index.js",
15
+ "dev": "node server/index.js",
16
+ "test": "jest --forceExit --detectOpenHandles",
17
+ "build:script": "node scripts/build.js",
18
+ "prepublishOnly": "npm test"
19
+ },
20
+ "keywords": [
21
+ "ai",
22
+ "agent",
23
+ "bridge",
24
+ "protocol",
25
+ "platform",
26
+ "automation",
27
+ "web",
28
+ "ai-agent",
29
+ "agent-mesh",
30
+ "sovereign-browser",
31
+ "phone-shield",
32
+ "dns-discovery",
33
+ "api-gateway",
34
+ "browser-automation",
35
+ "webdriver-bidi"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/abokenan444/web-agent-bridge.git"
40
+ },
41
+ "homepage": "https://github.com/abokenan444/web-agent-bridge#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/abokenan444/web-agent-bridge/issues"
44
+ },
45
+ "files": [
46
+ "bin/",
47
+ "server/",
48
+ "public/*.html",
49
+ "public/*.txt",
50
+ "public/*.xml",
51
+ "public/*.json",
52
+ "public/css/",
53
+ "public/js/",
54
+ "public/script/",
55
+ "public/assets/",
56
+ "public/.well-known/",
57
+ "script/",
58
+ "sdk/",
59
+ "templates/",
60
+ "examples/",
61
+ "README.md",
62
+ "README.ar.md",
63
+ "LICENSE"
64
+ ],
65
+ "engines": {
66
+ "node": ">=18.0.0"
67
+ },
68
+ "license": "MIT",
69
+ "dependencies": {
70
+ "bcryptjs": "^3.0.3",
71
+ "better-sqlite3": "^11.6.0",
72
+ "cors": "^2.8.5",
73
+ "dotenv": "^16.4.5",
74
+ "express": "^4.21.0",
75
+ "express-rate-limit": "^7.4.1",
76
+ "helmet": "^8.0.0",
77
+ "jsonwebtoken": "^9.0.2",
78
+ "nodemailer": "^8.0.7",
79
+ "stripe": "^20.4.1",
80
+ "ws": "^8.20.0"
81
+ },
82
+ "devDependencies": {
83
+ "all-contributors-cli": "^6.26.1",
84
+ "jest": "^30.3.0",
85
+ "supertest": "^7.2.2"
86
+ },
87
+ "jest": {
88
+ "testPathIgnorePatterns": [
89
+ "/node_modules/",
90
+ "/packages/"
91
+ ]
92
+ }
93
+ }
package/server/index.js CHANGED
@@ -232,6 +232,15 @@ const licenseLimiter = rateLimit({
232
232
  }
233
233
  });
234
234
 
235
+ // Visitor analytics — record every public page hit (HTML routes only) before
236
+ // they're served by express.static. Skips assets, /api, /admin and other noise.
237
+ try {
238
+ const visitorTracker = require('./services/visitor-tracker');
239
+ app.use(visitorTracker.middleware());
240
+ } catch (e) {
241
+ console.warn('[wab] visitor-tracker disabled:', e.message);
242
+ }
243
+
235
244
  // Whitepaper guard — must run BEFORE express.static so we can apply strict headers
236
245
  // and intercept both /whitepaper and /whitepaper.html with the same protections.
237
246
  const whitepaperHandler = (req, res) => {
@@ -0,0 +1,31 @@
1
+ -- ─────────────────────────────────────────────────────────────────────────────
2
+ -- Migration 021 — Visitor analytics (page_visits)
3
+ --
4
+ -- Captures every public page request (registered or anonymous) so the admin
5
+ -- panel can show real traffic data. IPs are hashed for privacy.
6
+ -- ─────────────────────────────────────────────────────────────────────────────
7
+
8
+ CREATE TABLE IF NOT EXISTS page_visits (
9
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
10
+ path TEXT NOT NULL,
11
+ query_string TEXT,
12
+ referrer TEXT,
13
+ host TEXT,
14
+ user_agent TEXT,
15
+ ip_hash TEXT, -- sha256(ip + salt), first 32 chars
16
+ country TEXT, -- best-effort from Cloudflare/CF-IPCountry; nullable
17
+ device TEXT, -- desktop | mobile | tablet | bot
18
+ is_bot INTEGER NOT NULL DEFAULT 0,
19
+ session_id TEXT, -- random per-visitor cookie or derived from ip_hash+UA
20
+ user_id TEXT, -- nullable; populated if request carried an auth cookie/token
21
+ status_code INTEGER, -- HTTP status that was returned
22
+ duration_ms INTEGER, -- server-side handler time
23
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
24
+ );
25
+
26
+ CREATE INDEX IF NOT EXISTS idx_pv_created ON page_visits(created_at);
27
+ CREATE INDEX IF NOT EXISTS idx_pv_path ON page_visits(path);
28
+ CREATE INDEX IF NOT EXISTS idx_pv_ip ON page_visits(ip_hash);
29
+ CREATE INDEX IF NOT EXISTS idx_pv_session ON page_visits(session_id);
30
+ CREATE INDEX IF NOT EXISTS idx_pv_is_bot ON page_visits(is_bot);
31
+ CREATE INDEX IF NOT EXISTS idx_pv_user ON page_visits(user_id);
@@ -23,6 +23,7 @@ const {
23
23
  } = require('../models/db');
24
24
  const { sendEmail } = require('../services/email');
25
25
  const { createCheckoutSession, createPortalSession, isStripeConfigured, getStripePrices } = require('../services/stripe');
26
+ const visitorTracker = require('../services/visitor-tracker');
26
27
 
27
28
  // ─── Auth ──────────────────────────────────────────────────────────────
28
29
 
@@ -60,6 +61,35 @@ router.get('/me', authenticateAdmin, (req, res) => {
60
61
  router.get('/stats', authenticateAdmin, (req, res) => {
61
62
  const stats = getAdminStats();
62
63
  stats.stripeConfigured = isStripeConfigured();
64
+
65
+ // Expose normalized keys that the overview dashboard expects, while keeping
66
+ // the legacy keys for any older clients still reading them.
67
+ stats.users = stats.totalUsers;
68
+ stats.sites = stats.totalSites;
69
+ stats.analytics_total = stats.totalAnalytics;
70
+ stats.analytics_today = stats.todayAnalytics;
71
+ stats.revenue30d = (() => {
72
+ try {
73
+ const r = db.prepare(
74
+ `SELECT COALESCE(SUM(amount),0) AS t FROM payments WHERE status='succeeded' AND created_at >= datetime('now','-30 days')`
75
+ ).get();
76
+ return r ? r.t : 0;
77
+ } catch { return 0; }
78
+ })();
79
+ stats.active_subscriptions = (() => {
80
+ try {
81
+ const r = db.prepare(
82
+ `SELECT COUNT(*) AS c FROM stripe_subscriptions WHERE status='active'`
83
+ ).get();
84
+ return r ? r.c : 0;
85
+ } catch { return 0; }
86
+ })();
87
+
88
+ // Visitor counts (registered + anonymous).
89
+ try {
90
+ Object.assign(stats, visitorTracker.getQuickCounts());
91
+ } catch { /* table may not exist on first boot before migration */ }
92
+
63
93
  res.json(stats);
64
94
  });
65
95
 
@@ -69,6 +99,26 @@ router.get('/analytics', authenticateAdmin, (req, res) => {
69
99
  res.json(data);
70
100
  });
71
101
 
102
+ // ─── Visitor analytics (page hits, registered + anonymous) ────────────
103
+
104
+ router.get('/analytics/visits', authenticateAdmin, (req, res) => {
105
+ try {
106
+ const data = visitorTracker.getVisitorAnalytics(req.query.days);
107
+ res.json({ ok: true, ...data });
108
+ } catch (e) {
109
+ res.status(500).json({ ok: false, error: e.message });
110
+ }
111
+ });
112
+
113
+ router.get('/analytics/visits/recent', authenticateAdmin, (req, res) => {
114
+ try {
115
+ const visits = visitorTracker.getRecentVisits(req.query.limit);
116
+ res.json({ ok: true, visits });
117
+ } catch (e) {
118
+ res.status(500).json({ ok: false, error: e.message });
119
+ }
120
+ });
121
+
72
122
  // ─── Users Management ─────────────────────────────────────────────────
73
123
 
74
124
  router.get('/users', authenticateAdmin, (req, res) => {
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Visitor tracking — records every public HTML page hit into `page_visits`.
3
+ * Anonymous-friendly: IPs are hashed (sha256 + IP_HASH_SALT) so we never store raw IPs.
4
+ *
5
+ * Exposes:
6
+ * - middleware() — Express middleware to mount before express.static
7
+ * - getVisitorAnalytics(days) — totals + timeline + breakdowns for /api/admin/analytics/visits
8
+ * - getRecentVisits(limit) — latest individual page hits for the admin live feed
9
+ * - getQuickCounts() — visits_24h / visitors_24h / visits_30d for /api/admin/stats
10
+ */
11
+
12
+ const crypto = require('crypto');
13
+ const { db } = require('../models/db');
14
+
15
+ const IP_SALT = process.env.IP_HASH_SALT || process.env.JWT_SECRET || 'wab-visitor-salt-v1';
16
+
17
+ // ── Bot detection ────────────────────────────────────────────────────
18
+ const BOT_RE = /(bot|crawler|spider|crawl|slurp|bingpreview|facebookexternalhit|preview|monitor|uptime|curl|wget|axios|node-fetch|python-requests|java\/|ahrefs|semrush|petalbot|yandex|baiduspider|duckduckbot|googlebot|applebot|gpt|claude|anthropic|openai|perplexity)/i;
19
+
20
+ // ── Path filter ──────────────────────────────────────────────────────
21
+ // We track real page requests, not asset/API noise.
22
+ const SKIP_PREFIX = ['/api/', '/css/', '/js/', '/assets/', '/script/', '/v3/', '/v2/', '/v1/', '/latest/', '/.well-known/', '/admin/', '/socket.io', '/favicon', '/sitemap', '/robots.txt', '/feed.xml', '/downloads/'];
23
+ const SKIP_EXT = /\.(?:js|mjs|css|map|png|jpe?g|gif|svg|webp|ico|woff2?|ttf|otf|eot|mp4|webm|mp3|pdf|xml|txt|wasm)$/i;
24
+
25
+ function shouldTrack(req) {
26
+ if (req.method !== 'GET' && req.method !== 'HEAD') return false;
27
+ const p = req.path || '/';
28
+ if (SKIP_EXT.test(p)) return false;
29
+ for (const pre of SKIP_PREFIX) if (p.startsWith(pre)) return false;
30
+ return true;
31
+ }
32
+
33
+ function hashIp(ip) {
34
+ if (!ip) return null;
35
+ return crypto.createHash('sha256').update(IP_SALT + ':' + ip).digest('hex').slice(0, 32);
36
+ }
37
+
38
+ function detectDevice(ua) {
39
+ if (!ua) return 'unknown';
40
+ if (BOT_RE.test(ua)) return 'bot';
41
+ if (/iPad|Tablet|PlayBook|Silk/i.test(ua)) return 'tablet';
42
+ if (/Mobi|Android|iPhone|iPod|Opera Mini|IEMobile/i.test(ua)) return 'mobile';
43
+ return 'desktop';
44
+ }
45
+
46
+ function extractUserId(req) {
47
+ // Best-effort: req.user (set by upstream auth middleware) wins; otherwise null.
48
+ if (req.user && req.user.id) return String(req.user.id);
49
+ if (req.session && req.session.userId) return String(req.session.userId);
50
+ return null;
51
+ }
52
+
53
+ // Lazily prepared so the require() of this module can run before migrations
54
+ // have created the page_visits table (e.g. in tests or during cold boot).
55
+ let _insertVisit = null;
56
+ function getInsertStmt() {
57
+ if (_insertVisit) return _insertVisit;
58
+ _insertVisit = db.prepare(`
59
+ INSERT INTO page_visits
60
+ (path, query_string, referrer, host, user_agent, ip_hash, country, device, is_bot, session_id, user_id, status_code, duration_ms)
61
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
62
+ `);
63
+ return _insertVisit;
64
+ }
65
+
66
+ function middleware() {
67
+ return function visitTracker(req, res, next) {
68
+ if (!shouldTrack(req)) return next();
69
+ const start = Date.now();
70
+ res.on('finish', () => {
71
+ try {
72
+ const ua = req.get('user-agent') || null;
73
+ const ref = req.get('referer') || req.get('referrer') || null;
74
+ const ipHash = hashIp(req.ip);
75
+ const device = detectDevice(ua);
76
+ const isBot = device === 'bot' ? 1 : 0;
77
+ const country = req.get('cf-ipcountry') || req.get('x-vercel-ip-country') || null;
78
+ const session = ipHash && ua
79
+ ? crypto.createHash('sha256').update(ipHash + ':' + ua).digest('hex').slice(0, 24)
80
+ : null;
81
+ const qs = req.url.includes('?') ? req.url.slice(req.url.indexOf('?') + 1, req.url.indexOf('?') + 257) : null;
82
+ getInsertStmt().run(
83
+ req.path.slice(0, 512),
84
+ qs,
85
+ ref ? ref.slice(0, 512) : null,
86
+ (req.get('host') || '').slice(0, 255),
87
+ ua ? ua.slice(0, 512) : null,
88
+ ipHash,
89
+ country ? country.slice(0, 4) : null,
90
+ device,
91
+ isBot,
92
+ session,
93
+ extractUserId(req),
94
+ res.statusCode,
95
+ Date.now() - start
96
+ );
97
+ } catch (e) {
98
+ if (process.env.NODE_ENV !== 'production') {
99
+ console.warn('[visit-tracker] insert failed:', e.message);
100
+ }
101
+ }
102
+ });
103
+ next();
104
+ };
105
+ }
106
+
107
+ // ── Read queries ────────────────────────────────────────────────────
108
+ function getVisitorAnalytics(days) {
109
+ const n = Math.max(1, Math.min(365, parseInt(days, 10) || 30));
110
+ const since = new Date(Date.now() - n * 86400000).toISOString();
111
+
112
+ const totalsRow = db.prepare(`
113
+ SELECT
114
+ COUNT(*) AS pageviews,
115
+ COUNT(DISTINCT session_id) AS visitors,
116
+ COALESCE(SUM(CASE WHEN is_bot=1 THEN 1 ELSE 0 END), 0) AS bot_hits,
117
+ COALESCE(SUM(CASE WHEN user_id IS NOT NULL THEN 1 ELSE 0 END), 0) AS authenticated_hits,
118
+ COUNT(DISTINCT user_id) AS authenticated_users
119
+ FROM page_visits WHERE created_at >= ?
120
+ `).get(since);
121
+
122
+ const last24Row = db.prepare(`
123
+ SELECT COUNT(*) AS pageviews, COUNT(DISTINCT session_id) AS visitors
124
+ FROM page_visits WHERE created_at >= datetime('now','-1 day')
125
+ `).get();
126
+
127
+ const todayRow = db.prepare(`
128
+ SELECT COUNT(*) AS pageviews, COUNT(DISTINCT session_id) AS visitors
129
+ FROM page_visits WHERE date(created_at) = date('now')
130
+ `).get();
131
+
132
+ const timeline = db.prepare(`
133
+ SELECT date(created_at) AS day,
134
+ COUNT(*) AS pageviews,
135
+ COUNT(DISTINCT session_id) AS visitors,
136
+ SUM(CASE WHEN is_bot=1 THEN 1 ELSE 0 END) AS bots
137
+ FROM page_visits WHERE created_at >= ?
138
+ GROUP BY day ORDER BY day
139
+ `).all(since);
140
+
141
+ const topPaths = db.prepare(`
142
+ SELECT path, COUNT(*) AS pageviews, COUNT(DISTINCT session_id) AS visitors
143
+ FROM page_visits WHERE created_at >= ? AND is_bot = 0
144
+ GROUP BY path ORDER BY pageviews DESC LIMIT 25
145
+ `).all(since);
146
+
147
+ const topReferrers = db.prepare(`
148
+ SELECT
149
+ COALESCE(NULLIF(substr(referrer, 1, instr(substr(referrer, 9), '/') + 7), ''), 'Direct') AS source,
150
+ COUNT(*) AS hits
151
+ FROM page_visits WHERE created_at >= ? AND is_bot = 0
152
+ GROUP BY source ORDER BY hits DESC LIMIT 15
153
+ `).all(since);
154
+
155
+ const devices = db.prepare(`
156
+ SELECT device, COUNT(*) AS hits
157
+ FROM page_visits WHERE created_at >= ?
158
+ GROUP BY device ORDER BY hits DESC
159
+ `).all(since);
160
+
161
+ const countries = db.prepare(`
162
+ SELECT COALESCE(country, 'Unknown') AS country, COUNT(*) AS hits
163
+ FROM page_visits WHERE created_at >= ? AND is_bot = 0
164
+ GROUP BY country ORDER BY hits DESC LIMIT 20
165
+ `).all(since);
166
+
167
+ const topBots = db.prepare(`
168
+ SELECT
169
+ CASE
170
+ WHEN user_agent LIKE '%Googlebot%' THEN 'Googlebot'
171
+ WHEN user_agent LIKE '%bingbot%' OR user_agent LIKE '%Bingbot%' THEN 'Bingbot'
172
+ WHEN user_agent LIKE '%DuckDuckBot%' THEN 'DuckDuckBot'
173
+ WHEN user_agent LIKE '%AhrefsBot%' THEN 'AhrefsBot'
174
+ WHEN user_agent LIKE '%SemrushBot%' THEN 'SemrushBot'
175
+ WHEN user_agent LIKE '%YandexBot%' THEN 'YandexBot'
176
+ WHEN user_agent LIKE '%Baiduspider%' THEN 'Baiduspider'
177
+ WHEN user_agent LIKE '%facebookexternalhit%' THEN 'Facebook'
178
+ WHEN user_agent LIKE '%GPTBot%' OR user_agent LIKE '%ChatGPT%' THEN 'GPTBot'
179
+ WHEN user_agent LIKE '%anthropic%' OR user_agent LIKE '%Claude%' THEN 'Claude / Anthropic'
180
+ WHEN user_agent LIKE '%PerplexityBot%' THEN 'Perplexity'
181
+ WHEN user_agent LIKE '%Applebot%' THEN 'Applebot'
182
+ ELSE 'Other bot'
183
+ END AS bot,
184
+ COUNT(*) AS hits
185
+ FROM page_visits WHERE created_at >= ? AND is_bot = 1
186
+ GROUP BY bot ORDER BY hits DESC LIMIT 15
187
+ `).all(since);
188
+
189
+ const signups = db.prepare(`
190
+ SELECT date(created_at) AS day, COUNT(*) AS count
191
+ FROM users WHERE created_at >= ? GROUP BY day ORDER BY day
192
+ `).all(since);
193
+
194
+ return {
195
+ period_days: n,
196
+ totals: {
197
+ pageviews: totalsRow.pageviews || 0,
198
+ visitors: totalsRow.visitors || 0,
199
+ bot_hits: totalsRow.bot_hits || 0,
200
+ human_hits: (totalsRow.pageviews || 0) - (totalsRow.bot_hits || 0),
201
+ authenticated_hits: totalsRow.authenticated_hits || 0,
202
+ authenticated_users: totalsRow.authenticated_users || 0,
203
+ pageviews_24h: last24Row.pageviews || 0,
204
+ visitors_24h: last24Row.visitors || 0,
205
+ pageviews_today: todayRow.pageviews || 0,
206
+ visitors_today: todayRow.visitors || 0,
207
+ },
208
+ timeline,
209
+ topPaths,
210
+ topReferrers,
211
+ devices,
212
+ countries,
213
+ topBots,
214
+ signups,
215
+ };
216
+ }
217
+
218
+ function getRecentVisits(limit) {
219
+ const n = Math.max(1, Math.min(500, parseInt(limit, 10) || 50));
220
+ return db.prepare(`
221
+ SELECT id, path, referrer, host, user_agent, country, device, is_bot, session_id, user_id, status_code, duration_ms, created_at
222
+ FROM page_visits ORDER BY id DESC LIMIT ?
223
+ `).all(n);
224
+ }
225
+
226
+ function getQuickCounts() {
227
+ const row24 = db.prepare(`
228
+ SELECT COUNT(*) AS pageviews, COUNT(DISTINCT session_id) AS visitors
229
+ FROM page_visits WHERE created_at >= datetime('now','-1 day')
230
+ `).get();
231
+ const row30 = db.prepare(`
232
+ SELECT COUNT(*) AS pageviews, COUNT(DISTINCT session_id) AS visitors
233
+ FROM page_visits WHERE created_at >= datetime('now','-30 days')
234
+ `).get();
235
+ const total = db.prepare(`SELECT COUNT(*) AS c FROM page_visits`).get();
236
+ return {
237
+ pageviews_24h: row24.pageviews || 0,
238
+ visitors_24h: row24.visitors || 0,
239
+ pageviews_30d: row30.pageviews || 0,
240
+ visitors_30d: row30.visitors || 0,
241
+ pageviews_total: total.c || 0,
242
+ };
243
+ }
244
+
245
+ module.exports = {
246
+ middleware,
247
+ getVisitorAnalytics,
248
+ getRecentVisits,
249
+ getQuickCounts,
250
+ };