Overlays the complete 80-file src/react surface (all 58 panels, globe island,
variant tabs, analyst dock, finance terminal, OIDC PKCE callback) onto the live
origin/main backend (v2.4.51 AI-plane + events-stream fixes, kept intact). The
React surface is served ONLY to sessions that opt in via ?react (sticky cookie,
cmd/world canaryHandler); the vanilla build stays the default — nothing changes
for anyone until we flip it. @hanzogui/shell 7.6.1 (monochrome focus ring + world
surface). All gates green: typecheck:react, build:react (221kB gz), vanilla build,
canary server tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The app-image build+deploy now runs in-cluster on Hanzo Git. This GitHub workflow is reduced to a workflow_dispatch sync-notice; GitHub is a downstream mirror.
The global 60s WriteTimeout (cmd/world) severed the long-lived ?stream=1 NDJSON
response every cycle — the world-gw ingester logged "unexpected EOF" and
reconnected in a loop. Clear the per-request write/read deadlines via
http.NewResponseController for the stream response ONLY; every other handler
keeps the global timeout.
Ships /v1/world/{events,conflicts,infra,vessel,news,markets,feeds} — thin
cache-first projections over existing backend sources, incl. the NDJSON/SSE
events stream that feeds the ZAP topic hub (ingest 404 loop ends). Also fixes
the latent asFloat int-coercion bug (UCDP severity always computed minor) and
carries the isolated (inert) react/gui foundation from the parallel stream.
The endpoints the world-gw MCP/ZAP gateway proxies to were never built (404 in
prod). Implement them as thin, cache-first projections over the backend's
EXISTING sources — no second fetcher for any upstream:
- /v1/world/events: one aggregator (handlers_events.go) mapping existing
sources to canonical ZAP topic records. Poll returns {events:[...]}; ?stream=1
is a long-lived NDJSON stream (snapshot then re-poll, new-only dedupe by stable
id, every specific record mirrored onto world.events.all, ~25s keepalive, ctx
cancellation). Sources: UCDP GED→conflicts, USGS→earthquakes, FIRMS→fires,
Open-Meteo climate→weather.alerts, curated RSS→news.live, CoinGecko→crypto,
Yahoo→quotes. ais/opensky are honestly not aggregated server-side.
- /v1/world/conflicts: UCDP GED filtered by country/since/severity.
- /v1/world/infra: real provider/service-status board (shared serviceStatusBoard
extraction); physical-infra geodata + geospatial 'near' honestly noted absent.
- /v1/world/vessel: AIS relay lookup; honest empty+note when unprovisioned.
- /v1/world/news: curated-feed headlines via the shared feed pipeline.
- /v1/world/markets: CoinGecko crypto + Yahoo quotes (equities/fx/commodities).
- /v1/world/feeds: ZAP topic catalog + backend feed categories.
Reuse one curated feed catalog (exported from internal/world/mcp). Fix asFloat to
coerce native int/int64 (UCDP deaths_best is a Go int; severity was always 0).
PanelGrid owns the rail layout, drag-to-reorder (reusing the vanilla
attachPanelDrag engine — ghost/FLIP/touch/Escape), and order persistence via
the shared panel-order key, so React and vanilla read the same saved layout.
MarketsPanel is re-authored onto the Panel chassis + a PanelGrid slot, reusing
fetchMultipleStocks/formatPrice/formatChange/sparkline verbatim (view-only
port). App renders it through PanelGrid; VariantTabs/AccountControl move to
longhands; the obsolete PanelCard is removed (chassis supersedes it).
Panel is the single chrome every ported panel renders into — the React
analogue of the vanilla `Panel` base: header/title/actions/dragHandle slots,
the four decomplected states (ready/loading/empty/error) with copy defaulting
to the same common.* i18n keys, optional tabs (reusing the vanilla PanelTab
shape), and a sparkline slot. Sparkline wraps the vanilla sparkline() util so
dense series draw identically. Primitive-based (YStack/XStack/SizableText/
Spinner/ScrollView), longhand-only, brand-token themed.
Resolve the Stage-0 typecheck:react friction at its root: v4 defaultConfig
sets settings.onlyAllowShorthands:true, and registering it via GuiCustomConfig
strips every longhand style prop and re-derives the media wrapper as an index
signature JSX children cannot satisfy — the whole error cascade. Neither a
strictness knob nor a component wrapper removes it. So the runtime/bundler owns
the config (GuiProvider config={guiConfig}), tsc stays a clean structural gate,
and components use one longhand vocabulary. main.tsx inits the shared i18n
layer before paint. panel-order.ts is the ONE reader/writer for the vanilla
`panel-order` key, so React and vanilla share one saved layout.
"Yellow" in the UI is the status amber (#ffaa00): stale/cached badges, elevated
threat, the legend's Monitoring/Nuclear markers. It was defined under THREE token
names per theme (--yellow, --status-cached, --semantic-elevated) plus a brighter
#ffcc44 drifting raw in the chain-nodes stale/provisional states.
Now: --yellow is the single palette knob (#ffaa00 dark / #d97706 light);
--status-cached and --semantic-elevated reference it; the 4 #ffcc44 literals
route through var(--semantic-elevated). Changing (or killing) the amber
brand-wide is now a one-line edit per theme.
Left alone deliberately: the separate 5-tier --threat-* scale (own palette,
scale-to-scale merge = visible change, deferred to the screenshot-diffed pass)
and TS/WebGL literals (canvas can't consume CSS vars; values match the token).
A bare var(--x) with no fallback where --x is never defined makes the whole
declaration invalid at computed-value time — the property silently becomes
inherit/initial. 12 phantom token names (--orange, --muted, --text-primary,
--danger, --success, --warning, --blue, --accent-red, --surface-light,
--surface-elevated, --bg-panel, --bg-hover) were used bare in 20 declarations,
so those styles never applied: the intel-findings context menu rendered with NO
background (initial = transparent), focal-point/convergence urgency borders fell
to currentColor instead of orange, findings-modal titles inherited, etc.
Rewrote each to its semantics-matched canonical token from the existing :root
vocabulary (--text-muted, --text, --red, --green, --yellow, --semantic-high,
--semantic-info, --surface-hover, --surface-active, --panel-bg-solid) — the one
vocabulary, theme-aware in both light and dark. Dynamic runtime knobs with
fallbacks (--marker-scale, --flash-duration, …) are intentional and untouched.
Same bug class as the rgba(var(--semantic-critical)) fills fixed in v2.4.46.
Build green; CSS-only.
Pure indirection, zero visual change: the token EQUALS the literal, so rendering is
byte-identical — this just removes the drift so future color changes go through one
place. Replaced usage-site `#ffaa00` → var(--yellow) and `#44ff88` → var(--green)
(the --status-cached/--status-live/--yellow/--green DEFINITIONS untouched). Left
`#ffcc44` alone — it's a distinct value, so mapping it would change the color (that's
the risky consolidation, deferred to the deliberate scale pass).
Build green. This is the safe core of audit §6; the type/radius/z-index SCALE
consolidation (which changes values → needs per-view screenshot diffing) stays a
separate deliberate pass.
Rides on the native @hanzo/event telemetry (one client → POST /v1/event). Renames
the AI news-brief panel to BriefPanel (it makes the world brief) and the
product-analytics panel over /v1/insights/events to InsightsPanel, dropping the
Org prefix and the name collision. GTM stays orthogonal. No duplicate telemetry.
Claude-Session: https://claude.ai/code/session_01NP4ehjWW2h98FkE4YjUKuJ
Converge world's client telemetry on the ONE canonical Hanzo Cloud front
door: pageviews, product events, and errors all leave through @hanzo/event
to POST /v1/event, lensed server-side into web analytics, product analytics,
and error tracking. This supersedes v2.4.47's fragmented client stack — the
third-party Sentry bundle and the direct analytics.hanzo.ai (Umami) page
script both fed lenses the /v1/event stream now feeds server-side, so loading
them client-side double-counted the same lenses.
- bootstrap/telemetry.ts: the one client. Framework-agnostic createAnalytics,
brand-resolved host (hanzo/lux/zoo), IAM bearer injected at the composition
root (else a write-only publishable key so the fail-closed door accepts
logged-out ingest), consent gate (DNT / GPC / opt-out), SPA pageview on
pathname change, and filtered global error capture that preserves the
curated WebGL/map noise-filter verbatim.
- Product moments captured with the shared EVENTS vocabulary: country
drill-down, map-layer toggle, variant switch, share, and the AI analyst
chat funnel (start + per message). identify/group on the signed-in session.
- Remove @sentry/browser + bootstrap/sentry.ts; drop the Umami injection +
track() from bootstrap/analytics.ts. GTM (marketing tags) stays — an
orthogonal, env-gated concern @hanzo/event does not cover.
Logged-out marketing/public views load it too — anonymous, consent-gated.
typecheck + vite build + data tests green.
Move the fullscreen country view out of the App god-object into its own
unit: the CountryBriefPage lifecycle + map country-click wiring, brief
generation (local CII + server intel + ML fallback), the seven-day
timeline, per-country signal counts, the shareable story modal, and the
country-name/geo static helpers (COUNTRY_BOUNDS / COUNTRY_ALIASES /
resolveCountryName / …). The controller owns countryBriefPage /
countryTimeline / briefRequestToken; App composes it with lazy accessors
(map / panels / news / clusters / predictions / intelligenceCache) plus
getShareUrl + buildAnalystHost callbacks. Behavior byte-for-byte
identical — the map registers onCountryClicked exactly once on the stable
this.map, same as before.
App keeps thin delegations (setupCountryIntel / openCountryBrief /
openCountryBriefByCode / openCountryStory) so every call site — the map
handler, search, deep links, share buttons, window.__app — is unchanged,
and re-exposes window.__app.countryBriefPage (read-only getter) for the
e2e boot-readiness hook. App.ts 5686 → 4747 lines across the three
extractions; 18 now-unused imports pruned.
Build green; variant-switch-inplace + stations-wall e2e green; 3/3
direct-drive country-view tests green (the deep-link case is gated on
real proxied-feed freshness and fails identically on main — environmental,
not a regression, verified by stash+rerun).
Move the agentic-control surface out of App into its own unit. App
exposes a narrow capability port (AnalystHostBridge); AnalystCommandHost
owns the SHAPE of what the AI analyst can see and do — the getState /
listPanels / listLayers projections, the one-time org priming, and the
1:1 command routing that builds the AnalystHost object. The command
implementations stay in App (they mutate shared panel/monitor/variant/
layer state and are called from UI paths too) and are reached through the
bridge — a thin adapter, behavior byte-for-byte identical.
buildAnalystHost() is now a one-line delegation to the host's build();
all three call sites (country brief, analyst panel, analyst dock) are
unchanged. Build + variant-switch-inplace + stations-wall e2e green.
Move the ⌘K search surface out of the App god-object into its own
controller: modal creation + per-variant source registration, result
routing (map fly-to / panel scroll / country brief), the live search
index, and the Cmd/Ctrl+K keydown listener. App composes it with lazy
accessors (getMap/getMapLayers/getPanels/getAllNews/getLatest*) so live
field reassignments read fresh — behavior byte-for-byte identical.
App.ts 5686 -> 5215 lines; 7 methods + 2 fields relocated; 16 now-unused
config imports pruned. Build + variant-switch-inplace + stations-wall e2e
green.
BaseModal: one base owning full-screen overlay behavior — .active/create
lifecycle via mountOverlay/unmountOverlay, backdrop-click + Escape close, and a
basic focus trap (focus first focusable on open, restore on close). SignalModal,
StoryModal, SearchModal and MobileWarningModal migrate onto it with no change to
their DOM or public methods; this adds Escape-to-close to the three that lacked
it.
embedIframe({src,title,allow,allowFullscreen,...}) in utils/embed.ts: one
canonical player-iframe factory with the agreed YouTube allow + sandbox. The
four raw embeds (LiveWebcamsPanel, LiveNewsPanel desktop bridge, WatchQueuePanel,
services/immersive) route through it.
Clickable-row a11y: utils/a11y.ts makeActivatable() gives click-handled rows
role=button + tabindex=0 + Enter/Space firing the same action — applied to
Displacement, Ucdp, Climate, Investments and StrategicRisk rows. SearchModal
result rows get role=button (already keyboard-operable via its input model).
MarketPanel context-menu rows get makeContextMenuActivatable() so the copy menu
is reachable by keyboard. NewsPanel and TechReadiness skipped (see report).
Behavior-neutral decomplect (same rendered output, fewer implementations):
- Panel.showEmpty() added; ~15 hand-rolled empty states (4 divergent class conventions
.empty-state/.panel-empty/.cloud-empty/.wq-empty) routed through the one base method.
- Loading spinner clones → this.showLoading() (the base radar).
- Live-dot / freshness → the base setDataBadge; duplicate keyframes/classes unified.
- 5 sparkline generators → one sparkline() in utils/market-format.
- ~7 local number/pct/currency reimplementations → the shared utils/cloud-format helpers.
- Several hand-rolled tab-switchers → Panel.renderTabs + one .panel-tab convention.
Build + globe/panel e2e green (variant-switch passes uncontended). Partial pass — the
tab migration (EconomicPanel), BaseModal/embedIframe unification, and clickable-row a11y
are still to land in a follow-up.
renderAddWidgetGrid iterated only this.panelSettings — the CURRENT variant's subset —
so ~20 instantiated panels (service-status, lux-book, startups, accelerators, …) were
unreachable from the palette even though its own comment calls it "a searchable palette
over the panel registry." Now it sources from Object.keys(this.panels) (every panel that
exists this session) ∪ panelSettings; display names resolve via the new canonical
PANEL_NAMES union (+ i18n); panelSettings is purely the enabled overlay. setPanelEnabled
now creates the overlay entry on demand, so a palette-added non-variant panel actually
shows. renderPanelToggles stays the curated per-view quick-toggle (no 40-item wall).
Result: any panel is addable from any view; registry and menu can no longer disagree.
Build + globe/panel e2e green.
- Contrast: dark-theme --text-muted #666 (~2.8:1) → #949494, --text-faint #555
(~2.0:1) → #8a8a8a — both now pass WCAG AA on the dark surface (muted text is
used ~99×). Light theme untouched.
- Keyboard focus: add a global, theme-aware :focus-visible ring (2px var(--accent))
on links/buttons/inputs/[role=button|tab]/[tabindex] — shows on tab-focus only,
never on mouse; low specificity so per-component focus styles still win.
- Mobile: viewport-cap the fixed-width flyout/map-popup/dropdown widths
(width: min(360|380px, calc(100vw - 24px))) so they stop overflowing narrow
phones — identical on desktop, only shrinks below the cap.
CSS-only, behavior-neutral; build green.
Grep-verified dead (zero instantiations, zero non-barrel imports) — deleted end to end:
- 11 orphaned panel components never built in createPanels/ensureVariantPanels:
Robotics/Quantum/PostQuantum/Weather/Sports/SpaceWeather (the "domain-lens" set),
plus Regulation (superseded by the NewsPanel 'regulation' feed), GeoHubs, TechHubs,
VerificationChecklist, and CountryIntelModal (superseded by CountryBriefPage).
- 5 dead backing services (robotics/quantum/post-quantum/space-weather/sports) +
config/post-quantum.ts (only importer was the dead service).
- The 6 dead domain-lens panel keys in FULL_PANELS — they rendered Panels-menu toggles
and Add-widget entries that did NOTHING (applyPanelSettings/getElement hit undefined),
and their now-orphaned `.domain-*` CSS block.
- Barrel exports for the deleted panels.
KEPT (verified live, NOT panels): config/quantum.ts + config/robotics.ts + services/
weather.ts (map layers), and services/geo-hub-index + tech-hub-index (feed the map's
geo/tech activity markers via geo-activity/tech-activity — the audit missed the relative
import; the build caught it before commit). Relocated `StockIndexData` into its only
consumer, CountryBriefPage.
CSS: `rgba(var(--semantic-critical), 0.2)` is invalid (the token is a hex, not an r,g,b
triple) so 5 alert-fill backgrounds silently rendered nothing → `color-mix(in srgb,
var(--semantic-critical) 20%, transparent)`.
Build + globe/panel e2e green. No feature loss.
Symptom: with several world.hanzo.ai tabs open in Safari, the page hits "a problem
occurred" and reloads to a BLACK globe. Root cause: each tab holds TWO WebGL contexts
— the visible native GlobeView (deck.gl) AND a PARKED mapbox (frozen in mercator behind
it, still holding a full GPU context). Several tabs × 2 contexts overruns Safari's
GPU/context ceiling → crash-reload → the fresh page's context is gone → black.
Fix: after a tab has been hidden MAP_GPU_PARK_MS (30s), drop the parked mapbox's WebGL
context via WEBGL_lose_context; restore it the instant the tab is shown. Reuses the
existing webglcontextlost/restored handlers (mapbox-gl + deck.gl both rebuild their GPU
resources on restore) — nothing is re-wired, and the visible GlobeView's own context is
untouched. Worst case (a restore that doesn't repaint) equals today's post-crash state
but with one fewer live context — strictly less GPU pressure, never worse.
DeckGLMap.parkGPU/restoreGPU + MapContainer delegation + App visibilitychange timer
(cleared on show and on destroy). Existing globe/map + panel e2e green (no regression).
Both were reachable in almost no view: News Wall existed only in the World variant,
Markets Bubble only in World (opt-in) + Finance — and Finance renders the terminal
(no panel grid), shadowing it there. Since renderPanelToggles AND the "+ Add widget"
palette only iterate the CURRENT variant's panelSettings, on the default Cloud view
(and AI/Crypto/Tech) both panels were invisible and unreachable.
Add both (opt-in, enabled:false) to CLOUD/AI/CRYPTO/TECH so they show in every grid
view's Panels dropdown + Add-widget palette. e2e-reprobed present in all four.
- Live Webcams: replace dead default feeds with browser-verified-live 24/7 streams
(grid = Kyiv/Tel-Aviv/Miami/Taipei, each confirmed PLAYING/isLive this session via
the YouTube IFrame API in a full-codec browser); prune 6 non-embeddable feeds, add
Monterey Bay. Fixes the "mostly not available" webcam panel.
- e2e (dev-only, no bundle impact): runtime-fetch fallback test → current /v1/world/*
paths + real host discriminator; keyword-spike test → mid-sentence proper-noun +
initI18n + opt-in badge flag; drop the obsolete arch-download test (feature removed
for Hanzo); loadMarkets test → yahoo-batch envelope, no commodities. 7/7 pass.
Also carries the analyst-dock hzc-* selector drift fixes (19/19 pass).
Claude-Session: https://claude.ai/code/session_01NP4ehjWW2h98FkE4YjUKuJ
The analyst copilot redesign (f54828f9) renamed .ai-dock-*/.ai-analyst-* → .hzc-*;
tests still clicked the dead .ai-dock-fab and timed out. Remap all dock selectors
and update assertions to the redesigned behavior (model picker is now a popover not
a <select>; tool-trace <details> opens by default; db glyph not 🔧 emoji). Also:
enso-training/real-models are cloud-variant panels (navigate ?variant=cloud) showing
the real served-model catalog not opaque arm-N; country-view waits for
__app.countryBriefPage; video-drag uses an on-screen drop coordinate + map-led reset
order. 19/19 pass across the touched specs. Golden-screenshot/headless-WebGL specs
left untouched (CI-env goldens). Dev-only; CI does not gate on e2e.
Claude-Session: https://claude.ai/code/session_01NP4ehjWW2h98FkE4YjUKuJ
Visual + runtime review across all variants surfaced five real defects; fixed each honestly (no fabricated data):
- china-macro: 10s frontend AbortController + 9s backend deadline + negativeTTL negative-cache — was hanging ~24s then aborting (cloud-3d/crypto/fund).
- AI Insights: summarization.ts raw fetches → fetchWithTimeout (root cause) + 12s render-race + briefSettled guard — panel no longer spins forever for signed-out users.
- finance: dark .fin-tv-skeleton overlay over TradingView iframes (removed on load) — no more white boxes on the dark theme during load.
- Live Webcams: prune 3 dead upstream feeds (Jerusalem/Tehran/DC), repoint default grid to working streams (Kyiv/Mecca/London/NY).
- Sentry: drop the stale hardcoded DSN, env-gate on VITE_SENTRY_DSN (no-op when unset) — kills 403 spam on every variant (was already over-quota/broken).
Claude-Session: https://claude.ai/code/session_01NP4ehjWW2h98FkE4YjUKuJ
Rolls up the 7 commits on main since v2.4.42 (News Wall, Christie's +
LuxuryEstate alt-asset feeds, Markets Bubble D3 circle-pack, org-shared
editable dashboard default, yahoo range/interval daily series). package.json
had regressed to 2.4.39 from a merge-conflict resolution while the highest
tag was already v2.4.42 and prod ran this same code — correct it forward to
2.4.43 so the deployed image, bundle version and /v1/world/version agree.
Claude-Session: https://claude.ai/code/session_01NP4ehjWW2h98FkE4YjUKuJ
handleYahooFinance now forwards a whitelisted range/interval to Yahoo's chart
endpoint (e.g. range=1y&interval=1d) instead of always returning the intraday
default. Absent params preserve prior behavior; the range keys the cache so
different spans of a symbol cache separately. Unlocks real 1-year daily
growth charts on lux.fund (was intraday-only).
Promote the world.hanzo.ai dashboard from per-user-only to an org/shared
scope: a hanzo.app org admin publishes a shared layout/feeds/channels that
every member of the org hydrates as their default, with per-user changes
still overriding it.
Backend (internal/world):
- New org-scoped dashboard doc keyed by owner via store.SharedSub (the same
Settings table, 'dashboard' namespace, no new table). Served at
/v1/world/dashboard/shared: GET returns the org default to any signed-in
member; PUT publishes it and is admin-ONLY (403 otherwise).
- Admin gate reuses isAdminOrg + a new isOrgAdmin honoring IAM's isAdmin
claim; writes are always scoped to the caller's own org (no cross-org).
- Decomplect: per-user and shared paths share ONE GET/PUT body
(serveIdentityBlob), parameterized by the resolved identity/scope.
Frontend (src):
- dashboard.ts hydrate is scope-parameterized: the org default is applied
first as the base, then the user's own doc overlays it (user wins). Both
scopes fetched concurrently so boot stays bounded.
- publishOrgDashboard() PUTs the current layout to the shared endpoint
(admin-only server-side). An admin-gated "Publish as org default" entry in
the workspace context menu triggers it; anonymous/non-org unchanged.
Tests: Go handler tests cover admin publish + round-trip, non-admin 403,
member read, per-user isolation, cross-org isolation, anon 401, and the
admin-org publish path. Playwright specs cover shared-then-user precedence.
Add a live "all markets in one bubble" panel: every asset class at once as a
D3 circle-pack — a cluster per class (equities/commodities/FX/rates/crypto),
a leaf circle per instrument, radius ∝ importance × how hard it's moving, fill
= diverging green(↑)→red(↓) by percent move (flipped for vol gauges VIX/MOVE).
Hover any bubble for name / price / signed %. Smooth radius+colour tween on the
~30s jittered live poll (circles reused by id), lazy-built on first visibility
and paused off-screen, re-packed on resize.
Decomplected around ONE source of truth:
- config/market-universe.ts: every tradable declared once (class, group,
weight, digits, inverse) with universeByClass / universeGroups / yahooUniverse.
- services/market-universe.ts: joins the Yahoo + CoinGecko boundary fetchers back
to that metadata into one flat, plot-ready MarketDatum[] (in-flight-guarded,
short-cached).
- CommoditiesPanel now DERIVES its symbol list from the universe (no inline
duplication). FX/Rates panels left untouched — they carry extra EM/curve symbols
and bespoke rendering the universe doesn't model, so migrating them would risk
working behaviour.
Wired into FINANCE_PANELS (on) and FULL_PANELS (opt-in). e2e mocks the market
endpoints and asserts bubbles across ≥3 classes, tooltip-on-hover with a %, and
survival across a live re-poll.
The finance-terminal "Art & Collectibles — Auctions" and "Luxury Real Estate"
panels rendered "live feed pending" because /v1/world/auctions and
/v1/world/luxury-realestate did not exist. Make them return REAL data.
world-gw (cmd/world) now scrapes two public sources server-side, hourly, and
serves every caller from the SWR cache:
- GET /v1/world/auctions — Christie's public results: realized SALE totals
(title, inferred category, price+currency, date, location, image, link).
Sotheby's (the requested headline) gates realized prices behind a login —
its /data/api/asset.saleresults.json returns 401 "Not signed in" — so the
honest public major house is Christie's, exactly the fallback the panel names.
- GET /v1/world/luxury-realestate — LuxuryEstate.com listings (title, location,
price+currency, type, image, link). JamesEdition (the requested headline)
sits behind a Cloudflare JS challenge that blocks datacenter egress, so it
cannot be fetched from the DOKS pod; LuxuryEstate is reachable and
robots-permitted.
Robust + respectful + honest:
- One request per source per hour (StartAltAssets warmer + 1h TTL / 24h stale).
- On a source failure: serve last-good; with no cache yet, honest empty
{items:[]} (panel shows "live feed pending"). NEVER fabricated data.
- Payload is exactly {items: AltFeedItem[]} — matches AltFeedPanel.ts
field-for-field. Source attributed per feed; SPA source labels updated to the
real sources (Christie's / LuxuryEstate).
- Parsers extract the embedded results JSON (robust to DOM churn); unit-tested
against real trimmed captures in testdata/.
New opt-in "News Wall" panel: every live news channel plays at once in a responsive
grid of controllable YouTube players (all muted). HOVER a tile → it gets audio focus
(unmuted, others muted); leaving the wall mutes everything. The wall + its N players
build lazily on first visibility and pause when it scrolls off (IntersectionObserver),
so it costs nothing until toggled on and never decodes a hidden wall.
Decomplect — one source each, shared with LiveNewsPanel, no duplication:
- src/services/youtube.ts: the ONE YouTube IFrame API loader + player types (was
braided into LiveNewsPanel; the grid embedder needed the same loader).
- src/config/live-channels.ts: the ONE channel list + liveChannels()/getDefaultLiveChannel()
(data belongs in config, not a panel). LiveNewsPanel + immersive now import it.
Wiring: registered in createPanels, exported from the barrel, added to FULL_PANELS
(enabled:false — opt-in like Live Webcams), styled (.stations-* responsive auto-fill
grid with a live focus ring).
e2e (stations-wall.spec.ts): one tile per channel; hover moves the single audio focus;
leaving mutes all. variant-switch-inplace still green (the LiveNewsPanel refactor is
behavior-preserving).
The finance terminal only mounted on the initial ?variant=finance load; the new
in-place variant switch (setSiteVariant, no reload) ran ensureVariantPanels but
never mounted the terminal — so clicking the Finance tab showed the panel
dashboard, not the Bloomberg terminal. Replace mountFinanceTerminal with
syncFinanceTerminal(variant): mounts the terminal overlay when entering finance,
removes it (destroy) when leaving. Wired into both init() and the switch. Now the
tab works both ways — verified: in-place switch mounts 27 chart cards.
A toggleable "Global flows" map layer draws the real backbone of world comms +
trade as animated great-circle arcs on the globe ("where everything is going"):
the principal submarine-cable / internet-backbone routes (cyan) and the major
maritime trade + energy lanes (amber), between real global hubs. Direction is
source → target with a travelling pulse (the existing AnimatedArcLayer + the
shared cloud-pulse clock).
- New src/config/global-flows.ts: 27 real corridors (NYC↔London transatlantic,
Tokyo↔LA transpacific, Shanghai↔Rotterdam via Suez, Gulf oil → Asia, etc.),
weighted by representative magnitude — no synthetic data.
- createGlobalFlowsLayer() in DeckGLMap: great-circle on the globe, alpha + width
scale with corridor weight, comms/trade colour-coded.
- Low-end degradation (device tier): draws only the heaviest maxFlowArcs()
corridors (40 low / 120 mid / 300 high) so a weak GPU never stalls.
- Toggle: "Global flows" in the layer panel (world + tech variants) + URL state;
default OFF (overlay). Animated only while visible (cloud-pulse gate).
Switching the header variant tabs [Cloud|AI|Crypto|Finance|Tech|World] did a full
window.location reload: it re-created the deck.gl WebGL context, respawned the ML
workers, re-mounted every video iframe and cold-refetched ~48 endpoints. That
cold-start churn WAS the freeze.
SITE_VARIANT is now runtime-mutable (getSiteVariant / setSiteVariantRuntime).
setSiteVariant switches IN PLACE — recompute the target's config and re-apply
panels/map-layers/feeds/tabs/URL live (history.replaceState), exactly like the
intra-variant panel toggle. Heavy singletons stay warm; the synchronous switch
cost is a few ms (e2e-gated <500ms via window.__switchT) vs a full reload.
Also:
- Header controls (H-toggle switcher, search, settings, reset) are wired BEFORE
`await this.mapReady` so the shell is interactive on first paint — they were
dead until the 2.7 MB map chunk loaded (a cold H-toggle click was lost).
- LiveNewsPanel pauses its YouTube player via IntersectionObserver when off-screen
(matching LiveWebcamsPanel) — no decoding a hidden news embed.
- device-tier: adaptive DPR/refresh/flow-arc knobs on low-end hardware.
- Sentry deferred off the critical path (code-split, after first paint).
e2e: variant-switch-inplace.spec.ts proves no reload (JS-context sentinel + same
deck.gl canvas survive), panel-set swap, URL/active-tab re-point, across 3 switches.
The finance variant now renders a dense, dark terminal of LIVE TradingView
charts instead of the news-heavy map dashboard — global indices (S&P 500,
Nasdaq 100, Dow, VIX, Nikkei, Hang Seng, China A50, DAX, FTSE, CAC, Euro Stoxx,
ASX), commodities (gold, silver, WTI, Brent, nat gas, copper), forex (DXY +
majors) and crypto (BTC/ETH) — plus a live ticker tape, a market-overview
widget, a Lux DEX trade card and alt-asset feeds (auctions + luxury real estate).
- Code-split: App dynamic-imports FinanceTerminal only for the finance variant,
so the TradingView embed logic + terminal CSS never ship in the entry bundle.
- Low-end degradation (device tier): fewer charts + lighter widget types
(mini-symbol-overview instead of advanced-chart), and every widget lazy-mounts
on viewport (IntersectionObserver) so a weak laptop never mounts dozens of
live embeds at once. The map is skipped entirely for this variant (−2.7 MB).
- CAPITALCOM CFD index symbols (free-embed compatible); exchange-native SP:SPX /
DJ:DJI etc. are account-gated and render an error card.
- Lux DEX: exchange.lux.network sets X-Frame-Options: DENY, so the SwapWidget
can't be iframed — surfaced as a prominent launch card to the real DEX.
- Auctions (Sotheby's) + luxury RE (JamesEdition) fetch /v1/world/{auctions,
luxury-realestate}; until that backend feed exists they show an honest
"live feed pending" state — never fabricated listings.
- CSP (index.html): allow s3/s.tradingview.com (script) + the tradingview-widget
frames. The production HANZO_STATIC_CSP is widened in the universe world CR.
On a low-end laptop the globe (mapbox-gl + deck.gl ≈ 2.7 MB) was eagerly
modulepreloaded and constructed synchronously during renderLayout, so nothing
painted until ~4.1 MB of JS downloaded + parsed + WebGL initialised (≈17.7 s to
app-shell under 6× CPU throttle on the live build). This code-splits the map and
degrades gracefully for weak hardware.
- Map loads via dynamic import() in App.mountMap(); init awaits it AFTER the
shell paints, before map-dependent wiring. Every this.map access was already
null-guarded, and mountMap replays the initEscalation / onTimeRangeChanged /
initial-URL-state wiring that used to run inline in createPanels.
- Isolate Vite's __vitePreload helper into its own chunk (manualChunks): it had
been co-located INTO the 'map' chunk, so the entry statically imported 'map'
just for that helper and Vite modulepreloaded the whole 2.7 MB before paint —
THE reason the map stayed eager despite the dynamic import.
- @/components barrel re-exports the map classes as TYPES only (their modules
have top-level side effects; value re-exports also dragged 'map' eager).
- Defer @sentry/browser (~460 KB raw / ~130 KB gzip) out of the entry to
requestIdleCallback, with a synchronous early-error buffer replayed on init so
no boot error is lost.
- New device-tier util (hardwareConcurrency + deviceMemory + reduced-motion,
?tier= override): low-end drops the deck.gl overlay DPR 2→1, skips the idle
globe auto-spin, and scales real-time refresh intervals up to 2×.
Eager JS before first paint: 4136 KB → 1440 KB (raw). Map (2.7 MB) + Sentry now
load after first paint.
Ships the fleet view (DOKS cluster nodes + gpu-jobs queue panels, gated by the
canonical SuperAdmin predicate) and the red-review fixes (adminOrgs base =
{admin,built-in} with operator org via WORLD_ADMIN_ORGS; no-store on all admin
Cloud reads). Pairs with universe WORLD_ADMIN_ORGS=hanzo.
1. adminOrgs() base is now EXACTLY {admin, built-in} — cloud's canonical
globalAdminOrgs / principal.IsSuperAdmin, ONE source of truth. The operator
org is additive-only via WORLD_ADMIN_ORGS (deploy env), never hardcoded; an
unset/empty env resolves to the base, never empty and never everyone. Tests
that exercise the operator (owner "hanzo") now declare WORLD_ADMIN_ORGS=hanzo,
proving the env-driven path; the red matrix gains an operator-via-env case and
an operator-without-env→403 case; isAdminOrg-exact denies "hanzo" by default.
2. Every admin Cloud read returns Cache-Control: private, no-store (clusters,
queue, and the family: fleet/services/analytics/llm) so a caller's own
admin-scoped aggregate is never stored in a shared cache. Infra tests assert it.
WORLD_ADMIN_ORGS=hanzo is set on the world App CR (universe) so the operator
keeps dashboard access via config, not code.