Konzola: sklonjen command input, detaljno WS logovanje

- 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>
This commit is contained in:
djuka 2026-02-20 15:52:51 +00:00
parent fa8aa59b29
commit d27eb900b1
2 changed files with 35 additions and 86 deletions

View File

@ -44,6 +44,8 @@ func (s *Server) handleConsoleWS(c *gin.Context) {
idx = 1 idx = 1
} }
log.Printf("WS[%s]: connected", sessionNum)
session := s.console.getSession(idx) session := s.console.getSession(idx)
// Wait for PTY session to be available (it gets set when a command is executed) // Wait for PTY session to be available (it gets set when a command is executed)
@ -52,12 +54,13 @@ func (s *Server) handleConsoleWS(c *gin.Context) {
session.mu.Unlock() session.mu.Unlock()
if ptySess == nil { if ptySess == nil {
// No active PTY — send message and wait log.Printf("WS[%s]: no PTY yet, polling...", sessionNum)
conn.WriteMessage(websocket.TextMessage, []byte("\r\n\033[33m[Nema aktivne sesije. Pokrenite komandu.]\033[0m\r\n")) conn.WriteMessage(websocket.TextMessage, []byte("\r\n\033[33m[Čekam pokretanje sesije...]\033[0m\r\n"))
// Poll for session to start // Poll for session to start (up to 30s)
ticker := time.NewTicker(500 * time.Millisecond) ticker := time.NewTicker(300 * time.Millisecond)
defer ticker.Stop() defer ticker.Stop()
timeout := time.After(30 * time.Second)
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
@ -65,25 +68,42 @@ func (s *Server) handleConsoleWS(c *gin.Context) {
ptySess = session.ptySess ptySess = session.ptySess
session.mu.Unlock() session.mu.Unlock()
if ptySess != nil { if ptySess != nil {
log.Printf("WS[%s]: PTY found after polling", sessionNum)
goto connected goto connected
} }
case <-timeout:
log.Printf("WS[%s]: timeout waiting for PTY", sessionNum)
conn.WriteMessage(websocket.TextMessage, []byte("\r\n\033[31m[Timeout — sesija nije pokrenuta]\033[0m\r\n"))
return
case <-c.Request.Context().Done(): case <-c.Request.Context().Done():
log.Printf("WS[%s]: client disconnected while polling", sessionNum)
return return
} }
} }
} }
connected: connected:
log.Printf("WS[%s]: subscribing to PTY", sessionNum)
subID := fmt.Sprintf("ws-%d", time.Now().UnixNano()) subID := fmt.Sprintf("ws-%d", time.Now().UnixNano())
outputCh := ptySess.Subscribe(subID) outputCh := ptySess.Subscribe(subID)
defer ptySess.Unsubscribe(subID) defer ptySess.Unsubscribe(subID)
// Send buffered output for reconnect // Send buffered output for reconnect
buffered := ptySess.GetBuffer() buffered := ptySess.GetBuffer()
log.Printf("WS[%s]: sending buffer (%d bytes)", sessionNum, len(buffered))
if len(buffered) > 0 { if len(buffered) > 0 {
conn.WriteMessage(websocket.BinaryMessage, buffered) conn.WriteMessage(websocket.BinaryMessage, buffered)
} }
// Check if already done
select {
case <-ptySess.Done():
log.Printf("WS[%s]: PTY already done, sending buffer only", sessionNum)
conn.WriteMessage(websocket.TextMessage, []byte("\r\n\033[33m[Sesija završena]\033[0m\r\n"))
return
default:
}
// Serialized write channel // Serialized write channel
writeCh := make(chan []byte, 256) writeCh := make(chan []byte, 256)
writeDone := make(chan struct{}) writeDone := make(chan struct{})
@ -109,11 +129,11 @@ connected:
// Watch for process exit // Watch for process exit
go func() { go func() {
<-ptySess.Done() <-ptySess.Done()
log.Printf("WS[%s]: PTY process exited", sessionNum)
select { select {
case writeCh <- []byte("\r\n\033[33m[Sesija završena]\033[0m\r\n"): case writeCh <- []byte("\r\n\033[33m[Sesija završena]\033[0m\r\n"):
default: default:
} }
// Give browser time to receive the message, then close
time.Sleep(500 * time.Millisecond) time.Sleep(500 * time.Millisecond)
conn.WriteMessage(websocket.CloseMessage, conn.WriteMessage(websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, "done")) websocket.FormatCloseMessage(websocket.CloseNormalClosure, "done"))
@ -124,7 +144,7 @@ connected:
_, msg, err := conn.ReadMessage() _, msg, err := conn.ReadMessage()
if err != nil { if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
log.Printf("WebSocket read error: %v", err) log.Printf("WS[%s]: read error: %v", sessionNum, err)
} }
break break
} }
@ -133,18 +153,19 @@ connected:
var resize wsResizeMsg var resize wsResizeMsg
if json.Unmarshal(msg, &resize) == nil && resize.Type == "resize" && resize.Cols > 0 && resize.Rows > 0 { if json.Unmarshal(msg, &resize) == nil && resize.Type == "resize" && resize.Cols > 0 && resize.Rows > 0 {
if err := ptySess.Resize(resize.Rows, resize.Cols); err != nil { if err := ptySess.Resize(resize.Rows, resize.Cols); err != nil {
log.Printf("PTY resize error: %v", err) log.Printf("WS[%s]: resize error: %v", sessionNum, err)
} }
continue continue
} }
// Regular keyboard input → PTY // Regular keyboard input → PTY
if _, err := ptySess.WriteInput(msg); err != nil { if _, err := ptySess.WriteInput(msg); err != nil {
log.Printf("PTY write error: %v", err) log.Printf("WS[%s]: PTY write error: %v", sessionNum, err)
break break
} }
} }
close(writeCh) close(writeCh)
<-writeDone <-writeDone
log.Printf("WS[%s]: disconnected", sessionNum)
} }

View File

@ -42,10 +42,6 @@
<button class="btn btn-kill" id="kill-1" onclick="killSession(1)" style="display:none">Prekini</button> <button class="btn btn-kill" id="kill-1" onclick="killSession(1)" style="display:none">Prekini</button>
</div> </div>
<div class="console-terminal" id="terminal-1"></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>
<div class="console-panel" id="panel-2" style="display:none"> <div class="console-panel" id="panel-2" style="display:none">
@ -55,10 +51,6 @@
<button class="btn btn-kill" id="kill-2" onclick="killSession(2)" style="display:none">Prekini</button> <button class="btn btn-kill" id="kill-2" onclick="killSession(2)" style="display:none">Prekini</button>
</div> </div>
<div class="console-terminal" id="terminal-2"></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> </div>
</div> </div>
@ -94,8 +86,6 @@ function getTermTheme() {
// ── Session state ──────────────────────────────────── // ── Session state ────────────────────────────────────
var sessions = [{}, {}]; var sessions = [{}, {}];
var historyIdx = [0, 0];
var cmdHistory = [[], []];
function initTerminal(idx) { function initTerminal(idx) {
var num = idx + 1; var num = idx + 1;
@ -142,10 +132,7 @@ function initTerminal(idx) {
sessions[idx].fitAddon = fitAddon; sessions[idx].fitAddon = fitAddon;
sessions[idx].ws = null; sessions[idx].ws = null;
setTimeout(function() { setTimeout(function() { fitAddon.fit(); }, 50);
fitAddon.fit();
term.write('\x1b[33mSesija ' + num + ' — upiši komandu dole i pritisni Enter\x1b[0m\r\n');
}, 50);
} }
// ── WebSocket connection ───────────────────────────── // ── WebSocket connection ─────────────────────────────
@ -158,6 +145,10 @@ function connectWS(idx) {
sess.ws = null; 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 proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
var url = proto + '//' + location.host + '/console/ws/' + num; var url = proto + '//' + location.host + '/console/ws/' + num;
var ws = new WebSocket(url); var ws = new WebSocket(url);
@ -165,7 +156,6 @@ function connectWS(idx) {
ws.onopen = function() { ws.onopen = function() {
setSessionUI(num, 'running'); setSessionUI(num, 'running');
// Send initial size
var term = sess.term; var term = sess.term;
ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows })); ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
term.focus(); term.focus();
@ -192,67 +182,7 @@ function connectWS(idx) {
sess.ws = ws; sess.ws = ws;
} }
// ── Command handling ───────────────────────────────── // ── UI helpers ───────────────────────────────────────
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) { function killSession(session) {
fetch('/console/kill/' + session, {method: 'POST'}) fetch('/console/kill/' + session, {method: 'POST'})
.then(function() { .then(function() {
@ -278,7 +208,6 @@ function togglePanel2() {
if (panel.style.display === 'none') { if (panel.style.display === 'none') {
panel.style.display = 'flex'; panel.style.display = 'flex';
btn.textContent = '- Sesija 2'; btn.textContent = '- Sesija 2';
// Initialize terminal 2 if not yet done
if (!sessions[1].term) { if (!sessions[1].term) {
initTerminal(1); initTerminal(1);
} else { } else {
@ -294,7 +223,6 @@ function togglePanel2() {
var origSetTheme = window.setTheme; var origSetTheme = window.setTheme;
window.setTheme = function(mode) { window.setTheme = function(mode) {
if (origSetTheme) origSetTheme(mode); if (origSetTheme) origSetTheme(mode);
// Update terminal themes after a tick
setTimeout(function() { setTimeout(function() {
var theme = getTermTheme(); var theme = getTermTheme();
for (var i = 0; i < 2; i++) { for (var i = 0; i < 2; i++) {