diff --git a/portal/static/app.css b/portal/static/app.css index 829dd4b..f7d1eef 100644 --- a/portal/static/app.css +++ b/portal/static/app.css @@ -405,3 +405,4 @@ button.button, input[type="submit"].button { font-family: inherit; } .svc-ram { margin-left: auto; font: var(--type-caption); color: var(--ink-faint); white-space: nowrap; } .button-sm { padding: 4px 10px; font-size: 12px; } [data-theme="dark"] .svc-dot.svc-fail { background: #ff6b5e; } +.svc-logs { font: var(--type-caption); color: var(--sticker-orange, #dd5b00); text-decoration: underline; } diff --git a/portal/templates/partials/_sections.html b/portal/templates/partials/_sections.html index 182c1c9..bf281e6 100644 --- a/portal/templates/partials/_sections.html +++ b/portal/templates/partials/_sections.html @@ -38,10 +38,12 @@ {% if tool.togglable and tool.service_id %}
… +
@@ -126,6 +128,16 @@ if (ram && svcState[id] && svcState[id].ram_mb) ram.textContent = "~" + svcState[id].ram_mb + "MB"; dot.className = "svc-dot svc-" + (st === "running" ? "on" : st === "stopped" ? "off" : st === "failed" ? "fail" : "trans"); if (lab) lab.textContent = st === "running" ? "Running" : st === "stopped" ? "Stopped" : st === "failed" ? (svcState[id].error || "Failed") : (st[0].toUpperCase() + st.slice(1)); + // failed -> offer the logs deep link (dozzle) + var logs = strip.querySelector("[data-svc-logs]"); + if (logs) { + var lu = strip.getAttribute("data-logs") || ""; + if (st === "failed" && lu) { + logs.style.display = ""; logs.href = lu; logs.title = "View logs"; + } else { + logs.style.display = "none"; logs.href = "#"; + } + } if (btn) { btn.disabled = !can || transitional; if (transitional) btn.textContent = st === "starting" ? "Starting…" : "Stopping…"; diff --git a/portal/tools.py b/portal/tools.py index 938d61e..edeffca 100644 --- a/portal/tools.py +++ b/portal/tools.py @@ -37,6 +37,8 @@ class Tool: service_id: str | None = None # controller allowlist id, e.g. "gimp" ram_mb: int | None = None # idle RAM shown on the card togglable: bool = False # only True if in the controller allowlist + logs_url: str | None = None # deep link for the failed state (dozzle) + admin_service: bool = False # toggle strip visible to admins only def section_label(category: str) -> str: @@ -92,23 +94,34 @@ def _cat() -> list[Tool]: Tool("paseo", "Paseo", "ai", "Multi-agent orchestration GUI (pi agents)", "https://paseo.lab.audasmedia.com.au"), Tool("paperclip", "Paperclip", "ai", - "Agent harness manager / AI ops", "https://paperclip.lab.audasmedia.com.au", login=True), + "Agent harness manager / AI ops", "https://paperclip.lab.audasmedia.com.au", login=True, + service_id="paperclip", ram_mb=495, togglable=True, + admin_service=True), + Tool("prefect-worker", "Prefect worker", "ai", + "Photo ingestion worker (keep on while the Google Photos pipeline runs)", + "https://prefect.lab.audasmedia.com.au", login=True, admin=True, + service_id="prefect-worker", ram_mb=129, togglable=True, + admin_service=True), Tool("supabase", "LangChain (Supabase)", "ai", "LangChain dev service", "https://supabase.home.lab", login=True, lan=True, admin=True), # ---- Image --------------------------------------------------------- Tool("gimp", "GIMP", "image", "Photo editing & retouching", "https://gimp.lab.audasmedia.com.au", - service_id="gimp", ram_mb=233, togglable=True), + service_id="gimp", ram_mb=233, togglable=True, + logs_url="https://dozzle.home.lab/#/containers/family-home-lab-gimp-1"), Tool("penpot", "Penpot", "image", "Open-source design & prototyping", "https://penpot.lab.audasmedia.com.au", login=True), - Tool("photo-filter", "Photo Filter", "image", - "Photo utility / filtering", "https://photo-filter.lab.audasmedia.com.au"), + Tool("photo-filter", "Photo Pipeline", "image", + "Google Photos ingestion review dashboard", "https://photo-filter.lab.audasmedia.com.au", + service_id="photo-dashboard", ram_mb=112, togglable=True, + admin_service=True), # ---- Video --------------------------------------------------------- Tool("video-editor", "Video Editor", "video", "Desktop video editing in your browser", "https://video.lab.audasmedia.com.au", - service_id="video-editor", ram_mb=237, togglable=True), + service_id="video-editor", ram_mb=237, togglable=True, + logs_url="https://dozzle.home.lab/#/containers/family-home-lab-video-editor-1"), Tool("jellyfin", "Jellyfin", "media", "Movies, TV & music server", "https://jellyfin.lab.audasmedia.com.au", login=True), Tool("jellyseerr", "Jellyseerr", "media", @@ -137,10 +150,12 @@ def _cat() -> list[Tool]: # ---- Audio --------------------------------------------------------- Tool("audio-editor", "Audio Editor", "audio", "Multi-track audio editing", "https://audio.lab.audasmedia.com.au", - service_id="audio-editor", ram_mb=201, togglable=True), + service_id="audio-editor", ram_mb=201, togglable=True, + logs_url="https://dozzle.home.lab/#/containers/family-home-lab-audio-editor-1"), Tool("lmms", "LMMS Music Studio", "audio", "Beats, melodies & full arrangements", "https://lmms.lab.audasmedia.com.au", - service_id="lmms", ram_mb=798, togglable=True), + service_id="lmms", ram_mb=798, togglable=True, + logs_url="https://dozzle.home.lab/#/containers/lmms"), Tool("snapcast", "Snapcast", "audio", "Sync audio to speakers around the house", "http://192.168.20.13:1780", lan=True), Tool("mopidy", "Mopidy", "audio", @@ -314,6 +329,8 @@ def tools_for_user(user) -> list[tuple[str, list[Tool]]]: for tool in TOOLS: if tool.admin and not user.is_admin: continue + if tool.admin_service and not user.is_admin: + continue if not allowed.get(tool.category, True): continue # Specialise the dsh chat link to the logged-in user's own instance.