KAOS/code/web/templates/console.html
djuka c970cb2419 Konzola: xterm.js + WebSocket + PTY real-time terminal
- Nova pty_session.go: RingBuffer, consolePTYSession, spawnConsolePTY
- Nova ws.go: WebSocket handler za PTY bidirekcioni I/O
- console.go: koristi consolePTYSession umesto starih pipe-ova
- console.html: xterm.js 5.5.0 CDN, FitAddon, WebLinksAddon
- Podrška za resize, binarni podaci, replay buffer (1MB)
- 8 novih testova (RingBuffer + xterm konzola) — ukupno 179

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 15:31:27 +00:00

320 lines
11 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">
<button class="btn" id="toggle-panel" onclick="togglePanel2()">+ Sesija 2</button>
</div>
<div class="console-panels">
<div class="console-panel" id="panel-1">
<div class="console-panel-header">
<span>🔧 Sesija 1</span>
<span class="session-status" id="status-1">idle</span>
<button class="btn btn-kill" id="kill-1" onclick="killSession(1)" style="display:none">Prekini</button>
</div>
<div class="console-terminal" id="terminal-1"></div>
<div class="console-input-row">
<input type="text" id="input-1" class="console-input" placeholder="Komanda za claude..." onkeydown="handleKey(event, 1)" autocomplete="off">
<button class="btn btn-move" onclick="sendCommand(1)"></button>
</div>
</div>
<div class="console-panel" id="panel-2" style="display:none">
<div class="console-panel-header">
<span>🔧 Sesija 2</span>
<span class="session-status" id="status-2">idle</span>
<button class="btn btn-kill" id="kill-2" onclick="killSession(2)" style="display:none">Prekini</button>
</div>
<div class="console-terminal" id="terminal-2"></div>
<div class="console-input-row">
<input type="text" id="input-2" class="console-input" placeholder="Komanda za claude..." onkeydown="handleKey(event, 2)" autocomplete="off">
<button class="btn btn-move" onclick="sendCommand(2)"></button>
</div>
</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: '#111', foreground: '#e0e0e0', cursor: '#e94560', cursorAccent: '#111',
selectionBackground: 'rgba(233,69,96,0.3)',
black: '#111', 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 sessions = [{}, {}];
var historyIdx = [0, 0];
var cmdHistory = [[], []];
function initTerminal(idx) {
var num = idx + 1;
var containerEl = document.getElementById('terminal-' + num);
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(containerEl);
// Keyboard input → WebSocket
term.onData(function(data) {
var ws = sessions[idx].ws;
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(data);
}
});
// Resize → WebSocket
term.onResize(function(size) {
var ws = sessions[idx].ws;
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'resize', cols: size.cols, rows: size.rows }));
}
});
containerEl.addEventListener('click', function() { term.focus(); });
sessions[idx].term = term;
sessions[idx].fitAddon = fitAddon;
sessions[idx].ws = null;
setTimeout(function() { fitAddon.fit(); }, 50);
}
// ── WebSocket connection ─────────────────────────────
function connectWS(idx) {
var num = idx + 1;
var sess = sessions[idx];
if (sess.ws) {
sess.ws.close();
sess.ws = null;
}
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
var url = proto + '//' + location.host + '/console/ws/' + num;
var ws = new WebSocket(url);
ws.binaryType = 'arraybuffer';
ws.onopen = function() {
setSessionUI(num, 'running');
// Send initial size
var term = sess.term;
ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
term.focus();
};
ws.onmessage = function(event) {
if (event.data instanceof ArrayBuffer) {
sess.term.write(new Uint8Array(event.data));
} else {
sess.term.write(event.data);
}
};
ws.onclose = function() {
sess.ws = null;
setSessionUI(num, 'idle');
};
ws.onerror = function() {
sess.ws = null;
setSessionUI(num, 'idle');
};
sess.ws = ws;
}
// ── Command handling ─────────────────────────────────
function handleKey(e, session) {
if (e.key === 'Enter') {
sendCommand(session);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
var idx = session - 1;
if (historyIdx[idx] > 0) {
historyIdx[idx]--;
document.getElementById('input-' + session).value = cmdHistory[idx][historyIdx[idx]] || '';
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
var idx = session - 1;
if (historyIdx[idx] < cmdHistory[idx].length) {
historyIdx[idx]++;
document.getElementById('input-' + session).value = cmdHistory[idx][historyIdx[idx]] || '';
}
}
}
function sendCommand(session) {
var input = document.getElementById('input-' + session);
var cmd = input.value.trim();
if (!cmd) return;
var idx = session - 1;
cmdHistory[idx].push(cmd);
historyIdx[idx] = cmdHistory[idx].length;
input.value = '';
// Clear terminal for new command
sessions[idx].term.clear();
setSessionUI(session, 'running');
fetch('/console/exec', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({cmd: cmd, session: session})
})
.then(function(resp) {
if (!resp.ok) {
return resp.json().then(function(data) {
sessions[idx].term.write('\r\n\x1b[31m' + data.error + '\x1b[0m\r\n');
setSessionUI(session, 'idle');
throw new Error(data.error);
});
}
return resp.json();
})
.then(function(data) {
if (!data) return;
// Connect WebSocket to the PTY session
connectWS(idx);
})
.catch(function(err) {
setSessionUI(session, 'idle');
});
}
function killSession(session) {
fetch('/console/kill/' + session, {method: 'POST'})
.then(function() {
var idx = session - 1;
sessions[idx].term.write('\r\n\x1b[33m--- prekinuto ---\x1b[0m\r\n');
if (sessions[idx].ws) {
sessions[idx].ws.close();
sessions[idx].ws = null;
}
setSessionUI(session, 'idle');
});
}
function setSessionUI(session, status) {
document.getElementById('status-' + session).textContent = status;
document.getElementById('status-' + session).className = 'session-status session-' + status;
document.getElementById('kill-' + session).style.display = status === 'running' ? 'inline-block' : 'none';
}
function togglePanel2() {
var panel = document.getElementById('panel-2');
var btn = document.getElementById('toggle-panel');
if (panel.style.display === 'none') {
panel.style.display = 'flex';
btn.textContent = '- Sesija 2';
// Initialize terminal 2 if not yet done
if (!sessions[1].term) {
initTerminal(1);
} else {
sessions[1].fitAddon.fit();
}
} else {
panel.style.display = 'none';
btn.textContent = '+ Sesija 2';
}
}
// ── Theme sync ───────────────────────────────────────
var origSetTheme = window.setTheme;
window.setTheme = function(mode) {
if (origSetTheme) origSetTheme(mode);
// Update terminal themes after a tick
setTimeout(function() {
var theme = getTermTheme();
for (var i = 0; i < 2; i++) {
if (sessions[i].term) {
sessions[i].term.options.theme = theme;
}
}
}, 50);
};
// ── Window resize ────────────────────────────────────
window.addEventListener('resize', function() {
for (var i = 0; i < 2; i++) {
if (sessions[i].fitAddon) {
sessions[i].fitAddon.fit();
}
}
});
// ── Initialize ───────────────────────────────────────
initTerminal(0);
</script>
</body>
</html>
{{end}}