Analytics: Verdichtung, Statistik, Heatmap-Daten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6233850bb3
commit
305939fdbd
9 files changed
+569
-2
No files matched your search
+17
-1
@@ -2,6 +2,8 @@ import http from 'node:http';
|
||||
import crypto from 'node:crypto';
|
||||
import { createIngest } from './ingest.js';
|
||||
import { createSalt } from './salt.js';
|
||||
import { createHeat } from './heat.js';
|
||||
import { computeStats } from './stats.js';
|
||||
|
||||
const MAX_ADMIN_BODY = 64 * 1024;
|
||||
const SITE_KEY = /^[a-z0-9]{16,40}$/;
|
||||
@@ -57,6 +59,8 @@ function validateSite(b) {
|
||||
|
||||
function createHandlerServer({ config, db, role, geo, salt, now, onSitesChanged }) {
|
||||
const ingest = role === 'public' ? createIngest({ db, geo, salt: salt || createSalt({ now }), now }) : null;
|
||||
const heat = role === 'public' ? createHeat({ db, config, now }) : null;
|
||||
const clock = now || Date.now;
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
const url = new URL(req.url, 'http://x');
|
||||
@@ -64,6 +68,7 @@ function createHandlerServer({ config, db, role, geo, salt, now, onSitesChanged
|
||||
const method = req.method;
|
||||
|
||||
if (ingest && await ingest(req, res, url.pathname)) return;
|
||||
if (heat && heat(req, res, url)) return;
|
||||
|
||||
if (method === 'GET' && url.pathname === '/a/health') return send(res, 200, { ok: true });
|
||||
|
||||
@@ -80,6 +85,7 @@ function createHandlerServer({ config, db, role, geo, salt, now, onSitesChanged
|
||||
db.prepare('DELETE FROM events_raw WHERE site = ?').run(key);
|
||||
db.prepare('DELETE FROM clicks WHERE site = ?').run(key);
|
||||
db.prepare('DELETE FROM daily WHERE site = ?').run(key);
|
||||
db.prepare('DELETE FROM agg_state WHERE site = ?').run(key);
|
||||
db.exec('COMMIT');
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
@@ -100,6 +106,16 @@ function createHandlerServer({ config, db, role, geo, salt, now, onSitesChanged
|
||||
onSitesChanged?.();
|
||||
return send(res, 200, { ok: true });
|
||||
}
|
||||
|
||||
if (role === 'admin' && method === 'GET' && parts[0] === 'a' && parts[1] === 'admin' && parts[2] === 'stats' && parts.length === 4) {
|
||||
if (!tokenOk(req.headers.authorization, config.adminToken)) return send(res, 401, { error: 'Nicht autorisiert' });
|
||||
const key = parts[3];
|
||||
if (!SITE_KEY.test(key)) return send(res, 400, { error: 'Ungültiger Website-Schlüssel' });
|
||||
const r = url.searchParams.get('range') ?? '30';
|
||||
if (!['7', '30', '90'].includes(r)) return send(res, 400, { error: 'Ungültiger Zeitraum (7, 30 oder 90)' });
|
||||
if (!db.prepare('SELECT 1 FROM sites WHERE siteKey = ?').get(key)) return send(res, 404, { error: 'Website nicht gefunden' });
|
||||
return send(res, 200, computeStats(db, key, Number(r), clock()));
|
||||
}
|
||||
return send(res, 404, { error: 'Nicht gefunden' });
|
||||
} catch (e) {
|
||||
const status = e.status || 500;
|
||||
@@ -115,4 +131,4 @@ function createHandlerServer({ config, db, role, geo, salt, now, onSitesChanged
|
||||
export const createPublicServer = ({ config, db, geo, salt, now }) => createHandlerServer({ config, db, role: 'public', geo, salt, now });
|
||||
// Interner Listener für die Website-Verwaltung.
|
||||
// onSitesChanged: wird nach jeder Änderung an Websites gerufen (public.invalidateSites()).
|
||||
export const createAdminServer = ({ config, db, onSitesChanged }) => createHandlerServer({ config, db, role: 'admin', onSitesChanged });
|
||||
export const createAdminServer = ({ config, db, onSitesChanged, now }) => createHandlerServer({ config, db, role: 'admin', onSitesChanged, now });
|
||||
Reference in new issue
Block a user