diff --git a/CHANGELOG.md b/CHANGELOG.md index 0804d4a..718fb1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,52 @@ ## Changelog +### v0.166.0 — Mobile nav drawer + sidebar cleanup + versioned logo/favicon URLs (2026-07-24) + +No agent coupling; MinAgent unchanged. Templates (layout/login/icons) + CSS + one login-handler data +key + tests only — no backend logic, no routes, no settings, no dependency changes. Desktop (>768px) +is unchanged. + +**Mobile navigation rework (Option A — off-canvas drawer).** The `@media(max-width:768px)` block +predated the v0.146.0 accordion: it flattened `.nav-links` into a horizontal `overflow-x` strip, and +because the accordion's nested sub-lists share the `.nav-links` class, sub-items laid out horizontally +inside an `overflow:hidden` grid row — everything past the first sub-item was clipped. The strip is +**deleted** (not patched — Option C was rejected) and replaced by: + +- a sticky **top bar** (`.mobile-topbar`, logo → `/`, single hamburger `.nav-burger` with + `aria-expanded`/`aria-controls="sidebar"`, new `#i-menu` icon); +- the existing vertical sidebar reused as an **off-canvas left drawer** (`.js .sidebar`, + `transform:translateX(-100%)`→`is-open`), a `.nav-backdrop`, body scroll-lock (`body.nav-open`); + drawer JS toggles on burger, closes on backdrop click or Escape. **The accordion handler is + untouched** — it works identically inside the drawer (all sub-items stack vertically, nothing clipped). +- a **no-JS fallback**: `` (swapped to `js` by an early head script); when JS is + off the sidebar renders static inline above the content and the burger is hidden, so no destination + dead-ends. +- z-index ladder: top bar 800 < backdrop 900 < drawer 950 < `.modal-overlay` 1000 (modals stay on top); + `height:100dvh`; drawer transition disabled under `prefers-reduced-motion`. + +**Sidebar customer-name removed.** The `` and its dead CSS rule are gone +from the sidebar header (logo only). `{{.CustomerName}}` stays in base data and on the **login page** +subtitle (identifies the box owner). + +**Cache-bust on logo/favicon.** `/static/felhom-logo.svg` and `/static/favicon.svg` now carry +`?v={{.Version}}` (sidebar logo, head favicon, login logo) — Cloudflare edge-caches `/static/*` for 4h, +so unversioned asset URLs kept serving the previous release's copy after a deploy (the 0.126.1 CSS +failure mode). `renderLogin` now passes `Version`. + +**Part 4 (outlined-logo swap) GATED OUT — not shipped.** The §3a precondition failed: live felhom.eu +`main` (`be9edb44`) still serves a `website/assets/logo.svg` with live ``/`font-family` (the +text-outlined master is Viktor's manual Inkscape push, still pending). The `FelhomLogoSVG` / +`FelhomFaviconSVG` constants are therefore **unchanged** and still contain live `` — the wordmark +renders in a fallback font under `` secure static mode until the outlined asset lands and Part 4 +ships. Only the `?v=` cache-bust portion of the logo work is in this release. + +- Tests (5 new, all through the real layout/CSS): topbar+drawer markup, CSS strip-removed/drawer-present + (scoped to the 768px block), sidebar-no-customer-name, login-customer-name-kept, versioned asset URLs. + RED-proofs recorded pre-change (strip present, no drawer, customer-name present, no `?v=`, and the + gated logo-constant proof). `nav_accordion_test.go` invariants pass unchanged. Focus-trap on the + drawer deliberately omitted (page navigations reset state). Android drawer feel + desktop + pixel-parity are Viktor's visual acceptance step. + ### v0.165.1 — Native "Megosztás…" button in the share modal (Web Share API) (2026-07-24) No agent coupling; MinAgent unchanged. Template JS + tests only — no backend, no routes, no diff --git a/CONTEXT.md b/CONTEXT.md index 51c73b2..aa4fe52 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -7,7 +7,28 @@ > > Ask Claude Code: "Please update CONTEXT.md with what we did today" -Last updated: 2026-07-24 (v0.165.1 — native "Megosztás…" button in the share modal via the Web Share API) +Last updated: 2026-07-24 (v0.166.0 — mobile nav drawer + sidebar cleanup + versioned logo/favicon URLs) + +> **2026-07-24 — v0.166.0 (mobile nav = off-canvas drawer; sidebar cleanup; ?v= on logo/favicon).** +> Mobile nav was broken: the ≤768px block predated the v0.146.0 accordion and flattened `.nav-links` +> into a horizontal `overflow-x` strip, clipping the accordion's nested sub-lists (they share the +> `.nav-links` class). **Decision: mobile nav = a sticky top bar + off-canvas left drawer that REUSES +> the vertical sidebar (Option A).** The accordion handler is untouched and works inside the drawer; +> a `no-js` html-class fallback renders the sidebar static inline so nothing dead-ends without JS. +> Options B (separate mobile menu) and C (exclude nested lists from the strip) were rejected. z-index +> ladder topbar 800 < backdrop 900 < drawer 950 < modal 1000; `100dvh`; reduced-motion disables the +> slide; focus-trap deliberately omitted (navigations reset state). **Sidebar customer-name removed** +> (logo only); `{{.CustomerName}}` stays in base data + login subtitle. **Logo policy decision: the +> wordmark must be OUTLINED paths, never live ``** — under `` secure static mode only +> locally-installed fonts resolve, so `font-family` in the SVG renders a fallback font everywhere. +> **Part 4 (swap `FelhomLogoSVG`/`FelhomFaviconSVG` to the outlined master) is GATED OUT** — §3a check +> against live felhom.eu `main` (`be9edb44`) found `website/assets/logo.svg` still has ``/ +> `font-family`; the outlined master is Viktor's manual Inkscape push, still pending. Only the `?v=` +> cache-bust (logo/favicon/login-logo, Cloudflare 4h edge-cache — the 0.126.1 failure mode) shipped +> from the logo work. Follow-up: when Viktor pushes the outlined asset, ship Part 4 (swap constants + +> clean the favicon's vestigial `` nodes). Separately, the website + hub still serve their own +> non-outlined logo copies — propagation is a distinct follow-up. + > **2026-07-24 — v0.165.1 (native "Megosztás…" in the share modal, Web Share API).** The share modal > gains a feature-detected `navigator.share` button (OS share sheet → Messenger/WhatsApp/email), diff --git a/controller/internal/web/auth.go b/controller/internal/web/auth.go index 9b522f9..1bc2d83 100644 --- a/controller/internal/web/auth.go +++ b/controller/internal/web/auth.go @@ -327,6 +327,7 @@ func (s *Server) renderLogin(w http.ResponseWriter, errorMsg, flashMsg string) { data := map[string]interface{}{ "Title": "Bejelentkezés", "CustomerName": s.cfg.Customer.Name, + "Version": s.version, // logo ?v= cache-bust (v0.166.0) "Error": errorMsg, "Flash": flashMsg, } diff --git a/controller/internal/web/mobile_nav_test.go b/controller/internal/web/mobile_nav_test.go new file mode 100644 index 0000000..88f9dbe --- /dev/null +++ b/controller/internal/web/mobile_nav_test.go @@ -0,0 +1,181 @@ +package web + +import ( + "bytes" + "strings" + "testing" +) + +// v0.166.0 mobile navigation rework. The old @media(max-width:768px) block predated the v0.146.0 +// accordion: it flattened .nav-links into a horizontal, overflow-x strip, which clipped the +// accordion's nested sub-lists (they share the .nav-links class). This suite pins the replacement: +// a hamburger + off-canvas drawer that REUSES the existing vertical sidebar, plus a no-JS static +// fallback so navigation never dead-ends. Desktop (>768px) is untouched — the accordion invariants +// stay in nav_accordion_test.go and must pass unchanged. + +// mediaBlock768 returns the full brace-balanced body of the @media(max-width: 768px) rule so CSS +// pins are scoped to the mobile block rather than matching an unrelated rule elsewhere in the sheet. +func mediaBlock768(t *testing.T, css string) string { + t.Helper() + marker := "@media(max-width: 768px)" + i := strings.Index(css, marker) + if i < 0 { + t.Fatalf("no %q media block found — the mobile breakpoint moved or was renamed", marker) + } + open := strings.IndexByte(css[i:], '{') + if open < 0 { + t.Fatal("no opening brace after the 768px media marker") + } + start := i + open + depth := 0 + for j := start; j < len(css); j++ { + switch css[j] { + case '{': + depth++ + case '}': + depth-- + if depth == 0 { + return css[start : j+1] + } + } + } + t.Fatal("unbalanced braces in the 768px media block") + return "" +} + +// Group B — CSS pins (Scenario A/B/C). +// +// Red-proof: run against the pre-edit style.css and the strip-absence assertions FAIL — the old +// block still carries `.nav-links { display:flex; ... overflow-x:auto }`. +func TestMobileCSS_StripRemovedDrawerPresent(t *testing.T) { + data, err := StyleCSS() + if err != nil { + t.Fatalf("StyleCSS(): %v", err) + } + css := string(data) + block := mediaBlock768(t, css) + + // The horizontal strip that clipped the accordion is gone from the mobile block. + if strings.Contains(block, "overflow-x") { + t.Error("768px block still contains an overflow-x rule — the horizontal .nav-links strip was not removed") + } + if strings.Contains(block, ".nav-links {") || strings.Contains(block, ".nav-links{") { + t.Error("768px block still flattens .nav-links — the strip pattern must be deleted, not patched") + } + + // The off-canvas drawer + no-JS fallback are present in the mobile block. + for _, want := range []string{ + ".js .sidebar", // JS path: sidebar becomes an off-canvas drawer + "translateX(-100%)", // parked off-canvas until opened + ".no-js .sidebar", // no-JS path: sidebar renders static inline + "body.nav-open", // scroll lock while the drawer is open + ".mobile-topbar", // the sticky top bar is styled inside the mobile block + } { + if !strings.Contains(block, want) { + t.Errorf("768px block missing %q — the drawer/fallback rules are incomplete", want) + } + } + + // The desktop defaults that keep >768px pixel-identical live OUTSIDE the media query. + for _, want := range []string{".mobile-topbar", ".nav-backdrop", ".nav-burger"} { + if !strings.Contains(css, want) { + t.Errorf("stylesheet missing base rule for %q", want) + } + } + if !strings.Contains(css, ".mobile-topbar { display: none") && + !strings.Contains(css, ".mobile-topbar{display:none") { + t.Error("no desktop-default `.mobile-topbar { display:none }` — the top bar would leak onto desktop") + } +} + +// Group A — drawer markup (Scenario A/C), asserted through the real layout render. +// +// Red-proof: remove aria-controls from the burger button in layout.html → the aria-controls +// assertion FAILS. (Recorded in REPORT.) +func TestMobileNav_TopbarAndDrawerMarkup(t *testing.T) { + html := renderNavFor(t, "dashboard") + + // Progressive-enhancement hook: the html element starts as no-js and the head script swaps it. + if !strings.Contains(html, ``) { + t.Error(" element does not carry class=\"no-js\" — the no-JS CSS fallback would never engage") + } + if !strings.Contains(html, `replace('no-js','js')`) { + t.Error("head script does not swap no-js → js") + } + + // Sticky top bar with a single burger toggle wired to the sidebar. + if !strings.Contains(html, `class="mobile-topbar"`) { + t.Error("no .mobile-topbar element rendered") + } + if !strings.Contains(html, `class="nav-burger"`) { + t.Error("no .nav-burger button rendered") + } + if !strings.Contains(html, `aria-expanded="false"`) { + t.Error("burger missing aria-expanded=\"false\" initial state") + } + if !strings.Contains(html, `aria-controls="sidebar"`) { + t.Error("burger missing aria-controls=\"sidebar\" — the button is not associated with the drawer") + } + + // The sidebar is the drawer target, and the backdrop starts hidden. + if !strings.Contains(html, `id="sidebar"`) { + t.Error("sidebar has no id=\"sidebar\" — aria-controls points at nothing") + } + if !strings.Contains(html, `class="nav-backdrop"`) { + t.Error("no .nav-backdrop element rendered") + } + // The backdrop must default hidden (both the attribute and its element must be present together). + if !strings.Contains(html, `class="nav-backdrop" hidden`) { + t.Error("nav-backdrop is not initially hidden") + } +} + +// Group D — customer name (Scenario D). +// +// Red-proof: re-add {{.CustomerName}} to the sidebar header → +// TestSidebar_NoCustomerName FAILS. +func TestSidebar_NoCustomerName(t *testing.T) { + html := renderNavFor(t, "dashboard") + if strings.Contains(html, "customer-name") { + t.Error("sidebar still renders the customer-name element — it must live only on the login page") + } +} + +func TestLogin_CustomerNameKept(t *testing.T) { + s := testServer(t) + s.loadTemplates() + var buf bytes.Buffer + data := map[string]interface{}{ + "Title": "Bejelentkezés", + "CustomerName": "Teszt Ügyfél", + "Version": "9.9.9", + } + if err := s.tmpl.ExecuteTemplate(&buf, "login", data); err != nil { + t.Fatalf("render login: %v", err) + } + out := buf.String() + // The login page still identifies whose box this is. + if !strings.Contains(out, "Teszt Ügyfél") { + t.Error("login page dropped the CustomerName subtitle — it identifies the box owner and must stay") + } + if !strings.Contains(out, `class="login-subtitle"`) { + t.Error("login-subtitle element missing") + } +} + +// Group E — versioned asset URLs (Scenario E). The logo-constant no-live-text test ships WITH +// Part 4 (the outlined-asset swap), which is gated on Viktor pushing the outlined master; it is +// intentionally not part of this suite while the constants still carry live . +// +// Red-proof: run against the pre-edit layout.html → both assertions FAIL (the /static/ URLs lack +// the ?v= cache-bust). Cloudflare edge-caches /static/* for 4h, so an unversioned logo/favicon keeps +// serving the previous release after a deploy (same failure mode as the 0.126.1 CSS incident). +func TestLayout_VersionedAssetURLs(t *testing.T) { + html := renderNavFor(t, "dashboard") + if !strings.Contains(html, "/static/felhom-logo.svg?v=") { + t.Error("sidebar logo URL is not cache-busted with ?v= — a deploy would keep serving the stale edge copy") + } + if !strings.Contains(html, "/static/favicon.svg?v=") { + t.Error("favicon URL is not cache-busted with ?v=") + } +} diff --git a/controller/internal/web/templates/icons.html b/controller/internal/web/templates/icons.html index 8667297..a0b36cc 100644 --- a/controller/internal/web/templates/icons.html +++ b/controller/internal/web/templates/icons.html @@ -31,4 +31,5 @@ + {{end}} diff --git a/controller/internal/web/templates/layout.html b/controller/internal/web/templates/layout.html index 2260c95..0057404 100644 --- a/controller/internal/web/templates/layout.html +++ b/controller/internal/web/templates/layout.html @@ -1,17 +1,20 @@ {{define "layout_start"}} - + {{.Title}} — Felhom.eu - {{/* ?v= cache-bust: Cloudflare edge-caches /static/* for 4h — an unversioned URL keeps - serving the PREVIOUS release's CSS after a deploy (0.126.1 live QA hit this). The - controller version busts it automatically on every release. */}} + serving the PREVIOUS release's asset after a deploy (0.126.1 live QA hit this on CSS; + the logo/favicon share the failure mode). The controller version busts it every release. */}} +