GUI updates, live log visibility

This commit is contained in:
2026-02-14 17:55:14 +01:00
parent 9d7a36a143
commit e0e8e88276
5 changed files with 134 additions and 32 deletions
+4 -2
View File
@@ -30,7 +30,7 @@ Current version: **v0.2.1**
- Dashboard with live container state (green/orange/yellow/red)
- Deploy form with password validation, auto-generation, and field locking
- Stack operations: start, stop, restart, update (pull + recreate)
- Log viewer for each stack
- Live-scrolling log viewer with auto-refresh (3s polling), pause/resume, and scroll position tracking
- Deploy page doubles as config viewer (read-only mode for deployed apps)
- Periodic stack rescanning (every 2 minutes)
- Manual rescan endpoint (`POST /api/stacks/rescan`)
@@ -42,6 +42,8 @@ Current version: **v0.2.1**
- Memory summary bar shown on deploy page before deployment
- Felhom.eu logo SVG in sidebar and login page
- Verbose debug logging with operation timing, post-start container state checks, and image pull detection
- Clickable app cards on dashboard and applications pages (navigate to detail/deploy page)
- Memory bar with two-segment visualization on deploy page (committed vs new app allocation)
### Known issues / next priorities
- Cloudflare Tunnel + Traefik TLS: paperless.demo-felhom.eu works locally but shows "Not secure" (certificate chain not fully validated through tunnel)
@@ -361,7 +363,7 @@ docker compose up -d
| POST | `/api/stacks/{name}/stop` | Yes | Stop stack (not protected) |
| POST | `/api/stacks/{name}/restart` | Yes | Restart stack |
| POST | `/api/stacks/{name}/update` | Yes | Pull images + recreate |
| GET | `/api/stacks/{name}/logs` | Yes | Container logs |
| GET | `/api/stacks/{name}/logs` | Yes | Container logs (add `?raw=1` for plain text) |
| POST | `/api/stacks/rescan` | Yes | Trigger manual stack discovery |
| GET | `/api/system/info` | Yes | System resource usage (RAM, disk, HDD) |
+9 -2
View File
@@ -373,10 +373,10 @@ func (s *Server) stacksHandler(w http.ResponseWriter, _ *http.Request) {
s.render(w, "stacks", data)
}
func (s *Server) logsHandler(w http.ResponseWriter, _ *http.Request, name string) {
func (s *Server) logsHandler(w http.ResponseWriter, r *http.Request, name string) {
stack, ok := s.stackMgr.GetStack(name)
if !ok {
http.NotFound(w, nil)
http.NotFound(w, r)
return
}
@@ -385,6 +385,13 @@ func (s *Server) logsHandler(w http.ResponseWriter, _ *http.Request, name string
logs = fmt.Sprintf("Hiba a naplók lekérésekor: %v", err)
}
// Raw mode: return plain text for AJAX polling
if r.URL.Query().Get("raw") == "1" {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprint(w, logs)
return
}
data := s.baseData("logs", stack.Meta.DisplayName+" — Naplók")
data["Stack"] = stack
data["Logs"] = logs
+110 -10
View File
@@ -37,6 +37,11 @@ const layoutTmpl = `
{{define "layout_end"}}
</main>
<script>
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
@@ -169,7 +174,7 @@ const dashboardTmpl = `
<div class="stack-list">
{{range .Stacks}}
<div class="stack-card stack-state-{{stateColor .State}}">
<div class="stack-card stack-state-{{stateColor .State}}"{{if not .Protected}} data-href="/stacks/{{.Name}}/deploy"{{end}}>
<div class="stack-info">
<img class="stack-logo" src="{{logoURL .Meta.Slug}}"
alt="{{.Meta.DisplayName}}" onerror="this.onerror=function(){this.style.display='none'};this.src='{{logoPNGURL .Meta.Slug}}'">
@@ -220,7 +225,7 @@ const stacksTmpl = `
<div class="stack-grid">
{{range .Stacks}}
<div class="stack-detail-card stack-state-{{stateColor .State}}">
<div class="stack-detail-card stack-state-{{stateColor .State}}"{{if not .Protected}} data-href="/stacks/{{.Name}}/deploy"{{end}}>
<div class="stack-detail-header">
<div class="stack-title-row">
<img class="stack-logo-lg" src="{{logoURL .Meta.Slug}}"
@@ -546,12 +551,70 @@ const logsTmpl = `
<a href="/stacks" class="btn btn-sm btn-outline">← Vissza</a>
<h2>{{.Stack.Meta.DisplayName}} — Naplók</h2>
</div>
<div class="logs-container">
<pre class="logs-output">{{.Logs}}</pre>
<div class="logs-container" id="logs-container">
<pre class="logs-output" id="logs-output">{{.Logs}}</pre>
</div>
<div class="logs-actions">
<button class="btn btn-outline" onclick="window.location.reload()">Frissítés</button>
<span class="logs-live-indicator" id="live-indicator">
<span class="logs-live-dot"></span> Élő
</span>
<button class="btn btn-outline btn-sm" id="logs-toggle" onclick="toggleLive()">Szüneteltetés</button>
<button class="btn btn-outline btn-sm" onclick="fetchLogs()">Frissítés</button>
</div>
<script>
(function() {
var container = document.getElementById('logs-container');
var output = document.getElementById('logs-output');
var indicator = document.getElementById('live-indicator');
var toggleBtn = document.getElementById('logs-toggle');
var live = true;
var timer = null;
var stackName = '{{.Stack.Name}}';
function isAtBottom() {
return container.scrollHeight - container.scrollTop - container.clientHeight < 50;
}
window.fetchLogs = function() {
fetch('/stacks/' + stackName + '/logs?raw=1')
.then(function(r) { return r.text(); })
.then(function(text) {
var wasAtBottom = isAtBottom();
output.textContent = text;
if (wasAtBottom) container.scrollTop = container.scrollHeight;
})
.catch(function() {});
};
function startPolling() {
if (timer) return;
timer = setInterval(window.fetchLogs, 3000);
}
function stopPolling() {
if (timer) { clearInterval(timer); timer = null; }
}
window.toggleLive = function() {
live = !live;
if (live) {
startPolling();
indicator.className = 'logs-live-indicator';
indicator.innerHTML = '<span class="logs-live-dot"></span> Élő';
toggleBtn.textContent = 'Szüneteltetés';
} else {
stopPolling();
indicator.className = 'logs-live-indicator logs-live-paused';
indicator.innerHTML = '⏸ Szünetelve';
toggleBtn.textContent = 'Folytatás';
}
};
// Auto-scroll to bottom on initial load
container.scrollTop = container.scrollHeight;
startPolling();
})();
</script>
{{template "layout_end" .}}
{{end}}
`
@@ -1221,19 +1284,21 @@ select.form-control option { background: var(--bg-secondary); color: var(--text-
}
.memory-bar-segment {
height: 100%;
min-width: 0;
position: relative;
z-index: 1;
transition: width 0.3s ease;
}
.memory-bar-segment:not([style*="width:0%"]) {
min-width: 3px;
}
.memory-bar-committed {
background: var(--green);
box-shadow: 0 0 6px rgba(35, 134, 54, 0.4);
border-radius: 5px 0 0 5px;
}
.memory-bar-new {
background: #4edf72;
box-shadow: 0 0 6px rgba(78, 223, 114, 0.3);
background: rgba(35, 134, 54, 0.45);
border-right: 2px solid #4edf72;
border-radius: 0 5px 5px 0;
}
.memory-bar-legend {
@@ -1259,7 +1324,8 @@ select.form-control option { background: var(--bg-secondary); color: var(--text-
background: var(--green);
}
.memory-legend-new {
background: #4edf72;
background: rgba(35, 134, 54, 0.45);
border: 1px solid #4edf72;
}
/* Logs */
@@ -1269,6 +1335,8 @@ select.form-control option { background: var(--bg-secondary); color: var(--text-
border-radius: var(--radius);
padding: 1rem;
overflow-x: auto;
overflow-y: auto;
max-height: 70vh;
margin-bottom: 1rem;
}
.logs-output {
@@ -1278,8 +1346,37 @@ select.form-control option { background: var(--bg-secondary); color: var(--text-
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
margin: 0;
}
.logs-actions {
display: flex;
gap: .5rem;
align-items: center;
}
.logs-live-indicator {
display: inline-flex;
align-items: center;
gap: .35rem;
font-size: .8rem;
font-weight: 600;
color: var(--green);
margin-right: .5rem;
}
.logs-live-indicator.logs-live-paused {
color: var(--text-muted);
}
.logs-live-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--green);
animation: logs-pulse 1.5s ease-in-out infinite;
}
@keyframes logs-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.logs-actions { display: flex; gap: .5rem; }
/* Sync toast */
.sync-toast {
@@ -1301,6 +1398,9 @@ select.form-control option { background: var(--bg-secondary); color: var(--text-
border-color: rgba(218, 54, 51, 0.3);
}
/* Clickable cards */
[data-href] { cursor: pointer; }
.empty-state { text-align: center; padding: 3rem; color: var(--text-muted); }
/* Login page */