Complete Track C roadmap: dashboard, job follow, actions, activity, roles
CI / test (push) Successful in 9m55s

Ship the modern dashboard, job tray log follow, container overflow actions, stacks search, activity history, role-gated controls, invite form with copy token, first-connect checklist, and mark Track C done on the roadmap.
This commit is contained in:
2026-07-10 22:31:16 -04:00
parent 2d8b24ec0e
commit a42f5e4dcf
7 changed files with 1350 additions and 523 deletions
+213 -125
View File
@@ -45,6 +45,23 @@
<span id="health-docker" class="health-docker">Docker —</span>
<span id="health-role" class="health-role"></span>
</div>
<!-- Recent activity (job history) -->
<div id="activity-tray" class="activity-tray">
<button type="button" id="activity-tray-toggle" class="notification-tray-toggle" title="Recent activity" aria-label="Recent activity" aria-expanded="false">
<i class="fas fa-list-check"></i>
</button>
<div id="activity-panel" class="activity-panel hidden" role="dialog" aria-label="Recent activity">
<div class="activity-panel-header">
<h5 class="mb-0">Recent activity</h5>
<button type="button" class="notification-action-btn" id="activity-panel-close" title="Close" aria-label="Close">
<i class="fas fa-times"></i>
</button>
</div>
<div id="activity-panel-list" class="activity-panel-list">
<div class="text-muted small p-3">No jobs yet</div>
</div>
</div>
</div>
<!-- Notification Tray - Global across all views -->
<div id="notification-tray" class="notification-tray">
<button id="notification-tray-toggle" class="notification-tray-toggle" aria-label="Notifications">
@@ -222,122 +239,156 @@
<i class="fas fa-plug me-2"></i>Add peer
</button>
</div>
<!-- Dashboard View — fit-to-viewport, no outer scroll -->
<!-- First-connect checklist (shown once after successful connect) -->
<div id="first-connect-checklist" class="first-connect-checklist hidden" role="dialog" aria-labelledby="first-connect-title">
<div class="first-connect-card">
<div class="first-connect-head">
<h3 id="first-connect-title"><i class="fas fa-circle-check me-2 text-success"></i>You're connected</h3>
<button type="button" class="btn-close btn-close-white" id="first-connect-dismiss" aria-label="Dismiss"></button>
</div>
<p class="text-muted small mb-3">A short tour of what to do next:</p>
<ol class="first-connect-steps">
<li><button type="button" class="first-connect-link" data-view="dashboard">Open the dashboard</button> for live host health</li>
<li><button type="button" class="first-connect-link" data-view="containers">Browse containers</button> or <button type="button" class="first-connect-link" data-view="deploy">deploy a template</button></li>
<li><button type="button" class="first-connect-link" data-view="settings">Settings</button> for density, auto-refresh, and template lists</li>
<li>Press <kbd>Ctrl</kbd>+<kbd>K</kbd> anytime for the command palette</li>
</ol>
<button type="button" class="btn btn-primary btn-sm" id="first-connect-got-it">Got it</button>
</div>
</div>
<!-- Dashboard View — modern fit-to-viewport shell -->
<div id="dashboard-view" class="view hidden">
<div class="dashboard-layout">
<div class="page-header dashboard-header">
<div>
<h2><i class="fas fa-gauge-high"></i>Dashboard</h2>
<p class="page-subtitle">Host overview and quick actions</p>
<div class="dashboard-layout dash">
<header class="dash-header">
<div class="dash-header-text">
<p class="dash-kicker">Overview</p>
<h2 class="dash-title">Dashboard</h2>
<p class="dash-subtitle">Live engine health, storage, and activity on the active peer</p>
</div>
</div>
<div class="dash-header-actions">
<button type="button" class="dash-icon-btn" onclick="window.loadDashboard && window.loadDashboard()" title="Refresh dashboard">
<i class="fas fa-rotate"></i>
</button>
<button type="button" class="dash-chip-btn" onclick="window.navigateToView('deploy')">
<i class="fas fa-rocket"></i><span>Deploy</span>
</button>
</div>
</header>
<!-- System Overview Cards -->
<div class="dashboard-stats-row">
<div class="stat-card">
<div class="stat-card-icon" style="background: rgba(16, 185, 129, 0.2); color: var(--accent-success);">
<i class="fas fa-box"></i>
<!-- KPI strip -->
<section class="dash-kpis" aria-label="Workload summary">
<article class="dash-kpi dash-kpi--ok">
<div class="dash-kpi-icon"><i class="fas fa-play"></i></div>
<div class="dash-kpi-meta">
<span class="dash-kpi-label">Running</span>
<span class="dash-kpi-value" id="stat-running-containers">0</span>
</div>
<div class="stat-card-content">
<div class="stat-card-value" id="stat-running-containers">0</div>
<div class="stat-card-label">Running</div>
<div class="dash-kpi-glow" aria-hidden="true"></div>
</article>
<article class="dash-kpi dash-kpi--stop">
<div class="dash-kpi-icon"><i class="fas fa-stop"></i></div>
<div class="dash-kpi-meta">
<span class="dash-kpi-label">Stopped</span>
<span class="dash-kpi-value" id="stat-stopped-containers">0</span>
</div>
</div>
<div class="stat-card">
<div class="stat-card-icon" style="background: rgba(239, 68, 68, 0.2); color: var(--accent-danger);">
<i class="fas fa-stop-circle"></i>
<div class="dash-kpi-glow" aria-hidden="true"></div>
</article>
<article class="dash-kpi dash-kpi--info">
<div class="dash-kpi-icon"><i class="fas fa-layer-group"></i></div>
<div class="dash-kpi-meta">
<span class="dash-kpi-label">Images</span>
<span class="dash-kpi-value" id="stat-total-images">0</span>
</div>
<div class="stat-card-content">
<div class="stat-card-value" id="stat-stopped-containers">0</div>
<div class="stat-card-label">Stopped</div>
<div class="dash-kpi-glow" aria-hidden="true"></div>
</article>
<article class="dash-kpi dash-kpi--teal">
<div class="dash-kpi-icon"><i class="fas fa-diagram-project"></i></div>
<div class="dash-kpi-meta">
<span class="dash-kpi-label">Networks</span>
<span class="dash-kpi-value" id="stat-total-networks">0</span>
</div>
</div>
<div class="stat-card">
<div class="stat-card-icon" style="background: rgba(59, 130, 246, 0.2); color: var(--accent-info);">
<i class="fas fa-images"></i>
</div>
<div class="stat-card-content">
<div class="stat-card-value" id="stat-total-images">0</div>
<div class="stat-card-label">Images</div>
</div>
</div>
<div class="stat-card">
<div class="stat-card-icon" style="background: rgba(20, 184, 166, 0.2); color: var(--accent-secondary);">
<i class="fas fa-network-wired"></i>
</div>
<div class="stat-card-content">
<div class="stat-card-value" id="stat-total-networks">0</div>
<div class="stat-card-label">Networks</div>
</div>
</div>
</div>
<div class="dash-kpi-glow" aria-hidden="true"></div>
</article>
</section>
<!-- Info panels fill remaining height -->
<div class="dashboard-panels">
<div class="info-card">
<h5 class="info-card-title"><i class="fas fa-info-circle me-2"></i>Docker Information</h5>
<div id="docker-info-content" class="info-card-body">
<div class="skeleton skeleton-text"></div>
<div class="skeleton skeleton-text mt-2"></div>
<!-- Main panels -->
<section class="dash-panels">
<article class="dash-card">
<div class="dash-card-head">
<div class="dash-card-title"><i class="fas fa-gears"></i><span>Engine</span></div>
</div>
</div>
<div class="info-card">
<h5 class="info-card-title"><i class="fas fa-server me-2"></i>System Resources</h5>
<div id="system-resources-content" class="info-card-body">
<div class="skeleton skeleton-text"></div>
<div class="skeleton skeleton-text mt-2"></div>
<div id="docker-info-content" class="dash-card-body">
<div class="dash-skeleton">
<div class="dash-skel-line"></div>
<div class="dash-skel-line"></div>
<div class="dash-skel-line short"></div>
</div>
</div>
</div>
<div class="info-card">
<div class="d-flex justify-content-between align-items-center gap-2 info-card-heading">
<h5 class="info-card-title mb-0 text-truncate"><i class="fas fa-hdd me-2"></i>Disk Usage</h5>
<button class="btn btn-sm btn-outline-secondary flex-shrink-0" type="button" onclick="window.refreshSystemDf && window.refreshSystemDf()" title="Refresh system df">
<i class="fas fa-sync-alt"></i>
</article>
<article class="dash-card">
<div class="dash-card-head">
<div class="dash-card-title"><i class="fas fa-microchip"></i><span>Host</span></div>
</div>
<div id="system-resources-content" class="dash-card-body">
<div class="dash-skeleton">
<div class="dash-skel-line"></div>
<div class="dash-skel-line"></div>
<div class="dash-skel-line short"></div>
</div>
</div>
</article>
<article class="dash-card">
<div class="dash-card-head">
<div class="dash-card-title"><i class="fas fa-hard-drive"></i><span>Disk</span></div>
<button type="button" class="dash-icon-btn dash-icon-btn--sm" onclick="window.refreshSystemDf && window.refreshSystemDf()" title="Refresh disk usage">
<i class="fas fa-rotate"></i>
</button>
</div>
<div id="system-df-content" class="info-card-body">
<div class="text-muted small">Connect to load Docker disk usage</div>
<div id="system-df-content" class="dash-card-body">
<div class="dash-empty">Connect a peer to load disk usage</div>
</div>
</div>
<div class="info-card">
<div class="d-flex justify-content-between align-items-center gap-2 info-card-heading">
<h5 class="info-card-title mb-0 text-truncate"><i class="fas fa-stream me-2"></i>Live Events</h5>
<button class="btn btn-sm btn-outline-secondary flex-shrink-0" type="button" onclick="window.clearEventTimeline && window.clearEventTimeline()" title="Clear">
</article>
<article class="dash-card dash-card--events">
<div class="dash-card-head">
<div class="dash-card-title"><i class="fas fa-bolt"></i><span>Live events</span></div>
<button type="button" class="dash-icon-btn dash-icon-btn--sm" onclick="window.clearEventTimeline && window.clearEventTimeline()" title="Clear events">
<i class="fas fa-eraser"></i>
</button>
</div>
<div id="docker-events-timeline" class="info-card-body events-timeline">
<div class="text-muted small">Docker events will appear here…</div>
<div id="docker-events-timeline" class="dash-card-body events-timeline dash-events">
<div class="dash-empty">Docker events will stream here…</div>
</div>
</div>
</div>
</article>
</section>
<!-- Quick Actions -->
<div class="info-card dashboard-actions">
<h5 class="info-card-title"><i class="fas fa-bolt me-2"></i>Quick Actions</h5>
<div class="info-card-body">
<div class="d-flex gap-2 flex-wrap dashboard-action-btns">
<button class="btn btn-sm btn-primary" onclick="window.navigateToView('containers')">
<i class="fas fa-box me-1"></i>Containers
</button>
<button class="btn btn-sm btn-outline-primary" onclick="window.navigateToView('images')">
<i class="fas fa-images me-1"></i>Images
</button>
<button class="btn btn-sm btn-outline-primary" onclick="window.navigateToView('networks')">
<i class="fas fa-network-wired me-1"></i>Networks
</button>
<button class="btn btn-sm btn-outline-primary" onclick="window.navigateToView('volumes')">
<i class="fas fa-database me-1"></i>Volumes
</button>
<button class="btn btn-sm btn-outline-warning" type="button" onclick="window.pruneResource && window.pruneResource('containers')" title="Prune stopped containers">
<i class="fas fa-broom me-1"></i>Prune Containers
</button>
<button class="btn btn-sm btn-outline-warning" type="button" onclick="window.pruneResource && window.pruneResource('images')" title="Prune unused images">
<i class="fas fa-broom me-1"></i>Prune Images
</button>
</div>
<!-- Quick actions dock -->
<footer class="dash-actions">
<div class="dash-actions-label"><i class="fas fa-bolt"></i> Quick actions</div>
<div class="dash-actions-row">
<button type="button" class="dash-action" onclick="window.navigateToView('containers')">
<i class="fas fa-cube"></i><span>Containers</span>
</button>
<button type="button" class="dash-action" onclick="window.navigateToView('images')">
<i class="fas fa-layer-group"></i><span>Images</span>
</button>
<button type="button" class="dash-action" onclick="window.navigateToView('networks')">
<i class="fas fa-diagram-project"></i><span>Networks</span>
</button>
<button type="button" class="dash-action" onclick="window.navigateToView('volumes')">
<i class="fas fa-hard-drive"></i><span>Volumes</span>
</button>
<button type="button" class="dash-action dash-action--warn" data-min-role="admin" onclick="window.pruneResource && window.pruneResource('containers')" title="Prune stopped containers">
<i class="fas fa-broom"></i><span>Prune containers</span>
</button>
<button type="button" class="dash-action dash-action--warn" data-min-role="admin" onclick="window.pruneResource && window.pruneResource('images')" title="Prune unused images">
<i class="fas fa-broom"></i><span>Prune images</span>
</button>
</div>
</div>
</footer>
</div>
</div>
@@ -931,7 +982,7 @@
<button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#buildImageModal">
<i class="fas fa-hammer me-2"></i>Build Image
</button>
<button class="btn btn-outline-warning" type="button" onclick="window.pruneResource && window.pruneResource('images')">
<button class="btn btn-outline-warning" type="button" data-min-role="admin" onclick="window.pruneResource && window.pruneResource('images')">
<i class="fas fa-broom me-2"></i>Prune
</button>
</div>
@@ -1007,7 +1058,7 @@
<button class="btn btn-primary" type="button" id="networks-create-btn" data-action="create-network">
<i class="fas fa-plus me-2"></i>Create Network
</button>
<button class="btn btn-outline-warning" type="button" onclick="window.pruneResource && window.pruneResource('networks')">
<button class="btn btn-outline-warning" type="button" data-min-role="admin" onclick="window.pruneResource && window.pruneResource('networks')">
<i class="fas fa-broom me-2"></i>Prune
</button>
</div>
@@ -1276,10 +1327,13 @@
<h2><i class="fas fa-boxes-stacked"></i>Stacks</h2>
<p class="page-subtitle">Compose stacks on the remote host</p>
</div>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#deploy-stack-modal">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#deploy-stack-modal" data-min-role="operator">
<i class="fas fa-plus me-2"></i>Deploy Stack
</button>
</div>
<div class="view-toolbar mb-3">
<input type="search" id="stack-search" class="form-control form-control-sm bg-dark text-white list-search-input" placeholder="Search stacks…" spellcheck="false" autocomplete="off">
</div>
<div id="stacks-list" class="table-responsive">
<table class="table table-dark table-hover">
<thead>
@@ -1349,7 +1403,7 @@ services:
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createVolumeModal">
<i class="fas fa-plus me-2"></i>Create Volume
</button>
<button class="btn btn-outline-warning" type="button" onclick="window.pruneResource && window.pruneResource('volumes')">
<button class="btn btn-outline-warning" type="button" data-min-role="admin" onclick="window.pruneResource && window.pruneResource('volumes')">
<i class="fas fa-broom me-2"></i>Prune
</button>
</div>
@@ -1404,7 +1458,7 @@ services:
<div class="card bg-dark border-secondary">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="fas fa-users me-2"></i>Peers</span>
<button class="btn btn-sm btn-primary" id="access-invite-btn" type="button">
<button class="btn btn-sm btn-primary" id="access-invite-btn" type="button" data-bs-toggle="modal" data-bs-target="#invitePeerModal" data-min-role="admin">
<i class="fas fa-ticket me-1"></i>Create invite
</button>
</div>
@@ -3346,35 +3400,69 @@ services:
</div>
</div>
<!-- Create Network Modal -->
<div class="modal fade" id="createNetworkModal" tabindex="-1" aria-labelledby="createNetworkModalLabel" aria-hidden="true">
<!-- Invite peer modal -->
<div class="modal fade" id="invitePeerModal" tabindex="-1" aria-labelledby="invitePeerModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content bg-dark text-white">
<div class="modal-header border-secondary">
<h5 class="modal-title" id="invitePeerModalLabel"><i class="fas fa-ticket me-2"></i>Create invite</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="invite-peer-form">
<div class="modal-body">
<p class="small text-muted">Generate a one-time (or multi-use) invite token for a new client key.</p>
<div class="mb-3">
<label class="form-label" for="invite-role">Role</label>
<select id="invite-role" class="form-select bg-dark text-white">
<option value="viewer">viewer</option>
<option value="operator" selected>operator</option>
<option value="admin">admin</option>
</select>
</div>
<div class="row g-2 mb-3">
<div class="col-6">
<label class="form-label" for="invite-ttl">TTL (hours)</label>
<input type="number" id="invite-ttl" class="form-control bg-dark text-white" min="1" max="720" value="72">
</div>
<div class="col-6">
<label class="form-label" for="invite-max-uses">Max uses</label>
<input type="number" id="invite-max-uses" class="form-control bg-dark text-white" min="1" max="100" value="1">
</div>
</div>
<div id="invite-token-result" class="invite-token-result hidden">
<label class="form-label">Invite token</label>
<div class="input-group input-group-sm">
<input type="text" id="invite-token-value" class="form-control bg-dark text-white font-monospace" readonly>
<button type="button" class="btn btn-outline-primary" id="invite-token-copy"><i class="fas fa-copy"></i></button>
</div>
<p class="small text-muted mt-2 mb-0">Copy this now — it will not be shown in full after you leave this dialog.</p>
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary" id="invite-peer-submit">
<i class="fas fa-ticket me-1"></i>Create invite
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Legacy create-network modal removed — use smart create (#createNetworkSmartModal) -->
<div class="modal fade d-none" id="createNetworkModal" tabindex="-1" aria-hidden="true" data-legacy="removed">
<div class="modal-dialog">
<div class="modal-content bg-dark text-white">
<div class="modal-header">
<h5 class="modal-title" id="createNetworkModalLabel"><i class="fas fa-plus me-2"></i>Create Network</h5>
<h5 class="modal-title">Create Network</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="create-network-form">
<div class="mb-3">
<label for="network-name" class="form-label">Network Name</label>
<input type="text" id="network-name" class="form-control bg-dark text-white" placeholder="my-network" required>
</div>
<div class="mb-3">
<label for="network-driver" class="form-label">Driver</label>
<select id="network-driver" class="form-select bg-dark text-white">
<option value="bridge">Bridge</option>
<option value="overlay">Overlay</option>
<option value="macvlan">Macvlan</option>
<option value="host">Host</option>
</select>
</div>
<div class="mb-3">
<label for="network-subnet" class="form-label">Subnet (Optional)</label>
<input type="text" id="network-subnet" class="form-control bg-dark text-white" placeholder="172.20.0.0/16">
<small class="text-muted">CIDR notation (e.g., 172.20.0.0/16)</small>
</div>
</form>
<p class="text-muted small mb-0">This form was replaced by the smart Create network dialog.</p>
<form id="create-network-form" class="d-none">
<input type="text" id="network-name">
<select id="network-driver"><option value="bridge">bridge</option></select>
<input type="text" id="network-subnet">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>