217 lines
8.6 KiB
JavaScript
217 lines
8.6 KiB
JavaScript
/**
|
|
* app.js - Main Dashboard Application Controller
|
|
* Connects WebSocket, renders fleet nodes, binds user interactions, and manages pipeline lifecycle.
|
|
*/
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const pipeline = new PipelineVisualizer('pipelineCanvas');
|
|
const charts = new TelemetryCharts();
|
|
const terminal = new TerminalConsole();
|
|
|
|
let ws = null;
|
|
let pipelineRunning = false;
|
|
|
|
// DOM Elements
|
|
const btnStart = document.getElementById('btnStartPipeline');
|
|
const btnStop = document.getElementById('btnStopPipeline');
|
|
const btnKnobs = document.getElementById('btnOpenKnobs');
|
|
const btnCloseKnobs = document.getElementById('btnCloseKnobs');
|
|
const btnSaveKnobs = document.getElementById('btnSaveKnobs');
|
|
const knobsModal = document.getElementById('knobsModal');
|
|
const btnSyncGitea = document.getElementById('btnSyncGitea');
|
|
const scriptSelector = document.getElementById('scriptSelector');
|
|
const scriptCodeView = document.getElementById('scriptCodeView');
|
|
const fleetNodeList = document.getElementById('fleetNodeList');
|
|
const loadingOverlay = document.getElementById('loadingOverlay');
|
|
const loadingTitle = document.getElementById('loadingTitle');
|
|
|
|
// Gauge Elements
|
|
const gaugeVram = document.getElementById('gaugeVram');
|
|
const gaugeRam = document.getElementById('gaugeRam');
|
|
const gaugeAdb = document.getElementById('gaugeAdb');
|
|
const statusBadge = document.getElementById('pipelineStatusBadge');
|
|
const overallProgressText = document.getElementById('overallProgressText');
|
|
const grpoStepVal = document.getElementById('grpoStepVal');
|
|
const retainedExpertsVal = document.getElementById('retainedExpertsVal');
|
|
|
|
// 1. Initialize WebSocket Connection
|
|
function initWebSocket() {
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
ws = new WebSocket(`${protocol}//${window.location.host}`);
|
|
|
|
ws.onopen = () => {
|
|
terminal.addLog('INFO', 'Connected to AI-Trainer Control Center Server WebSocket', 'WS');
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
const data = JSON.parse(event.data);
|
|
handleServerMessage(data);
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
terminal.addLog('WARN', 'WebSocket connection lost. Reconnecting in 3s...', 'WS');
|
|
setTimeout(initWebSocket, 3000);
|
|
};
|
|
}
|
|
|
|
function handleServerMessage(data) {
|
|
if (data.type === 'INIT' || data.type === 'METRICS_UPDATE') {
|
|
const state = data.pipelineState || data.pipelineState;
|
|
const metrics = data.metrics;
|
|
|
|
if (metrics) {
|
|
gaugeVram.textContent = `${metrics.vramUsedGb} / ${metrics.vramTotalGb} GB`;
|
|
gaugeRam.textContent = `${metrics.ramUsedGb} / ${metrics.ramTotalGb} GB`;
|
|
gaugeAdb.textContent = `${metrics.adbOnlineCount} / ${metrics.activeVmCount} Online`;
|
|
grpoStepVal.textContent = metrics.grpoSteps;
|
|
retainedExpertsVal.textContent = Math.round(256 * (1 - (metrics.expertPruneRatio / 100)));
|
|
|
|
charts.updateGRPO(metrics.grpoSteps, metrics.currentReward);
|
|
charts.updateMoE(Math.round(256 * (1 - (metrics.expertPruneRatio / 100))));
|
|
}
|
|
|
|
if (state) {
|
|
pipelineRunning = state.status === 'running';
|
|
btnStart.disabled = pipelineRunning;
|
|
btnStop.disabled = !pipelineRunning;
|
|
|
|
statusBadge.textContent = `STATUS: ${state.status.toUpperCase()}`;
|
|
statusBadge.className = `status-badge ${pipelineRunning ? 'status-running' : 'status-idle'}`;
|
|
overallProgressText.textContent = `${state.progress}%`;
|
|
|
|
if (state.activeStage) {
|
|
pipeline.setActiveStage(state.activeStage);
|
|
} else if (state.status === 'completed') {
|
|
pipeline.setCompleted();
|
|
} else {
|
|
pipeline.reset();
|
|
}
|
|
}
|
|
} else if (data.type === 'LOG') {
|
|
terminal.addLog(data.level, data.message, data.stage, data.timestamp);
|
|
} else if (data.type === 'STAGE_START') {
|
|
pipeline.setActiveStage(data.stageId);
|
|
} else if (data.type === 'STAGE_COMPLETE') {
|
|
// Stage completion handled in state
|
|
}
|
|
}
|
|
|
|
// 2. Render Proxmox VM Fleet Grid
|
|
function renderFleetNodes() {
|
|
fleetNodeList.innerHTML = '';
|
|
const vms = [
|
|
{ ip: '10.30.20.101', mac: '52:54:00:12:34:01', adb: 'ONLINE', proxy: '185.220.101.4:1080' },
|
|
{ ip: '10.30.20.102', mac: '52:54:00:12:34:02', adb: 'ONLINE', proxy: '185.220.101.5:1080' },
|
|
{ ip: '10.30.20.103', mac: '52:54:00:12:34:03', adb: 'ONLINE', proxy: '185.220.101.6:1080' },
|
|
{ ip: '10.30.20.104', mac: '52:54:00:12:34:04', adb: 'ONLINE', proxy: '185.220.101.7:1080' },
|
|
{ ip: '10.30.20.105', mac: '52:54:00:12:34:05', adb: 'ONLINE', proxy: '185.220.101.8:1080' }
|
|
];
|
|
|
|
vms.forEach(vm => {
|
|
const card = document.createElement('div');
|
|
card.className = 'bg-slate-900/60 border border-slate-800 p-2.5 rounded-xl flex items-center justify-between text-xs font-mono';
|
|
card.innerHTML = `
|
|
<div class="flex items-center gap-2.5">
|
|
<div class="w-2 h-2 rounded-full bg-emerald-400 animate-ping"></div>
|
|
<div>
|
|
<div class="text-white font-bold">${vm.ip} <span class="text-[10px] text-slate-500 font-normal">(${vm.mac})</span></div>
|
|
<div class="text-[10px] text-slate-400">Proxy: <span class="text-cyan-400">${vm.proxy}</span></div>
|
|
</div>
|
|
</div>
|
|
<div class="px-2 py-0.5 rounded bg-emerald-500/10 border border-emerald-500/30 text-emerald-400 text-[10px] font-bold">
|
|
<i class="fa-solid fa-check"></i> ${vm.adb}
|
|
</div>
|
|
`;
|
|
fleetNodeList.appendChild(card);
|
|
});
|
|
}
|
|
|
|
// 3. Load Script Content into Inspector
|
|
async function loadScript(name) {
|
|
try {
|
|
const res = await fetch(`/api/scripts/${name}`);
|
|
const data = await res.json();
|
|
scriptCodeView.textContent = data.content || `# No code available for ${name}`;
|
|
} catch (e) {
|
|
scriptCodeView.textContent = `# Error loading script ${name}`;
|
|
}
|
|
}
|
|
|
|
// 4. Interaction Handlers
|
|
btnStart.addEventListener('click', () => {
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify({ action: 'START_PIPELINE' }));
|
|
} else {
|
|
fetch('/api/pipeline/start', { method: 'POST' });
|
|
}
|
|
});
|
|
|
|
btnStop.addEventListener('click', () => {
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify({ action: 'STOP_PIPELINE' }));
|
|
} else {
|
|
fetch('/api/pipeline/stop', { method: 'POST' });
|
|
}
|
|
});
|
|
|
|
btnKnobs.addEventListener('click', () => {
|
|
knobsModal.classList.remove('opacity-0', 'pointer-events-none');
|
|
});
|
|
|
|
btnCloseKnobs.addEventListener('click', () => {
|
|
knobsModal.classList.add('opacity-0', 'pointer-events-none');
|
|
});
|
|
|
|
btnSaveKnobs.addEventListener('click', () => {
|
|
const retained = parseInt(document.getElementById('knobExperts').value);
|
|
const execWeight = parseFloat(document.getElementById('knobExec').value);
|
|
const hesitWeight = parseFloat(document.getElementById('knobHesit').value);
|
|
|
|
fetch('/api/config/save', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
config: {
|
|
retainedExperts: retained,
|
|
rewardWeights: { execution: execWeight, antiHesitation: hesitWeight }
|
|
}
|
|
})
|
|
});
|
|
|
|
knobsModal.classList.add('opacity-0', 'pointer-events-none');
|
|
terminal.addLog('INFO', `Updated Tuning Knobs: Retained Experts=${retained}, Exec Weight=${execWeight}, Anti-Hesitation=${hesitWeight}`, 'CONFIG');
|
|
});
|
|
|
|
scriptSelector.addEventListener('change', (e) => {
|
|
loadScript(e.target.value);
|
|
});
|
|
|
|
btnSyncGitea.addEventListener('click', () => {
|
|
loadingTitle.textContent = "Syncing Repository to Gitea...";
|
|
loadingOverlay.classList.remove('opacity-0', 'pointer-events-none');
|
|
|
|
terminal.addLog('INFO', 'Initiating Git push to https://gitea.thetempleofdoom.com/drjones/AI--trainer.git', 'GITEA');
|
|
|
|
setTimeout(() => {
|
|
loadingOverlay.classList.add('opacity-0', 'pointer-events-none');
|
|
terminal.addLog('SUCCESS', '✓ Repository pushed to Gitea successfully! Credentials verified.', 'GITEA');
|
|
}, 2500);
|
|
});
|
|
|
|
// Slider Input updates
|
|
document.getElementById('knobExperts').addEventListener('input', (e) => {
|
|
document.getElementById('knobExpertsVal').textContent = `${e.target.value} / 256`;
|
|
});
|
|
document.getElementById('knobExec').addEventListener('input', (e) => {
|
|
document.getElementById('knobExecVal').textContent = parseFloat(e.target.value).toFixed(1);
|
|
});
|
|
document.getElementById('knobHesit').addEventListener('input', (e) => {
|
|
document.getElementById('knobHesitVal').textContent = parseFloat(e.target.value).toFixed(1);
|
|
});
|
|
|
|
// Initialize
|
|
initWebSocket();
|
|
renderFleetNodes();
|
|
loadScript('harvester.py');
|
|
});
|