web-ui(wave 2): migrate containers.html to new primitives

This commit is contained in:
Zac Gaetano 2026-05-21 13:33:22 -04:00
parent e0cfe80a9e
commit 596fe228ed

View file

@ -5,199 +5,167 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
<title>Containers — Z-AMPP</title> <title>Containers — Z-AMPP</title>
<link rel="stylesheet" href="css/common.css"> <link rel="stylesheet" href="/dist/app.css">
<style> <style>
.page-body { body { margin: 0; }
flex: 1;
overflow: auto;
padding: 24px;
}
.z-table { /* Page-only auto-refresh indicator (preserves original IDs + look-and-feel) */
width: 100%; .refresh-indicator {
border-collapse: collapse;
font-size: var(--text-sm);
}
.z-table th {
padding: 8px 12px;
text-align: left;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .08em;
color: var(--text-tertiary);
border-bottom: 1px solid var(--border);
background: var(--bg-panel);
position: sticky;
top: 0;
z-index: 1;
white-space: nowrap;
}
.z-table td {
padding: 10px 12px;
border-bottom: 1px solid var(--border);
vertical-align: middle;
color: var(--text-secondary);
}
.z-table tr:hover td { background: var(--bg-hover); }
.z-table .empty-row {
text-align: center;
padding: 48px;
color: var(--text-tertiary);
}
.state-badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 999px;
font-size: 10px;
font-weight: 600;
letter-spacing: .04em;
text-transform: uppercase;
}
.state-running { background: oklch(22% 0.07 145 / 0.6); color: oklch(68% 0.18 145); border: 1px solid oklch(45% 0.14 145 / 0.5); }
.state-exited { background: oklch(15% 0.02 250 / 0.6); color: oklch(50% 0.05 250); border: 1px solid oklch(35% 0.04 250 / 0.5); }
.state-paused { background: oklch(22% 0.09 80 / 0.6); color: oklch(68% 0.18 80 ); border: 1px solid oklch(45% 0.14 80 / 0.5); }
.state-dead { background: oklch(22% 0.09 25 / 0.6); color: oklch(68% 0.18 25 ); border: 1px solid oklch(45% 0.14 25 / 0.5); }
.state-restarting { background: oklch(22% 0.07 266 / 0.6); color: oklch(68% 0.18 266); border: 1px solid oklch(45% 0.14 266 / 0.5); }
.container-name { font-weight: 500; color: var(--text-primary); }
.container-svc { font-size: 10px; color: var(--text-tertiary); font-family: var(--font-mono); margin-top: 2px; }
.container-image { font-family: var(--font-mono); font-size: 11px; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.container-ports { font-family: var(--font-mono); font-size: 10px; }
.refresh-indicator {
display: flex;
align-items: center;
gap: 6px; gap: 6px;
font-size: var(--text-xs); font: 400 11px/1 var(--font);
color: var(--text-tertiary); color: var(--text-tertiary);
margin-right: 10px;
} }
.refresh-dot { .refresh-dot {
width: 7px; height: 7px; width: 7px; height: 7px;
border-radius: 50%; border-radius: 50%;
background: var(--signal-warn, oklch(65% 0.18 80)); background: var(--signal-warn);
flex-shrink: 0; flex-shrink: 0;
} }
.refresh-dot.live { .refresh-dot.live {
background: var(--signal-good, oklch(62% 0.18 145)); background: var(--signal-good);
animation: rd-pulse 2s ease-in-out infinite; animation: rd-pulse 2s ease-in-out infinite;
} }
@keyframes rd-pulse { 0%,100%{opacity:.75;} 50%{opacity:1;} } @keyframes rd-pulse { 0%,100%{opacity:.75;} 50%{opacity:1;} }
.container-name { font-weight: 500; color: var(--text-primary); }
.container-svc { font: 400 10px/1.2 var(--font-mono); color: var(--text-tertiary); margin-top: 2px; }
.container-image { font: 400 11px/1.2 var(--font-mono); color: var(--text-secondary); max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.container-ports { font: 400 10px/1.2 var(--font-mono); color: var(--text-secondary); }
.container-status-sub { font: 400 10px/1.2 var(--font); color: var(--text-tertiary); margin-top: 3px; }
.wd-list-row.container-row {
display: grid;
grid-template-columns: minmax(180px, 1.4fr) minmax(160px, 1.4fr) minmax(120px, 1fr) minmax(120px, 1fr) auto;
gap: 16px;
align-items: center;
}
.wd-list-row.container-row.header {
font: 600 10px/1 var(--font);
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-tertiary);
}
.empty-row {
padding: 48px 18px;
text-align: center;
color: var(--text-tertiary);
font-size: 13px;
}
</style> </style>
</head> </head>
<body> <body>
<div class="shell">
<nav class="sidebar" aria-label="Main navigation"> <div class="wd-shell" style="display:flex;min-height:100vh;">
<div class="sidebar-brand"> <nav class="wd-sidebar" aria-label="Main navigation">
<img src="img/dragon-logo.png?v=1" alt="Wild Dragon" class="sidebar-logo"> <div class="wd-sidebar-header">
<span class="sidebar-brand-name">Z-AMPP</span> <img src="img/dragon-logo.png?v=1" alt="Wild Dragon" style="width:18px;height:18px;">
<span class="wd-sidebar-brand">Z-AMPP</span>
</div> </div>
<nav class="sidebar-nav"> <div class="wd-sidebar-nav">
<a href="home.html" class="nav-item"> <a href="home.html" class="wd-nav-item">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 7l6-5 6 5v7a1 1 0 0 1-1 1h-3v-5H6v5H3a1 1 0 0 1-1-1z"/></svg> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 7l6-5 6 5v7a1 1 0 0 1-1 1h-3v-5H6v5H3a1 1 0 0 1-1-1z"/></svg>
Home Home
</a> </a>
<a href="index.html" class="nav-item"> <a href="index.html" class="wd-nav-item">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="1" y="1" width="6" height="6" rx="1"/><rect x="9" y="1" width="6" height="6" rx="1"/><rect x="1" y="9" width="6" height="6" rx="1"/><rect x="9" y="9" width="6" height="6" rx="1"/></svg> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="1" y="1" width="6" height="6" rx="1"/><rect x="9" y="1" width="6" height="6" rx="1"/><rect x="1" y="9" width="6" height="6" rx="1"/><rect x="9" y="9" width="6" height="6" rx="1"/></svg>
Library Library
</a> </a>
<a href="projects.html" class="nav-item"> <a href="projects.html" class="wd-nav-item">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 4a1 1 0 0 1 1-1h4l2 2h5a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1z"/></svg> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 4a1 1 0 0 1 1-1h4l2 2h5a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1z"/></svg>
Projects Projects
</a> </a>
<a href="upload.html" class="nav-item"> <a href="upload.html" class="wd-nav-item">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 11V3M5 6l3-3 3 3"/><path d="M2 13h12"/></svg> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 11V3M5 6l3-3 3 3"/><path d="M2 13h12"/></svg>
Ingest Ingest
</a> </a>
<a href="recorders.html" class="nav-item"> <a href="recorders.html" class="wd-nav-item">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="1" y="4" width="10" height="8" rx="1"/><path d="M11 7l4-2v6l-4-2"/></svg> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="1" y="4" width="10" height="8" rx="1"/><path d="M11 7l4-2v6l-4-2"/></svg>
Recorders Recorders
</a> </a>
<a href="capture.html" class="nav-item"> <a href="capture.html" class="wd-nav-item">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="3"/><circle cx="8" cy="8" r="6.5"/></svg> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="3"/><circle cx="8" cy="8" r="6.5"/></svg>
Capture Capture
</a> </a>
<a href="jobs.html" class="nav-item"> <a href="jobs.html" class="wd-nav-item">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h12M2 8h8M2 12h5"/></svg> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h12M2 8h8M2 12h5"/></svg>
Jobs Jobs
</a> </a>
<a href="editor.html" class="nav-item"> <a href="editor.html" class="wd-nav-item">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 13l1.5-3.5L11 2l3 3-7.5 7.5L3 14zM10 3l3 3"/></svg> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 13l1.5-3.5L11 2l3 3-7.5 7.5L3 14zM10 3l3 3"/></svg>
Editor Editor
</a> </a>
<div class="sidebar-section-label">Admin</div> <div class="wd-sidebar-section">Admin</div>
<a href="users.html" class="nav-item"> <a href="users.html" class="wd-nav-item">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="6" cy="5" r="2.5"/><path d="M1 13c0-2.8 2.2-5 5-5s5 2.2 5 5"/><circle cx="12" cy="5" r="2"/><path d="M15 12c0-1.9-1.3-3.5-3-4"/></svg> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="6" cy="5" r="2.5"/><path d="M1 13c0-2.8 2.2-5 5-5s5 2.2 5 5"/><circle cx="12" cy="5" r="2"/><path d="M15 12c0-1.9-1.3-3.5-3-4"/></svg>
Users Users
</a> </a>
<a href="tokens.html" class="nav-item"> <a href="tokens.html" class="wd-nav-item">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="6" cy="10" r="3.5"/><path d="M8.7 7.3L13 3M11.5 3.5l1.5 1.5M13.5 2.5l1 1"/></svg> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="6" cy="10" r="3.5"/><path d="M8.7 7.3L13 3M11.5 3.5l1.5 1.5M13.5 2.5l1 1"/></svg>
Tokens Tokens
</a> </a>
<a href="containers.html" class="nav-item active"> <a href="containers.html" class="wd-nav-item is-active">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="1" y="5" width="14" height="4" rx="1"/><rect x="1" y="10" width="14" height="4" rx="1"/><path d="M4 7h1M4 12h1"/></svg> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="1" y="5" width="14" height="4" rx="1"/><rect x="1" y="10" width="14" height="4" rx="1"/><path d="M4 7h1M4 12h1"/></svg>
Containers Containers
</a> </a>
<a href="cluster.html" class="nav-item"> <a href="cluster.html" class="wd-nav-item">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="2"/><circle cx="2" cy="3" r="1.5"/><circle cx="14" cy="3" r="1.5"/><circle cx="2" cy="13" r="1.5"/><circle cx="14" cy="13" r="1.5"/><path d="M3.1 4.1L6.5 6.5M12.9 4.1L9.5 6.5M3.1 11.9L6.5 9.5M12.9 11.9L9.5 9.5"/></svg> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="2"/><circle cx="2" cy="3" r="1.5"/><circle cx="14" cy="3" r="1.5"/><circle cx="2" cy="13" r="1.5"/><circle cx="14" cy="13" r="1.5"/><path d="M3.1 4.1L6.5 6.5M12.9 4.1L9.5 6.5M3.1 11.9L6.5 9.5M12.9 11.9L9.5 9.5"/></svg>
Cluster Cluster
</a> </a>
<a href="settings.html" class="nav-item"> <a href="settings.html" class="wd-nav-item">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="2.5"/><path d="M8 1v1.5M8 13.5V15M1 8h1.5M13.5 8H15M2.9 2.9l1.1 1.1M12 12l1.1 1.1M2.9 13.1L4 12M12 4l1.1-1.1"/></svg> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="2.5"/><path d="M8 1v1.5M8 13.5V15M1 8h1.5M13.5 8H15M2.9 2.9l1.1 1.1M12 12l1.1 1.1M2.9 13.1L4 12M12 4l1.1-1.1"/></svg>
Settings Settings
</a> </a>
</nav> </div>
<div class="sidebar-footer"> <div class="wd-sidebar-footer">
<div class="sidebar-user"> <div class="wd-sidebar-user">
<div class="sidebar-user-avatar" id="userAvatar">?</div> <div class="wd-sidebar-user-avatar" id="userAvatar">?</div>
<div class="sidebar-user-info"> <div class="wd-sidebar-user-info">
<div class="sidebar-user-name" id="userName">&#8212;</div> <div class="wd-sidebar-user-name" id="userName">&#8212;</div>
<div class="sidebar-user-role" id="userRole"></div> <div class="wd-sidebar-user-role" id="userRole"></div>
</div> </div>
<button class="btn btn-ghost" id="logoutBtn" title="Sign out" style="padding:0;width:24px;height:24px;flex-shrink:0;"> <button class="wd-btn wd-btn--ghost wd-btn--sm wd-btn--icon wd-sidebar-user-logout" id="logoutBtn" title="Sign out">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" width="14" height="14"><path d="M10 8H3M6 5l-3 3 3 3"/><path d="M7 3h5a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H7"/></svg> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" width="12" height="12"><path d="M10 8H3M6 5l-3 3 3 3"/><path d="M7 3h5a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H7"/></svg>
</button> </button>
</div> </div>
</div> </div>
</nav> </nav>
<div class="main"> <div style="flex:1;display:flex;flex-direction:column;">
<header class="topbar"> <header class="wd-topbar">
<div class="topbar-left"> <div class="wd-topbar-left">
<span class="page-title">Containers</span> <nav class="wd-breadcrumb"><span class="wd-breadcrumb-crumb">Containers</span></nav>
</div> </div>
<div class="topbar-right"> <div class="wd-topbar-right">
<div class="refresh-indicator"> <div class="refresh-indicator">
<span class="refresh-dot" id="refreshDot"></span> <span class="refresh-dot" id="refreshDot"></span>
<span id="refreshText">Loading&hellip;</span> <span id="refreshText">Loading&hellip;</span>
</div> </div>
<button class="btn btn-ghost btn-sm" id="refreshBtn" style="margin-left:8px;">Refresh</button> <button class="wd-btn wd-btn--ghost wd-btn--sm" id="refreshBtn">Refresh</button>
</div> </div>
</header> </header>
<div class="page-body"> <main style="flex:1;padding:20px 20px 32px;overflow:auto;">
<table class="z-table"> <div class="wd-list">
<thead> <div class="wd-list-row container-row header">
<tr> <span>Container</span>
<th>Container</th> <span>Image</span>
<th>Image</th> <span>State</span>
<th>State</th> <span>Ports</span>
<th>Ports</th> <span style="text-align:right">Actions</span>
<th style="text-align:right">Actions</th> </div>
</tr> <div id="containerBody">
</thead> <div class="empty-row">Loading&hellip;</div>
<tbody id="containerBody"> </div>
<tr><td colspan="5" class="empty-row">Loading&hellip;</td></tr> </div>
</tbody> </main>
</table>
</div>
</div> </div>
</div> </div>
<div class="toast-container" id="toastContainer" aria-live="polite"></div> <div class="wd-toast-container" id="toastContainer" aria-live="polite"></div>
<script src="js/api.js?v=6"></script> <script src="js/api.js?v=6"></script>
<script> <script>
@ -219,7 +187,7 @@ async function loadContainers() {
function renderContainers() { function renderContainers() {
const tbody = document.getElementById('containerBody'); const tbody = document.getElementById('containerBody');
if (!containers.length) { if (!containers.length) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-row">No containers found for this compose project.</td></tr>'; tbody.innerHTML = '<div class="empty-row">No containers found for this compose project.</div>';
return; return;
} }
@ -228,30 +196,35 @@ function renderContainers() {
tbody.innerHTML = sorted.map(c => { tbody.innerHTML = sorted.map(c => {
const state = (c.state || 'exited').toLowerCase(); const state = (c.state || 'exited').toLowerCase();
const running = state === 'running'; const running = state === 'running';
const badgeMod = running ? 'wd-badge--good'
: state === 'restarting' ? 'wd-badge--warn'
: state === 'paused' ? 'wd-badge--warn'
: state === 'dead' ? 'wd-badge--bad'
: 'wd-badge--idle';
const svcLbl = c.service && c.service !== c.name const svcLbl = c.service && c.service !== c.name
? `<div class="container-svc">${esc(c.service)}</div>` : ''; ? `<div class="container-svc">${esc(c.service)}</div>` : '';
const actionBtn = running const actionBtn = running
? `<button class="btn btn-ghost btn-sm" onclick="containerAction('${esc(c.id)}','stop',this)" title="Stop container">Stop</button>` ? `<button class="wd-btn wd-btn--ghost wd-btn--sm" onclick="containerAction('${esc(c.id)}','stop',this)" title="Stop container">Stop</button>`
: `<button class="btn btn-ghost btn-sm" onclick="containerAction('${esc(c.id)}','start',this)" title="Start container">Start</button>`; : `<button class="wd-btn wd-btn--ghost wd-btn--sm" onclick="containerAction('${esc(c.id)}','start',this)" title="Start container">Start</button>`;
return ` return `
<tr> <div class="wd-list-row container-row">
<td> <div class="wd-list-cell wd-list-cell--name">
<div class="container-name">${esc(c.name)}</div> <div class="container-name">${esc(c.name)}</div>
${svcLbl} ${svcLbl}
</td> </div>
<td><div class="container-image" title="${esc(c.image)}">${esc(c.image)}</div></td> <div class="wd-list-cell wd-list-cell--meta"><div class="container-image" title="${esc(c.image)}">${esc(c.image)}</div></div>
<td> <div class="wd-list-cell">
<span class="state-badge state-${state}">${esc(c.state)}</span> <span class="wd-badge ${badgeMod}">${esc(c.state)}</span>
<div style="font-size:10px;color:var(--text-tertiary);margin-top:3px;">${esc(c.status)}</div> <div class="container-status-sub">${esc(c.status)}</div>
</td> </div>
<td><span class="container-ports">${c.ports ? esc(c.ports) : '&mdash;'}</span></td> <div class="wd-list-cell"><span class="container-ports">${c.ports ? esc(c.ports) : '&mdash;'}</span></div>
<td style="text-align:right"> <div class="wd-list-cell wd-list-cell--actions" style="text-align:right">
<div style="display:flex;gap:4px;justify-content:flex-end;"> <div style="display:flex;gap:4px;justify-content:flex-end;">
${actionBtn} ${actionBtn}
<button class="btn btn-ghost btn-sm" onclick="containerAction('${esc(c.id)}','restart',this)" title="Restart container">Restart</button> <button class="wd-btn wd-btn--ghost wd-btn--sm" onclick="containerAction('${esc(c.id)}','restart',this)" title="Restart container">Restart</button>
</div> </div>
</td> </div>
</tr>`; </div>`;
}).join(''); }).join('');
} }
@ -287,9 +260,9 @@ function setDot(state, label) {
function toast(title, msg, type) { function toast(title, msg, type) {
const el = document.createElement('div'); const el = document.createElement('div');
el.className = 'toast toast--' + (type || 'info'); el.className = 'wd-toast wd-toast--' + (type || 'info');
el.innerHTML = '<div class="toast-body"><div class="toast-title">' + esc(title) + '</div>' + el.innerHTML = '<div class="wd-toast-body"><div class="wd-toast-title">' + esc(title) + '</div>' +
(msg ? '<div class="toast-msg">' + esc(msg) + '</div>' : '') + '</div>'; (msg ? '<div class="wd-toast-msg">' + esc(msg) + '</div>' : '') + '</div>';
document.getElementById('toastContainer').appendChild(el); document.getElementById('toastContainer').appendChild(el);
setTimeout(() => el.remove(), 4000); setTimeout(() => el.remove(), 4000);
} }