441 lines
14 KiB
JavaScript
Executable File
441 lines
14 KiB
JavaScript
Executable File
/**
|
|
* XIC Pipeline Monitor - Real-time Telemetry Dashboard
|
|
* Subscribes to FedMart telemetry feed and renders live pipeline state
|
|
*/
|
|
|
|
class XICMonitor {
|
|
constructor() {
|
|
this.ws = null;
|
|
this.telemetryBuffer = [];
|
|
this.currentRun = null;
|
|
this.glyphs = new Map(); // glyph_id → metrics
|
|
this.specStatus = {};
|
|
this.isConnected = false;
|
|
|
|
this.connectBtn = document.getElementById('connectBtn');
|
|
this.runStatusEl = document.getElementById('runStatus');
|
|
this.timelineContent = document.getElementById('timelineContent');
|
|
this.stepCountEl = document.getElementById('stepCount');
|
|
this.execTimeEl = document.getElementById('execTime');
|
|
this.heatmapCanvas = document.getElementById('heatmapCanvas');
|
|
this.glyphDetailsEl = document.getElementById('glyphDetails');
|
|
this.glyphSelect = document.getElementById('glyphSelect');
|
|
this.inspectorContent = document.getElementById('inspectorContent');
|
|
this.guardrailList = document.getElementById('guardrailList');
|
|
this.pauseBtn = document.getElementById('pauseBtn');
|
|
this.throttleBtn = document.getElementById('throttleBtn');
|
|
this.specStatusEl = document.getElementById('specStatus');
|
|
|
|
this.initEventListeners();
|
|
this.initCanvas();
|
|
}
|
|
|
|
initEventListeners() {
|
|
this.connectBtn.addEventListener('click', () => this.connectToFeed());
|
|
this.glyphSelect.addEventListener('change', (e) => this.showGlyphMetrics(e.target.value));
|
|
this.pauseBtn.addEventListener('click', () => this.pauseRun());
|
|
this.throttleBtn.addEventListener('click', () => this.throttleRun());
|
|
}
|
|
|
|
initCanvas() {
|
|
this.ctx = this.heatmapCanvas.getContext('2d');
|
|
this.drawHeatmapPlaceholder();
|
|
}
|
|
|
|
connectToFeed() {
|
|
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
|
const wsUrl = `${protocol}://${window.location.host}/ws/fedmart/xic`;
|
|
|
|
console.log(`[XIC] Connecting to ${wsUrl}`);
|
|
this.connectBtn.disabled = true;
|
|
this.connectBtn.textContent = 'Connecting...';
|
|
|
|
this.ws = new WebSocket(wsUrl);
|
|
|
|
this.ws.onopen = () => {
|
|
console.log('[XIC] WebSocket connected');
|
|
this.setStatus('Connected', 'active');
|
|
this.connectBtn.textContent = 'Connected ✓';
|
|
this.isConnected = true;
|
|
};
|
|
|
|
this.ws.onmessage = (event) => {
|
|
try {
|
|
const telemetry = JSON.parse(event.data);
|
|
this.processTelemetry(telemetry);
|
|
} catch (e) {
|
|
console.error('[XIC] Failed to parse telemetry:', e);
|
|
}
|
|
};
|
|
|
|
this.ws.onerror = (error) => {
|
|
console.error('[XIC] WebSocket error:', error);
|
|
this.setStatus('Connection Error', 'error');
|
|
this.connectBtn.textContent = 'Reconnect';
|
|
this.connectBtn.disabled = false;
|
|
this.isConnected = false;
|
|
};
|
|
|
|
this.ws.onclose = () => {
|
|
console.log('[XIC] WebSocket closed');
|
|
this.setStatus('Disconnected', 'error');
|
|
this.connectBtn.textContent = 'Connect to Feed';
|
|
this.connectBtn.disabled = false;
|
|
this.isConnected = false;
|
|
};
|
|
}
|
|
|
|
processTelemetry(telemetry) {
|
|
this.telemetryBuffer.push(telemetry);
|
|
this.currentRun = telemetry;
|
|
|
|
// Update timeline
|
|
this.renderTimeline(telemetry);
|
|
|
|
// Update glyph data
|
|
if (telemetry.resonance_map_summary && telemetry.resonance_map_summary.top_glyphs) {
|
|
this.updateGlyphData(telemetry.resonance_map_summary.top_glyphs);
|
|
this.populateGlyphSelector(telemetry.glyph_ids || []);
|
|
}
|
|
|
|
// Update heatmap
|
|
if (telemetry.resonance_map_summary && telemetry.resonance_map_summary.top_glyphs) {
|
|
this.renderHeatmap(telemetry.resonance_map_summary.top_glyphs, telemetry.global_resonance_score);
|
|
}
|
|
|
|
// Update guardrails
|
|
if (telemetry.guardrails_triggered && telemetry.guardrails_triggered.length > 0) {
|
|
this.showGuardrailAlerts(telemetry.guardrails_triggered);
|
|
} else {
|
|
this.clearGuardrailAlerts();
|
|
}
|
|
|
|
// Update spec status if available
|
|
if (telemetry.spec_status) {
|
|
this.updateSpecStatus(telemetry.spec_status);
|
|
}
|
|
|
|
// Update control button states
|
|
this.updateControlButtons(telemetry);
|
|
}
|
|
|
|
renderTimeline(telemetry) {
|
|
// Clear existing timeline
|
|
this.timelineContent.innerHTML = '';
|
|
|
|
// Parse steps from telemetry
|
|
const steps = [];
|
|
const rawPayload = telemetry.raw_payload || {};
|
|
|
|
// Add initial step
|
|
if (telemetry.program) {
|
|
steps.push({
|
|
name: `Program: ${telemetry.program}`,
|
|
kind: 'program',
|
|
timestamp: telemetry.timestamp,
|
|
});
|
|
}
|
|
|
|
// Add chain label if present
|
|
if (telemetry.chain_label) {
|
|
steps.push({
|
|
name: `Chain: ${telemetry.chain_label}`,
|
|
kind: 'chain',
|
|
timestamp: telemetry.timestamp,
|
|
});
|
|
}
|
|
|
|
// Add multi-glyph resonance step if glyphs present
|
|
if (telemetry.glyph_count > 0) {
|
|
steps.push({
|
|
name: `Multi-Glyph Resonance (${telemetry.glyph_count} glyphs)`,
|
|
kind: 'glyph',
|
|
timestamp: telemetry.timestamp,
|
|
});
|
|
}
|
|
|
|
// Add guardrail step if triggered
|
|
if (telemetry.guardrails_triggered && telemetry.guardrails_triggered.length > 0) {
|
|
steps.push({
|
|
name: `Guardrail: ${telemetry.guardrails_triggered[0]}`,
|
|
kind: 'guardrail',
|
|
timestamp: telemetry.timestamp,
|
|
});
|
|
}
|
|
|
|
// Add fusion step if fused symbol present
|
|
if (rawPayload.fused_symbol_summary) {
|
|
steps.push({
|
|
name: 'Fusion',
|
|
kind: 'fused_symbol',
|
|
timestamp: telemetry.timestamp,
|
|
});
|
|
}
|
|
|
|
// Render steps
|
|
if (steps.length === 0) {
|
|
this.timelineContent.innerHTML = '<p class="placeholder">No execution steps</p>';
|
|
} else {
|
|
steps.forEach((step) => {
|
|
const stepEl = document.createElement('div');
|
|
stepEl.className = `timeline-step ${step.kind}`;
|
|
stepEl.innerHTML = `<strong>${step.name}</strong>`;
|
|
this.timelineContent.appendChild(stepEl);
|
|
});
|
|
}
|
|
|
|
// Update metadata
|
|
this.stepCountEl.textContent = `Steps: ${steps.length}`;
|
|
this.execTimeEl.textContent = `Time: ${telemetry.steps_executed * 10}ms`; // Estimate
|
|
}
|
|
|
|
updateGlyphData(topGlyphs) {
|
|
this.glyphs.clear();
|
|
topGlyphs.forEach((glyph) => {
|
|
this.glyphs.set(glyph.glyph_id, {
|
|
weight: glyph.weight,
|
|
id: glyph.glyph_id,
|
|
});
|
|
});
|
|
|
|
// Update glyph details
|
|
this.renderGlyphDetails(topGlyphs);
|
|
}
|
|
|
|
renderGlyphDetails(topGlyphs) {
|
|
this.glyphDetailsEl.innerHTML = '';
|
|
|
|
if (topGlyphs.length === 0) {
|
|
this.glyphDetailsEl.innerHTML = '<p class="placeholder">No glyph data</p>';
|
|
return;
|
|
}
|
|
|
|
topGlyphs.forEach((glyph) => {
|
|
const itemEl = document.createElement('div');
|
|
itemEl.className = 'glyph-item';
|
|
const percentage = (glyph.weight * 100).toFixed(1);
|
|
itemEl.innerHTML = `
|
|
<strong>${glyph.glyph_id}</strong>: ${percentage}%
|
|
`;
|
|
this.glyphDetailsEl.appendChild(itemEl);
|
|
});
|
|
}
|
|
|
|
populateGlyphSelector(glyphIds) {
|
|
const currentValue = this.glyphSelect.value;
|
|
this.glyphSelect.innerHTML = '<option value="">-- Select a glyph --</option>';
|
|
|
|
glyphIds.forEach((id) => {
|
|
const option = document.createElement('option');
|
|
option.value = id;
|
|
option.textContent = id;
|
|
this.glyphSelect.appendChild(option);
|
|
});
|
|
|
|
// Restore previous selection if still available
|
|
if (currentValue && glyphIds.includes(currentValue)) {
|
|
this.glyphSelect.value = currentValue;
|
|
}
|
|
}
|
|
|
|
showGlyphMetrics(glyphId) {
|
|
if (!glyphId || !this.glyphs.has(glyphId)) {
|
|
this.inspectorContent.innerHTML = '<p class="placeholder">Select a glyph to view metrics...</p>';
|
|
return;
|
|
}
|
|
|
|
const glyph = this.glyphs.get(glyphId);
|
|
this.inspectorContent.innerHTML = `
|
|
<div class="metric-row">
|
|
<span class="metric-label">Glyph ID</span>
|
|
<span class="metric-value">${glyphId}</span>
|
|
</div>
|
|
<div class="metric-row">
|
|
<span class="metric-label">Resonance Weight</span>
|
|
<span class="metric-value">${(glyph.weight * 100).toFixed(1)}%</span>
|
|
</div>
|
|
<div class="metric-row">
|
|
<span class="metric-label">Status</span>
|
|
<span class="metric-value">Active</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderHeatmap(topGlyphs, globalScore) {
|
|
const width = this.heatmapCanvas.width;
|
|
const height = this.heatmapCanvas.height;
|
|
|
|
// Clear canvas
|
|
this.ctx.fillStyle = '#1a1a1a';
|
|
this.ctx.fillRect(0, 0, width, height);
|
|
|
|
if (topGlyphs.length === 0) {
|
|
this.ctx.fillStyle = '#666';
|
|
this.ctx.font = '14px sans-serif';
|
|
this.ctx.textAlign = 'center';
|
|
this.ctx.fillText('No glyph data', width / 2, height / 2);
|
|
return;
|
|
}
|
|
|
|
// Draw heatmap bars
|
|
const barWidth = width / topGlyphs.length;
|
|
const maxWeight = Math.max(...topGlyphs.map((g) => g.weight), globalScore);
|
|
|
|
topGlyphs.forEach((glyph, index) => {
|
|
const normalized = glyph.weight / (maxWeight || 1);
|
|
const color = this.colorForWeight(normalized);
|
|
|
|
// Draw bar
|
|
const barHeight = (normalized * height * 0.8) + 10;
|
|
const x = index * barWidth;
|
|
const y = height - barHeight;
|
|
|
|
this.ctx.fillStyle = color;
|
|
this.ctx.fillRect(x, y, barWidth - 2, barHeight);
|
|
|
|
// Draw label
|
|
this.ctx.fillStyle = '#e0e0e0';
|
|
this.ctx.font = '10px sans-serif';
|
|
this.ctx.textAlign = 'center';
|
|
this.ctx.fillText(glyph.glyph_id.slice(0, 6), x + barWidth / 2, height - 2);
|
|
});
|
|
|
|
// Draw border
|
|
this.ctx.strokeStyle = '#444';
|
|
this.ctx.lineWidth = 1;
|
|
this.ctx.strokeRect(0, 0, width, height);
|
|
}
|
|
|
|
drawHeatmapPlaceholder() {
|
|
const width = this.heatmapCanvas.width;
|
|
const height = this.heatmapCanvas.height;
|
|
|
|
this.ctx.fillStyle = '#1a1a1a';
|
|
this.ctx.fillRect(0, 0, width, height);
|
|
|
|
this.ctx.fillStyle = '#666';
|
|
this.ctx.font = '14px sans-serif';
|
|
this.ctx.textAlign = 'center';
|
|
this.ctx.fillText('Waiting for telemetry...', width / 2, height / 2);
|
|
|
|
this.ctx.strokeStyle = '#444';
|
|
this.ctx.lineWidth = 1;
|
|
this.ctx.strokeRect(0, 0, width, height);
|
|
}
|
|
|
|
colorForWeight(normalized) {
|
|
// Color gradient: blue (low) → green (mid) → orange (high)
|
|
if (normalized < 0.5) {
|
|
// Blue to green
|
|
const t = normalized * 2; // 0 to 1
|
|
const r = Math.round(0 * 255);
|
|
const g = Math.round(204 + (102 - 204) * (1 - t));
|
|
const b = Math.round(255);
|
|
return `rgb(${r}, ${g}, ${b})`;
|
|
} else {
|
|
// Green to orange
|
|
const t = (normalized - 0.5) * 2; // 0 to 1
|
|
const r = Math.round(153 + (255 - 153) * t);
|
|
const g = Math.round(204 - (204 - 153) * t);
|
|
const b = 0;
|
|
return `rgb(${r}, ${g}, ${b})`;
|
|
}
|
|
}
|
|
|
|
showGuardrailAlerts(guardrails) {
|
|
this.guardrailList.innerHTML = '';
|
|
|
|
guardrails.forEach((guardrail) => {
|
|
const eventEl = document.createElement('div');
|
|
eventEl.className = 'guardrail-event warning';
|
|
eventEl.innerHTML = `<strong>⚠ Guardrail Triggered</strong><br>${guardrail}`;
|
|
this.guardrailList.appendChild(eventEl);
|
|
});
|
|
|
|
// Enable control buttons
|
|
this.pauseBtn.disabled = false;
|
|
this.throttleBtn.disabled = false;
|
|
}
|
|
|
|
clearGuardrailAlerts() {
|
|
this.guardrailList.innerHTML = '<p class="placeholder">No guardrails triggered</p>';
|
|
this.pauseBtn.disabled = true;
|
|
this.throttleBtn.disabled = true;
|
|
}
|
|
|
|
updateControlButtons(telemetry) {
|
|
const hasGuardrails = telemetry.guardrails_triggered && telemetry.guardrails_triggered.length > 0;
|
|
|
|
if (hasGuardrails) {
|
|
this.pauseBtn.disabled = false;
|
|
this.throttleBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
updateSpecStatus(specMap) {
|
|
this.specStatusEl.innerHTML = '';
|
|
|
|
Object.entries(specMap).forEach(([instruction, status]) => {
|
|
const entryEl = document.createElement('div');
|
|
entryEl.className = `spec-entry ${status.status}`;
|
|
|
|
entryEl.innerHTML = `
|
|
<div class="spec-entry-name">${instruction}</div>
|
|
<span class="spec-entry-status">${status.status.toUpperCase()}</span>
|
|
<div style="font-size: 11px; color: #aaa; margin-top: 4px;">Phase ${status.phase} · ${status.coverage}% coverage</div>
|
|
`;
|
|
|
|
this.specStatusEl.appendChild(entryEl);
|
|
});
|
|
}
|
|
|
|
pauseRun() {
|
|
if (!this.currentRun) return;
|
|
|
|
const runId = this.currentRun.run_id || 'unknown';
|
|
console.log(`[XIC] Pausing run ${runId}`);
|
|
|
|
fetch('/fedmart/control/pause', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ run_id: runId }),
|
|
})
|
|
.then((r) => r.json())
|
|
.then((data) => {
|
|
console.log('[XIC] Pause response:', data);
|
|
this.setStatus('Run Paused ⏸', 'warning');
|
|
})
|
|
.catch((e) => console.error('[XIC] Pause failed:', e));
|
|
}
|
|
|
|
throttleRun() {
|
|
if (!this.currentRun) return;
|
|
|
|
const runId = this.currentRun.run_id || 'unknown';
|
|
console.log(`[XIC] Throttling run ${runId}`);
|
|
|
|
fetch('/fedmart/control/throttle', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ run_id: runId, factor: 0.5 }),
|
|
})
|
|
.then((r) => r.json())
|
|
.then((data) => {
|
|
console.log('[XIC] Throttle response:', data);
|
|
this.setStatus('Run Throttled 50%', 'warning');
|
|
})
|
|
.catch((e) => console.error('[XIC] Throttle failed:', e));
|
|
}
|
|
|
|
setStatus(text, cssClass) {
|
|
this.runStatusEl.textContent = text;
|
|
this.runStatusEl.className = `run-status ${cssClass}`;
|
|
}
|
|
}
|
|
|
|
// Initialize monitor when DOM is ready
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.xicMonitor = new XICMonitor();
|
|
console.log('[XIC] Monitor initialized. Click "Connect to Feed" to start.');
|
|
});
|