From 6bf7f456d4eda3f0243c076a5f7ac0e4b8acf309 Mon Sep 17 00:00:00 2001 From: ducoterra Date: Fri, 28 Aug 2026 23:24:23 -0400 Subject: [PATCH] show sync button for admin --- frontend/assets/sources.js | 13 +- tests/e2e/test_background_animation.py | 4 +- tests/e2e/test_background_no_motion.py | 4 +- tests/e2e/test_chat_persistence.py | 10 +- tests/e2e/test_dark_tech_theme.py | 10 +- tests/e2e/test_document_viewer.py | 11 +- tests/e2e/test_global_tuning.py | 7 +- tests/e2e/test_honest_deflection.py | 5 +- tests/e2e/test_sources_midstream_bug.py | 18 +- tests/e2e/test_suggestion_chips.py | 4 +- tests/e2e/test_summary_in_viewer.py | 9 +- tests/e2e/test_sync_model_down.py | 9 +- tests/unit/test_background_animation.py | 50 +++-- tests/unit/test_background_no_motion.py | 31 +-- tests/unit/test_chat_persistence.py | 76 ++++--- tests/unit/test_shared_header.py | 126 +++++++---- tests/unit/test_sync_button.py | 265 +++++++++++++----------- 17 files changed, 368 insertions(+), 284 deletions(-) diff --git a/frontend/assets/sources.js b/frontend/assets/sources.js index 028f9f3..ce8b809 100644 --- a/frontend/assets/sources.js +++ b/frontend/assets/sources.js @@ -30,9 +30,11 @@ import { fetchIsAdmin, initSharedHeader } from "./header.js"; * * The §7.4 never-stale lifecycle: idle → click → POST /api/sync * → running (2 s poll of GET /api/sync/status) → success | failed. - * Admin-only: the button is hidden for anonymous users (initSharedHeader - * hides it). A failed run opens an error modal (same as the former - * header.js module — recreated here since the navbar button is gone). + * Admin-only: the button SHIPS hidden and the page boot below reveals it + * for the admin on the SAME cached whoami initSharedHeader() used (no + * extra fetch); the 403 branches stay as defense in depth. A failed run + * opens an error modal (same as the former header.js module — recreated + * here since the navbar button is gone). * * Elements: #sync-btn (the button), #sync-label (the text), * #sync-icon (the spinner icon), #sync-result (aria-live result @@ -165,7 +167,7 @@ function closeSyncModal() { function applySyncSuccess(status) { const time = fmtSyncTime(status.finished_at); settleSyncButton(time ? `Synced ${time}` : "Synced"); - if (syncResult) syncResult.textContent = fmtSyncResult(status); + if (syncResult) syncResult.textContent = fmtSyncResult(status.detail); hideSyncError(); emitSyncStatus(status); // Refresh the catalog live — the KB just changed. @@ -411,7 +413,8 @@ function showEmpty() { (async () => { - await initSharedHeader(); // phase 19: Sign in/out + Sources link in the shared bar + const admin = await initSharedHeader(); // phase 19: shared bar (cached whoami) + if (syncBtn) syncBtn.hidden = !admin; // admin-only: ship-hidden, revealed on the same whoami if (!(await isAdmin())) { // Anonymous: gate in, catalog out, and no /api/docs request at all. if (statCards) statCards.hidden = true; diff --git a/tests/e2e/test_background_animation.py b/tests/e2e/test_background_animation.py index a4be127..0e8c73c 100644 --- a/tests/e2e/test_background_animation.py +++ b/tests/e2e/test_background_animation.py @@ -47,7 +47,7 @@ Test → story mapping (Playwright Mapping Rule): ::before``/``::after`` spots): ``position: fixed``, ``z-index: -1``, ``pointer-events: none``, ``inset: 0`` (UI Structure Check: behind content, click-through, full-viewport); the page canvas stays on - ```` (``rgb(10, 14, 23)`` = ``var(--bg)``) and ```` + ```` (``rgb(15, 10, 10)`` = ``var(--bg)``) and ```` stays transparent (``rgba(0, 0, 0, 0)``) — the no-occlusion contract. 5. ``test_no_horizontal_overflow_with_layers`` — at a 360px viewport @@ -71,7 +71,7 @@ from playwright.sync_api import Browser, Page GRID_OLD = "bg-grid-drift" # deleted in phase 25 — must not exist anywhere GLOWS = ("bg-glow-a", "bg-glow-b", "bg-glow-c") # the three phase-25 spot fades -PAGE_BG = "rgb(10, 14, 23)" # var(--bg) — the canvas (phase-08 palette) +PAGE_BG = "rgb(15, 10, 10)" # var(--bg) — the canvas (dark-red rebrand #0f0a0a) # Computed styles of all four pseudo-layers + the html/body background # contract (single evaluate — one round-trip per test). diff --git a/tests/e2e/test_background_no_motion.py b/tests/e2e/test_background_no_motion.py index 22fce0c..e73f504 100644 --- a/tests/e2e/test_background_no_motion.py +++ b/tests/e2e/test_background_no_motion.py @@ -67,7 +67,7 @@ Test → story mapping (Playwright Mapping Rule): 6. ``test_background_layers_contracts`` — all four pseudo-layers: ``position: fixed``, ``z-index: -1``, ``pointer-events: none``, top/right/bottom/left all ``0px``; the ``documentElement`` computed - background is ``rgb(10, 14, 23)`` (``var(--bg)`` — the canvas stays + background is ``rgb(15, 10, 10)`` (``var(--bg)`` — the canvas stays on ``html``); ``document.body`` computed background is ``rgba(0, 0, 0, 0)`` (no occlusion). 7. ``test_reduced_motion_stills_all_layers`` — @@ -99,7 +99,7 @@ GLOW_B = "bg-glow-b" # html::before — phase-08 cyan spot, 34s, -12s delay GLOW_C = "bg-glow-c" # html::after — third indigo spot, 42s, -23s delay GLOWS = (GLOW_A, GLOW_B, GLOW_C) # the three spot keyframe names EASE_IN_OUT = "ease-in-out" # computed animationTimingFunction for ease-in-out -PAGE_BG = "rgb(10, 14, 23)" # var(--bg) — the canvas (phase-08 palette) +PAGE_BG = "rgb(15, 10, 10)" # var(--bg) — the canvas (dark-red rebrand #0f0a0a) # Computed styles of all four pseudo-layers + the html/body background # contract (single evaluate — one round-trip per test). diff --git a/tests/e2e/test_chat_persistence.py b/tests/e2e/test_chat_persistence.py index f50c731..7b6a74a 100644 --- a/tests/e2e/test_chat_persistence.py +++ b/tests/e2e/test_chat_persistence.py @@ -272,13 +272,13 @@ def test_persists_across_page_navigation( _ask_deflected(page, OFF_TOPIC) # mixed conversation: grounded + deflected # A trip to Sources (phase 16: the catalog is admin-only — the trip - # starts with a real form login). Phase 19 (owner permission - # 2026-08-23): the New chat control is part of the SHARED bar, so it - # is present on the sources page too — only clicking it would clear - # the conversation (pinned in tests/e2e/test_shared_header.py). + # starts with a real form login). The New chat button is chat-page + # only (owner rework 2026-08-28 — it left the shared bar), so the + # sources page carries none of it (pinned in + # tests/e2e/test_shared_header.py). login(page, app_url, next="/sources.html") expect(page.locator("#docs-tbody tr").first).to_be_visible(timeout=15_000) - expect(page.locator("#new-chat-btn")).to_be_visible() + expect(page.locator("#new-chat-btn")).to_have_count(0) # Back to the chat: the conversation is exactly as left — both turns, # the source chip, and the amber deflected bubble with its chips. diff --git a/tests/e2e/test_dark_tech_theme.py b/tests/e2e/test_dark_tech_theme.py index dd54d3d..b6fcb77 100644 --- a/tests/e2e/test_dark_tech_theme.py +++ b/tests/e2e/test_dark_tech_theme.py @@ -173,13 +173,15 @@ def test_dark_palette_and_contrast( >= 4.5:1 from live computed styles (not eyeballed).""" for path in ("/", "/sources.html"): page.goto(f"{app_url}{path}") - # The visible page background is the canvas (rgb(10, 14, 23) - # = #0a0e17). itself stays transparent so the z-index:-1 - # grid/glow layers (test_animated_background) are not painted over. + # The visible page background is the canvas (rgb(15, 10, 10) + # = #0f0a0a — the 2026-08-28 dark-red rebrand; it was the phase-08 + # #0a0e17 canvas). itself stays transparent so the + # z-index:-1 grid/glow layers (test_animated_background) are not + # painted over. bg = page.evaluate( "() => getComputedStyle(document.documentElement).backgroundColor" ) - assert bg == "rgb(10, 14, 23)", f"expected the dark page bg on {path}, got {bg}" + assert bg == "rgb(15, 10, 10)", f"expected the dark page bg on {path}, got {bg}" body_bg = page.evaluate("() => getComputedStyle(document.body).backgroundColor") assert body_bg == "rgba(0, 0, 0, 0)", ( f"{path}: body must stay transparent (the background layers need to show)" diff --git a/tests/e2e/test_document_viewer.py b/tests/e2e/test_document_viewer.py index c08e481..d5cd8f7 100644 --- a/tests/e2e/test_document_viewer.py +++ b/tests/e2e/test_document_viewer.py @@ -370,7 +370,7 @@ def test_standalone_page_still_works( page.goto(f"{app_url}/document.html?source=docs&path=homelab%2Fkubernetes.md") expect(page.locator("#doc-content .doc-md")).not_to_be_empty() bg = page.evaluate("() => getComputedStyle(document.documentElement).backgroundColor") - assert bg == "rgb(10, 14, 23)" + assert bg == "rgb(15, 10, 10)", "the dark-red rebrand canvas (#0f0a0a)" refs = page.evaluate( """() => [...document.querySelectorAll("script[src], link[href]")] @@ -409,14 +409,15 @@ def test_modal_theme_and_no_cdn( expect(page.locator("#doc-modal-title")).to_have_text("Kubernetes Homelab Cluster") expect(page.locator("#doc-modal-content .doc-md")).not_to_be_empty() - # Dark theme (phase 08): the page background is untouched, and the - # modal panel sits on the Phase-08 surface colour (#121a2e). + # Dark theme (phase 08, dark-red rebrand 2026-08-28): the page + # background is untouched, and the modal panel sits on the --surface + # colour (#1a0f0f). bg = page.evaluate("() => getComputedStyle(document.documentElement).backgroundColor") - assert bg == "rgb(10, 14, 23)" + assert bg == "rgb(15, 10, 10)" surface = page.evaluate( "() => getComputedStyle(document.querySelector('.doc-modal-panel')).backgroundColor" ) - assert surface == "rgb(18, 26, 46)", f"panel not on the Phase-08 surface: {surface}" + assert surface == "rgb(26, 15, 15)", f"panel not on the --surface colour: {surface}" # No-CDN: every script/link reference on the chat page (the touched # page) is same-origin or a data: URI — the modal adds no assets. diff --git a/tests/e2e/test_global_tuning.py b/tests/e2e/test_global_tuning.py index e9f4619..10cc1bc 100644 --- a/tests/e2e/test_global_tuning.py +++ b/tests/e2e/test_global_tuning.py @@ -416,12 +416,13 @@ def test_tuning_page_a11y_and_no_cdn( assert announcer.get_attribute("role") == "status" assert announcer.get_attribute("aria-live") == "polite" - # ≥44px touch targets on every interactive control. + # ≥44px touch targets on every interactive control (the New chat + # button is chat-page only — owner rework 2026-08-28 — so it is not + # a tuning-page target). for selector in ( "#tune-save", "#tune-note", "#nav-tuning", - ".new-chat-btn", "#sign-out-btn", ".tuning-edit", ".tuning-delete", @@ -447,7 +448,7 @@ def test_tuning_page_a11y_and_no_cdn( # Dark theme: the page canvas is the phase-08 dark bg (body stays # transparent for the background layers). bg = page.evaluate("() => getComputedStyle(document.documentElement).backgroundColor") - assert bg == "rgb(10, 14, 23)", f"expected the dark page bg, got {bg}" + assert bg == "rgb(15, 10, 10)", f"expected the dark page bg (rebrand #0f0a0a), got {bg}" # Contrast: the live text/background pairs compute ≥ 4.5:1 (AA). pairs = page.evaluate( diff --git a/tests/e2e/test_honest_deflection.py b/tests/e2e/test_honest_deflection.py index 991a069..4a778ea 100644 --- a/tests/e2e/test_honest_deflection.py +++ b/tests/e2e/test_honest_deflection.py @@ -114,8 +114,9 @@ def test_off_topic_question_deflects_honestly( expect(group.first).to_have_attribute("role", "list") # Chip component contract: brand pill, ≥44px touch target. chip_style = chips.first.evaluate("el => getComputedStyle(el)") - assert chip_style["backgroundColor"] == "rgb(35, 43, 82)" # --brand-soft #232b52 (dark theme) - assert chip_style["color"] == "rgb(165, 180, 252)" # --brand-ink #a5b4fc + # --brand-soft #2d0a0a / --brand-ink #fca5a5 (dark-red rebrand) + assert chip_style["backgroundColor"] == "rgb(45, 10, 10)" + assert chip_style["color"] == "rgb(252, 165, 165)" box = chips.first.bounding_box() assert box is not None and box["height"] >= 44 diff --git a/tests/e2e/test_sources_midstream_bug.py b/tests/e2e/test_sources_midstream_bug.py index 8b75d0a..9b94a5d 100644 --- a/tests/e2e/test_sources_midstream_bug.py +++ b/tests/e2e/test_sources_midstream_bug.py @@ -328,8 +328,9 @@ def test_completed_turn_unaffected( # --------------------------------------------------------------------------- -# 4. The DELIBERATE clear is untouched: New Chat from the sources page -# still clears the conversation (phase 14/19 contract) +# 4. The DELIBERATE clear is untouched: New chat (chat-page only since +# the owner rework 2026-08-28) still clears the conversation +# (phase 14 contract) # --------------------------------------------------------------------------- @@ -341,18 +342,15 @@ def test_new_chat_still_clears_conversation( _ask(page, QUESTION) assert _stored(page) is not None - # To the sources page (anonymous is fine — the shared bar carries the - # New Chat control regardless of auth, phase 19). - page.goto(app_url + "/sources.html") + # The New chat button is chat-page only (it left the shared bar at + # owner request, 2026-08-28) — the deliberate clear runs right here. new_chat = page.locator("#new-chat-btn") expect(new_chat).to_be_visible() new_chat.click() - # "New chat" on a non-chat page means "go to the chat, fresh": the - # land page shows the empty state and the conversation key is GONE — - # the deliberate clearChatStorage() is unaffected by the phase-20 - # pagehide save point. - expect(page).to_have_url(app_url + "/") + # The chat lands on its empty state and the conversation key is + # GONE — the deliberate clear is unaffected by the phase-20 pagehide + # save point. expect(page.locator("#empty-state")).to_be_visible() expect(page.locator(".msg")).to_have_count(0) assert _stored(page) is None, "New Chat must clear the localStorage key" diff --git a/tests/e2e/test_suggestion_chips.py b/tests/e2e/test_suggestion_chips.py index 1ed0b5f..f9a090b 100644 --- a/tests/e2e/test_suggestion_chips.py +++ b/tests/e2e/test_suggestion_chips.py @@ -108,8 +108,8 @@ def test_onboarding_chips_render( expect(page.locator("#empty-state")).to_be_visible() # Chip component contract: brand pill, >=44px touch target. style = chips.first.evaluate("el => getComputedStyle(el)") - assert style["backgroundColor"] == "rgb(35, 43, 82)" # --brand-soft #232b52 (dark theme) - assert style["color"] == "rgb(165, 180, 252)" # --brand-ink #a5b4fc + assert style["backgroundColor"] == "rgb(45, 10, 10)" # --brand-soft #2d0a0a (dark-red rebrand) + assert style["color"] == "rgb(252, 165, 165)" # --brand-ink #fca5a5 (dark-red rebrand) assert style["borderRadius"] == "999px" box = chips.first.bounding_box() assert box is not None and box["height"] >= 44 diff --git a/tests/e2e/test_summary_in_viewer.py b/tests/e2e/test_summary_in_viewer.py index 8659d9e..1ab2304 100644 --- a/tests/e2e/test_summary_in_viewer.py +++ b/tests/e2e/test_summary_in_viewer.py @@ -181,15 +181,16 @@ def test_full_page_shows_summary_and_original_together( assert order[0] == "doc-summary", f"panel not first: {order}" assert "doc-raw" in order and order.index("doc-raw") > order.index("doc-summary") - # Theme (phase 08): panel on the surface colour, title in brand-ink - # (#a5b4fc on #121a2e ≈8.7:1 ≥4.5:1), and the panel matches the + # Theme (phase 08, dark-red rebrand 2026-08-28): panel on the + # surface colour, title in brand-ink (--brand-ink #fca5a5 on + # --surface #1a0f0f = 9.0:1 ≥4.5:1), and the panel matches the # raw-content width — it does not break the content layout. bg = panel.evaluate("el => getComputedStyle(el).backgroundColor") - assert bg == "rgb(18, 26, 46)", f"panel not on the Phase-08 surface: {bg}" + assert bg == "rgb(26, 15, 15)", f"panel not on the --surface colour (rebrand #1a0f0f): {bg}" title_color = panel.locator(".doc-summary-title").evaluate( "el => getComputedStyle(el).color" ) - assert title_color == "rgb(165, 180, 252)", f"title not brand-ink: {title_color}" + assert title_color == "rgb(252, 165, 165)", f"title not brand-ink: {title_color}" panel_box = panel.bounding_box() pre_box = pre.bounding_box() assert panel_box is not None and pre_box is not None diff --git a/tests/e2e/test_sync_model_down.py b/tests/e2e/test_sync_model_down.py index 7c491f8..9e8cb49 100644 --- a/tests/e2e/test_sync_model_down.py +++ b/tests/e2e/test_sync_model_down.py @@ -376,10 +376,11 @@ def test_sync_error_surfaces_unaffected( app) the ``#sync-error-banner`` still renders the error off ``bor:sync-status`` — the phase-32/34 contract the modal is additive to.""" - # Trigger the failure on the chat page (the modal is module-owned — - # it follows the button onto every page). The re-attach modal from - # the prior test's last failure closes first (clean trigger state). - login(page, dead_app_url, next="/") + # Trigger the failure on the Sources page (the button's home since + # the owner rework 2026-08-28; the modal is page-local there). The + # re-attach modal from the prior test's last failure closes first + # (clean trigger state). + login(page, dead_app_url) _dismiss_reattached_modal(page) _trigger_dead_failure(page, dead_app_url) diff --git a/tests/unit/test_background_animation.py b/tests/unit/test_background_animation.py index 6b8b76a..e3e3b97 100644 --- a/tests/unit/test_background_animation.py +++ b/tests/unit/test_background_animation.py @@ -14,12 +14,15 @@ The new design (styles.css, pure CSS, zero JS, no `filter` — A11): - grid (body::before): a STATIC texture — the drift animation and its keyframes are deleted (the 0.73px/s sub-pixel drift rasterizes as a once-per-second down-right jitter); - - three independent soft glow spots — body::after (phase-08 indigo, - 26s), html::before (phase-08 cyan, 34s, -12s delay), html::after - (third indigo, 42s, -23s delay) — each on its own SLOW opacity-only - fade (the whole-layer breathe keyframes are deleted), so the total - light fluxuates smoothly and irregularly; LCM(26, 34, 42) = 4641s, - so the composite pattern never repeats within a viewing session. + - three independent soft glow spots — body::after (26s), html::before + (34s, -12s delay), html::after (42s, -23s delay) — each on its own + SLOW opacity-only fade (the whole-layer breathe keyframes are + deleted), so the total light fluxuates smoothly and irregularly; + LCM(26, 34, 42) = 4641s, so the composite pattern never repeats + within a viewing session. The 2026-08-28 rebrand recolored the + phase-08 indigo/cyan spots to the warm dark-red theme palette + (rose / orange / red) and the grid lines to the warm line tone — + structure (sizes, positions, alphas, periods) unchanged. This file keeps the generic layer-plumbing pins (the no-occlusion contract, fixed / z-index -1 / pointer-events none — now across all @@ -153,17 +156,20 @@ def test_grid_is_static_no_drift() -> None: def test_grid_cells_and_line_contrast() -> None: - """44px cells with 1px lines at the phase-22 fixed 60% --line alpha - (the static texture keeps the values that made the grid readable — - see tests/unit/test_background_no_motion.py for the phase-25 story).""" + """44px cells with 1px lines at the phase-22 fixed 60% line alpha, + in the rebrand warm line tone (2026-08-28; the phase-22 indigo + value rgb(38 48 74 / 0.6) was recolored with the dark-red theme) + — the static texture keeps the values that made the grid readable + (see tests/unit/test_background_no_motion.py for the phase-25 + story).""" block = _grid_rule(_css()) assert "background-size: 44px 44px" in block - line = "linear-gradient(to right, rgb(38 48 74 / 0.6) 1px, transparent 1px)" - assert line in block, "grid must keep horizontal 1px lines at 60% --line" + line = "linear-gradient(to right, rgb(74 38 38 / 0.6) 1px, transparent 1px)" + assert line in block, "grid must keep horizontal 1px lines at 60% line alpha" assert ( - "linear-gradient(to bottom, rgb(38 48 74 / 0.6) 1px, transparent 1px)" + "linear-gradient(to bottom, rgb(74 38 38 / 0.6) 1px, transparent 1px)" in block - ), "grid must keep vertical 1px lines at 60% --line" + ), "grid must keep vertical 1px lines at 60% line alpha" assert "0.35" not in block, "the too-faint 35% line alpha must not return" @@ -216,21 +222,23 @@ def test_glow_keyframes_are_opacity_only() -> None: ) -def test_glow_spots_keep_phase08_colors_and_add_a_third() -> None: - """The phase-08 spots keep their colors, radii and positions — the - indigo top-left stays on body::after, the cyan bottom-right moves to - html::before — and a third soft indigo spot (52rem at 14% 86%) - joins on html::after. All spots fade to transparent at 62%.""" +def test_glow_spots_use_the_rebrand_warm_palette() -> None: + """The three spots keep their radii and positions from the phase-25 + layout (rose top-left on body::after, orange bottom-right on + html::before, red bottom-left 52rem at 14% 86% on html::after) but + wear the 2026-08-28 rebrand palette (warm dark-red theme; the + phase-08 indigo/cyan values are gone). All spots fade to + transparent at 62%.""" assert ( - "radial-gradient(circle 56rem at 12% 8%, rgb(109 120 242 / 0.14), " + "radial-gradient(circle 56rem at 12% 8%, rgb(244 63 94 / 0.10), " "transparent 62%)" in _glow_rule(_css()) ) assert ( - "radial-gradient(circle 60rem at 88% 92%, rgb(34 211 238 / 0.10), " + "radial-gradient(circle 60rem at 88% 92%, rgb(251 146 60 / 0.08), " "transparent 62%)" in _rule_block(_css(), "html::before") ) assert ( - "radial-gradient(circle 52rem at 14% 86%, rgb(109 120 242 / 0.09), " + "radial-gradient(circle 52rem at 14% 86%, rgb(239 68 68 / 0.08), " "transparent 62%)" in _rule_block(_css(), "html::after") ) diff --git a/tests/unit/test_background_no_motion.py b/tests/unit/test_background_no_motion.py index 7ff5ef3..ece3dcc 100644 --- a/tests/unit/test_background_no_motion.py +++ b/tests/unit/test_background_no_motion.py @@ -34,13 +34,16 @@ from tests.unit.test_background_animation import _css, _css_no_comments, _rule_b ALL_LAYERS = ("body::before", "body::after", "html::before", "html::after") # (layer, keyframes name, duration shorthand, single-spot gradient) +# The 2026-08-28 rebrand recolored the phase-08 indigo/cyan spots to the +# warm dark-red theme palette (rose / orange / red) — structure +# (radius, position, alpha, period, delay) is the phase-25 design. GLOW_SPOTS = ( ("body::after", "bg-glow-a", "animation: bg-glow-a 26s ease-in-out infinite", - "radial-gradient(circle 56rem at 12% 8%, rgb(109 120 242 / 0.14), transparent 62%)"), + "radial-gradient(circle 56rem at 12% 8%, rgb(244 63 94 / 0.10), transparent 62%)"), ("html::before", "bg-glow-b", "animation: bg-glow-b 34s ease-in-out -12s infinite", - "radial-gradient(circle 60rem at 88% 92%, rgb(34 211 238 / 0.10), transparent 62%)"), + "radial-gradient(circle 60rem at 88% 92%, rgb(251 146 60 / 0.08), transparent 62%)"), ("html::after", "bg-glow-c", "animation: bg-glow-c 42s ease-in-out -23s infinite", - "radial-gradient(circle 52rem at 14% 86%, rgb(109 120 242 / 0.09), transparent 62%)"), + "radial-gradient(circle 52rem at 14% 86%, rgb(239 68 68 / 0.08), transparent 62%)"), ) @@ -87,16 +90,17 @@ def test_drift_and_breathe_keyframes_are_deleted() -> None: def test_grid_keeps_its_static_texture() -> None: """The owner rejected the grid's MOTION, not the grid: 44px cells, - 1px lines at 60% --line alpha, and the widened radial mask (both the - -webkit- and standard mask properties) stay.""" + 1px lines at the fixed 60% line alpha (rebrand warm tone, + 2026-08-28), and the widened radial mask (both the -webkit- and + standard mask properties) stay.""" block = _rule_block(_css(), "body::before") assert "background-size: 44px 44px" in block assert ( - "linear-gradient(to right, rgb(38 48 74 / 0.6) 1px, transparent 1px)" in block - ), "grid must keep horizontal 1px lines at 60% --line" + "linear-gradient(to right, rgb(74 38 38 / 0.6) 1px, transparent 1px)" in block + ), "grid must keep horizontal 1px lines at 60% line alpha" assert ( - "linear-gradient(to bottom, rgb(38 48 74 / 0.6) 1px, transparent 1px)" in block - ), "grid must keep vertical 1px lines at 60% --line" + "linear-gradient(to bottom, rgb(74 38 38 / 0.6) 1px, transparent 1px)" in block + ), "grid must keep vertical 1px lines at 60% line alpha" mask = "radial-gradient(140% 110% at 50% 0%, black 40%, transparent 90%)" assert f"-webkit-mask-image: {mask};" in block assert f"mask-image: {mask};" in block @@ -138,10 +142,11 @@ def test_glow_layers_declare_no_transform_or_position_animation() -> None: def test_each_spot_runs_its_own_slow_opacity_fade() -> None: - """body::after (phase-08 indigo, 26s), html::before (phase-08 cyan, - 34s, -12s delay), html::after (third indigo, 42s, -23s delay) — each - glow layer's background-image is EXACTLY the single radial gradient - from the spec (color, radius, position, 62% transparent stop).""" + """body::after (rose, 26s), html::before (orange, 34s, -12s delay), + html::after (red, 42s, -23s delay) — the rebrand warm palette on + the phase-25 layout; each glow layer's background-image is EXACTLY + the single radial gradient from the spec (color, radius, position, + 62% transparent stop).""" for sel, _name, animation, gradient in GLOW_SPOTS: block = _rule_block(_css(), sel) assert animation in block, f"{sel} must run {animation}" diff --git a/tests/unit/test_chat_persistence.py b/tests/unit/test_chat_persistence.py index 8c49809..6c1ee03 100644 --- a/tests/unit/test_chat_persistence.py +++ b/tests/unit/test_chat_persistence.py @@ -10,8 +10,10 @@ Pinned design (PLAN §7.4 note / phase 14): * save points: user message on send, brain message on ``done``; * every ``localStorage`` access wrapped in try/catch (failure-safe); * size budget ~700k chars, oldest dropped first; -* ``#new-chat-btn`` in the shared header bar (chat, sources, viewer — - phase 19, owner permission 2026-08-23), ≥44px, ghost pill. +* ``#new-chat-btn`` chat-page only — phase 19 shipped it in the shared + bar (chat, sources, viewer, owner permission 2026-08-23); it moved + from the navbar to index.html's .chat-shell at owner request + (2026-08-28). ≥44px, solid brand pill. """ from __future__ import annotations @@ -22,7 +24,10 @@ FRONTEND = Path(__file__).resolve().parents[2] / "frontend" APP_JS = FRONTEND / "assets" / "app.js" INDEX_HTML = FRONTEND / "index.html" SOURCES_HTML = FRONTEND / "sources.html" +GIT_SOURCES_HTML = FRONTEND / "git-sources.html" DOCUMENT_HTML = FRONTEND / "document.html" +LOGIN_HTML = FRONTEND / "login.html" +TUNING_HTML = FRONTEND / "tuning.html" STYLES_CSS = FRONTEND / "assets" / "styles.css" @@ -151,53 +156,58 @@ def test_new_chat_clears_key_and_ui() -> None: assert "removeItem(STORAGE_KEY)" in js -def test_new_chat_button_in_the_shared_header_bar() -> None: - """#new-chat-btn is a real type=button with an accessible name in the - chat header (index.html). Phase 19 (owner permission 2026-08-23): it - is part of the SHARED bar — it also appears in sources.html and the - document viewer, where it means "go to the chat, fresh" (the - clear-storage + navigate binding is pinned in test_shared_header.py).""" - for html in (INDEX_HTML, SOURCES_HTML, DOCUMENT_HTML): - text = html.read_text(encoding="utf-8") - btn = re.search(r']*id="new-chat-btn"[^>]*>', text) - assert btn, f"{html.name} must contain #new-chat-btn" - tag = btn.group(0) - assert 'type="button"' in tag - assert 'aria-label="New chat"' in tag - # Chat page specifics: the button sits after the nav, inside the header. +def test_new_chat_button_lives_only_on_the_chat_page() -> None: + """#new-chat-btn is a real type=button with an accessible name. Moved + from the shared header bar to the chat page at owner request + (2026-08-28): it lives ONLY in index.html — inside
, in + .chat-shell, above the #messages section (the single module binding + + the no-op guard are pinned in test_shared_header.py).""" html = _index() btn = re.search(r']*id="new-chat-btn"[^>]*>', html) assert btn, "index.html must contain #new-chat-btn" - nav_idx = html.find('