Feat: Add System Control (Standby Mode) and Resource Monitor
This commit is contained in:
19
package-lock.json
generated
19
package-lock.json
generated
@@ -62,6 +62,7 @@
|
|||||||
"papaparse": "^5.5.3",
|
"papaparse": "^5.5.3",
|
||||||
"pdfmake": "^0.2.20",
|
"pdfmake": "^0.2.20",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
|
"pidusage": "^4.0.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-contenteditable": "^3.3.7",
|
"react-contenteditable": "^3.3.7",
|
||||||
"react-diff-viewer-continued": "^3.4.0",
|
"react-diff-viewer-continued": "^3.4.0",
|
||||||
@@ -84,6 +85,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.11.0",
|
||||||
|
"@types/pidusage": "^2.0.5",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.48",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.18",
|
||||||
"autoprefixer": "^10.4.18",
|
"autoprefixer": "^10.4.18",
|
||||||
@@ -8105,6 +8107,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
|
||||||
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="
|
"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": {
|
"node_modules/@types/prismjs": {
|
||||||
"version": "1.26.5",
|
"version": "1.26.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
|
||||||
@@ -14689,6 +14697,17 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"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": {
|
"node_modules/pify": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"papaparse": "^5.5.3",
|
"papaparse": "^5.5.3",
|
||||||
"pdfmake": "^0.2.20",
|
"pdfmake": "^0.2.20",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
|
"pidusage": "^4.0.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-contenteditable": "^3.3.7",
|
"react-contenteditable": "^3.3.7",
|
||||||
"react-diff-viewer-continued": "^3.4.0",
|
"react-diff-viewer-continued": "^3.4.0",
|
||||||
@@ -86,6 +87,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.11.0",
|
||||||
|
"@types/pidusage": "^2.0.5",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.48",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.18",
|
||||||
"autoprefixer": "^10.4.18",
|
"autoprefixer": "^10.4.18",
|
||||||
@@ -96,4 +98,4 @@
|
|||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-inspect": "^11.3.3"
|
"vite-plugin-inspect": "^11.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
131
src/components/admin/SystemControl.tsx
Normal file
131
src/components/admin/SystemControl.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { system } from '@/lib/system/SystemController';
|
||||||
|
|
||||||
interface BatchConfig {
|
interface BatchConfig {
|
||||||
batchSize: number; // How many items to grab at once (e.g. 100)
|
batchSize: number; // How many items to grab at once (e.g. 100)
|
||||||
concurrency: number; // How many to process in parallel (e.g. 5)
|
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);
|
const chunk = items.slice(i, i + this.config.batchSize);
|
||||||
console.log(`Processing Batch ${(i / this.config.batchSize) + 1}...`);
|
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
|
// Within each chunk, limit concurrency
|
||||||
const chunkResults = await this.runWithConcurrency(chunk, workerFunction);
|
const chunkResults = await this.runWithConcurrency(chunk, workerFunction);
|
||||||
results.push(...chunkResults);
|
results.push(...chunkResults);
|
||||||
|
|||||||
68
src/lib/system/SystemController.ts
Normal file
68
src/lib/system/SystemController.ts
Normal 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();
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import AdminLayout from '../../layouts/AdminLayout.astro';
|
import AdminLayout from '../../layouts/AdminLayout.astro';
|
||||||
import SystemMonitor from '../../components/admin/dashboard/SystemMonitor';
|
import SystemMonitor from '../../components/admin/dashboard/SystemMonitor';
|
||||||
|
import SystemControl from '../../components/admin/SystemControl';
|
||||||
---
|
---
|
||||||
|
|
||||||
<AdminLayout title="Mission Control">
|
<AdminLayout title="Mission Control">
|
||||||
@@ -9,6 +10,10 @@ import SystemMonitor from '../../components/admin/dashboard/SystemMonitor';
|
|||||||
<h1 class="text-3xl font-bold text-white mb-2">Command Station</h1>
|
<h1 class="text-3xl font-bold text-white mb-2">Command Station</h1>
|
||||||
<p class="text-slate-400">System Monitoring, Sub-Station Status, and Content Integrity.</p>
|
<p class="text-slate-400">System Monitoring, Sub-Station Status, and Content Integrity.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-8">
|
||||||
|
<SystemControl client:load />
|
||||||
|
</div>
|
||||||
|
|
||||||
<SystemMonitor client:load />
|
<SystemMonitor client:load />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
33
src/pages/api/god/system/control.ts
Normal file
33
src/pages/api/god/system/control.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
"extends": "astro/tsconfigs/strict",
|
"extends": "astro/tsconfigs/strict",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
|
"esModuleInterop": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "react",
|
"jsxImportSource": "react",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
|||||||
Reference in New Issue
Block a user