KAOS/code/web/templates/console.html
djuka 510b75c0bf Konzola: dinamičke task sesije sa PTY per task
- Zamena fiksnih 2 sesija sa taskSessionManager (map po task ID)
- "Pusti" pokreće interaktivni claude u PTY, šalje task prompt
- "Proveri" pokreće review claude sesiju za task u review/
- WS se konektuje na postojeću PTY sesiju po task ID-u
- Konzola stranica dinamički prikazuje terminale za aktivne sesije
- Replay buffer za reconnect na postojeće sesije
- Novi testovi za session manager, prompt buildere, review endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 04:32:34 +00:00

265 lines
9.2 KiB
HTML

{{define "console"}}
<!DOCTYPE html>
<html lang="sr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>KAOS — Konzola</title>
<script>(function(){var m=localStorage.getItem('kaos-theme')||'dark',t=m;if(m==='auto'){t=window.matchMedia('(prefers-color-scheme:light)').matches?'light':'dark'}document.documentElement.setAttribute('data-theme',t)})()</script>
<link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.css">
<script src="/static/htmx.min.js"></script>
<script src="/static/theme.js"></script>
</head>
<body>
<div class="header">
<h1>KAOS Dashboard</h1>
<div class="header-right">
<div class="theme-toggle">
<button class="theme-btn" data-theme-mode="light" onclick="setTheme('light')" title="Svetla tema">☀️</button>
<button class="theme-btn" data-theme-mode="dark" onclick="setTheme('dark')" title="Tamna tema">🌙</button>
<button class="theme-btn" data-theme-mode="auto" onclick="setTheme('auto')" title="Sistemska tema">🔄</button>
</div>
<nav class="nav">
<a href="/" class="btn">Kanban</a>
<a href="/docs" class="btn">Dokumenti</a>
<a href="/console" class="btn btn-active">Konzola</a>
<a href="/submit" class="btn">Prijava</a>
</nav>
</div>
</div>
<div class="console-container">
<div class="console-toolbar">
<span id="session-info">Sesije: 0</span>
</div>
<div class="console-panels" id="panels">
<div class="console-empty" id="empty-state">
<p>Nema aktivnih sesija. Kliknite "Pusti" na tasku da pokrenete rad.</p>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
<script>
// ── Terminal themes ──────────────────────────────────
var TERM_THEMES = {
dark: {
background: '#0d1117', foreground: '#e0e0e0', cursor: '#e94560', cursorAccent: '#0d1117',
selectionBackground: 'rgba(233,69,96,0.3)',
black: '#0d1117', red: '#f44336', green: '#4caf50', yellow: '#ff9800',
blue: '#2196f3', magenta: '#e94560', cyan: '#00bcd4', white: '#e0e0e0',
brightBlack: '#6c6c80', brightRed: '#ff6b81', brightGreen: '#66bb6a', brightYellow: '#ffb74d',
brightBlue: '#64b5f6', brightMagenta: '#ff6b81', brightCyan: '#4dd0e1', brightWhite: '#ffffff'
},
light: {
background: '#f5f6fa', foreground: '#1e293b', cursor: '#d63851', cursorAccent: '#f5f6fa',
selectionBackground: 'rgba(214,56,81,0.15)',
black: '#1e293b', red: '#dc322f', green: '#859900', yellow: '#b58900',
blue: '#268bd2', magenta: '#d63851', cyan: '#2aa198', white: '#eee8d5',
brightBlack: '#586e75', brightRed: '#cb4b16', brightGreen: '#586e75', brightYellow: '#657b83',
brightBlue: '#839496', brightMagenta: '#6c71c4', brightCyan: '#93a1a1', brightWhite: '#002b36'
}
};
function getTermTheme() {
var t = document.documentElement.getAttribute('data-theme') || 'dark';
return TERM_THEMES[t] || TERM_THEMES.dark;
}
// ── Session state ────────────────────────────────────
var terminals = {};
function sessionKey(sess) {
return sess.type === 'review' ? sess.task_id + '-review' : sess.task_id;
}
function createTerminal(sess) {
var key = sessionKey(sess);
if (terminals[key]) return;
document.getElementById('empty-state').style.display = 'none';
var panel = document.createElement('div');
panel.className = 'console-panel';
panel.id = 'panel-' + key;
var header = document.createElement('div');
header.className = 'console-panel-header';
var label = sess.task_id + (sess.type === 'review' ? ' [pregled]' : ' [rad]');
header.innerHTML = '<span>' + label + '</span>' +
'<span class="session-status session-running" id="status-' + key + '">' + sess.status + '</span>' +
'<button class="btn btn-sm" onclick="killSession(\'' + sess.task_id + '\', \'' + sess.type + '\')">Ugasi</button>';
var termDiv = document.createElement('div');
termDiv.className = 'console-terminal';
termDiv.id = 'terminal-' + key;
panel.appendChild(header);
panel.appendChild(termDiv);
document.getElementById('panels').appendChild(panel);
var theme = getTermTheme();
var term = new Terminal({
cursorBlink: true,
cursorStyle: 'block',
cursorInactiveStyle: 'outline',
fontSize: 14,
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace",
theme: theme,
allowProposedApi: true,
scrollback: 10000,
convertEol: false,
drawBoldTextInBrightColors: true
});
var fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.loadAddon(new WebLinksAddon.WebLinksAddon());
term.open(termDiv);
terminals[key] = { term: term, fitAddon: fitAddon, ws: null };
termDiv.addEventListener('click', function() { term.focus(); });
setTimeout(function() {
fitAddon.fit();
connectWS(key, term);
}, 100);
}
// ── WebSocket connection ─────────────────────────────
function connectWS(key, term) {
var sess = terminals[key];
if (!sess) return;
if (sess.ws) {
sess.ws.close();
sess.ws = null;
}
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
var url = proto + '//' + location.host + '/console/ws/' + key;
var ws = new WebSocket(url);
ws.binaryType = 'arraybuffer';
ws.onopen = function() {
var el = document.getElementById('status-' + key);
if (el) { el.textContent = 'connected'; el.className = 'session-status session-running'; }
ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
term.focus();
};
ws.onmessage = function(event) {
if (event.data instanceof ArrayBuffer) {
term.write(new Uint8Array(event.data));
} else {
term.write(event.data);
}
};
ws.onclose = function() {
sess.ws = null;
var el = document.getElementById('status-' + key);
if (el) { el.textContent = 'disconnected'; el.className = 'session-status'; }
};
ws.onerror = function() {
sess.ws = null;
};
// Keyboard input → WebSocket
term.onData(function(data) {
if (sess.ws && sess.ws.readyState === WebSocket.OPEN) {
sess.ws.send(data);
}
});
// Resize → WebSocket
term.onResize(function(size) {
if (sess.ws && sess.ws.readyState === WebSocket.OPEN) {
sess.ws.send(JSON.stringify({ type: 'resize', cols: size.cols, rows: size.rows }));
}
});
sess.ws = ws;
}
// ── Session management ───────────────────────────────
function killSession(taskID, type) {
fetch('/console/kill/' + taskID + '?type=' + type, { method: 'POST' })
.then(function(resp) { return resp.json(); })
.then(function() { refreshSessions(); });
}
function refreshSessions() {
fetch('/console/sessions')
.then(function(resp) { return resp.json(); })
.then(function(sessions) {
var info = document.getElementById('session-info');
info.textContent = 'Sesije: ' + sessions.length;
if (sessions.length === 0) {
document.getElementById('empty-state').style.display = 'block';
} else {
document.getElementById('empty-state').style.display = 'none';
}
var currentKeys = {};
sessions.forEach(function(sess) {
var key = sessionKey(sess);
currentKeys[key] = true;
createTerminal(sess);
var el = document.getElementById('status-' + key);
if (el && sess.status === 'exited') {
el.textContent = 'finished';
el.className = 'session-status session-done';
}
});
// Remove panels for sessions that no longer exist
Object.keys(terminals).forEach(function(key) {
if (!currentKeys[key]) {
var panel = document.getElementById('panel-' + key);
if (panel) panel.remove();
if (terminals[key].ws) terminals[key].ws.close();
delete terminals[key];
}
});
});
}
// ── Theme sync ───────────────────────────────────────
var origSetTheme = window.setTheme;
window.setTheme = function(mode) {
if (origSetTheme) origSetTheme(mode);
setTimeout(function() {
var theme = getTermTheme();
Object.keys(terminals).forEach(function(key) {
if (terminals[key].term) {
terminals[key].term.options.theme = theme;
}
});
}, 50);
};
// ── Window resize ────────────────────────────────────
window.addEventListener('resize', function() {
Object.keys(terminals).forEach(function(key) {
if (terminals[key].fitAddon) {
terminals[key].fitAddon.fit();
}
});
});
// ── Initialize ───────────────────────────────────────
refreshSessions();
setInterval(refreshSessions, 5000);
</script>
</body>
</html>
{{end}}