Implement XIC v2 control flow with IF, MATCH, LOOP operations

PHASE A: Safe predicate evaluator (glyphos/control/predicate.py)
- AST-based safe expression evaluation
- Supports comparisons, boolean ops, attribute access
- Helper function: dominant_contains()
- Protected against code injection attacks

PHASE B: XICContext queue helpers
- enqueue_chain(label) for FIFO chain scheduling
- pop_next_chain() to get next scheduled chain
- jump_to(label) for immediate destination changes

PHASE C: Control flow operations (xic_ops.py)
- op_IF: Conditional branching with optional else
- op_MATCH: Pattern matching against fused fields
- op_LOOP: Iterative execution with guardrails
- Added to OP_TABLE for operation dispatch

PHASE D: Execution loop enhancement (xic_vm.py)
- Chain queue scheduling with label matching
- Total steps tracking for guardrail enforcement
- max_total_steps limit across all operations
- Graceful execution stop on guardrail trigger

PHASE E: Comprehensive test suite (tests/test_control_flow.py)
- 14 unit tests covering all operations
- Predicate evaluator tests
- IF/MATCH/LOOP operation tests
- Queue helper and guardrail tests
- All tests passing (14/14)

PHASE F: Example programs
- demo_control_flow_if.gx.json: IF branching example
- demo_control_flow_loop.gx.json: LOOP iteration example

PHASE G: Complete documentation
- XIC_V2_CONTROL_FLOW_SUMMARY.md: Technical guide
- XIC_V2_QUICK_REFERENCE.md: Developer quick reference
- FedMart UI and integration documentation

Integration points:
- FedMart telemetry captures control flow steps
- UI dashboard displays control branching
- Symbolic pipeline predicate evaluation
- 100% backward compatible with XIC v1.5

Test results: 36/36 passing (14 control flow + 12 FedMart + 10 UI)
Status: Production ready
This commit is contained in:
GlyphRunner System
2026-05-21 03:40:39 -04:00
parent 8f55949b11
commit c3a826b65c
17 changed files with 4299 additions and 3 deletions
+87
View File
@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XIC Pipeline Monitor - FedMart</title>
<link rel="stylesheet" href="xic_panel.css">
</head>
<body>
<div class="xic-monitor-container">
<header class="xic-header">
<h1>XIC Symbolic Pipeline Monitor</h1>
<div class="header-info">
<span class="run-status" id="runStatus">Ready</span>
<button id="connectBtn" class="btn-primary">Connect to Feed</button>
</div>
</header>
<main class="xic-main">
<!-- Pipeline Timeline Panel -->
<section class="panel run-timeline">
<h2>Pipeline Execution Timeline</h2>
<div class="timeline-content" id="timelineContent">
<p class="placeholder">Waiting for pipeline execution...</p>
</div>
<div class="timeline-meta">
<span id="stepCount">Steps: 0</span>
<span id="execTime">Time: 0ms</span>
</div>
</section>
<!-- Resonance Heatmap Panel -->
<section class="panel resonance-heatmap">
<h2>Glyph Resonance Heatmap</h2>
<div class="heatmap-legend">
<span><strong>Weight Scale:</strong></span>
<div class="legend-bar">
<span class="low">0.0</span>
<span class="mid">0.5</span>
<span class="high">1.0</span>
</div>
</div>
<canvas id="heatmapCanvas" width="600" height="200"></canvas>
<div id="glyphDetails" class="glyph-details"></div>
</section>
<!-- Glyph Inspector Panel -->
<section class="panel glyph-inspector">
<h2>Glyph Resonance Inspector</h2>
<div class="inspector-toolbar">
<label for="glyphSelect">Select Glyph:</label>
<select id="glyphSelect"></select>
</div>
<div id="inspectorContent" class="inspector-content">
<p class="placeholder">Select a glyph to view metrics...</p>
</div>
</section>
<!-- Guardrail Control Panel -->
<section class="panel guardrail-control">
<h2>Guardrail Status & Control</h2>
<div id="guardrailList" class="guardrail-list">
<p class="placeholder">No guardrails triggered</p>
</div>
<div class="control-buttons">
<button id="pauseBtn" class="btn-warning" disabled>⏸ Pause Run</button>
<button id="throttleBtn" class="btn-warning" disabled>⚠ Throttle 50%</button>
</div>
</section>
<!-- Spec Coverage Panel -->
<section class="panel spec-coverage">
<h2>XIC Specification Coverage</h2>
<div id="specStatus" class="spec-status">
<p class="placeholder">Loading specification status...</p>
</div>
</section>
</main>
<footer class="xic-footer">
<p>XIC v1.5 Symbolic Pipeline | Real-time Telemetry via FedMart</p>
</footer>
</div>
<script src="xic_panel.js"></script>
</body>
</html>
+428
View File
@@ -0,0 +1,428 @@
/* XIC Pipeline Monitor Stylesheet */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #1e1e1e;
color: #e0e0e0;
line-height: 1.6;
}
.xic-monitor-container {
display: flex;
flex-direction: column;
min-height: 100vh;
max-width: 1400px;
margin: 0 auto;
}
/* Header */
.xic-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px 30px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
display: flex;
justify-content: space-between;
align-items: center;
}
.xic-header h1 {
font-size: 28px;
font-weight: 600;
letter-spacing: 0.5px;
}
.header-info {
display: flex;
gap: 15px;
align-items: center;
}
.run-status {
padding: 8px 16px;
background: rgba(255, 255, 255, 0.2);
border-radius: 6px;
font-size: 14px;
font-weight: 500;
}
.run-status.active {
background: #4caf50;
color: white;
}
.run-status.error {
background: #f44336;
color: white;
}
/* Buttons */
.btn-primary, .btn-warning {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary {
background: #4caf50;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #45a049;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.btn-warning {
background: #ff9800;
color: white;
}
.btn-warning:hover:not(:disabled) {
background: #e68900;
}
.btn-warning:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Main Content */
.xic-main {
flex: 1;
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 20px;
padding: 30px;
overflow-y: auto;
}
.panel {
background: #2d2d2d;
border: 1px solid #444;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
}
.panel h2 {
font-size: 18px;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
color: #667eea;
}
.panel > :nth-child(2) {
flex: 1;
}
/* Timeline */
.run-timeline {
grid-column: 1 / -1;
}
.timeline-content {
flex: 1;
overflow-y: auto;
max-height: 200px;
border: 1px solid #444;
border-radius: 4px;
padding: 10px;
}
.timeline-step {
padding: 8px 12px;
margin: 4px 0;
background: #3a3a3a;
border-left: 3px solid #667eea;
border-radius: 3px;
font-size: 13px;
transition: background 0.2s;
}
.timeline-step:hover {
background: #444;
}
.timeline-step.control {
border-left-color: #ff9800;
}
.timeline-step.guardrail {
border-left-color: #f44336;
background: #3a2a2a;
}
.timeline-meta {
display: flex;
gap: 20px;
margin-top: 10px;
font-size: 13px;
color: #aaa;
}
/* Heatmap */
.heatmap-legend {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
font-size: 13px;
}
.legend-bar {
display: flex;
width: 200px;
height: 20px;
background: linear-gradient(90deg, #0066cc 0%, #00cc66 50%, #ff9900 100%);
border-radius: 3px;
position: relative;
}
.legend-bar span {
position: absolute;
font-size: 11px;
color: white;
font-weight: bold;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.legend-bar .low { left: 5px; }
.legend-bar .mid { left: 50%; transform: translateX(-50%); }
.legend-bar .high { right: 5px; }
#heatmapCanvas {
width: 100%;
height: 200px;
border: 1px solid #444;
border-radius: 4px;
display: block;
margin: 10px 0;
background: #1a1a1a;
}
.glyph-details {
font-size: 13px;
margin-top: 10px;
max-height: 100px;
overflow-y: auto;
}
.glyph-item {
padding: 5px 0;
border-bottom: 1px solid #444;
}
.glyph-item:last-child {
border-bottom: none;
}
/* Inspector */
.inspector-toolbar {
display: flex;
gap: 10px;
margin-bottom: 15px;
align-items: center;
}
.inspector-toolbar label {
font-weight: 500;
font-size: 14px;
}
.inspector-toolbar select {
flex: 1;
padding: 8px 12px;
background: #3a3a3a;
color: #e0e0e0;
border: 1px solid #444;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
}
.inspector-content {
flex: 1;
overflow-y: auto;
padding: 10px;
background: #1a1a1a;
border-radius: 4px;
border: 1px solid #444;
}
.metric-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #3a3a3a;
font-size: 13px;
}
.metric-row:last-child {
border-bottom: none;
}
.metric-label {
font-weight: 500;
color: #aaa;
}
.metric-value {
color: #667eea;
font-family: 'Courier New', monospace;
}
/* Guardrail Control */
.guardrail-list {
flex: 1;
overflow-y: auto;
margin-bottom: 15px;
}
.guardrail-event {
padding: 10px;
margin: 5px 0;
background: #3a2a2a;
border-left: 4px solid #f44336;
border-radius: 3px;
font-size: 13px;
}
.guardrail-event.warning {
border-left-color: #ff9800;
background: #3a3a2a;
}
.control-buttons {
display: flex;
gap: 10px;
}
.control-buttons button {
flex: 1;
}
/* Spec Coverage */
.spec-status {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
}
.spec-entry {
padding: 12px;
background: #3a3a3a;
border-radius: 4px;
font-size: 12px;
border: 1px solid #444;
}
.spec-entry.implemented {
border-left: 4px solid #4caf50;
}
.spec-entry.validated {
border-left: 4px solid #2196f3;
}
.spec-entry.pending {
border-left: 4px solid #ff9800;
}
.spec-entry-name {
font-weight: 600;
margin-bottom: 5px;
color: #e0e0e0;
}
.spec-entry-status {
display: inline-block;
padding: 2px 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
font-size: 11px;
text-transform: uppercase;
}
.spec-entry.implemented .spec-entry-status {
background: #4caf50;
color: white;
}
.spec-entry.validated .spec-entry-status {
background: #2196f3;
color: white;
}
.spec-entry.pending .spec-entry-status {
background: #ff9800;
color: white;
}
/* Placeholder & Empty States */
.placeholder {
color: #666;
font-style: italic;
text-align: center;
padding: 20px;
}
/* Footer */
.xic-footer {
background: #1a1a1a;
border-top: 1px solid #444;
padding: 15px 30px;
text-align: center;
color: #888;
font-size: 12px;
}
/* Responsive */
@media (max-width: 1024px) {
.xic-main {
grid-template-columns: 1fr;
}
.run-timeline {
grid-column: 1;
}
}
/* Alert styles */
.alert {
padding: 12px 16px;
border-radius: 4px;
margin-bottom: 10px;
font-size: 13px;
}
.alert.error {
background: #3a2a2a;
border-left: 4px solid #f44336;
color: #ff6b6b;
}
.alert.warning {
background: #3a3a2a;
border-left: 4px solid #ff9800;
color: #ffb74d;
}
.alert.success {
background: #2a3a2a;
border-left: 4px solid #4caf50;
color: #81c784;
}
+440
View File
@@ -0,0 +1,440 @@
/**
* 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.');
});