feat(ui): expose Global Tuning from Chat + Sources headers (admin-only Tuning nav link)
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
# Phase 29 — Global Tuning nav link
|
||||
|
||||
**Source:** `.agent/phases/complete/27_global_tuning` — the `/tuning.html` page and its create/edit/delete machinery shipped, but the "Tuning" header link that was supposed to *expose* the page (`27` task 04: *"Expose the tuning page via an admin-only header link"`) only exists on `tuning.html` itself. It is not present on the Chat or Sources pages, so there is no way to reach `/tuning.html` from anywhere except typing the URL.
|
||||
**Story:** `.agent/user_stories/global-tuning.md`
|
||||
**Context:** The shared header (phase 19) renders `[Chat, Sources — admin only]` in `<nav class="app-nav">` on the Chat and Sources pages. `frontend/assets/header.js` already contains logic to reveal an admin-only `#nav-tuning` link for admins (lines 69–70) — but that element is never in `index.html`/`sources.html` markup, so the reveal is a no-op. This phase adds the missing markup so the link the JS already handles actually exists.
|
||||
|
||||
## Objective
|
||||
Make the Global Tuning page reachable by adding the admin-only **"Tuning"** nav link to the shared headers of the **Chat** (`index.html`) and **Sources** (`sources.html`) pages — hidden by default, revealed for admins by the existing `header.js` whoami gate (the exact same pattern as the Sources link). No backend, schema, or API change.
|
||||
|
||||
## Dependencies
|
||||
- `19_shared_header` (complete) — the `<nav class="app-nav">` contract, the shared header markup/ids, and the 64px header-height rule on the Chat and Sources pages.
|
||||
- `16_admin_auth` (complete) — the `/api/whoami` gate + `header.js` `fetchIsAdmin()` that reveals admin-only nav links.
|
||||
- `27_global_tuning` (complete) — the `/tuning.html` page and `PUT/POST/DELETE /api/steering` endpoints that this link points to.
|
||||
|
||||
## Tasks
|
||||
1. `01_tuning_links_markup.md` — add the admin-only `#nav-tuning` link to the Chat (`index.html`) and Sources (`sources.html`) headers, hidden by default.
|
||||
2. `02_tuning_nav_e2e_and_regression.md` — add the story E2E suite (`test_tuning_nav_link.py`) + verify no regressions in the header/shared-header/no-CDN suites and commit.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit/integration: no new backend logic (frontend-only markup). The existing no-CDN integration test (`tests/integration/test_api.py::test_index_html_served_locally`) must still pass — the new link is same-origin `<a>`, no new tags.
|
||||
- Coverage: frontend-only; the `app/` >90% gate is unaffected (unchanged).
|
||||
- E2E (mandatory, A16): `tests/e2e/test_tuning_nav_link.py` — the story gate, run in isolation.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] On the **Chat** page (`/`), a signed-in admin sees a **"Tuning"** link (next to Chat/Sources) that navigates to `/tuning.html`; an anonymous visitor does **not** see it (ships `hidden`, `header.js` reveals only for admin).
|
||||
- [ ] Same on the **Sources** page (`/sources.html`).
|
||||
- [ ] The link is absent from the anonymous DOM-reveal (verified via `test_anonymous` behavior); header markup stays valid and semantic.
|
||||
- [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` TOTAL unchanged (no backend change).
|
||||
- [ ] Existing header suites stay green in isolation: `test_header_consistency.py` (64px height + nav structure unchanged), `test_shared_header.py`, `test_smoke.py`, `test_global_tuning.py`.
|
||||
- [ ] `uv run pytest tests/e2e/test_tuning_nav_link.py -v --no-cov` green in isolation.
|
||||
- [ ] `uv run ruff check . && uv run pyright` clean (no Python change, but run the gate).
|
||||
- [ ] UI Structure Check (AGENTS.md rule 5): the link is a labeled, focus-visible `<a>`; header landmarks/contrast unchanged; no CDN.
|
||||
- [ ] One `--no-gpg-sign` commit staging only this phase's files; `.agent/phases/todo/29_tuning_nav_link/` moved to `.agent/phases/complete/`.
|
||||
|
||||
## Locked decisions
|
||||
- **A11 untouched** — vanilla HTML/CSS/JS, no CDN, no new packages; the link is a same-origin `<a href="/tuning.html">`.
|
||||
- **A10 untouched** — no new endpoint or auth surface; the link reuses the existing `#nav-sources` reveal path (`header.js` already gates `#nav-tuning` on `fetchIsAdmin()`).
|
||||
- **No schema / migration** — purely a markup addition.
|
||||
- **A16 / A17 honoured** — one new story E2E suite + one atomic `--no-gpg-sign` commit.
|
||||
- **Scope boundary** — only the Chat and Sources pages (the two that share the standard `app-nav` with the Sources link). The viewer page (`document.html`) uses a different `.doc-header-actions` header variant and the login page is the auth gate; both stay out of scope for this fix.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Task 01 — Tuning nav link on the Chat and Sources pages
|
||||
|
||||
**Phase:** `29_tuning_nav_link` · **Story:** `.agent/user_stories/global-tuning.md`
|
||||
|
||||
## Objective
|
||||
Add the admin-only **"Tuning"** nav link to the shared headers of the **Chat** (`index.html`) and **Sources** (`sources.html`) pages, so `/tuning.html` is reachable from both.
|
||||
|
||||
## Work
|
||||
Add one link to each page, inside the existing `<nav class="app-nav" aria-label="Primary">`, **immediately after** the Sources link, mirroring the Sources pattern exactly.
|
||||
|
||||
1. `frontend/index.html` (Chat page) — after the `#nav-sources` link, add:
|
||||
```html
|
||||
<!-- Phase 29: the Global Tuning link is admin-only (owner permission
|
||||
2026-08-25) — hidden by default, header.js reveals it once
|
||||
whoami says admin, exactly like the Sources link above. Points at
|
||||
the standalone /tuning.html manager (phase 27). -->
|
||||
<a href="/tuning.html" class="nav-link" id="nav-tuning" hidden>Tuning</a>
|
||||
```
|
||||
2. `frontend/sources.html` (Sources page) — same link, placed after its `#nav-sources` link:
|
||||
```html
|
||||
<!-- Phase 29: the Global Tuning link is admin-only (owner permission
|
||||
2026-08-25) — hidden by default, header.js reveals it once
|
||||
whoami says admin, exactly like the Sources link above. -->
|
||||
<a href="/tuning.html" class="nav-link" id="nav-tuning" hidden>Tuning</a>
|
||||
```
|
||||
|
||||
Rules for both edits:
|
||||
- Keep `hidden` by default (anonymous-safe ship-hidden, identical to `#nav-sources`).
|
||||
- Do **not** add `is-active` / `aria-current="page"` — those are "current page" visual states; this link is always hidden on the Chat/Sources pages, and `tuning.html` already sets `is-active` on its own link.
|
||||
- Preserve surrounding markup and indentation so the header layout and the pinned 64px height (phases 12/19) are unchanged. The link is inline in the nav, so height is unaffected.
|
||||
- **Do not modify `frontend/assets/header.js`.** It already handles this element (lines 69–70: `const navTuning = document.querySelector("#nav-tuning"); if (navTuning) navTuning.hidden = !admin;`). The link now exists for that existing reveal code to act on. If, against expectation, that reveal code were absent, add it next to the `navSources` reveal — but it is present, so leave `header.js` untouched.
|
||||
|
||||
## Testing & Quality
|
||||
- No new unit/integration logic (frontend markup only).
|
||||
- Coverage: frontend-only; `app/` coverage unaffected.
|
||||
- This is a same-origin `<a>` — the existing no-CDN integration test (`tests/integration/test_api.py::test_index_html_served_locally`) still passes.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `index.html` and `sources.html` each contain an admin-only `#nav-tuning` `<a href="/tuning.html">` placed after `#nav-sources`, `hidden` by default, without `is-active`.
|
||||
- [ ] Header structure/height unchanged (confirmed in task 02 via `test_header_consistency.py` / `test_shared_header.py`).
|
||||
- [ ] Full test suite green; no behavior change in completed phases.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Task 02 — Tuning nav-link E2E suite, regression checks, and commit
|
||||
|
||||
**Phase:** `29_tuning_nav_link` · **Story:** `.agent/user_stories/global-tuning.md`
|
||||
|
||||
## Objective
|
||||
Prove the new admin-only "Tuning" link works (and is hidden for anonymous users) on the Chat and Sources pages, confirm no header/no-CDN regressions, and commit.
|
||||
|
||||
## Work
|
||||
1. **New story E2E suite** — create `tests/e2e/test_tuning_nav_link.py`, following the harness already used in `tests/e2e/test_global_tuning.py`:
|
||||
- Reuse `from e2e.auth_helpers import login` (real form login on `/login.html`; `login(page, app_url, next=...)` redirects to `next`) and the `db_ready` fixture. No KB seeding is needed for the link-visibility tests (the link is pure header markup gated by `/api/whoami`), so a light module fixture that just brings up the app + mock LLM is enough — mirror `test_global_tuning.py`'s `mock_llm`/`app_url`/`db_ready` params and its `_reset_db` if you want a clean DB.
|
||||
- The visible `#nav-tuning` on an admin page is the sync point (exactly the Sources-link contract): `header.js` reveals it only after whoami says admin.
|
||||
- Write these tests (Playwright `expect`, sync API):
|
||||
1. `test_admin_sees_tuning_link_on_chat` — `login(page, app_url, next="/")`; on the Chat page assert `#nav-tuning` is visible and its `href` is `/tuning.html`; click it and assert the URL navigates to `/tuning.html` (and `#nav-tuning` becomes `is-active`, matching the page's own link).
|
||||
2. `test_admin_sees_tuning_link_on_sources` — `login(page, app_url, next="/sources.html")`; assert `#nav-tuning` is visible on the Sources page and clicking it navigates to `/tuning.html`.
|
||||
3. `test_anonymous_hides_tuning_link_on_chat_and_sources` — visit `/` and `/sources.html` **without** logging in; assert `#nav-tuning` is `hidden` on both pages, `#sign-in-link` visible, `#nav-sources` also hidden (consistent with the existing Sources gate).
|
||||
4. `test_tuning_link_a11y_and_no_cdn` — on the admin Chat page, assert the link is a labeled, focus-visible `<a>` (tab through the header, focus lands on it), and assert every `script[src]`/`link[href]` on the page is same-origin or `data:` (no CDN) — reuse the same `evaluate` scan `test_global_tuning.py::test_tuning_page_a11y_and_no_cdn` uses.
|
||||
2. **Regression verification** — after the new suite is green, run the header- and page-sensitive suites **in isolation** to prove no regression from the added markup/height:
|
||||
```bash
|
||||
uv run pytest tests/e2e/test_header_consistency.py -v --no-cov
|
||||
uv run pytest tests/e2e/test_shared_header.py -v --no-cov
|
||||
uv run pytest tests/e2e/test_smoke.py -v --no-cov
|
||||
uv run pytest tests/e2e/test_global_tuning.py -v --no-cov
|
||||
uv run pytest tests/integration/test_api.py::test_index_html_served_locally -v --no-cov
|
||||
```
|
||||
Fix anything the new link breaks (it should not — the link is inline in the existing nav and does not change header height).
|
||||
3. **Full gate** — run the whole suite with coverage and confirm `app/` coverage TOTAL is **unchanged** (this phase adds no backend logic):
|
||||
```bash
|
||||
uv run pytest
|
||||
uv run pytest --cov=app --cov-report=term-missing # TOTAL must not drop
|
||||
```
|
||||
4. **Lint / types** — `uv run ruff check . && uv run pyright` (no Python change expected, but run the gate).
|
||||
5. **Commit** — one atomic `--no-gpg-sign` commit staging only this phase's files (the new E2E file + any committed edits), then move the phase directory to `.agent/phases/complete/`:
|
||||
```bash
|
||||
git add -f .agent/phases/todo/29_tuning_nav_link tests/e2e/test_tuning_nav_link.py
|
||||
git commit --no-gpg-sign -m "feat(ui): expose Global Tuning from Chat + Sources headers (admin-only Tuning nav link)"
|
||||
git mv .agent/phases/todo/29_tuning_nav_link .agent/phases/complete/29_tuning_nav_link
|
||||
```
|
||||
(`.agent/` is gitignored — use `git add -f` / `git mv -f` as needed.)
|
||||
|
||||
## Testing & Quality
|
||||
- E2E (mandatory, A16): `tests/e2e/test_tuning_nav_link.py` green **in isolation**:
|
||||
```bash
|
||||
uv run pytest tests/e2e/test_tuning_nav_link.py -v --no-cov
|
||||
```
|
||||
- No new unit/integration logic (frontend markup only); `app/` coverage unchanged.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] Admin sees a visible **"Tuning"** link on the Chat and Sources pages → clicking it opens `/tuning.html`.
|
||||
- [ ] Anonymous visitors see **no** Tuning link on either page (ships `hidden`, revealed only for admin).
|
||||
- [ ] Link is labeled, focus-visible; page stays CDN-free.
|
||||
- [ ] `test_tuning_nav_link.py` green in isolation; `test_header_consistency.py`, `test_shared_header.py`, `test_smoke.py`, `test_global_tuning.py` green in isolation; `test_index_html_served_locally` green.
|
||||
- [ ] `uv run pytest` green; `app/` coverage TOTAL unchanged; `ruff` + `pyright` clean.
|
||||
- [ ] One `--no-gpg-sign` commit; phase moved to `.agent/phases/complete/`.
|
||||
Reference in New Issue
Block a user