diff --git a/cmd/satoru/progress_ws.go b/cmd/satoru/progress_ws.go
index 5ddfc13..42b1f8c 100644
--- a/cmd/satoru/progress_ws.go
+++ b/cmd/satoru/progress_ws.go
@@ -26,6 +26,7 @@ type liveSiteStatus struct {
SiteID int64 `json:"site_id"`
LastRunStatus string `json:"last_run_status"`
LastRunOutput string `json:"last_run_output"`
+ LastScanState string `json:"last_scan_state"`
LastRunAt time.Time `json:"last_run_at,omitempty"`
}
@@ -121,10 +122,14 @@ func (a *app) buildLiveProgressPayload(ctx context.Context) (liveProgressPayload
SiteID: site.ID,
LastRunStatus: strings.TrimSpace(site.LastRunStatus.String),
LastRunOutput: strings.TrimSpace(site.LastRunOutput.String),
+ LastScanState: strings.TrimSpace(site.LastScanState.String),
}
if row.LastRunStatus == "" {
row.LastRunStatus = "pending"
}
+ if row.LastScanState == "" {
+ row.LastScanState = "pending"
+ }
if site.LastRunAt.Valid {
row.LastRunAt = site.LastRunAt.Time
}
diff --git a/internal/webui/dashboard.go b/internal/webui/dashboard.go
index 3039bd5..6d9b10b 100644
--- a/internal/webui/dashboard.go
+++ b/internal/webui/dashboard.go
@@ -62,8 +62,10 @@ func Dashboard(data DashboardData) templ.Component {
}
var sites strings.Builder
+ var siteNav strings.Builder
if len(data.Sites) == 0 {
sites.WriteString(`
No sites added yet. Add your first Linux SSH site below.
`)
+ siteNav.WriteString(`No sites yet.
`)
}
for _, site := range data.Sites {
last := "Never run"
@@ -90,6 +92,17 @@ func Dashboard(data DashboardData) templ.Component {
if site.LastScanNotes.Valid && site.LastScanNotes.String != "" {
scanNotes = site.LastScanNotes.String
}
+ siteNav.WriteString(fmt.Sprintf(``,
+ site.ID,
+ html.EscapeString(site.SSHUser),
+ html.EscapeString(site.Host),
+ html.EscapeString(runStatus),
+ site.ID,
+ html.EscapeString(runStatus),
+ html.EscapeString(scanState),
+ site.ID,
+ html.EscapeString(scanState),
+ ))
var targets strings.Builder
if len(site.Targets) == 0 {
@@ -238,55 +251,81 @@ func Dashboard(data DashboardData) templ.Component {
%s
-
- Host Runtime Status
-
-
-
- Planned Workflow
- %s
-
-
-
- Live Backup Progress
- Connecting...
-
- - Waiting for job updates.
-
-
-
-
- No live events yet.
-
-
+
+
+
+
+
+
+ Active Site Logs
+
+ - Waiting for job updates.
+
+
+
+
+ No live events yet.
+
+
+ Planned Workflow
+ %s
+
+
+
@@ -449,9 +515,10 @@ func Dashboard(data DashboardData) templ.Component {
html.EscapeString(data.User.Username),
html.EscapeString(role),
flash,
+ siteNav.String(),
checks.String(),
- flows.String(),
sites.String(),
+ flows.String(),
))
return err
})
diff --git a/web/static/app.css b/web/static/app.css
index 5b78ff4..10f83a0 100644
--- a/web/static/app.css
+++ b/web/static/app.css
@@ -14,8 +14,7 @@
body {
margin: 0;
min-height: 100vh;
- display: grid;
- place-items: center;
+ display: block;
background: radial-gradient(circle at top, #1e293b 0, var(--bg) 55%);
color: var(--fg);
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
@@ -30,7 +29,51 @@ body {
}
.dashboard-card {
- width: min(980px, calc(100vw - 2rem));
+ width: calc(100vw - 2rem);
+ max-width: none;
+ margin: 1rem;
+}
+
+.dashboard-shell {
+ margin-top: 1.2rem;
+ display: grid;
+ grid-template-columns: 320px minmax(0, 1fr);
+ gap: 1rem;
+}
+
+.site-sidebar {
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 0.8rem;
+ height: fit-content;
+ position: sticky;
+ top: 1rem;
+}
+
+.site-main-panel {
+ min-width: 0;
+}
+
+.site-nav-list {
+ display: grid;
+ gap: 0.5rem;
+ margin: 0.75rem 0 1rem;
+}
+
+.site-nav-item {
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ background: color-mix(in srgb, var(--card) 75%, black);
+ color: var(--fg);
+ text-align: left;
+ padding: 0.55rem 0.65rem;
+ display: grid;
+ gap: 0.35rem;
+ cursor: pointer;
+}
+
+.site-nav-item.active {
+ border-color: #38bdf8;
}
.eyebrow {
@@ -142,6 +185,24 @@ h2 {
gap: 0.75rem;
}
+.site-sidebar .status-list {
+ grid-template-columns: 1fr;
+ gap: 0.4rem;
+}
+
+.site-sidebar .status {
+ padding: 0.4rem 0.5rem;
+ gap: 0.2rem;
+}
+
+.site-sidebar .status strong {
+ font-size: 0.82rem;
+}
+
+.site-sidebar .status span {
+ font-size: 0.74rem;
+}
+
.status {
border: 1px solid var(--border);
border-radius: 12px;
@@ -328,6 +389,16 @@ textarea {
to { transform: rotate(360deg); }
}
+@media (max-width: 980px) {
+ .dashboard-shell {
+ grid-template-columns: 1fr;
+ }
+
+ .site-sidebar {
+ position: static;
+ }
+}
+
a {
color: #7dd3fc;
}