- 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>
265 lines
9.2 KiB
HTML
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}}
|