console: admin ON/OFF cards (Paperclip, Prefect worker, Photo Pipeline — admin-only toggle strips) + failed-state dozzle log link on media cards; per-user toggle gating verified (finn: media only, 403 on admin)
This commit is contained in:
@@ -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; }
|
.svc-ram { margin-left: auto; font: var(--type-caption); color: var(--ink-faint); white-space: nowrap; }
|
||||||
.button-sm { padding: 4px 10px; font-size: 12px; }
|
.button-sm { padding: 4px 10px; font-size: 12px; }
|
||||||
[data-theme="dark"] .svc-dot.svc-fail { background: #ff6b5e; }
|
[data-theme="dark"] .svc-dot.svc-fail { background: #ff6b5e; }
|
||||||
|
.svc-logs { font: var(--type-caption); color: var(--sticker-orange, #dd5b00); text-decoration: underline; }
|
||||||
|
|||||||
@@ -38,10 +38,12 @@
|
|||||||
{% if tool.togglable and tool.service_id %}
|
{% if tool.togglable and tool.service_id %}
|
||||||
<div class="svc-strip" data-svc="{{ tool.service_id }}"
|
<div class="svc-strip" data-svc="{{ tool.service_id }}"
|
||||||
data-ram="{{ tool.ram_mb or '' }}" data-name="{{ tool.name }}"
|
data-ram="{{ tool.ram_mb or '' }}" data-name="{{ tool.name }}"
|
||||||
|
data-logs="{{ tool.logs_url or '' }}"
|
||||||
onclick="event.preventDefault();event.stopPropagation();">
|
onclick="event.preventDefault();event.stopPropagation();">
|
||||||
<span class="svc-state" data-svc-state>
|
<span class="svc-state" data-svc-state>
|
||||||
<span class="svc-dot" data-svc-dot></span><span data-svc-label>…</span>
|
<span class="svc-dot" data-svc-dot></span><span data-svc-label>…</span>
|
||||||
</span>
|
</span>
|
||||||
|
<a class="svc-logs" data-svc-logs style="display:none" target="_blank" rel="noopener">logs</a>
|
||||||
<span class="svc-ram" data-svc-ram></span>
|
<span class="svc-ram" data-svc-ram></span>
|
||||||
<button type="button" class="button button-utility button-sm" data-svc-btn disabled>…</button>
|
<button type="button" class="button button-utility button-sm" data-svc-btn disabled>…</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,6 +128,16 @@
|
|||||||
if (ram && svcState[id] && svcState[id].ram_mb) ram.textContent = "~" + svcState[id].ram_mb + "MB";
|
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");
|
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));
|
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) {
|
if (btn) {
|
||||||
btn.disabled = !can || transitional;
|
btn.disabled = !can || transitional;
|
||||||
if (transitional) btn.textContent = st === "starting" ? "Starting…" : "Stopping…";
|
if (transitional) btn.textContent = st === "starting" ? "Starting…" : "Stopping…";
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ class Tool:
|
|||||||
service_id: str | None = None # controller allowlist id, e.g. "gimp"
|
service_id: str | None = None # controller allowlist id, e.g. "gimp"
|
||||||
ram_mb: int | None = None # idle RAM shown on the card
|
ram_mb: int | None = None # idle RAM shown on the card
|
||||||
togglable: bool = False # only True if in the controller allowlist
|
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:
|
def section_label(category: str) -> str:
|
||||||
@@ -92,23 +94,34 @@ def _cat() -> list[Tool]:
|
|||||||
Tool("paseo", "Paseo", "ai",
|
Tool("paseo", "Paseo", "ai",
|
||||||
"Multi-agent orchestration GUI (pi agents)", "https://paseo.lab.audasmedia.com.au"),
|
"Multi-agent orchestration GUI (pi agents)", "https://paseo.lab.audasmedia.com.au"),
|
||||||
Tool("paperclip", "Paperclip", "ai",
|
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",
|
Tool("supabase", "LangChain (Supabase)", "ai",
|
||||||
"LangChain dev service", "https://supabase.home.lab", login=True, lan=True, admin=True),
|
"LangChain dev service", "https://supabase.home.lab", login=True, lan=True, admin=True),
|
||||||
|
|
||||||
# ---- Image ---------------------------------------------------------
|
# ---- Image ---------------------------------------------------------
|
||||||
Tool("gimp", "GIMP", "image",
|
Tool("gimp", "GIMP", "image",
|
||||||
"Photo editing & retouching", "https://gimp.lab.audasmedia.com.au",
|
"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",
|
Tool("penpot", "Penpot", "image",
|
||||||
"Open-source design & prototyping", "https://penpot.lab.audasmedia.com.au", login=True),
|
"Open-source design & prototyping", "https://penpot.lab.audasmedia.com.au", login=True),
|
||||||
Tool("photo-filter", "Photo Filter", "image",
|
Tool("photo-filter", "Photo Pipeline", "image",
|
||||||
"Photo utility / filtering", "https://photo-filter.lab.audasmedia.com.au"),
|
"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 ---------------------------------------------------------
|
# ---- Video ---------------------------------------------------------
|
||||||
Tool("video-editor", "Video Editor", "video",
|
Tool("video-editor", "Video Editor", "video",
|
||||||
"Desktop video editing in your browser", "https://video.lab.audasmedia.com.au",
|
"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",
|
Tool("jellyfin", "Jellyfin", "media",
|
||||||
"Movies, TV & music server", "https://jellyfin.lab.audasmedia.com.au", login=True),
|
"Movies, TV & music server", "https://jellyfin.lab.audasmedia.com.au", login=True),
|
||||||
Tool("jellyseerr", "Jellyseerr", "media",
|
Tool("jellyseerr", "Jellyseerr", "media",
|
||||||
@@ -137,10 +150,12 @@ def _cat() -> list[Tool]:
|
|||||||
# ---- Audio ---------------------------------------------------------
|
# ---- Audio ---------------------------------------------------------
|
||||||
Tool("audio-editor", "Audio Editor", "audio",
|
Tool("audio-editor", "Audio Editor", "audio",
|
||||||
"Multi-track audio editing", "https://audio.lab.audasmedia.com.au",
|
"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",
|
Tool("lmms", "LMMS Music Studio", "audio",
|
||||||
"Beats, melodies & full arrangements", "https://lmms.lab.audasmedia.com.au",
|
"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",
|
Tool("snapcast", "Snapcast", "audio",
|
||||||
"Sync audio to speakers around the house", "http://192.168.20.13:1780", lan=True),
|
"Sync audio to speakers around the house", "http://192.168.20.13:1780", lan=True),
|
||||||
Tool("mopidy", "Mopidy", "audio",
|
Tool("mopidy", "Mopidy", "audio",
|
||||||
@@ -314,6 +329,8 @@ def tools_for_user(user) -> list[tuple[str, list[Tool]]]:
|
|||||||
for tool in TOOLS:
|
for tool in TOOLS:
|
||||||
if tool.admin and not user.is_admin:
|
if tool.admin and not user.is_admin:
|
||||||
continue
|
continue
|
||||||
|
if tool.admin_service and not user.is_admin:
|
||||||
|
continue
|
||||||
if not allowed.get(tool.category, True):
|
if not allowed.get(tool.category, True):
|
||||||
continue
|
continue
|
||||||
# Specialise the dsh chat link to the logged-in user's own instance.
|
# Specialise the dsh chat link to the logged-in user's own instance.
|
||||||
|
|||||||
Reference in New Issue
Block a user