diff --git a/docs/superpowers/plans/2026-06-29-dashboard-main-page-polish.md b/docs/superpowers/plans/2026-06-29-dashboard-main-page-polish.md new file mode 100644 index 0000000..bf91e15 --- /dev/null +++ b/docs/superpowers/plans/2026-06-29-dashboard-main-page-polish.md @@ -0,0 +1,307 @@ +# Dashboard Main-Page Polish Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Polish the manager dashboard so Services is the clear hero, Notifications is a slim self-explanatory rail, LOGS/DOCS read as buttons, and short content no longer leaves a top-heavy void. + +**Architecture:** Frontend-only. Two files: `manager/routers/dashboard_api.py` (Dash layout markup — one added subtitle element) and `manager/assets/dashboard_styles.css` (all styling). Guard tests are string-assert checks in `tests/manager_test/test_ui_redesign.py`, matching the existing pattern (read the CSS file / `str(app.layout)` and assert substrings). + +**Tech Stack:** Dash (Plotly) layout in Python, plain CSS with design tokens from `tokens.css`, pytest guard tests via `test_client` fixtures in `tests/conftest.py`. + +## Global Constraints + +- No backend, API, endpoint, or data-model changes. CSS + one markup element only. +- No external resources: CSS must contain no `http://` or `https://` (enforced by `test_dashboard_css_uses_tokens`). +- Reuse existing design tokens from `tokens.css` (`var(--muted)`, `var(--line)`, `var(--accent)`, etc.) — introduce no new colors. +- The logs view page (`logs.html`), login, and `max-width:1600px` on `.page` stay as-is. Do not reintroduce `max-width:1180px` (guarded by `test_dashboard_page_width_widened`). +- Run before pushing: `/home/kaveh/miniconda3/bin/python -m black --check`, `... -m flake8`, `... -m pylint manager`, and `./venv/bin/python -m pytest tests/manager_test/test_ui_redesign.py`. +- Mobile rule `@media (max-width:760px){ .grid{grid-template-columns:1fr} ... }` must keep collapsing the grid to one column. + +--- + +### Task 1: Widen Services / slim sidebar + vertical fill + +**Files:** +- Modify: `manager/assets/dashboard_styles.css` (the `.page` rule at line 32 and the `.grid` rule at line 33) +- Test: `tests/manager_test/test_ui_redesign.py` + +**Interfaces:** +- Consumes: nothing from other tasks. +- Produces: the `.grid` / `.page` CSS shape that Task 2 and Task 3 visually sit inside; no code symbols. + +Current CSS (lines 31-33): + +```css +/* ---------- page / grid ---------- */ +.page{max-width:1600px;margin:0 auto;padding:1.8rem 1.6rem 4rem} +.grid{display:grid;grid-template-columns:1.85fr 1fr;gap:1.4rem;align-items:start} +``` + +- [ ] **Step 1: Write the failing test** + +Add to `tests/manager_test/test_ui_redesign.py`: + +```python +def test_dashboard_grid_widens_services_and_fills_viewport(): + css = (ASSETS / "dashboard_styles.css").read_text().replace(" ", "") + # Services hero is wider than the slim Notifications rail + assert "grid-template-columns:2.6fr0.9fr" in css + assert "grid-template-columns:1.85fr1fr" not in css + # Page fills the viewport height (minus the banner) and centers when short + assert "min-height:calc(100vh-4rem)" in css + assert "margin-block:auto" in css + assert "box-sizing:border-box" in css +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `./venv/bin/python -m pytest "tests/manager_test/test_ui_redesign.py::test_dashboard_grid_widens_services_and_fills_viewport" -v` +Expected: FAIL (`assert "grid-template-columns:2.6fr0.9fr" in css` is False — file still has `1.85fr 1fr`). + +- [ ] **Step 3: Write minimal implementation** + +Replace lines 31-33 of `manager/assets/dashboard_styles.css` with: + +```css +/* ---------- page / grid ---------- */ +.page{ + max-width:1600px;margin:0 auto;padding:1.8rem 1.6rem 4rem; + box-sizing:border-box;min-height:calc(100vh - 4rem); + display:flex;flex-direction:column; +} +.grid{ + display:grid;grid-template-columns:2.6fr 0.9fr;gap:1.4rem;align-items:start; + margin-block:auto;width:100%; +} +``` + +Notes for the implementer: +- `vh` is viewport-relative, so `min-height:calc(100vh - 4rem)` makes `.page` nearly fill the screen without touching `body` or Dash's wrapper divs. The `4rem` budgets for the top banner. +- `box-sizing:border-box` is required because there is no global reset and `.page` has padding; without it the padding would add to the `100vh` height and force a scrollbar. +- `margin-block:auto` on `.grid` centers it vertically inside the tall `.page` when there is spare room; when the grid is taller than the page the auto margins collapse to 0 and the page grows/scrolls normally. +- `width:100%` keeps the grid full-width inside the flex column (a flex item would otherwise shrink to content width). + +- [ ] **Step 4: Run test to verify it passes** + +Run: `./venv/bin/python -m pytest "tests/manager_test/test_ui_redesign.py::test_dashboard_grid_widens_services_and_fills_viewport" -v` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add manager/assets/dashboard_styles.css tests/manager_test/test_ui_redesign.py +git commit -m "Dashboard: widen Services, slim sidebar, fill viewport height" +``` + +--- + +### Task 2: Explain the Notifications panel + +**Files:** +- Modify: `manager/routers/dashboard_api.py` (the Notifications panel head in `app.layout`, around lines 334-340) +- Modify: `manager/assets/dashboard_styles.css` (add `.panel-sub` near the `.panel-head` rules, ~line 44) +- Test: `tests/manager_test/test_ui_redesign.py` + +**Interfaces:** +- Consumes: nothing from other tasks. +- Produces: a `

` element with the literal subtitle text below; relied on only by its own test. + +Current markup (lines 334-345 of `dashboard_api.py`): + +```python + # Notifications panel + html.Section( + className="panel", + children=[ + html.Div( + className="panel-head", + children=[html.H2("Notifications")], + ), + html.Div( + id="notifications-content", + children=[generate_table_notifications()], + ), + ], + ), +``` + +- [ ] **Step 1: Write the failing test** + +Add to `tests/manager_test/test_ui_redesign.py`: + +```python +def test_notifications_panel_has_explanation(): + from manager.routers import dashboard_api + + layout = str(dashboard_api.app.layout) + assert "Alerts services raise" in layout # one-line description of the panel + css = (ASSETS / "dashboard_styles.css").read_text() + assert ".panel-sub" in css +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `./venv/bin/python -m pytest "tests/manager_test/test_ui_redesign.py::test_notifications_panel_has_explanation" -v` +Expected: FAIL (`assert "Alerts services raise" in layout` is False — no subtitle yet). + +- [ ] **Step 3: Write minimal implementation** + +In `manager/routers/dashboard_api.py`, change the Notifications `panel-head` children to include a subtitle (replace the single-child `panel-head` Div shown above): + +```python + html.Div( + className="panel-head", + children=[ + html.Div( + [ + html.H2("Notifications"), + html.P( + "Alerts services raise via the SDK" + " notify() — info, warnings, critical.", + className="panel-sub", + ), + ] + ) + ], + ), +``` + +Then add the `.panel-sub` style to `manager/assets/dashboard_styles.css` immediately after the `.panel-head h2` rule (line 44): + +```css +.panel-sub{margin:.25rem 0 0;font-size:11px;line-height:1.4;color:var(--muted)} +``` + +Note: keep the `()` in `notify()` but do not include any URL — the CSS/markup must stay free of `http(s)://` (Task constraint). The text uses a plain hyphen. + +- [ ] **Step 4: Run test to verify it passes** + +Run: `./venv/bin/python -m pytest "tests/manager_test/test_ui_redesign.py::test_notifications_panel_has_explanation" -v` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add manager/routers/dashboard_api.py manager/assets/dashboard_styles.css tests/manager_test/test_ui_redesign.py +git commit -m "Dashboard: explain the Notifications panel with a subtitle" +``` + +--- + +### Task 3: Make LOGS / DOCS read as buttons + +**Files:** +- Modify: `manager/assets/dashboard_styles.css` (the `.lnk` rules at lines 90-95) +- Test: `tests/manager_test/test_ui_redesign.py` + +**Interfaces:** +- Consumes: nothing from other tasks. +- Produces: restyled `.lnk` (markup unchanged — `get_service_log_link` / `get_service_docs_link` still emit ``). + +Current CSS (lines 88-95): + +```css +/* ---------- service actions ---------- */ +.svc-actions{grid-area:actions;display:flex;gap:.4rem} +.lnk{ + font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase; + color:var(--muted);text-decoration:none;border-bottom:1px solid transparent;padding:.1rem 0; + transition:color .15s,border-color .15s; +} +.lnk:hover{color:var(--accent);border-color:var(--accent)} +``` + +- [ ] **Step 1: Write the failing test** + +Add to `tests/manager_test/test_ui_redesign.py`: + +```python +def test_service_links_look_like_buttons(): + css = (ASSETS / "dashboard_styles.css").read_text().replace(" ", "") + # .lnk is now a bordered pill, not underline-on-hover text + assert ".lnk{" in css + lnk_block = css.split(".lnk{", 1)[1].split("}", 1)[0] + assert "border:1px solid" in lnk_block + assert "border-radius:" in lnk_block + # keyboard focus ring, matching .act + assert ".lnk:focus-visible{" in css +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `./venv/bin/python -m pytest "tests/manager_test/test_ui_redesign.py::test_service_links_look_like_buttons" -v` +Expected: FAIL (the current `.lnk` block has `border-bottom`, not `border:1px solid`, and no `:focus-visible`). + +- [ ] **Step 3: Write minimal implementation** + +Replace lines 90-95 of `manager/assets/dashboard_styles.css` with: + +```css +.lnk{ + font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase; + color:var(--muted);text-decoration:none; + border:1px solid var(--line);border-radius:7px;padding:.34rem .6rem; + transition:color .15s,border-color .15s,background .15s; +} +.lnk:hover{color:var(--accent);border-color:var(--accent-dim);background:rgba(94,230,208,.06)} +.lnk:focus-visible{outline:2px solid var(--accent);outline-offset:2px} +``` + +Note: this mirrors the existing `.act` button look (border + rounded + accent hover + focus ring) so the two LOGS/DOCS controls clearly read as clickable. `.svc-actions` already has `gap:.4rem`, which spaces the two pills correctly — no change needed there. + +- [ ] **Step 4: Run test to verify it passes** + +Run: `./venv/bin/python -m pytest "tests/manager_test/test_ui_redesign.py::test_service_links_look_like_buttons" -v` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add manager/assets/dashboard_styles.css tests/manager_test/test_ui_redesign.py +git commit -m "Dashboard: style LOGS/DOCS as buttons with focus ring" +``` + +--- + +### Task 4: Full guard-suite + lint pass + +**Files:** +- No new code. Verification only. + +- [ ] **Step 1: Run the full UI guard suite** + +Run: `./venv/bin/python -m pytest tests/manager_test/test_ui_redesign.py -v` +Expected: PASS (all existing tests + the three new ones). Confirms no earlier guard (e.g. `test_dashboard_layout_builds`, `test_dashboard_page_width_widened`, `test_notifications_panel_is_live`) regressed. + +- [ ] **Step 2: Lint the changed files** + +Run: +```bash +/home/kaveh/miniconda3/bin/python -m black --check manager/routers/dashboard_api.py tests/manager_test/test_ui_redesign.py +/home/kaveh/miniconda3/bin/python -m flake8 manager/routers/dashboard_api.py tests/manager_test/test_ui_redesign.py +/home/kaveh/miniconda3/bin/python -m pylint manager +``` +Expected: black "would be left unchanged", flake8 no output, pylint 10.00/10. (CSS is not linted.) If black reports a reformat on `dashboard_api.py`, run it without `--check` to apply, then re-commit. + +- [ ] **Step 3: Commit any formatting fixup (only if Step 2 changed files)** + +```bash +git add -A +git commit -m "Dashboard polish: black formatting" +``` + +--- + +## Self-Review + +**Spec coverage:** +- Widen Services / slim sidebar → Task 1 (`2.6fr 0.9fr`). ✓ +- Explain Notifications → Task 2 (`panel-sub` subtitle). ✓ +- LOGS/DOCS obvious → Task 3 (bordered buttons + focus ring). ✓ +- Fix top-heavy emptiness → Task 1 (`min-height:calc(100vh - 4rem)` + `margin-block:auto`). ✓ +- Testing section (guard-test asserts) → Tasks 1-3 each add one test; Task 4 runs the full suite + lint. ✓ +- Non-goals (no stats bar, no badges, no logs-view change, no backend) → respected; no task touches them. ✓ + +**Placeholder scan:** No TBD/TODO; every code step shows full CSS/markup and exact commands. ✓ + +**Type/string consistency:** The subtitle literal "Alerts services raise via the SDK notify() — info, warnings, critical." in Task 2's implementation matches the test's substring "Alerts services raise". The class names `.panel-sub`, `.lnk`, `.grid`, `.page` are used identically across implementation and tests. The `2.6fr 0.9fr` value (space-stripped to `2.6fr0.9fr` in tests) is consistent. ✓ diff --git a/docs/superpowers/specs/2026-06-29-dashboard-main-page-polish-design.md b/docs/superpowers/specs/2026-06-29-dashboard-main-page-polish-design.md new file mode 100644 index 0000000..111829f --- /dev/null +++ b/docs/superpowers/specs/2026-06-29-dashboard-main-page-polish-design.md @@ -0,0 +1,97 @@ +# Dashboard main-page polish — design + +**Date:** 2026-06-29 +**Branch:** `dashboard-main-page-polish` → `develop` +**Scope:** Frontend-only polish of the manager dashboard (the Dash "main page"). No backend, no API, no new data. The logs *view* page is explicitly out of scope (unchanged). + +## Problem + +On a wide monitor the dashboard reads top-heavy and sparse: content hugs the top with a large empty void below. Secondary issues raised by the user: + +1. The **Services** panel feels too narrow relative to its importance. +2. The **Notifications** panel is unexplained — an empty "No notifications." reads as "what is this section?" rather than "nothing wrong." +3. The per-service **LOGS / DOCS** links are not obviously clickable (tiny uppercase text, underline only on hover). + +## Goals + +- Make Services the clear hero (wider) and Notifications a slim, self-explanatory right rail. +- Remove the top-heavy empty void without inventing new content. +- Make LOGS / DOCS read as actionable controls. + +## Non-goals (YAGNI) + +- No new stats/summary bar, service counts, or notification badges. +- No structural rewrite of the Dash layout or callbacks. +- No change to the logs view page, login, or any backend/endpoint. + +## Design + +All changes are in `manager/routers/dashboard_api.py` (layout markup) and +`manager/assets/dashboard_styles.css` (styling). The design tokens in +`tokens.css` are reused; no new colors. + +### 1. Widen Services, slim the sidebar +`.grid` columns change from `1.85fr 1fr` to **`2.6fr 0.9fr`**. Services gets +the dominant width; Notifications becomes a narrow rail. The existing +`@media (max-width:760px)` rule already collapses to a single column, so the +mobile path is unchanged. + +### 2. Explain Notifications +Add a one-line muted subtitle beneath the "Notifications" heading in the panel +head, e.g.: + +> Alerts services raise via the SDK `notify()` — info, warnings, critical. + +Implemented as a small `

` element in the Notifications +panel head. A new `.panel-sub` style (muted, ~11px) renders it quietly. The +Services panel head is left as-is (no subtitle needed). + +### 3. Make LOGS / DOCS obvious +Restyle the per-service `.lnk` links to small bordered pill buttons that reuse +the look of the existing `.act` controls (1px border, rounded, accent on +hover) instead of underline-on-hover text. Keep them compact so the row height +is unchanged. Markup stays the same (``); only CSS changes, +plus an accessible `:focus-visible` ring to match `.act`. + +### 4. Fix top-heavy emptiness +Vertically center the page content when it is shorter than the viewport, while +falling back to normal top-aligned scrolling once there are enough services to +fill the screen. Implemented with a viewport-relative min-height plus flex +auto-margins (which center but never clip): + +- The dashboard is a Dash app: the layout is mounted inside Dash's own wrapper + divs, so a `body { flex }` chain would not reliably reach `.page`. Instead, + size `.page` directly against the viewport. +- `.page` gets `min-height:calc(100vh - 4rem)` (the `4rem` budgets for the top + banner), `box-sizing:border-box` (there is no global reset, and `.page` has + padding), and `display:flex; flex-direction:column`. `vh` is + viewport-relative regardless of ancestor heights, so this needs no changes to + body or the Dash wrappers. +- The inner `.grid` gets `margin-block:auto`. Auto margins center the grid + vertically when there is spare room; when the grid is taller than the + available space the margins collapse to zero and the page scrolls normally — + no overflow clipping. + +This removes the void below the cards on a tall screen without adding content. + +## Testing + +Extend the existing guard tests in `tests/manager_test/test_ui_redesign.py` +(asset/markup string asserts, consistent with the current approach): + +- Notifications subtitle text (`panel-sub`) is present in the rendered layout. +- `.panel-sub` style exists in `dashboard_styles.css`. +- `.lnk` is styled as a bordered button (border + border-radius) and has a + `:focus-visible` rule. +- `.grid` uses the new `2.6fr 0.9fr` template. +- Vertical-fill rule present (`.page` has `min-height:calc(100vh - 4rem)` and + `.grid` has `margin-block:auto`). + +Run locally before pushing: `black --check`, `flake8`, `pylint manager`, and +`pytest tests/manager_test/test_ui_redesign.py`. + +## Rollout + +Frontend-only; ships in the next `develop` → `master` release alongside the +logs-viewer work. The live server picks it up on the next manager image build +(connectors untouched). diff --git a/manager/assets/dashboard_styles.css b/manager/assets/dashboard_styles.css index bf3f0ef..f1722c2 100644 --- a/manager/assets/dashboard_styles.css +++ b/manager/assets/dashboard_styles.css @@ -29,8 +29,15 @@ a{color:inherit;text-decoration:none;} .act.danger:hover{color:var(--crit);border-color:var(--crit)} /* ---------- page / grid ---------- */ -.page{max-width:1600px;margin:0 auto;padding:1.8rem 1.6rem 4rem} -.grid{display:grid;grid-template-columns:1.85fr 1fr;gap:1.4rem;align-items:start} +.page{ + max-width:1600px;margin:0 auto;padding:1.8rem 1.6rem 4rem; + box-sizing:border-box;min-height:calc(100vh - 4rem); + display:flex;flex-direction:column; +} +.grid{ + display:grid;grid-template-columns:2.6fr 0.9fr;gap:1.4rem;align-items:start; + margin-block:auto;width:100%; +} /* ---------- panels ---------- */ .panel{ @@ -42,6 +49,7 @@ a{color:inherit;text-decoration:none;} padding:1rem 1.2rem .85rem;border-bottom:1px solid var(--line-soft); } .panel-head h2{font-size:.82rem;letter-spacing:.04em;font-weight:600;margin:0} +.panel-sub{margin:.25rem 0 0;font-size:11px;line-height:1.4;color:var(--muted)} .count{font-family:var(--mono);font-size:11px;color:var(--faint)} /* ---------- service rows ---------- */ @@ -88,11 +96,13 @@ a{color:inherit;text-decoration:none;} /* ---------- service actions ---------- */ .svc-actions{grid-area:actions;display:flex;gap:.4rem} .lnk{ - font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase; - color:var(--muted);text-decoration:none;border-bottom:1px solid transparent;padding:.1rem 0; - transition:color .15s,border-color .15s; + font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase; + color:var(--muted);text-decoration:none; + border:1px solid var(--line);border-radius:7px;padding:.34rem .6rem; + transition:color .15s,border-color .15s,background .15s; } -.lnk:hover{color:var(--accent);border-color:var(--accent)} +.lnk:hover{color:var(--accent);border-color:var(--accent-dim);background:rgba(94,230,208,.06)} +.lnk:focus-visible{outline:2px solid var(--accent);outline-offset:2px} /* ---------- notifications ---------- */ .note{ diff --git a/manager/routers/dashboard_api.py b/manager/routers/dashboard_api.py index 67ef13f..8897545 100644 --- a/manager/routers/dashboard_api.py +++ b/manager/routers/dashboard_api.py @@ -336,7 +336,19 @@ def get_severity_colors(notifications): children=[ html.Div( className="panel-head", - children=[html.H2("Notifications")], + children=[ + html.Div( + [ + html.H2("Notifications"), + html.P( + "Alerts services raise via the SDK" + " notify() — info, warnings," + " critical.", + className="panel-sub", + ), + ] + ) + ], ), html.Div( id="notifications-content", diff --git a/tests/manager_test/test_ui_redesign.py b/tests/manager_test/test_ui_redesign.py index e754f71..eda4a77 100644 --- a/tests/manager_test/test_ui_redesign.py +++ b/tests/manager_test/test_ui_redesign.py @@ -165,3 +165,34 @@ def test_logs_toolbar_has_search_and_export(): assert "EXPORT_BASENAME" in html and "FULL_URL" in html assert "a.download" in html # triggers a file download assert "createTextNode" in html # XSS-safe highlight + + +def test_dashboard_grid_widens_services_and_fills_viewport(): + css = (ASSETS / "dashboard_styles.css").read_text().replace(" ", "") + # Services hero is wider than the slim Notifications rail + assert "grid-template-columns:2.6fr0.9fr" in css + assert "grid-template-columns:1.85fr1fr" not in css + # Page fills the viewport height (minus the banner) and centers when short + assert "min-height:calc(100vh-4rem)" in css + assert "margin-block:auto" in css + assert "box-sizing:border-box" in css + + +def test_notifications_panel_has_explanation(): + from manager.routers import dashboard_api + + layout = str(dashboard_api.app.layout) + assert "Alerts services raise" in layout # one-line description of the panel + css = (ASSETS / "dashboard_styles.css").read_text() + assert ".panel-sub" in css + + +def test_service_links_look_like_buttons(): + css = (ASSETS / "dashboard_styles.css").read_text().replace(" ", "") + # .lnk is now a bordered pill, not underline-on-hover text + assert ".lnk{" in css + lnk_block = css.split(".lnk{", 1)[1].split("}", 1)[0] + assert "border:1pxsolid" in lnk_block + assert "border-radius:" in lnk_block + # keyboard focus ring, matching .act + assert ".lnk:focus-visible{" in css