feat(weeks4-5): operations endpoints + UI components with Recharts & Leaflet
This commit is contained in:
78
src/pages/api/god/logs.ts
Normal file
78
src/pages/api/god/logs.ts
Normal 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' }
|
||||
});
|
||||
}
|
||||
};
|
||||
125
src/pages/api/god/mechanic/execute.ts
Normal file
125
src/pages/api/god/mechanic/execute.ts
Normal 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' }
|
||||
});
|
||||
};
|
||||
139
src/pages/api/god/system/config.ts
Normal file
139
src/pages/api/god/system/config.ts
Normal 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' }
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user