- Uklonjena input polja iz konzole — rad samo kroz Pusti dugme - Detaljno logovanje WS: connect, poll, subscribe, buffer, disconnect - WS timeout 30s ako nema PTY sesije - Provera da li je PTY already done pre nego subscribe Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
269 lines
9.1 KiB
HTML
269 lines
9.1 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>
|
|
|
|
<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>
|
|
</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 = [{}, {}];
|
|
|
|
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;
|
|
}
|
|
|
|
// Clear terminal for new session
|
|
sess.term.clear();
|
|
sess.term.write('\x1b[33mPovezivanje na sesiju ' + num + '...\x1b[0m\r\n');
|
|
|
|
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');
|
|
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;
|
|
}
|
|
|
|
// ── UI helpers ───────────────────────────────────────
|
|
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';
|
|
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);
|
|
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);
|
|
|
|
// Auto-connect to running sessions on page load
|
|
fetch('/console/sessions')
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
for (var i = 0; i < data.length; i++) {
|
|
if (data[i].status === 'running') {
|
|
var idx = data[i].session - 1;
|
|
if (idx === 1 && !sessions[1].term) {
|
|
document.getElementById('panel-2').style.display = 'flex';
|
|
document.getElementById('toggle-panel').textContent = '- Sesija 2';
|
|
initTerminal(1);
|
|
}
|
|
connectWS(idx);
|
|
}
|
|
}
|
|
})
|
|
.catch(function() {});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
{{end}}
|