Feat: Add System Control (Standby Mode) and Resource Monitor

This commit is contained in:
cawcenter
2025-12-14 19:50:05 -05:00
parent f7997cdd88
commit 88d3157cd9
8 changed files with 274 additions and 1 deletions

19
package-lock.json generated
View File

@@ -62,6 +62,7 @@
"papaparse": "^5.5.3",
"pdfmake": "^0.2.20",
"pg": "^8.16.3",
"pidusage": "^4.0.1",
"react": "^18.3.1",
"react-contenteditable": "^3.3.7",
"react-diff-viewer-continued": "^3.4.0",
@@ -84,6 +85,7 @@
},
"devDependencies": {
"@types/node": "^20.11.0",
"@types/pidusage": "^2.0.5",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"autoprefixer": "^10.4.18",
@@ -8105,6 +8107,12 @@
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="
},
"node_modules/@types/pidusage": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/pidusage/-/pidusage-2.0.5.tgz",
"integrity": "sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==",
"dev": true
},
"node_modules/@types/prismjs": {
"version": "1.26.5",
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
@@ -14689,6 +14697,17 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pidusage": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pidusage/-/pidusage-4.0.1.tgz",
"integrity": "sha512-yCH2dtLHfEBnzlHUJymR/Z1nN2ePG3m392Mv8TFlTP1B0xkpMQNHAnfkY0n2tAi6ceKO6YWhxYfZ96V4vVkh/g==",
"dependencies": {
"safe-buffer": "^5.2.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",

View File

@@ -64,6 +64,7 @@
"papaparse": "^5.5.3",
"pdfmake": "^0.2.20",
"pg": "^8.16.3",
"pidusage": "^4.0.1",
"react": "^18.3.1",
"react-contenteditable": "^3.3.7",
"react-diff-viewer-continued": "^3.4.0",
@@ -86,6 +87,7 @@
},
"devDependencies": {
"@types/node": "^20.11.0",
"@types/pidusage": "^2.0.5",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"autoprefixer": "^10.4.18",

View File

@@ -0,0 +1,131 @@
import React, { useEffect, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Activity, Power, Cpu, Server } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
interface SystemMetrics {
cpu: number;
memoryMB: number;
uptime: number;
state: 'active' | 'standby';
}
export default function SystemControl() {
const [metrics, setMetrics] = useState<SystemMetrics | null>(null);
const [loading, setLoading] = useState(false);
const fetchMetrics = async () => {
try {
const res = await fetch('/api/god/system/control');
if (res.ok) {
const data = await res.json();
setMetrics(data);
}
} catch (e) {
console.error("Failed to fetch metrics", e);
}
};
const toggleSystem = async () => {
setLoading(true);
try {
const res = await fetch('/api/god/system/control', {
method: 'POST',
body: JSON.stringify({ action: 'toggle' }),
headers: { 'Content-Type': 'application/json' }
});
if (res.ok) {
await fetchMetrics(); // Refresh immediately
}
} catch (e) {
console.error("Failed to toggle", e);
} finally {
setLoading(false);
}
};
// Poll every 2 seconds
useEffect(() => {
fetchMetrics();
const interval = setInterval(fetchMetrics, 2000);
return () => clearInterval(interval);
}, []);
const isActive = metrics?.state === 'active';
return (
<Card className="border-slate-800 bg-slate-950/50 backdrop-blur">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div className="space-y-1">
<CardTitle className="text-xl font-bold flex items-center gap-2">
<Server className="h-5 w-5 text-indigo-400" />
Core System Control
</CardTitle>
<CardDescription>
Monitor resource usage and toggle processing engine.
</CardDescription>
</div>
<div className="flex items-center gap-2">
<Badge variant={isActive ? 'default' : 'destructive'}
className={isActive ? 'bg-green-500/10 text-green-500' : 'bg-red-500/10 text-red-500'}>
{isActive ? 'ONLINE' : 'STANDBY'}
</Badge>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
{/* CPU Monitor */}
<div className="p-4 rounded-lg bg-slate-900 border border-slate-800 flex flex-col justify-between">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-slate-400">CPU Usage</span>
<Cpu className="h-4 w-4 text-blue-400" />
</div>
<div className="flex items-baseline gap-2">
<span className="text-2xl font-bold text-slate-100">{metrics?.cpu || 0}%</span>
</div>
<div className="w-full bg-slate-800 h-1.5 mt-2 rounded-full overflow-hidden">
<div className="bg-blue-500 h-full transition-all duration-500" style={{ width: `${Math.min(metrics?.cpu || 0, 100)}%` }} />
</div>
</div>
{/* RAM Monitor */}
<div className="p-4 rounded-lg bg-slate-900 border border-slate-800 flex flex-col justify-between">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-slate-400">RAM Usage</span>
<Activity className="h-4 w-4 text-purple-400" />
</div>
<div className="flex items-baseline gap-2">
<span className="text-2xl font-bold text-slate-100">{metrics?.memoryMB || 0} MB</span>
</div>
<div className="w-full bg-slate-800 h-1.5 mt-2 rounded-full overflow-hidden">
{/* Assumes 2GB typical limit for visualization, though actual is 16GB */}
<div className="bg-purple-500 h-full transition-all duration-500" style={{ width: `${Math.min((metrics?.memoryMB || 0) / 2048 * 100, 100)}%` }} />
</div>
</div>
{/* Master Switch */}
<div className="flex items-center justify-center">
<Button
variant={isActive ? 'destructive' : 'default'}
size="lg"
className={`w-full h-full min-h-[100px] text-lg font-bold shadow-lg transition-all ${isActive
? 'bg-red-500 hover:bg-red-600 shadow-red-900/20'
: 'bg-green-500 hover:bg-green-600 shadow-green-900/20'
}`}
onClick={toggleSystem}
disabled={loading}
>
<Power className="mr-2 h-6 w-6" />
{isActive ? 'DEACTIVATE ENGINE' : 'ACTIVATE ENGINE'}
</Button>
</div>
</div>
<p className="text-xs text-slate-500 text-center">
* Activating the engine will enable heavy resource consumption (`npm` processes). Deactivating puts the system in standby, reducing CPU/RAM usage.
</p>
</CardContent>
</Card>
);
}

View File

@@ -1,3 +1,5 @@
import { system } from '@/lib/system/SystemController';
interface BatchConfig {
batchSize: number; // How many items to grab at once (e.g. 100)
concurrency: number; // How many to process in parallel (e.g. 5)
@@ -17,6 +19,18 @@ export class BatchProcessor {
const chunk = items.slice(i, i + this.config.batchSize);
console.log(`Processing Batch ${(i / this.config.batchSize) + 1}...`);
// Check System State (Standby Mode)
if (!system.isActive()) {
console.log('[God Mode] System in STANDBY. Pausing Batch Processor...');
// Wait until active again (check every 2s)
while (!system.isActive()) {
await new Promise(r => setTimeout(r, 2000));
}
console.log('[God Mode] System RESUMED.');
}
// Within each chunk, limit concurrency
// Within each chunk, limit concurrency
const chunkResults = await this.runWithConcurrency(chunk, workerFunction);
results.push(...chunkResults);

View File

@@ -0,0 +1,68 @@
import pidusage from 'pidusage';
export type SystemState = 'active' | 'standby';
export interface SystemMetrics {
cpu: number;
memory: number; // in bytes
memoryMB: number;
uptime: number; // seconds
state: SystemState;
timestamp: number;
}
class SystemController {
private state: SystemState = 'active'; // Default to active
private lastMetrics: SystemMetrics | null = null;
// Toggle System State
toggle(): SystemState {
this.state = this.state === 'active' ? 'standby' : 'active';
console.log(`[God Mode] System State Toggled: ${this.state.toUpperCase()}`);
return this.state;
}
// Set conform state
setState(newState: SystemState) {
this.state = newState;
}
getState(): SystemState {
return this.state;
}
isActive(): boolean {
return this.state === 'active';
}
// Get Live Resource Usage
async getMetrics(): Promise<SystemMetrics> {
try {
const stats = await pidusage(process.pid);
this.lastMetrics = {
cpu: parseFloat(stats.cpu.toFixed(1)),
memory: stats.memory,
memoryMB: Math.round(stats.memory / 1024 / 1024),
uptime: stats.elapsed,
state: this.state,
timestamp: Date.now()
};
return this.lastMetrics;
} catch (e) {
console.error("Failed to get pidusage", e);
// Return cached or empty if fail
return this.lastMetrics || {
cpu: 0,
memory: 0,
memoryMB: 0,
uptime: 0,
state: this.state,
timestamp: Date.now()
};
}
}
}
export const system = new SystemController();

View File

@@ -1,6 +1,7 @@
---
import AdminLayout from '../../layouts/AdminLayout.astro';
import SystemMonitor from '../../components/admin/dashboard/SystemMonitor';
import SystemControl from '../../components/admin/SystemControl';
---
<AdminLayout title="Mission Control">
@@ -10,6 +11,10 @@ import SystemMonitor from '../../components/admin/dashboard/SystemMonitor';
<p class="text-slate-400">System Monitoring, Sub-Station Status, and Content Integrity.</p>
</div>
<div class="mb-8">
<SystemControl client:load />
</div>
<SystemMonitor client:load />
</div>
</AdminLayout>

View File

@@ -0,0 +1,33 @@
import type { APIRoute } from 'astro';
import { system } from '@/lib/system/SystemController';
// GET: Retrieve current metrics and state
export const GET: APIRoute = async () => {
const metrics = await system.getMetrics();
return new Response(JSON.stringify(metrics), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
};
// POST: Toggle state
export const POST: APIRoute = async ({ request }) => {
try {
const body = await request.json();
if (body.action === 'toggle') {
const newState = system.toggle();
return new Response(JSON.stringify({
success: true,
state: newState,
message: `System is now ${newState.toUpperCase()}`
}));
}
return new Response(JSON.stringify({ error: 'Invalid action' }), { status: 400 });
} catch (e: any) {
return new Response(JSON.stringify({ error: e.message }), { status: 500 });
}
};

View File

@@ -2,6 +2,7 @@
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"moduleResolution": "node",
"esModuleInterop": true,
"jsx": "react-jsx",
"jsxImportSource": "react",
"baseUrl": ".",