SQLite was too slow for analytical aggregations on traffic_events and waf_events (millions of rows, GROUP BY, COUNT DISTINCT). ClickHouse is a columnar OLAP database purpose-built for this workload. - Add ClickHouse container to Docker Compose with health check - Create src/lib/clickhouse/client.ts with singleton client, table DDL, insert helpers, and all analytics query functions - Update log-parser.ts and waf-log-parser.ts to write to ClickHouse - Remove purgeOldEntries — ClickHouse TTL handles 90-day retention - Rewrite analytics-db.ts and waf-events.ts to query ClickHouse - Remove trafficEvents/wafEvents from SQLite schema, add migration - CLICKHOUSE_PASSWORD is required (no hardcoded default) - Update .env.example, README, and test infrastructure API response shapes are unchanged — no frontend modifications needed. Parse state (file offsets) remains in SQLite. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
86 lines
3.0 KiB
TypeScript
86 lines
3.0 KiB
TypeScript
import { chromium } from '@playwright/test';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { mkdirSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
|
|
const COMPOSE_ARGS = [
|
|
'compose',
|
|
'-f', 'docker-compose.yml',
|
|
'-f', 'tests/docker-compose.test.yml',
|
|
];
|
|
const HEALTH_URL = 'http://localhost:3000/api/health';
|
|
export const AUTH_DIR = resolve(__dirname, '.auth');
|
|
export const AUTH_FILE = resolve(AUTH_DIR, 'admin.json');
|
|
const MAX_WAIT_MS = 180_000;
|
|
const POLL_INTERVAL_MS = 3_000;
|
|
|
|
async function waitForHealth(): Promise<void> {
|
|
const start = Date.now();
|
|
let attempt = 0;
|
|
while (Date.now() - start < MAX_WAIT_MS) {
|
|
attempt++;
|
|
try {
|
|
const res = await fetch(HEALTH_URL);
|
|
if (res.status === 200) {
|
|
console.log(`[global-setup] App is healthy (attempt ${attempt})`);
|
|
return;
|
|
}
|
|
console.log(`[global-setup] Health check attempt ${attempt}: HTTP ${res.status}, retrying...`);
|
|
} catch (err: unknown) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
console.log(`[global-setup] Health check attempt ${attempt}: ${msg}, retrying in ${POLL_INTERVAL_MS / 1000}s...`);
|
|
}
|
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
}
|
|
|
|
console.error('[global-setup] Health check timed out. Container logs:');
|
|
try {
|
|
execFileSync('docker', [...COMPOSE_ARGS, 'logs', '--tail=50'], { stdio: 'inherit', cwd: process.cwd(), env: { ...process.env, CLICKHOUSE_PASSWORD: 'test-clickhouse-password-2026' } });
|
|
} catch { /* ignore */ }
|
|
|
|
throw new Error(`App did not become healthy within ${MAX_WAIT_MS}ms`);
|
|
}
|
|
|
|
async function seedAuthState(): Promise<void> {
|
|
console.log('[global-setup] Seeding auth state via browser login...');
|
|
mkdirSync(AUTH_DIR, { recursive: true });
|
|
|
|
const browser = await chromium.launch();
|
|
const page = await browser.newPage();
|
|
|
|
try {
|
|
await page.goto('http://localhost:3000/login');
|
|
await page.getByRole('textbox', { name: /username/i }).fill('testadmin');
|
|
await page.getByRole('textbox', { name: /password/i }).fill('TestPassword2026!');
|
|
await page.getByRole('button', { name: /sign in/i }).click();
|
|
|
|
// Wait for redirect away from /login
|
|
await page.waitForURL((url) => !url.pathname.includes('/login'), { timeout: 15_000 });
|
|
console.log(`[global-setup] Login succeeded, landed on: ${page.url()}`);
|
|
|
|
await page.context().storageState({ path: AUTH_FILE });
|
|
console.log('[global-setup] Auth state saved to', AUTH_FILE);
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
}
|
|
|
|
export default async function globalSetup() {
|
|
console.log('[global-setup] Starting Docker Compose test stack...');
|
|
execFileSync('docker', [
|
|
...COMPOSE_ARGS,
|
|
'up', '-d', '--build',
|
|
'--wait', '--wait-timeout', '120',
|
|
], {
|
|
stdio: 'inherit',
|
|
cwd: process.cwd(),
|
|
env: { ...process.env, CLICKHOUSE_PASSWORD: 'test-clickhouse-password-2026' },
|
|
});
|
|
|
|
console.log('[global-setup] Containers up. Waiting for /api/health...');
|
|
await waitForHealth();
|
|
await seedAuthState();
|
|
|
|
console.log('[global-setup] Done.');
|
|
}
|