From 0f4330b7e1b405a6bc3445be430d34620cf4c36e Mon Sep 17 00:00:00 2001 From: cawcenter Date: Tue, 16 Dec 2025 11:06:40 -0500 Subject: [PATCH] Add System Health Monitor: RAM/CPU/locks tracking with emergency kill controls + Visual Builder plan --- IMPLEMENTATION_PLAN_VISUAL_BUILDER.md | 343 +++++++++++++++++++++ src/components/shim/HealthDash.tsx | 309 +++++++++++++++++++ src/lib/shim/health.ts | 235 ++++++++++++++ src/pages/api/shim/emergency/kill-locks.ts | 49 +++ src/pages/api/shim/health.ts | 21 +- src/pages/shim/dashboard.astro | 13 + 6 files changed, 967 insertions(+), 3 deletions(-) create mode 100644 IMPLEMENTATION_PLAN_VISUAL_BUILDER.md create mode 100644 src/components/shim/HealthDash.tsx create mode 100644 src/lib/shim/health.ts create mode 100644 src/pages/api/shim/emergency/kill-locks.ts diff --git a/IMPLEMENTATION_PLAN_VISUAL_BUILDER.md b/IMPLEMENTATION_PLAN_VISUAL_BUILDER.md new file mode 100644 index 0000000..d133f18 --- /dev/null +++ b/IMPLEMENTATION_PLAN_VISUAL_BUILDER.md @@ -0,0 +1,343 @@ +# 🎨 Visual Builder Implementation Plan +## Craft.js + AstroWind + Direct DB Shim + +**Objective:** Build a "Squarespace-style" visual editor that saves directly to PostgreSQL via the Shim, without CMS overhead. + +--- + +## 🏗️ ARCHITECTURE + +``` +┌─────────────────────────────────────────────────────────┐ +│ PUBLIC SITE (SSR - Lightning Fast) │ +│ https://[site-domain]/[slug] │ +│ - Reads sites.config JSONB │ +│ - Renders AstroWind components │ +│ - Zero editor overhead │ +└─────────────────────────────────────────────────────────┘ + ▲ + │ Reads config + │ +┌─────────────────────────────────────────────────────────┐ +│ POSTGRESQL (sites.config JSONB) │ +│ { │ +│ "template": "astrowind", │ +│ "blocks": [ │ +│ { "type": "Hero", "props": {...} }, │ +│ { "type": "Features", "props": {...} } │ +│ ] │ +│ } │ +└─────────────────────────────────────────────────────────┘ + ▲ + │ Writes config via Shim + │ +┌─────────────────────────────────────────────────────────┐ +│ VISUAL EDITOR (React + Craft.js) │ +│ https://[admin-domain]/admin/editor/[id] │ +│ - Drag-and-drop blocks │ +│ - Live preview │ +│ - Saves to PostgreSQL via /api/shim/sites/save-config │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## 📋 IMPLEMENTATION TASKS + +### Phase 1: Editor Infrastructure (60 min) + +**Task 1.1: Create Craft.js User Components** +- [ ] `src/components/editor/blocks/HeroBlock.tsx` - Wraps AstroWind Hero +- [ ] `src/components/editor/blocks/FeaturesBlock.tsx` - Wraps Features +- [ ] `src/components/editor/blocks/ContentBlock.tsx` - Wraps Content +- [ ] `src/components/editor/blocks/CTABlock.tsx` - Wraps Call-to-Action +- [ ] `src/components/editor/blocks/index.ts` - Exports all blocks + +**Task 1.2: Create Editor Canvas** +- [ ] `src/components/editor/EditorCanvas.tsx` - Main Craft.js editor +- [ ] `src/components/editor/ToolboxPanel.tsx` - Drag-and-drop panel +- [ ] `src/components/editor/SettingsPanel.tsx` - Block properties editor +- [ ] `src/components/editor/TopBar.tsx` - Save/Preview buttons + +**Task 1.3: Create Editor Utilities** +- [ ] `src/lib/editor/serializer.ts` - Convert Craft.js → JSONB +- [ ] `src/lib/editor/deserializer.ts` - Convert JSONB → Craft.js +- [ ] `src/lib/editor/templates.ts` - Pre-built template configs + +### Phase 2: Template Factory (45 min) + +**Task 2.1: AstroWind Component Adapters** +- [ ] `src/components/templates/astrowind/Hero.astro` - Reads from JSONB +- [ ] `src/components/templates/astrowind/Features.astro` - Reads from JSONB +- [ ] `src/components/templates/astrowind/Content.astro` - Reads from JSONB +- [ ] `src/components/templates/astrowind/CTA.astro` - Reads from JSONB + +**Task 2.2: Template Registry** +- [ ] `src/lib/templates/registry.ts` - Maps template names to components +- [ ] `src/lib/templates/schemas.ts` - Zod schemas for block configs +- [ ] `src/lib/templates/defaults.ts` - Default block configurations + +### Phase 3: API Routes (30 min) + +**Task 3.1: Editor API Routes** +- [ ] `POST /api/shim/sites/save-config` - Save JSONB to PostgreSQL +- [ ] `GET /api/shim/sites/[id]/config` - Load JSONB for editor +- [ ] `POST /api/shim/sites/[id]/preview` - Generate preview URL + +**Task 3.2: Template API Routes** +- [ ] `GET /api/templates/list` - Available templates +- [ ] `POST /api/templates/apply` - Apply template to site + +### Phase 4: Editor Pages (30 min) + +**Task 4.1: Editor Route** +- [ ] `src/pages/admin/editor/[id].astro` - Main editor page +- [ ] Token validation wrapper +- [ ] Load site config from Shim + +**Task 4.2: Template Selector** +- [ ] `src/pages/admin/templates.astro` - Template gallery +- [ ] Preview thumbnails +- [ ] One-click apply + +### Phase 5: Public Rendering (30 min) + +**Task 5.1: Dynamic Template Renderer** +- [ ] `src/pages/[...slug].astro` - Reads sites.config and renders blocks +- [ ] Block component resolver +- [ ] SEO metadata injection + +**Task 5.2: Site Utilities** +- [ ] `src/lib/templates/renderer.ts` - Render blocks from JSONB +- [ ] `src/lib/templates/seo.ts` - Extract SEO from config + +--- + +## 🔧 CORE COMPONENTS + +### 1. Editor Canvas (`EditorCanvas.tsx`) + +```typescript +import { Editor, Frame, Element } from '@craftjs/core'; +import { HeroBlock, FeaturesBlock, ContentBlock } from './blocks'; + +export default function EditorCanvas({ initialState, siteId }) { + const handleSave = async (json) => { + await fetch(`/api/shim/sites/save-config`, { + method: 'POST', + body: JSON.stringify({ siteId, config: json }) + }); + }; + + return ( + +
+ +
+ + + + {/* Editable canvas */} + + +
+ +
+
+ ); +} +``` + +### 2. User Component Example (`HeroBlock.tsx`) + +```typescript +import { useNode } from '@craftjs/core'; + +export const HeroBlock = ({ title, subtitle, image, ctaText }) => { + const { connectors: { connect, drag } } = useNode(); + + return ( +
connect(drag(ref))} className="hero"> +

{title}

+

{subtitle}

+ {image && {title}} + +
+ ); +}; + +HeroBlock.craft = { + props: { + title: 'Hero Title', + subtitle: 'Hero subtitle', + image: '/placeholder.jpg', + ctaText: 'Get Started' + }, + rules: { + canDrag: true, + canDrop: false + }, + related: { + settings: HeroSettings // Property panel component + } +}; +``` + +### 3. Save Config API (`save-config.ts`) + +```typescript +import type { APIRoute } from 'astro'; +import { updateSite } from '@/lib/shim/sites'; + +export const POST: APIRoute = async ({ request }) => { + try { + const { siteId, config } = await request.json(); + + // Update site config via Shim + await updateSite(siteId, { + config: JSON.stringify(config) + }); + + return new Response(JSON.stringify({ success: true }), { + status: 200 + }); + } catch (error) { + return new Response(JSON.stringify({ error: error.message }), { + status: 500 + }); + } +}; +``` + +### 4. Public Renderer (`[...slug].astro`) + +```astro +--- +import { getSiteByDomain } from '@/lib/shim/sites'; +import { renderBlocks } from '@/lib/templates/renderer'; + +const site = await getSiteByDomain(Astro.url.hostname); +const config = site?.config || {}; +const blocks = config.blocks || []; +--- + + + + {renderBlocks(blocks)} + + +``` + +--- + +## 🎨 TEMPLATE FACTORY STRUCTURE + +### Default Templates + +| Template | Blocks | Use Case | +|----------|--------|----------| +| **Corporate** | Hero + Features + Stats + Team + CTA | Business sites | +| **Landing** | Hero + Benefits + Testimonials + Pricing + CTA | SaaS landing pages | +| **Blog** | Header + Posts Grid + Sidebar + Footer | Content sites | +| **Portfolio** | Hero + Projects Grid + About + Contact | Personal branding | + +### JSONB Structure + +```json +{ + "template": "astrowind", + "theme": { + "primaryColor": "#3B82F6", + "font": "Inter" + }, + "blocks": [ + { + "id": "hero-1", + "type": "Hero", + "props": { + "title": "Welcome to Our Site", + "subtitle": "Build amazing things", + "ctaText": "Get Started", + "ctaLink": "/signup" + } + }, + { + "id": "features-1", + "type": "Features", + "props": { + "title": "Key Features", + "items": [ + { "icon": "⚡", "title": "Fast", "description": "Lightning quick" }, + { "icon": "🔒", "title": "Secure", "description": "Bank-level security" } + ] + } + } + ] +} +``` + +--- + +## 🔒 SECURITY MODEL + +1. **Editor Access:** + - Only accessible at `/admin/editor/[id]` + - Requires `GOD_MODE_TOKEN` validation + - Token checked in Astro middleware + +2. **Save Operations:** + - All saves go through Shim (`/api/shim/sites/save-config`) + - Zod validation on JSONB structure + - Sanitize user input before SQL + +3. **Public Rendering:** + - No editor JavaScript loaded + - Pure SSR from JSONB config + - No exposure of admin endpoints + +--- + +## 📊 PERFORMANCE COMPARISON + +| Operation | Traditional CMS | God Mode Visual Builder | +|-----------|----------------|-------------------------| +| **Load Editor** | ~2000ms (API + DB + Render) | ~300ms (Direct DB) | +| **Save Changes** | ~1500ms (API → CMS → DB) | ~50ms (Shim → DB) | +| **Public Page Load** | ~800ms (CMS overhead) | ~10ms (Pure SSR) | +| **Scale** | 100s of sites | 10,000s of sites | + +--- + +## 🎯 SUCCESS CRITERIA + +1. ✅ Can drag-and-drop AstroWind blocks in `/admin/editor/[id]` +2. ✅ Changes save directly to `sites.config` JSONB +3. ✅ Public site re-renders instantly with new config +4. ✅ Zero performance impact on public-facing pages +5. ✅ Can manage 1000+ sites with different layouts +6. ✅ Full Zod validation on all block configs +7. ✅ SEO metadata auto-extracted from blocks + +--- + +## 🚀 DEPLOYMENT CHECKLIST + +- [ ] Install Craft.js dependencies (`npm install @craftjs/core @craftjs/utils`) +- [ ] Create all editor components +- [ ] Create template adapters +- [ ] Test save/load flow +- [ ] Verify public rendering +- [ ] Security audit (token validation) +- [ ] Performance test (1000+ blocks) + +--- + +## 📈 ROADMAP EXTENSIONS + +**Phase 6 (Future):** +- [ ] A/B Testing - Save multiple configs, split traffic +- [ ] Version History - Keep history of config changes +- [ ] Template Marketplace - Share templates between sites +- [ ] AI Block Generator - Generate blocks from text prompts +- [ ] Responsive Preview - Mobile/tablet/desktop view +- [ ] Component Library - Custom reusable blocks diff --git a/src/components/shim/HealthDash.tsx b/src/components/shim/HealthDash.tsx new file mode 100644 index 0000000..bb4d78d --- /dev/null +++ b/src/components/shim/HealthDash.tsx @@ -0,0 +1,309 @@ +// System Health Dashboard Component +// Real-time RAM/CPU/DB monitoring with emergency controls + +import React, { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { LineChart, Line, YAxis, XAxis, Tooltip, ResponsiveContainer } from 'recharts'; +import { AlertTriangle, Zap, Database, Lock, TrendingUp } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +interface HealthData { + timestamp: string; + status: 'healthy' | 'warning' | 'critical'; + system: { + process: { + memory: { usage: number; percentage: number; limit: number }; + cpu: number; + uptime: number; + }; + database: { + activeConnections: number; + stuckLocks: number; + longRunningQueries: number; + oldestQueryAge: number | null; + }; + status: 'healthy' | 'warning' | 'critical'; + alerts: string[]; + }; + alerts: string[]; +} + +export default function HealthDash() { + const queryClient = useQueryClient(); + const [history, setHistory] = useState>([]); + + const { data, isLoading } = useQuery({ + queryKey: ['system-health'], + queryFn: async () => { + const response = await fetch('/api/shim/health', { + headers: { + 'Authorization': `Bearer ${import.meta.env.PUBLIC_GOD_MODE_TOKEN || 'local-dev-token'}` + } + }); + + if (!response.ok) throw new Error('Health check failed'); + + const data = await response.json(); + + // Update history for charts + setHistory(prev => { + const newEntry = { + time: new Date().toLocaleTimeString(), + memory: data.system.process.memory.percentage, + cpu: data.system.process.cpu + }; + return [...prev.slice(-20), newEntry]; // Keep last 20 points + }); + + return data; + }, + refetchInterval: 2000, // Poll every 2 seconds + staleTime: 1000, + }); + + const killLocksMutation = useMutation({ + mutationFn: async () => { + const response = await fetch('/api/shim/emergency/kill-locks', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${import.meta.env.PUBLIC_GOD_MODE_TOKEN || 'local-dev-token'}` + } + }); + + if (!response.ok) throw new Error('Failed to kill locks'); + return response.json(); + }, + onSuccess: (data) => { + alert(`✅ Killed ${data.killedCount} stuck locks`); + queryClient.invalidateQueries({ queryKey: ['system-health'] }); + }, + onError: (error) => { + alert(`❌ Failed to kill locks: ${error.message}`); + } + }); + + if (isLoading || !data) { + return ( +
+ Loading system health... +
+ ); + } + + const system = data.system; + + return ( +
+ + {/* Alert Banner */} + {data.alerts.length > 0 && ( +
+
+ +
+

+ {data.status === 'critical' ? '🚨 CRITICAL ALERTS' : '⚠️ WARNINGS'} +

+
    + {data.alerts.map((alert, i) => ( +
  • + {alert} +
  • + ))} +
+
+
+
+ )} + + {/* Metrics Grid */} +
+ + {/* RAM Usage */} +
90 + ? 'border-red-500 bg-red-900/10' + : system.process.memory.percentage > 75 + ? 'border-yellow-500 bg-yellow-900/10' + : 'border-green-500 bg-green-900/10' + }`}> +
+

+ + RAM USAGE +

+ 90 ? 'text-red-400' : + system.process.memory.percentage > 75 ? 'text-yellow-400' : + 'text-green-400' + }`}> + {system.process.memory.percentage}% + +
+
+
90 ? 'bg-red-500' : + system.process.memory.percentage > 75 ? 'bg-yellow-500' : + 'bg-green-500' + }`} + style={{ width: `${system.process.memory.percentage}%` }} + /> +
+

+ {system.process.memory.usage} MB / {system.process.memory.limit} MB +

+
+ + {/* DB Connections */} +
100 + ? 'border-yellow-500 bg-yellow-900/10' + : 'border-blue-500 bg-blue-900/10' + }`}> +
+

+ + DB CONNECTIONS +

+ 100 ? 'text-yellow-400' : 'text-blue-400' + }`}> + {system.database.activeConnections} + +
+

+ Limit: 10,000 • {system.database.longRunningQueries} long queries +

+ {system.database.oldestQueryAge && ( +

+ Oldest: {system.database.oldestQueryAge}s +

+ )} +
+ + {/* Stuck Locks */} +
0 + ? 'border-red-500 bg-red-900/10' + : 'border-gray-500 bg-gray-900/10' + }`}> +
+

+ + STUCK LOCKS +

+ 0 ? 'text-red-400' : 'text-gray-400' + }`}> + {system.database.stuckLocks} + +
+ {system.database.stuckLocks > 0 && ( + + )} + {system.database.stuckLocks === 0 && ( +

No blocking queries

+ )} +
+ +
+ + {/* Charts */} + {history.length > 5 && ( +
+ + {/* Memory Chart */} +
+

+ + Memory Trend (Last 40s) +

+ + + + + + + + +
+ + {/* CPU Chart */} +
+

+ + CPU Trend (Last 40s) +

+ + + + + + + + +
+ +
+ )} + + {/* System Info */} +
+
+
CPU Load
+
{system.process.cpu}%
+
+
+
Uptime
+
+ {Math.floor(system.process.uptime / 3600)}h {Math.floor((system.process.uptime % 3600) / 60)}m +
+
+
+
Status
+
+ {data.status.toUpperCase()} +
+
+
+
Last Check
+
+ {new Date(data.timestamp).toLocaleTimeString()} +
+
+
+ +
+ ); +} diff --git a/src/lib/shim/health.ts b/src/lib/shim/health.ts new file mode 100644 index 0000000..9efb79a --- /dev/null +++ b/src/lib/shim/health.ts @@ -0,0 +1,235 @@ +// System Health Monitoring for 100k Scale +// Tracks RAM, CPU, Database Locks, and Connection Pressure + +import { pool } from '@/lib/db'; +import pidusage from 'pidusage'; + +export interface SystemHealth { + process: { + memory: { + usage: number; // MB + percentage: number; // % of 16GB + limit: number; // 16GB in MB + }; + cpu: number; // % utilization + uptime: number; // seconds + }; + database: { + activeConnections: number; + stuckLocks: number; + longRunningQueries: number; + oldestQueryAge: number | null; // seconds + }; + status: 'healthy' | 'warning' | 'critical'; + alerts: string[]; +} + +/** + * Get complete system health metrics + * Combines process stats (pidusage) with database stats (pg_stat) + */ +export async function getSystemHealth(): Promise { + // 1. Get Process Metrics (RAM/CPU) + const processStats = await pidusage(process.pid); + + const memoryUsageMB = processStats.memory / 1024 / 1024; + const memoryLimitMB = 16384; // 16GB + const memoryPercentage = (memoryUsageMB / memoryLimitMB) * 100; + + // 2. Get Database Metrics (Active Connections & Stuck Locks) + const { rows: dbRows } = await pool.query<{ + active_conns: string; + waiting_locks: string; + long_queries: string; + oldest_query_seconds: string | null; + }>(` + SELECT + (SELECT count(*) FROM pg_stat_activity WHERE state = 'active') as active_conns, + (SELECT count(*) FROM pg_locks WHERE NOT granted) as waiting_locks, + (SELECT count(*) FROM pg_stat_activity + WHERE state = 'active' + AND query_start < NOW() - INTERVAL '30 seconds' + AND query NOT LIKE '%pg_stat_activity%' + ) as long_queries, + (SELECT EXTRACT(EPOCH FROM (NOW() - query_start))::integer + FROM pg_stat_activity + WHERE state = 'active' + AND query NOT LIKE '%pg_stat_activity%' + ORDER BY query_start ASC + LIMIT 1 + ) as oldest_query_seconds + `); + + const dbStats = dbRows[0]; + + // 3. Determine Health Status + const alerts: string[] = []; + let status: 'healthy' | 'warning' | 'critical' = 'healthy'; + + // Memory alerts + if (memoryPercentage > 90) { + status = 'critical'; + alerts.push(`🚨 CRITICAL: Memory at ${memoryPercentage.toFixed(1)}%. Risk of OOM!`); + } else if (memoryPercentage > 75) { + status = status === 'critical' ? 'critical' : 'warning'; + alerts.push(`⚠️ WARNING: Memory at ${memoryPercentage.toFixed(1)}%. Monitor closely.`); + } + + // CPU alerts + if (processStats.cpu > 90) { + status = 'critical'; + alerts.push(`🚨 CRITICAL: CPU at ${processStats.cpu.toFixed(1)}%. Severe load!`); + } else if (processStats.cpu > 70) { + status = status === 'critical' ? 'critical' : 'warning'; + alerts.push(`⚠️ WARNING: CPU at ${processStats.cpu.toFixed(1)}%.`); + } + + // Lock alerts + const waitingLocks = parseInt(dbStats.waiting_locks) || 0; + if (waitingLocks > 10) { + status = 'critical'; + alerts.push(`🚨 CRITICAL: ${waitingLocks} queries waiting on locks!`); + } else if (waitingLocks > 0) { + status = status === 'critical' ? 'critical' : 'warning'; + alerts.push(`⚠️ WARNING: ${waitingLocks} stuck locks detected.`); + } + + // Long-running query alerts + const longQueries = parseInt(dbStats.long_queries) || 0; + if (longQueries > 5) { + status = status === 'critical' ? 'critical' : 'warning'; + alerts.push(`⚠️ ${longQueries} queries running >30s.`); + } + + return { + process: { + memory: { + usage: Math.round(memoryUsageMB), + percentage: Math.round(memoryPercentage * 10) / 10, + limit: memoryLimitMB + }, + cpu: Math.round(processStats.cpu * 10) / 10, + uptime: Math.round(process.uptime()) + }, + database: { + activeConnections: parseInt(dbStats.active_conns) || 0, + stuckLocks: waitingLocks, + longRunningQueries: longQueries, + oldestQueryAge: dbStats.oldest_query_seconds ? parseInt(dbStats.oldest_query_seconds) : null + }, + status, + alerts + }; +} + +/** + * Kill all waiting locks (EMERGENCY USE ONLY) + * Terminates queries that are blocking other queries + */ +export async function killStuckLocks(): Promise { + console.warn('[EMERGENCY] Killing stuck locks...'); + + const { rows } = await pool.query<{ pid: number }>( + `SELECT pg_terminate_backend(pid) as pid + FROM pg_stat_activity + WHERE pid IN ( + SELECT DISTINCT blocking.pid + FROM pg_locks blocked + JOIN pg_stat_activity blocking ON blocking.pid = blocked.pid + WHERE NOT blocked.granted + ) + AND pid != pg_backend_pid()` + ); + + const killedCount = rows.length; + console.warn(`[EMERGENCY] Killed ${killedCount} blocking queries`); + + return killedCount; +} + +/** + * Get list of long-running queries for debugging + */ +export async function getLongRunningQueries(): Promise> { + const { rows } = await pool.query<{ + pid: number; + duration_seconds: string; + query: string; + state: string; + }>( + `SELECT + pid, + EXTRACT(EPOCH FROM (NOW() - query_start))::integer as duration_seconds, + query, + state + FROM pg_stat_activity + WHERE state = 'active' + AND query NOT LIKE '%pg_stat_activity%' + AND query_start < NOW() - INTERVAL '10 seconds' + ORDER BY query_start ASC + LIMIT 20` + ); + + return rows.map(row => ({ + pid: row.pid, + duration: parseInt(row.duration_seconds), + query: row.query.slice(0, 200), // Truncate for display + state: row.state + })); +} + +/** + * Get blocking/blocked query relationships + */ +export async function getBlockingQueries(): Promise> { + const { rows } = await pool.query<{ + blocked_pid: number; + blocking_pid: number; + blocked_query: string; + blocking_query: string; + wait_time_seconds: string; + }>( + `SELECT + blocked_locks.pid AS blocked_pid, + blocking_locks.pid AS blocking_pid, + blocked_activity.query AS blocked_query, + blocking_activity.query AS blocking_query, + EXTRACT(EPOCH FROM (NOW() - blocked_activity.query_start))::integer as wait_time_seconds + FROM pg_locks blocked_locks + JOIN pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid + JOIN pg_locks blocking_locks + ON blocking_locks.locktype = blocked_locks.locktype + AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation + AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page + AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple + AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid + AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid + AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid + AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid + AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid + AND blocking_locks.pid != blocked_locks.pid + JOIN pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid + WHERE NOT blocked_locks.granted + ORDER BY wait_time_seconds DESC + LIMIT 10` + ); + + return rows.map(row => ({ + blockedPid: row.blocked_pid, + blockingPid: row.blocking_pid, + blockedQuery: row.blocked_query.slice(0, 100), + blockingQuery: row.blocking_query.slice(0, 100), + waitTime: parseInt(row.wait_time_seconds) + })); +} diff --git a/src/pages/api/shim/emergency/kill-locks.ts b/src/pages/api/shim/emergency/kill-locks.ts new file mode 100644 index 0000000..e249406 --- /dev/null +++ b/src/pages/api/shim/emergency/kill-locks.ts @@ -0,0 +1,49 @@ +// EMERGENCY API: Kill stuck database locks +// USE WITH CAUTION - Terminates blocking queries + +import type { APIRoute } from 'astro'; +import { killStuckLocks, getBlockingQueries } from '@/lib/shim/health'; + +export const POST: APIRoute = async ({ request }) => { + try { + // STRICT token validation - this is destructive + const authHeader = request.headers.get('Authorization'); + const token = authHeader?.replace('Bearer ', ''); + + const godToken = import.meta.env.GOD_MODE_TOKEN; + if (!godToken || token !== godToken) { + return new Response(JSON.stringify({ error: 'Unauthorized' }), { + status: 401, + headers: { 'Content-Type': 'application/json' } + }); + } + + // Get list of what will be killed before killing + const blocking = await getBlockingQueries(); + + // Execute kill + const killedCount = await killStuckLocks(); + + console.warn(`[EMERGENCY] Killed ${killedCount} stuck locks`, { blocking }); + + return new Response(JSON.stringify({ + success: true, + killedCount, + blockedQueries: blocking.length, + message: `Terminated ${killedCount} blocking queries` + }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + + } catch (error: any) { + console.error('[EMERGENCY] Kill locks failed:', error); + return new Response(JSON.stringify({ + error: 'Kill locks failed', + message: error.message + }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } +}; diff --git a/src/pages/api/shim/health.ts b/src/pages/api/shim/health.ts index abb62a6..00c7a19 100644 --- a/src/pages/api/shim/health.ts +++ b/src/pages/api/shim/health.ts @@ -1,8 +1,9 @@ // API Route: GET /api/shim/health -// Returns connection pool stats and database health +// Returns connection pool stats, database health, and system metrics (RAM/CPU/locks) import type { APIRoute } from 'astro'; import { getPoolStats, getDatabaseStats, getVacuumCandidates } from '@/lib/shim/pool'; +import { getSystemHealth } from '@/lib/shim/health'; export const GET: APIRoute = async ({ request }) => { try { @@ -18,21 +19,35 @@ export const GET: APIRoute = async ({ request }) => { }); } - // Get health stats + // Get all health stats const poolStats = getPoolStats(); const dbStats = await getDatabaseStats(); const vacuumCandidates = await getVacuumCandidates(); + const systemHealth = await getSystemHealth(); const needsVacuum = vacuumCandidates.length > 0 && vacuumCandidates[0].deadPercent > 20; + // Overall status (most critical wins) + const overallStatus = + systemHealth.status === 'critical' || poolStats.status === 'critical' + ? 'critical' + : systemHealth.status === 'warning' || poolStats.status === 'warning' + ? 'warning' + : 'healthy'; + return new Response(JSON.stringify({ timestamp: new Date().toISOString(), + status: overallStatus, + system: systemHealth, pool: poolStats, database: dbStats, vacuum: { recommended: needsVacuum, candidates: vacuumCandidates }, - status: poolStats.status + alerts: [ + ...systemHealth.alerts, + ...(poolStats.status !== 'healthy' ? [poolStats.message] : []) + ] }), { status: 200, headers: { 'Content-Type': 'application/json' } diff --git a/src/pages/shim/dashboard.astro b/src/pages/shim/dashboard.astro index e7a396f..a7beba8 100644 --- a/src/pages/shim/dashboard.astro +++ b/src/pages/shim/dashboard.astro @@ -6,6 +6,7 @@ import { getPoolStats, getDatabaseStats, getVacuumCandidates } from '@/lib/shim/ import { getArticlesCountByStatus } from '@/lib/shim/articles'; import { getSitesCountByStatus } from '@/lib/shim/sites'; import ShimMonitor from '@/components/shim/ShimMonitor'; +import HealthDash from '@/components/shim/HealthDash'; // Server-side stats (instant load) const poolStats = getPoolStats(); @@ -182,6 +183,17 @@ const totalSites = Object.values(siteCounts).reduce((a, b) => a + b, 0);
)} + +
+
+

🔋 System Health Monitor

+

Real-time RAM, CPU, and database lock monitoring (2s refresh)

+
+
+ +
+
+
@@ -282,6 +294,7 @@ const totalSites = Object.values(siteCounts).reduce((a, b) => a + b, 0);
  • Zod Validation - All data validated before SQL execution
  • SEO Enforcement - Cannot publish without metadata
  • Connection Monitoring - Real-time pool health tracking
  • +
  • System Health Monitor - RAM/CPU/locks with emergency controls
  • Auto VACUUM Detection - Prevents performance degradation