GUI updates, live log visibility
This commit is contained in:
@@ -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) |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user