/* ============================================================ lrg-portal.js — shared portal behaviors for all LRG apps v1.5.4 · 2026-11-03 · ship alongside lrg.css at /lrg-assets/ (v1.5.4: the v1.5.2 analytics change merged onto v1.5.3 -- both below.) (v1.5.3: a "What's New" tile (release notes, everyone) and a "What's new" row in the account menu. See release-notes/README.md. v1.5.2 is the analytics change on claude/analytics-review; this one doesn't depend on it.) (v1.5.2: analytics — every page's FIRST pageview is now attributed (cached pseudonymous id stamped via Umami's data-before-send; ~50% of events were landing in anonymous sessions); team-qa. reports to its own "Team Launchpad (QA)" property so events can be checked before prod; automatic App Action / App Action Failed / Content Copied / File Downloaded / Page Printed events in every app, no per-app code. Catalog: docs/analytics-events.md.) (v1.5.1: the Apps and account menus slide back on screen if a crowded header wraps their button to the left. Ships with lrg.css v1.4.1, which keeps the header on one row from 641 to 960 px.) (v1.5.0: header — an account menu (name, role, My Profile, Getting started, Account & security, Sign out) replaces the Account rows in the Apps menu; a QA badge on team-qa.; the Apps menu redraws once Page Permissions arrive (it was drawn from /whoami alone, so Access Control edits never reached it); "New" badges come from launchpad-managed dates (appMeta) with a dated registry fallback (newUntil) instead of a permanent isNew; env:"qa" tiles stay off prod; the logo is a real link; the theme button reports its state and the theme follows other open tabs; a failed /whoami no longer invents a "Preview Agent" outside local dev. How the header fits together: HEADER.md.) (v1.4.2: search hardening — Recent is per signed-in user, never holds My Contacts/Recruiting rows, and is wiped on Sign out (the old shared per-browser list is deleted); analytics never log client/recruit names; a provider failure or expired sign-in says so instead of "No matches"; provider icons/links are never trusted as markup/schemes; headshot fallback works under docs.' CSP; Esc/↓/Tab behave; the panel and box carry proper ARIA; ⌘K no longer hijacks editors; Agents "See all" keeps the query.) (v1.4.1: the Knowledge Base hides BookStack's own header search once our KB search box attaches -- one search box on docs.) (v1.4.0: search — curated best bets (LRG.bestBets, "Top results"), search-only app synonyms (LRG.appKeywords), typo tolerance for 5+ letter words, "Recent" results on empty focus, agent results deep-link to the People directory (#directory?q=). How it all fits: SEARCH.md.) (v1.3.3: search fixes — browse-on-focus lists EVERY visible app (the cap of 8 hid the newest tiles); multi-word queries match in any order ("blast email"); Enter opens the top result; async groups render into fixed-order slots (no shuffling, no lost keyboard focus); remote providers need 2+ chars, abort superseded queries and cache answers for 60s; "Search Performed" counts results only after every provider settles. One remote engine (LRG.searchRemote) now backs both the header dropdown and launchpad's hero.) (v1.3.2: fix identity coverage — identify() now waits for an ESTABLISHED Umami session (getSession().cache set by the first pageview) before firing, then re-asserts once, instead of racing the pageview; ~78% of sessions were landing unidentified, which capped role analysis and blocked retention.) (v1.3.1: (a) focusSearch also matches a "Search…" placeholder input, so ⌘K/'/' reaches apps with their own header search (Audit's #hSearch); (b) search-query telemetry — "Search Performed" now logs {query, results} (capped 60ch, self- hosted Umami) so results:0 surfaces gaps, and result rows carry the query + destination for click correlation.) (v1.3.0: universal-search upgrade — shared result helpers (LRG.scoreMatch/ matchHTML/iconInner/initialsOf), relevance ranking + highlighting for local groups, agent headshot avatars (r.avatar, asset-host-gated, initials fallback), per-group caps with "See all" tails, and a global '/' + ⌘K/Ctrl-K focus-search hotkey. Same helpers back launchpad's hero search + the BookStack head. Agents provider now returns `avatar` from the roster Headshot Slug.) (v1.2.2: Audit tile → v2 container (/audit) on ALL hosts — v2 promoted to prod; the strangler host-gate ternary is retired now that team.loyaltyrealtygroup.com/audit is live behind its Authentik app binding.) (v1.2.1: enable web-vitals — Umami v3 gates the performance collector behind data-performance="true"; without it LCP/FCP/CLS/INP/TTFB were never sent, so the Performance tab stayed empty. Server stores them as event_type=5 rows.) (v1.2.0: analytics depth — umami.identify() attaches a pseudonymous (hashed) user id + role/groups so usage segments by WHO/role; global JS-error events as a friction signal; universal-search usage + result-open events. Event taxonomy in docs/analytics-events.md.) (v1.1.1: Audit tile → v2 container (/audit) on team-qa; prod stays on the v1 n8n app (/fub-audit) until v2 is promoted — strangler cutover.) (v1.1.0: same-origin analytics — load the tracker from team.'s /stats/ proxy instead of analytics., so web-vitals beacons (flushed on tab-hide) deliver reliably; cross-origin unload fetches were being dropped, leaving the Performance tab empty. + apps-launcher navigation events via data-umami-event, and an LRG.track(name, data) helper for apps.) (v1.0.9: centralized Umami analytics — inject the tracker from here so every app that loads lrg-portal.js is tracked as ONE property (the team portal). Prod team. host only, so team-qa, the NocoDB roster (people.), BookStack (docs.) and local dev never send events. Plus a hashchange hook so hash-routed tabs (e.g. People #my-profile) count as distinct pageviews — Umami auto-tracks pushState but not hashchange.) (v1.0.8: fix "Sign out" — the old /outpost.goauthentik.io/sign_out link crashes with a 502 on this Authentik instance (server-side bug, no log line even emitted); point it at the real global invalidation flow instead, which was confirmed working.) (v1.0.7: "My Contacts" universal search group — live FUB lookup for the signed-in agent's own book via the gated /api/search/contacts proxy; non-linked users just get no group. Opens in a new tab.) (v1.0.6: permanent "Getting started" link in the account sections) (v1.0.5: clicking the header logo returns to the Launchpad from any app page; no-op on the Launchpad itself) (v1.0.4: env-aware app links — team-hosted apps use host-relative hrefs on team./team-qa. so QA stays in QA; other hosts still link prod. Re-engagement deep-links /webhook/reengage, valid on both envs) (v1.0.1: group hierarchy — admins⊃all, team-lead/recruiting⊃staff) (v1.0.2: `accountLink` apps (My Profile) render in the account rows of the apps-launcher menu, not the tile grid — see LRG.apps + buildAppsMenu) (v1.0.3: P2 — auto-register live Docs/Agents search providers on team./team-qa. hosts against the gated /api/search/* proxy endpoints) Provides: theme init/toggle · identity (whoami) · app registry · apps-launcher menu · universal search (browse-on-focus, New badges, grouped results) · account links. Usage (backslashes only escape the tag for safe inlining):