GUI updates, live log visibility
This commit is contained in:
@@ -99,6 +99,9 @@ ssh kisfenyo@192.168.0.162 "cd /opt/docker/felhom-controller && docker pull gite
|
|||||||
- Protected stacks (traefik, cloudflared, felhom-controller) can't be stopped from UI
|
- Protected stacks (traefik, cloudflared, felhom-controller) can't be stopped from UI
|
||||||
- `app.yaml` persists deploy config; `deployed: true` flag controls UI state
|
- `app.yaml` persists deploy config; `deployed: true` flag controls UI state
|
||||||
- Password fields require explicit user input or generation (no silent auto-fill)
|
- Password fields require explicit user input or generation (no silent auto-fill)
|
||||||
|
- App cards on dashboard and stacks pages are clickable via `data-href` attribute (skip protected stacks)
|
||||||
|
- Logs page uses AJAX polling (`?raw=1` query param returns plain text) with auto-scroll and pause/resume
|
||||||
|
- Memory bar on deploy page uses two-segment stacked bar (committed = solid green, new = translucent green)
|
||||||
|
|
||||||
## Git sync module (internal/sync)
|
## Git sync module (internal/sync)
|
||||||
|
|
||||||
|
|||||||
+8
-18
@@ -31,24 +31,14 @@ Last updated: 2026-02-14 (session 3)
|
|||||||
### What was just completed (2026-02-14 session 3)
|
### What was just completed (2026-02-14 session 3)
|
||||||
- **Enhanced debug logging** across all stack operations in `internal/stacks/`:
|
- **Enhanced debug logging** across all stack operations in `internal/stacks/`:
|
||||||
- **Operation timing**: All stack ops (start, stop, restart, update, deploy) now log elapsed time
|
- **Operation timing**: All stack ops (start, stop, restart, update, deploy) now log elapsed time
|
||||||
- Success: `[INFO] Stack immich started successfully (took 45.2s)`
|
- **Post-start container state check**: Async goroutine after start/restart/update/deploy
|
||||||
- Failure: `[ERROR] Stack immich start failed after 3.1s: exit code 1`
|
- **Image pull detection**: Checks local images before deploy/update (debug level)
|
||||||
- **`composeExecCustomEnv` improvements**:
|
- **GetLogs/ScanStacks improvements**: Byte count logging, deployed/available counts
|
||||||
- Logs env var **keys** at debug level (never values — secrets stay safe)
|
- All verbose checks gated on `cfg.Logging.Level == "debug"`; timing always at INFO
|
||||||
- Logs exit code, truncated stdout/stderr (max 500 chars) on failure
|
- **UI improvements** in `internal/web/templates.go` and `server.go`:
|
||||||
- Logs command completion time on success
|
- **Memory bar fix on deploy page**: Bar segments now always visible (min-width: 3px), new app segment uses translucent green with distinct border for clear visual separation from committed memory
|
||||||
- **Post-start container state check** (StartStack, RestartStack, UpdateStack, DeployStack):
|
- **Clickable app cards**: Cards on Vezérlőpult and Alkalmazások pages are now clickable (navigates to deploy/detail page). Uses `data-href` attribute + delegated click handler. Protected stacks excluded
|
||||||
- Async goroutine: sleeps 3s, runs `docker compose ps -a`, logs each container's state
|
- **Live-scrolling logs**: Logs page now auto-refreshes every 3s via AJAX polling (`?raw=1` returns plain text). Fixed-height container (70vh) with auto-scroll to bottom. Pulsing green "Élő" indicator. Pause/resume toggle ("Szüneteltetés"/"Folytatás"). User scroll position preserved when scrolled up to read history
|
||||||
- Critical for detecting crash-loops that `docker compose up -d` wouldn't surface
|
|
||||||
- Non-blocking — never fails the operation, just logs a warning if check fails
|
|
||||||
- **Image pull detection** (DeployStack, UpdateStack at debug level):
|
|
||||||
- Parses `docker-compose.yml` for `image:` lines
|
|
||||||
- Runs `docker image inspect` per image to check local availability
|
|
||||||
- Skips images with `${VAR}` interpolation (can't resolve at check time)
|
|
||||||
- **GetLogs improvement**: Logs byte count of returned logs (distinguishes empty vs failure)
|
|
||||||
- **ScanStacks improvement**: `[INFO] Scanned stacks: 10 found (3 deployed, 7 available)`
|
|
||||||
- **New helpers added to manager.go**: `isDebug()`, `truncateStr()`, `logPostStartStatus()`, `checkLocalImages()`
|
|
||||||
- All verbose checks gated on `cfg.Logging.Level == "debug"`; timing and container states always logged at INFO
|
|
||||||
|
|
||||||
### Previously completed (2026-02-15 session 2)
|
### Previously completed (2026-02-15 session 2)
|
||||||
- **Phase 4: Git Sync + App Catalog Audit** — major milestone
|
- **Phase 4: Git Sync + App Catalog Audit** — major milestone
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ Current version: **v0.2.1**
|
|||||||
- Dashboard with live container state (green/orange/yellow/red)
|
- Dashboard with live container state (green/orange/yellow/red)
|
||||||
- Deploy form with password validation, auto-generation, and field locking
|
- Deploy form with password validation, auto-generation, and field locking
|
||||||
- Stack operations: start, stop, restart, update (pull + recreate)
|
- 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)
|
- Deploy page doubles as config viewer (read-only mode for deployed apps)
|
||||||
- Periodic stack rescanning (every 2 minutes)
|
- Periodic stack rescanning (every 2 minutes)
|
||||||
- Manual rescan endpoint (`POST /api/stacks/rescan`)
|
- 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
|
- Memory summary bar shown on deploy page before deployment
|
||||||
- Felhom.eu logo SVG in sidebar and login page
|
- Felhom.eu logo SVG in sidebar and login page
|
||||||
- Verbose debug logging with operation timing, post-start container state checks, and image pull detection
|
- 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
|
### 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)
|
- 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}/stop` | Yes | Stop stack (not protected) |
|
||||||
| POST | `/api/stacks/{name}/restart` | Yes | Restart stack |
|
| POST | `/api/stacks/{name}/restart` | Yes | Restart stack |
|
||||||
| POST | `/api/stacks/{name}/update` | Yes | Pull images + recreate |
|
| 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 |
|
| POST | `/api/stacks/rescan` | Yes | Trigger manual stack discovery |
|
||||||
| GET | `/api/system/info` | Yes | System resource usage (RAM, disk, HDD) |
|
| 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)
|
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)
|
stack, ok := s.stackMgr.GetStack(name)
|
||||||
if !ok {
|
if !ok {
|
||||||
http.NotFound(w, nil)
|
http.NotFound(w, r)
|
||||||
return
|
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)
|
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 := s.baseData("logs", stack.Meta.DisplayName+" — Naplók")
|
||||||
data["Stack"] = stack
|
data["Stack"] = stack
|
||||||
data["Logs"] = logs
|
data["Logs"] = logs
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ const layoutTmpl = `
|
|||||||
{{define "layout_end"}}
|
{{define "layout_end"}}
|
||||||
</main>
|
</main>
|
||||||
<script>
|
<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() {
|
async function syncTemplates() {
|
||||||
const btn = document.getElementById('sync-btn');
|
const btn = document.getElementById('sync-btn');
|
||||||
const toast = document.getElementById('sync-toast');
|
const toast = document.getElementById('sync-toast');
|
||||||
@@ -169,7 +174,7 @@ const dashboardTmpl = `
|
|||||||
|
|
||||||
<div class="stack-list">
|
<div class="stack-list">
|
||||||
{{range .Stacks}}
|
{{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">
|
<div class="stack-info">
|
||||||
<img class="stack-logo" src="{{logoURL .Meta.Slug}}"
|
<img class="stack-logo" src="{{logoURL .Meta.Slug}}"
|
||||||
alt="{{.Meta.DisplayName}}" onerror="this.onerror=function(){this.style.display='none'};this.src='{{logoPNGURL .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">
|
<div class="stack-grid">
|
||||||
{{range .Stacks}}
|
{{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-detail-header">
|
||||||
<div class="stack-title-row">
|
<div class="stack-title-row">
|
||||||
<img class="stack-logo-lg" src="{{logoURL .Meta.Slug}}"
|
<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>
|
<a href="/stacks" class="btn btn-sm btn-outline">← Vissza</a>
|
||||||
<h2>{{.Stack.Meta.DisplayName}} — Naplók</h2>
|
<h2>{{.Stack.Meta.DisplayName}} — Naplók</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="logs-container">
|
<div class="logs-container" id="logs-container">
|
||||||
<pre class="logs-output">{{.Logs}}</pre>
|
<pre class="logs-output" id="logs-output">{{.Logs}}</pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="logs-actions">
|
<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>
|
</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" .}}
|
{{template "layout_end" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
`
|
`
|
||||||
@@ -1221,19 +1284,21 @@ select.form-control option { background: var(--bg-secondary); color: var(--text-
|
|||||||
}
|
}
|
||||||
.memory-bar-segment {
|
.memory-bar-segment {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 0;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
transition: width 0.3s ease;
|
transition: width 0.3s ease;
|
||||||
}
|
}
|
||||||
|
.memory-bar-segment:not([style*="width:0%"]) {
|
||||||
|
min-width: 3px;
|
||||||
|
}
|
||||||
.memory-bar-committed {
|
.memory-bar-committed {
|
||||||
background: var(--green);
|
background: var(--green);
|
||||||
box-shadow: 0 0 6px rgba(35, 134, 54, 0.4);
|
box-shadow: 0 0 6px rgba(35, 134, 54, 0.4);
|
||||||
border-radius: 5px 0 0 5px;
|
border-radius: 5px 0 0 5px;
|
||||||
}
|
}
|
||||||
.memory-bar-new {
|
.memory-bar-new {
|
||||||
background: #4edf72;
|
background: rgba(35, 134, 54, 0.45);
|
||||||
box-shadow: 0 0 6px rgba(78, 223, 114, 0.3);
|
border-right: 2px solid #4edf72;
|
||||||
border-radius: 0 5px 5px 0;
|
border-radius: 0 5px 5px 0;
|
||||||
}
|
}
|
||||||
.memory-bar-legend {
|
.memory-bar-legend {
|
||||||
@@ -1259,7 +1324,8 @@ select.form-control option { background: var(--bg-secondary); color: var(--text-
|
|||||||
background: var(--green);
|
background: var(--green);
|
||||||
}
|
}
|
||||||
.memory-legend-new {
|
.memory-legend-new {
|
||||||
background: #4edf72;
|
background: rgba(35, 134, 54, 0.45);
|
||||||
|
border: 1px solid #4edf72;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Logs */
|
/* Logs */
|
||||||
@@ -1269,6 +1335,8 @@ select.form-control option { background: var(--bg-secondary); color: var(--text-
|
|||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 70vh;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
.logs-output {
|
.logs-output {
|
||||||
@@ -1278,8 +1346,37 @@ select.form-control option { background: var(--bg-secondary); color: var(--text-
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-all;
|
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 */
|
||||||
.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);
|
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); }
|
.empty-state { text-align: center; padding: 3rem; color: var(--text-muted); }
|
||||||
|
|
||||||
/* Login page */
|
/* Login page */
|
||||||
|
|||||||
Reference in New Issue
Block a user