show sync button for admin
Build and Push Containers / build-and-push-app (push) Successful in 1m38s
Build and Push Containers / build-and-push-db (push) Successful in 11s

This commit is contained in:
2026-08-28 23:24:23 -04:00
parent 3a404eb161
commit 6bf7f456d4
17 changed files with 368 additions and 284 deletions
+80 -46
View File
@@ -26,6 +26,7 @@ STYLES_CSS = ASSETS / "styles.css"
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"
@@ -77,8 +78,15 @@ def test_init_shared_header_toggles_only_elements_that_exist() -> None:
assert fn != -1
body = js[fn : js.find("\n}", fn)]
assert "await fetchIsAdmin()" in body
for selector in ("#sign-in-link", "#sign-out-btn", "#nav-sources", "#nav-tuning"):
for selector in ("#nav-sources", "#nav-git-sources", "#nav-tuning"):
assert f'querySelector("{selector}")' in body
# Sign in: both the bar copy AND the mobile dropdown copy (phase 46)
# carry .sign-in-link — one class-based toggle (with the ?next= href
# rewrite on every copy) covers both.
assert 'querySelectorAll(".sign-in-link")' in body
# Sign out: both the bar copy AND the mobile dropdown copy (phase 46)
# carry .sign-out-btn — one class-based toggle covers both.
assert 'querySelectorAll(".sign-out-btn")' in body
assert "return admin" in body, "callers may reuse the flag"
@@ -95,15 +103,19 @@ def test_clear_chat_storage_removes_the_phase14_key_silently() -> None:
def test_sign_out_binding_lives_in_the_shared_module() -> None:
"""The #sign-out-btn click binding (disable → POST /api/logout →
reload) is owned by header.js at module import — exactly one
implementation for every page that loads it."""
"""The sign-out click binding (disable → POST /api/logout → reload)
is owned by header.js at module import — exactly one implementation
for every page that loads it. It binds to ALL .sign-out-btn
elements (the bar copy for desktop + the mobile dropdown copy for
≤640px, phase 46), so both copies log out."""
js = _text(HEADER_JS)
assert "querySelector(\"#sign-out-btn\")" in js
assert "signOutBtn.addEventListener" in js
assert "signOutBtn.disabled = true" in js
assert js.count('querySelectorAll(".sign-out-btn")') >= 2, (
"the boot toggle + the click binding both use the class (bar + mobile copy)"
)
assert "btn.addEventListener(\"click\"" in js
assert "btn.disabled = true" in js
assert 'fetch("/api/logout", { method: "POST" })' in js
assert "location.reload()" in js
assert "window.location.reload()" in js
# ---------- HTML wiring: one shared bar on every page ----------
@@ -112,7 +124,7 @@ def test_sign_out_binding_lives_in_the_shared_module() -> None:
def test_nav_sources_ships_hidden_on_every_nav_page() -> None:
"""Phase 19 UX revision (owner permission 2026-08-23), completed on
all five pages by phase 34 task 03 (owner confirmation 2026-08-26):
the Sources nav link is hidden for anonymous — so it SHIPS with the
the RAG nav link is hidden for anonymous — so it SHIPS with the
hidden attribute (anonymous-safe default) on every page (they all
carry the nav now, viewer included)."""
for html in (INDEX_HTML, SOURCES_HTML, DOCUMENT_HTML, LOGIN_HTML, TUNING_HTML):
@@ -122,28 +134,37 @@ def test_nav_sources_ships_hidden_on_every_nav_page() -> None:
)
def test_all_five_pages_share_the_header_control_order() -> None:
"""Phase 34 task 03 (owner confirmation 2026-08-26): every page
ships the IDENTICAL header control inventory in the IDENTICAL
order — brand, nav [Chat, Sources, Tuning], #sync-btn, #new-chat-btn,
def test_all_six_pages_share_the_header_control_order() -> None:
"""Phase 34 task 03 (owner confirmation 2026-08-26) + phase 35/46
(the sixth page, git-sources): every page ships the IDENTICAL
header control inventory in the IDENTICAL order — brand, the mobile
hamburger, nav [Chat, #nav-sources, #nav-git-sources, #nav-tuning],
Sign in, Sign out (the #steering-toggle was removed from the navbar
at owner request, 2026-08-28) — inside the shared
.header-inner row (the document viewer's row 1). Only the
current-page is-active nav marker and the static ?next= fallback
may differ per page (task 05's story E2E pins the rendered
result)."""
at owner request, 2026-08-28) — inside the shared .header-inner row
(the document viewer's row 1). The #sync-btn (Sources page only)
and the #new-chat-btn (chat page only, moved from the navbar at
owner request 2026-08-28) are NOT part of the shared bar anymore.
Only the current-page is-active nav marker and the static ?next=
fallback may differ per page (task 05's story E2E pins the
rendered result)."""
markers = (
'class="brand"',
'<nav class="app-nav"',
'href="/"',
'id="nav-sources"',
'id="nav-git-sources"',
'id="nav-tuning"',
'id="sync-btn"',
'id="new-chat-btn"',
'id="sign-in-link"',
'id="sign-out-btn"',
)
for html in (INDEX_HTML, SOURCES_HTML, DOCUMENT_HTML, TUNING_HTML, LOGIN_HTML):
for html in (
INDEX_HTML,
SOURCES_HTML,
GIT_SOURCES_HTML,
DOCUMENT_HTML,
TUNING_HTML,
LOGIN_HTML,
):
text = _text(html)
start = text.find('<div class="container header-inner">')
assert start != -1, f"{html.name}: missing the shared .header-inner row"
@@ -233,14 +254,18 @@ def test_viewer_carries_the_standard_nav() -> None:
def test_sources_and_viewer_carry_the_shared_controls() -> None:
"""Sources AND the document viewer gain the New Chat button + the
Sign in / Sign out pair (both starting hidden — initSharedHeader
reveals exactly one after whoami), and their page scripts load
header.js (phase 23: via the page script's relative import, not a
direct script tag)."""
"""Sources AND the document viewer carry the Sign in / Sign out pair
(both starting hidden — initSharedHeader reveals exactly one after
whoami), and their page scripts load header.js (phase 23: via the
page script's relative import, not a direct script tag). The New
chat button is NOT on non-chat pages — it moved from the navbar to
the chat page at owner request (2026-08-28; the single binding is
pinned in test_new_chat_binding_is_single_and_module_owned)."""
for html in (SOURCES_HTML, DOCUMENT_HTML):
text = _text(html)
assert 'id="new-chat-btn"' in text
assert 'id="new-chat-btn"' not in text, (
f"{html.name}: the New chat button is chat-page only (owner request)"
)
assert re.search(r'id="sign-in-link"[^>]*\bhidden\b', text)
assert re.search(r'id="sign-out-btn"[^>]*\bhidden\b', text)
for js_file in (SOURCES_JS, DOCUMENT_JS):
@@ -287,23 +312,26 @@ def test_header_module_loads_before_the_page_script() -> None:
def test_login_page_carries_the_full_header() -> None:
"""Phase 34 task 03 (owner confirmation 2026-08-26): the noted
boundary is reversed by owner decision — the login page finally
carries the FULL shared header: the nav gains the #nav-tuning link
(same ship-hidden markup as the other pages), plus the admin-only
Sync button, New chat, and the Sign in / Sign out pair (ship hidden
— initSharedHeader reveals exactly one after whoami; the static
carries the FULL shared header: the nav with #nav-sources /
#nav-git-sources / #nav-tuning (same ship-hidden markup as the
other pages) plus the Sign in / Sign out pair (ship hidden —
initSharedHeader reveals exactly one after whoami; the static
?next= fallback is the login page itself). The Tuning toggle is NOT
among them — removed from the navbar at owner request (2026-08-28).
No nav link is "current" on the auth page."""
among them — removed from the navbar at owner request (2026-08-28) —
and neither is the Sync button or the New chat button (page-specific
controls, not shared-bar ones). No nav link is "current" on the
auth page."""
text = _text(LOGIN_HTML)
for marker in (
'id="nav-sources"',
'id="nav-git-sources"',
'id="nav-tuning"',
'id="sync-btn"',
'id="new-chat-btn"',
'id="sign-in-link"',
'id="sign-out-btn"',
):
assert marker in text, f"login.html must carry {marker} (phase 34 full header)"
assert 'id="sync-btn"' not in text, "the Sync button lives on the Sources page only"
assert 'id="new-chat-btn"' not in text, "the New chat button is chat-page only"
assert 'href="/login.html?next=/login.html"' in text, (
"the login page's Sign in returns to the login page (no-JS fallback)"
)
@@ -354,21 +382,27 @@ def test_login_js_uses_the_shared_fetch_is_admin() -> None:
def test_new_chat_binding_is_single_and_module_owned() -> None:
"""Phase 34 task 02: header.js owns the SINGLE #new-chat-btn binding
(module import, like the sign-out binding): on the chat page
(#messages exists) it dispatches window "bor:new-chat" — app.js acts
through its own in-flight-turn guard + list reset; on every other
page it means "go to the chat, fresh" (clearChatStorage + navigate
to "/"). NO page script binds #new-chat-btn anymore, and each page
still runs initSharedHeader() on the shared cached whoami. Phase 23:
the import is relative (`./header.js`)."""
"""Phase 34 task 02 + owner rework (2026-08-28): header.js owns the
SINGLE #new-chat-btn binding (module import, like the sign-out
binding). The button now lives ONLY on the chat page (inside
.chat-shell, above #messages — moved from the navbar at owner
request), so the click ALWAYS dispatches window "bor:new-chat" and
app.js acts through its own in-flight-turn guard + list reset: no
#messages branch, no clearChatStorage + navigate-to-"/" path
anymore. NO page script binds #new-chat-btn, and each page still
runs initSharedHeader() on the shared cached whoami. Phase 23: the
import is relative (`./header.js`)."""
js = _text(HEADER_JS)
assert 'querySelector("#new-chat-btn")' in js
assert "newChatBtn.addEventListener" in js
assert 'querySelector("#messages")' in js, "the chat-page branch key"
assert "if (newChatBtn)" in js, "a page without the button is a no-op"
assert 'new CustomEvent("bor:new-chat")' in js
assert "clearChatStorage();" in js
assert 'window.location.href = "/"' in js
assert 'querySelector("#messages")' not in js, (
"no chat-page branch — the button is chat-page only (owner request)"
)
assert 'window.location.href = "/"' not in js, (
"no navigate-to-chat branch left (the button left the navbar)"
)
for js_file in (SOURCES_JS, DOCUMENT_JS, TUNING_JS):
page_js = _text(js_file)
assert 'from "./header.js"' in page_js