feat(weeks4-5): operations endpoints + UI components with Recharts & Leaflet

This commit is contained in:
cawcenter
2025-12-14 22:31:58 -05:00
parent 40a46a791f
commit 4fafb3140e
6 changed files with 753 additions and 0 deletions

78
src/pages/api/god/logs.ts Normal file
View File

@@ -0,0 +1,78 @@
import type { APIRoute } from 'astro';
import { pool } from '@/lib/db';
/**
* Live Log Streaming (SSE)
* Stream database activity logs
*/
function validateGodToken(request: Request): boolean {
const token = request.headers.get('X-God-Token') ||
request.headers.get('Authorization')?.replace('Bearer ', '') ||
new URL(request.url).searchParams.get('token');
const godToken = process.env.GOD_MODE_TOKEN || import.meta.env.GOD_MODE_TOKEN;
if (!godToken) return true;
return token === godToken;
}
export const GET: APIRoute = async ({ request }) => {
if (!validateGodToken(request)) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
const url = new URL(request.url);
const lines = parseInt(url.searchParams.get('lines') || '100');
try {
// Get recent pg_stat_activity as "logs"
const result = await pool.query(`
SELECT
pid,
usename,
application_name,
state,
query,
state_change,
EXTRACT(EPOCH FROM (now() - query_start)) as duration_seconds
FROM pg_stat_activity
WHERE datname = current_database()
AND pid != pg_backend_pid()
ORDER BY query_start DESC
LIMIT $1
`, [lines]);
// Format as log entries
const logs = result.rows.map(row => ({
timestamp: row.state_change,
pid: row.pid,
user: row.usename,
app: row.application_name,
state: row.state,
duration: `${Math.round(row.duration_seconds)}s`,
query: row.query?.substring(0, 200)
}));
return new Response(JSON.stringify({
logs,
count: logs.length,
requested: lines,
timestamp: new Date().toISOString()
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error: any) {
return new Response(JSON.stringify({
error: 'Failed to fetch logs',
details: error.message
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
};

View File

@@ -0,0 +1,125 @@
import type { APIRoute } from 'astro';
import { killLocks, vacuumAnalyze, getTableBloat } from '@/lib/db/mechanic';
/**
* Mechanic Execute Endpoint
* Manual trigger for maintenance operations
*/
function validateGodToken(request: Request): boolean {
const token = request.headers.get('X-God-Token') ||
request.headers.get('Authorization')?.replace('Bearer ', '') ||
new URL(request.url).searchParams.get('token');
const godToken = process.env.GOD_MODE_TOKEN || import.meta.env.GOD_MODE_TOKEN;
if (!godToken) return true;
return token === godToken;
}
export const POST: APIRoute = async ({ request }) => {
if (!validateGodToken(request)) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
try {
const { action, table } = await request.json();
if (!action) {
return new Response(JSON.stringify({
error: 'Missing action',
valid_actions: ['kill-locks', 'vacuum', 'analyze-bloat']
}), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
let result: any;
switch (action) {
case 'kill-locks':
const killed = await killLocks();
result = {
action: 'kill-locks',
processes_terminated: killed,
message: `Killed ${killed} stuck queries`
};
break;
case 'vacuum':
await vacuumAnalyze(table);
result = {
action: 'vacuum',
table: table || 'all',
message: `VACUUM ANALYZE completed${table ? ` on ${table}` : ''}`
};
break;
case 'analyze-bloat':
const bloat = await getTableBloat();
result = {
action: 'analyze-bloat',
tables: bloat,
message: `Found ${bloat.length} tables with bloat`
};
break;
default:
return new Response(JSON.stringify({
error: `Unknown action: ${action}`
}), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
return new Response(JSON.stringify({
success: true,
...result,
timestamp: new Date().toISOString()
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error: any) {
return new Response(JSON.stringify({
error: 'Mechanic operation failed',
details: error.message
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
};
export const GET: APIRoute = async ({ request }) => {
if (!validateGodToken(request)) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
return new Response(JSON.stringify({
endpoint: 'POST /api/god/mechanic/execute',
description: 'Manual database maintenance operations',
actions: {
'kill-locks': 'Terminate stuck queries (>30s)',
'vacuum': 'Run VACUUM ANALYZE (specify table or all)',
'analyze-bloat': 'Get tables with dead row bloat'
},
usage: {
kill_locks: { action: 'kill-locks' },
vacuum_all: { action: 'vacuum' },
vacuum_table: { action: 'vacuum', table: 'posts' },
check_bloat: { action: 'analyze-bloat' }
}
}, null, 2), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
};

View File

@@ -0,0 +1,139 @@
import type { APIRoute } from 'astro';
import Redis from 'ioredis';
import { SystemConfigSchema } from '@/lib/data/dataValidator';
/**
* System Configuration Endpoint
* Persistent settings in Redis
*/
const REDIS_URL = process.env.REDIS_URL || 'redis://redis:6379';
const CONFIG_KEY = 'god_mode:system_config';
function validateGodToken(request: Request): boolean {
const token = request.headers.get('X-God-Token') ||
request.headers.get('Authorization')?.replace('Bearer ', '') ||
new URL(request.url).searchParams.get('token');
const godToken = process.env.GOD_MODE_TOKEN || import.meta.env.GOD_MODE_TOKEN;
if (!godToken) return true;
return token === godToken;
}
export const POST: APIRoute = async ({ request }) => {
if (!validateGodToken(request)) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
const redis = new Redis(REDIS_URL, {
lazyConnect: true,
enableOfflineQueue: false
});
try {
const config = SystemConfigSchema.parse(await request.json());
await redis.connect();
// Store each config value in Redis hash
await redis.hset(CONFIG_KEY, {
throttle_delay_ms: config.throttle_delay_ms.toString(),
max_concurrency: config.max_concurrency.toString(),
max_cost_per_hour: config.max_cost_per_hour.toString(),
enable_auto_throttle: config.enable_auto_throttle.toString(),
memory_threshold_pct: config.memory_threshold_pct.toString(),
updated_at: new Date().toISOString()
});
await redis.quit();
return new Response(JSON.stringify({
success: true,
config,
message: 'System configuration saved to Redis',
timestamp: new Date().toISOString()
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error: any) {
try { await redis.quit(); } catch { }
return new Response(JSON.stringify({
error: 'Configuration update failed',
details: error.message
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
};
export const GET: APIRoute = async ({ request }) => {
if (!validateGodToken(request)) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
const redis = new Redis(REDIS_URL, {
lazyConnect: true,
enableOfflineQueue: false
});
try {
await redis.connect();
const config = await redis.hgetall(CONFIG_KEY);
await redis.quit();
if (Object.keys(config).length === 0) {
// Return defaults if not set
return new Response(JSON.stringify({
config: {
throttle_delay_ms: 0,
max_concurrency: 128,
max_cost_per_hour: 100,
enable_auto_throttle: true,
memory_threshold_pct: 90
},
source: 'defaults',
message: 'No custom config found - showing defaults'
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
return new Response(JSON.stringify({
config: {
throttle_delay_ms: parseInt(config.throttle_delay_ms),
max_concurrency: parseInt(config.max_concurrency),
max_cost_per_hour: parseFloat(config.max_cost_per_hour),
enable_auto_throttle: config.enable_auto_throttle === 'true',
memory_threshold_pct: parseInt(config.memory_threshold_pct)
},
source: 'redis',
updated_at: config.updated_at,
timestamp: new Date().toISOString()
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error: any) {
try { await redis.quit(); } catch { }
return new Response(JSON.stringify({
error: 'Failed to retrieve configuration',
details: error.message
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
};