show sync button for admin
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user