Four macro signal sources under /v1/world/*:
insider — SEC EDGAR Form 4 filing flow (keyless, live)
defi — DeFiLlama TVL + stablecoin float (keyless, live)
layoffs — Texas WARN notices (keyless, live)
congress — Quiver Quant congressional trades (QUIVER_API_KEY, degrades clean)
Bumps the vestigial VERSION file to the release for the first time in a while.
Completes the four macro signal sources on the alt-data plane:
insider (SEC Form 4), defi (DeFiLlama), layoffs (TX WARN), congress (Quiver).
layoffs reads the Texas Workforce Commission WARN Socrata feed (keyless, live).
congress reads Quiver Quant when QUIVER_API_KEY is set and degrades to a clean
unavailable payload otherwise — the free house/senate feeds are all gated now,
so this follows the same key-gated pattern as finnhub/eia/acled. Pure parsers
are unit-tested; the route sweep confirms both degrade without 5xx or HTML leak.
DeFi snapshot: total TVL across chains, top-10 chains by share, stablecoin
float (top-6 by market cap) — one cached pull shared by all viewers.
Insider flow: SEC EDGAR Form 4 filing stream (buys/sells by officers,
directors, 10% holders) — the filing-velocity macro signal. SEC-compliant
descriptive User-Agent; pure atom parsing unit-tested.
Both cached, single-flighted via cachedJSON, degrade to a clean 200
unavailable payload. Routes swept by TestRoutesRespond.
The globe-render acceptance suite went red as the flagship Cloud view evolved
(cloud now opens on the native 3D globe, controls collapsed into dropdowns); none
of it was a globe-render bug — the sphere renders and occludes correctly — the
harness just no longer matched the app. Restore it to 6/6:
- e2e/globe-render: pin the 2D->3D toggle path with ?mode=2d. Cloud defaults to the
immersive 3D globe, which parks the mapbox map and mounts .deckgl-map-wrapper
hidden, so the old `toBeVisible` precondition could never pass. Enter 3D through
the proj-btn's REAL click handler (the immersive dock floats over the full-view
globe canvas, so a headless synthesized mouse click's hit-test lands on the
canvas, not the collapsed-dropdown button). This spec verifies GLOBE render
(occlusion/terrain/parity/live dots); dock pointer reachability is out of scope.
- DeckGLMap: expose __deckMap/__mapboxMap as soon as the instance is FUNCTIONAL
(right after initBasemap creates this.mapboxMap) instead of on the async mapbox
'load'. When Cloud opens straight into the parked-mapbox native globe, 'load' can
trail the globe by seconds, so the e2e read an undefined __deckMap right after
go3D. buildLayers()/asGlobeSource()/setOcclusionCenter()/occludeFarSide() are all
live at construction; __deckOverlay stays exposed in initDeck where it is built.
- vite: optimizeDeps.include the deck.gl / luma.gl / mapbox graph so a cold dev/e2e
server pre-bundles @luma.gl/webgl's device adapter up front and never re-optimizes
mid-load — that reload otherwise races GlobeNative's `new Deck()` and the 3D globe
never initialises. Prod is unaffected (Rollup build, no dep optimizer).
Verified: e2e/globe-render.spec.ts 6/6 green on an unminified dev server; the dot
globe renders (4726 land dots), traffic dots occlude (back-hemisphere alpha 0),
count badges cull, terrain drapes single-context, live dots update.
The admin CloudServicesPanel set each subsystem's up from /v1/o11y/status,
so any subsystem o11y lacked telemetry for rendered as DOWN — a live service
shown 'not active'. probeService now treats o11y as metrics enrichment and
derives up from a real liveness probe (livenessURL) when o11y has no
per-deployment data; instrumented stays a separate signal. Live-but-
uninstrumented ⇒ UP with no metrics, never a false down. TDD: 2 new tests green.
Repo had no agent doc. Records the browser-control preference (Hanzo MCP
extension for interactive UI, Playwright only for the deterministic e2e
suite or when the extension is absent), the dot-lattice cybermap default
(one getLandDots() shared by 2D DeckGLMap + 3D GlobeNative), and the
bump-patch/tag/dispatch release convention.
Claude-Session: https://claude.ai/code/session_01NP4ehjWW2h98FkE4YjUKuJ
world.hanzo.ai showed a blank center (globe gone). Root cause: the mapbox basemap
was created with projection:'globe' in 3D mode, and mapbox-gl v3.26 throws
"Missing theme" when a plain (non-Standard) style loads in globe projection —
which aborts the whole map init, taking the native deck.gl globe down with it.
(Surfaced now because the v2.4.33 .dockerignore switched CI from runner-cached
node_modules to a clean lockfile npm ci = mapbox-gl 3.26.0.)
When the native deck.gl globe is on (the default), the mapbox map is only a parked,
invisible basemap behind GlobeNative — it never needs globe projection. Add
mapboxProjection(): globe ONLY when 3D AND native globe is off; otherwise mercator.
Applied at map construction + in applyProjection(). No visual change (parked map is
invisible); the native globe now renders.
Verified with a swiftshader (software WebGL) playwright run: the dot-globe renders,
.globe-native-canvas mounts, no 'Missing theme'. tsc + vite build green.
Claude-Session: https://claude.ai/code/session_81b00bd9
A reproducible numpy/scipy reference implementation of the RRG rotation
model that powers the engine. Loads a 6-month daily snapshot of the
32-symbol universe (research/data), formalises RS-Ratio / RS-Momentum and
the quadrant classification, reproduces the current read and the Lux Book
top-10 allocation, and backtests the rotate-out-of-Weakening /
into-Improving thesis.
The numpy read cross-validates the Go engine exactly (Uranium 97.4/103.1,
Hyperscalers weakening 101.4/95.9, Great Rotation 0.55 WATCH). The backtest
is reported as indicative on a single window, not an alpha claim.
Executed notebook (rotation_model.ipynb) + percent-format source + the
data snapshot + the RRG plot.
Add a `fund` variant: the same World 3D-globe engine as finance, Lux
white-labeled and sharpened for the fund's macro read. lux.fund defaults
to it; the Lux wordmark replaces the Hanzo mark on that host.
The Lux Book (LuxBookPanel) derives the top-10 model allocation live from
the rotation engine — conviction weights each theme by quadrant, momentum
tilt and oversold-base bonus, normalised to a 100% book, and rebalances
every refresh. Each position carries a globe anchor and the book is
emitted on the `lux-book` document event for the globe layer to plot.
Rotation scanner and market radar lead the layout; feeds reuse the finance
set; energy/commodity hubs join the globe. Build meta + dev/build:fund
scripts added.
Red review fixes:
- Drop non-positive (0/negative) closes in the rotation fetch path via
positiveCloses. A spurious Yahoo 0 tick on a thin symbol was surviving
compact() (which keeps 0 for legitimate zero-volume bars) and could flip
a lead theme's quadrant, toggling the public Great Rotation signal. The
shared compact() is untouched so volume series stay correct.
- Guard round2s against NaN/Inf (return 0) so a stray non-finite can never
reach json.Marshal after cachedJSON has cached the value — an airtight
"never 5xx".
- RotationScannerPanel drops an aborted fetch silently instead of flashing
"unavailable" over the still-valid render on rapid refresh/destroy.
Tests: positiveCloses drops bad bars; a 0 bar no longer flips the quadrant.
Add /v1/world/rotation: a Relative Rotation Graph engine scoring theme
baskets (AI/semis, hyperscalers, energy, natural gas, uranium, nuclear
power, plus the GICS sectors as context) on RS-Ratio x RS-Momentum
against SPY, into four rotation quadrants and named thesis triggers —
AI distribution, energy-complex accumulation, and the combined Great
Rotation that needs both legs to fire.
RotationScannerPanel renders the quadrant plot with rotation tails, the
signal chips and a momentum-ranked leaderboard. Computed server-side,
cached 15m, single-flighted; degrades to a clean unavailable 200 when
upstream data is thin.
Registered in the finance, tech and full variants.
Ships the corrected benchmark closeout (already on main, now released):
- summary.json: real measured accuracy for GPQA-Diamond / LiveCodeBench / HLE
/ CharXiv across the frontier arm pool + enso/enso-flash/enso-ultra; degraded
(content-filter/length-truncated) rows carry NO number and are dropped rather
than shown as a low score.
- SWE-Bench Pro + Terminal-Bench honestly marked pending (not yet run), not faked.
- Dropped the aspirational reference table; the panel stands on measured numbers.
- .dockerignore so host node_modules can't clobber the image npm ci.
Total measured spend $564.25, all real. Handler gate + honest-data tests pass.
The snapshot is resynced to enso-bench, where a run lost mostly to blanks now reports no
accuracy at all. The handler already drops degraded rows, so the dashboard shows only clean
complete-run measurements; this keeps the embedded data honest for any other reader of the
snapshot too.
The benchmark snapshot is resynced to the corrected enso-bench numbers, which are
complete-run scores rather than blank-excluded estimates. Excluding blanks inflated the
numbers because blanks land on the hard items a model gets wrong, so the dashboard was
about to show fable-5 at a spurious 93.6 against its true 81.3. A run that lost most of its
items to blanks is now marked degraded and dropped from the table entirely rather than
displayed with a misleading number, so glm-5.2's 65%-blank run no longer appears as a
score.
The enso benchmark snapshot carried an enso_reported block whose figures did not match our
own measurements -- it showed enso-ultra at 50.0 on HLE where we measured 34.6, and 93.2 on
LiveCodeBench where we measured 81.6 -- and its columns named other vendors. A dashboard
that shows fabricated superiority beside real measurements is worse than one that shows
less, so the block is removed and the dashboard now stands on the corrected measured tables
alone. The tests assert the reference table and its columns are absent rather than present.
The dashboard embedded a snapshot in which infrastructure blanks -- upstream 200s carrying
no answer -- were scored as wrong, so it displayed glm-5.2 at 31.8 and fable-5 at 81.3
against their true 94.0 and 93.6. The measured block is regenerated from the corrected
enso-bench summary, which computes accuracy over the items the API actually answered, and
the enso family's clean-path results are included.
The head-to-head test asserted the raw livecodebench figure and the old HLE preflight stub;
it now checks the corrected accuracy within a band and treats HLE as the real n=500
measurement it has become.
No .dockerignore existed, so `COPY . .` (Dockerfile:31) copied the host's
node_modules over the fresh `npm ci` tree — poisoning BuildKit's node_modules
cache-mount ('cannot replace directory with file') and breaking every world
image build (main + PR #19). The web stage installs its own deps, so the host
node_modules/dist/.git must never enter the build context.
Frontend — the "cool 3D dot map" is now the hero everywhere:
- DEFAULT_BASEMAP_STYLE = 'dot' for ALL variants (was cloud/ai only); the
glowing dot lattice opens every mode. Persisted choice + style switcher win.
- GlobeNative: translucent glowing dot-globe — two passes (a DIM far lattice
drawn through the sphere + a BRIGHT near pass depth-tested for real back-of-
globe occlusion), additive blend so dense continents bloom; cool ice-blue rim
glow atmosphere; radial-vignette background as the premium first-frame state
(never a dead black hole). Shared position accessor → zero per-frame alloc.
- DeckGLMap: the SAME dot lattice + palette on the 2D mercator basemap (glow
underlay + crisp core), so 2D ↔ 3D read as one surface; 'Dot' added to the
style switcher; DEFAULT_BASEMAP_STYLE drives the default/fallback.
- land-dots: LAND_DOT_NEAR/FAR palette lives in ONE home, imported by both
renderers (values, not places).
- Immersive: panels dock to a translucent GLASS right-rail with the globe as the
centre hero; light-theme frosted variant + mobile glass bottom-sheet; legend
becomes glass. CRITICAL: no transform/filter/backdrop-filter on .panels-grid
(parent of the fixed globe → would trap it) — glass lives on the sibling cards.
- Desktop opens at zoom 1.35 so the globe fills the immersive viewport.
Backend — Enso benchmark caveats are fully data-driven (no literal drift):
- LiveCodeBench headline + premium-arm reference computed from the parsed table
(premium arm read from the table's own Opus row when present).
- HLE "preflight only" note emitted ONLY when HLE is genuinely a preflight
(n≤1) in the snapshot — never claims "not scored" over a real scored run.
CI gate green: tsc + vite build + go build (-tags sqlite_fts5) all pass.
Claude-Session: https://claude.ai/code/session_81b00bd9
- Right/middle click never picks-and-selects a country (deck.gl's tap recognizer
fires onClick for those too); only LEFT click (button 0) selects. Guard in both
handleClick paths (DeckGLMap flat + GlobeNative globe) via shared isNonLeftClick +
MapClickEvent, event threaded through onClick + the globe source.
- Left-click on ocean/empty space no longer opens the 'identifying country'
interstitial — onCountryClick only fires when a country actually resolves.
- Mobile: panels reflow to a denser >=2-up adaptive grid (was one giant column),
honoring the cell-size control; min(--panel-col-min,46vw) guarantees 2 columns.
- Immersive video reserves the footer dock height so the full video sits ABOVE the
footer — nothing occluded by the controls bar.
- Backend: GET/PUT /v1/world/history via the SAME per-identity settings store, new
'history' namespace. Extracted shared handleIdentityBlob (dashboard + history are
one opaque-blob mechanism, two namespaces — DRY). Never fabricated.
- Frontend: generalized the dashboard-sync observer to route each localStorage key to
its namespace — dashboard keys -> /v1/world/dashboard, history keys
(worldmonitor_recent_searches, hanzo-world-watch-queue) -> /v1/world/history. One
observer, boot-hydrates both, debounced per-group PUTs. SearchModal + watch-queue
untouched (observer transparent).
- Tests: Go history round-trip + isolation + namespace separation + 401/400; e2e
history routing + boot hydration. Dashboard e2e still green (5/5).
Cloud-variant panel surfacing two shipped router features:
- Section A: a Savings↔Quality bias slider (0..1) wired to a new org-scoped
proxy GET|PUT /v1/world/cloud/router-preference → ai /v1/router/preference
(caller IAM bearer forwarded). Debounced PUT + "saved" confirmation; stays
interactive and shows an honest preview-only note when the gateway route
isn't deployed yet.
- Section B: the Mean-Field Judge Panel via GET /v1/world/cloud/judge-panel →
ai /v1/router/judge-panel?scope=platform — per-judge reliability-weight bars,
calibrated means + n, sample rate + enabled state, and the published
rank-corr-with-ground-truth benchmark (MFJP 0.966 vs single-judge 0.110).
Honest "warming up" empty state when unavailable.
Both proxies never 5xx — they answer a well-formed {available:false} on any
upstream failure (including a 404 before the gateway routes ship), so the panel
degrades gracefully. New files only (handlers_cloud_router_pref.go,
EnsoRouterPanel.ts, router-preference.ts, judge-panel.ts) plus mount/route/CSS
wiring; reuses the cloud-* design system and theme tokens.
Verified with Playwright on ?variant=cloud: slider drags 0.5→0.90 with a live
label + honest "couldn't save" chip, judge section renders its warming-up
state, zero JS errors (only the expected not-yet-deployed 404s); dark + light
theme parity and mobile (374px, no overflow) confirmed.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
The inference plane is org-scoped ("metered to your org"). Signed out, the
public pulse only ever returns a zero-stub ($0 spend, 0 GPUs, 0/0 machines),
which rendered as a "live"-badged grid of zeros — exactly the "zero dressed up
as live traffic" the panel promises never to show, and reads as a dead/broken
platform. Gate on !isAuthenticated() at the top of render() (same sign-in
affordance as Fleet / My Usage), completing the honest-gate direction of v2.4.23.
The now-redundant inner signed-out branch is removed (one way to gate).
Signed-in behaviour unchanged. Release v2.4.28.
Signed-in dashboards persist server-side per identity and follow the user across
devices: panels/order/spans, layers, sources, custom feeds, map mode, the free
layout geometry, text-size and grid-size — hydrated on boot (server precedence),
debounce-synced on change via a localStorage observer. Anon stays localStorage-only.
Backend reuses store.Settings (namespace 'dashboard'); sqlite v0.3.0->v0.3.2 (fts5
build tag). Go tests + 3/3 frontend e2e green.
Signed in, the composed dashboard (panels/order/spans/layers/sources/custom feeds/
map mode + the free-layout geometry) persists per identity and follows the user
across devices; signed out is unchanged (localStorage only).
- services/dashboard.ts: on boot, hydrate this identity's dashboard from
GET /v1/world/dashboard into localStorage (server precedence) before the app
reads it; then a Storage.prototype setItem/removeItem observer debounce-PUTs a
snapshot of the dashboard keys on any change. One decoupled hook — App,
grid-config and Panel stay unaware. Captures the worldmonitor-layout:* family so
the free {x,y,w,h}/mode geometry follows the user too.
- main.ts: await initDashboardSync() before new App() (auth-gated, bounded, never
blocks boot).
- e2e: hydration (server precedence) + change→debounced PUT of the snapshot + anon
no-sync — 3/3 green (real app, faked IAM + stubbed endpoint).
Not released. Ships as v2.4.21 with sources + usage history.
- GET/PUT /v1/world/dashboard: per-identity dashboard config through the existing
store.Settings (namespace 'dashboard') — no new table; mirrors /v1/world/monitors
- generalize identityFor -> identityForDoc: one bearer->identity gate for the
namespaced settings store (monitors + dashboard resolve through it)
- bump hanzoai/sqlite v0.3.0 -> v0.3.2 (vendored pure-Go, zero modernc.org/*).
The vendored engine gates FTS5 behind -tags sqlite_fts5; add it to the Dockerfile
go build (was implicit under v0.3.0's modernc backend) or store.Open degrades
- tests: dashboard round-trip + per-identity isolation + 401/400/405 boundary gates
Not released. Ships as 2.4.18 AFTER 2.4.17 (layout) is live+stable.
User feedback: the panel showed "random amounts." Verified there is NO random data
anywhere in the Enso path — it rendered the upstream OPAQUE "arm-N" routing mix (real
counts relabeled to meaningless "Enso arm 1/2/3" with no model identity), which reads
as arbitrary.
- EnsoTrainingPanel now renders the REAL served-model catalog from the existing public
/v1/world/cloud/models (→ ai /v1/models): real model names, tiers, providers, context
and pricing — the same real models served on the platform.
- The real aggregates (cost-saved proxy, learned-engine share, events, throughput,
cumulative index, shadow agreement, retrain gate) are unchanged — all real.
- Dropped the opaque by_model arm rows (armRows/armIndex). Honest empty states; never
fabricated.
- e2e: stubs both feeds (router-stats incl. an arm-N map) and asserts the panel shows
real model names + "models served" and NEVER renders "Enso arm"/"arm-N". Green.
Not released — hanzo assigns version/sequence.
Owner feedback on the free layout, batched with the snap-to-grid fix:
- No visible corner/edge resize glyphs (#2): the dots + corner brackets are hidden
(display:none on the handle ::after); the handles stay invisible hit areas so
dragging from any edge/corner still resizes — the resize cursor is the affordance.
- Finer grid (#3): MIN_CELL_SIZE 120->80 + dock "Widget size" slider min 120->80,
step 20->10, so the grid — and the snap, which reads cellSize — goes finer.
- Text-size / UI-scale control (#4, accessibility): a dock "Text size" slider drives
a --ui-scale var that zooms the readable panel CONTENT only (never the panels-grid,
so drag/snap coordinates are untouched); persisted in localStorage (utils/ui-scale.ts),
applied before first paint. Per-user SQLite dashboard-sync picks up the key.
- e2e: glyphs-hidden-but-still-resizes, finer-80px-floor, text-size-scales+persists.
Full layout suite 21/21 green; build clean.
Not released — batched with the snap fix; hanzo assigns version/sequence.
User feedback on the free layout: "the snap is not logical." Free mode placed
panels at arbitrary px, which felt loose to drag. Now free-mode drag snaps {x,y}
to grid-line origins and resize snaps {w,h} to whole-cell multiples, so panels
land on consistent, aligned tracks — while keeping the independence that made free
mode good (resize/move one panel, siblings never move; opposite edge stays pinned).
- ONE logical grid, DRY: grid-config.freeSnap() = { cell: getCellSize() (the
slider-driven --panel-col-min), row: --panel-row (16), gap }. Free and grid share
one grid definition — no second magic number.
- panel-drag: snapLine (position -> nearest grid line) + snapCells (size -> whole
cells, >= floor); threaded through moveFree + applyFreeResize. Hold Alt for fine,
un-snapped placement. Grid mode untouched.
- e2e: two new proofs (drag -> two panels align to the same track; resize -> whole-
cell width + opposite edge pinned); the arbitrary-px tests now hold Alt. 18/18 green.
Not released — version/sequence assigned by hanzo.
Dropped the external "reported" comparison column + the self-deprecating footnote
from the public benchmark. The panel now shows ONLY Enso's own live-measured accuracy
and per-system cost, with a confident, true caveat: every number is measured against
real APIs through the Hanzo gateway, Enso reaches frontier-competitive accuracy at a
fraction of the blended cost by routing each task to the right model, and it reports
what it spends. No external names, no incoherent framing.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
The Enso benchmark panel + summary data carried the pre-release codename in labels,
JSON keys, Go structs, and the TS consumer. Renamed consistently to Enso / Enso-Ultra
across the data, the /v1/world backend handler, and the frontend so the public
benchmark shows only the Enso brand. No numbers changed; identifiers stay in sync
(measured Enso vs Enso's originally-reported figures).
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
Sweeps the unreleased range: geo-aware consent toggle in the account menu (reflects
the backend's effective default — opt-out non-EU, opt-in EU/UK/EEA) with honest copy;
and the AI Compute panel showing a plain "Sign in" gate instead of a scary
"unreachable" when signed out. (Signup disclosure + privacy/terms shipped separately
in hanzo.id + hanzo.ai.)
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
The "Help improve Hanzo AI" account toggle already reads the effective consent
from ai (getTrainingContribution) and paints it once loaded. Make the microcopy
and comments honest for the GEO-AWARE model — opt-in/default-OFF in the EU, UK &
EEA; opt-out/default-ON, disclosed at signup, elsewhere — instead of claiming a
universal "off by default". No logic change: the toggle stays a verbatim mirror
of ai's answer, so a non-EU org's ON effective default now reads as an accurate
label rather than a contradicted one, and the service/loader comments no longer
claim "never opted in => OFF".
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
Two org-scoped event-platform cards for the Cloud view, sourced from the
live native analytics warehouse on api.hanzo.ai (verified live):
- Analytics (org-analytics): /v1/analytics/overview + timeseries + top —
per-org requests/visitors/tokens/spend, requests trend, top models.
- Insights (org-insights): /v1/insights/events — active users, top events,
activity trend derived client-side from the real event stream.
Org pinned server-side from the validated bearer owner claim (principal.Org),
reusing org-scope.scopedHeaders() like getMyBilling — no new fetch mechanism.
Reuses the existing Panel + cloud-format primitives + cloud-* CSS. Honest
loading/signed-out/empty/error states; never fabricated numbers.
Signed-out, the live compute pulse is account-scoped, but the panel rendered
"Compute telemetry unavailable — compute plane unreachable" which reads as a backend
outage. Match the Fleet / My Usage pattern: signed-out shows a plain "Sign in to see
the live inference plane…"; the unreachable error copy is reserved for a genuine
failure while signed in.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
At <=680px both the Ctrl/Cmd-K hint (already hidden <=900px) and the
'Search' label were hidden, leaving the search button an empty bordered
box. Add an always-present magnifier icon that reveals once the K hint
collapses, so the button is never empty at any width. (2.4.20)
Co-authored-by: hanzo-dev <dev@hanzo.ai>
Add a model-improvement consent toggle to the account menu, wired to ai's
OrgSettings.TrainingContribution (the source of truth the automated judge + trainer
read). Honest, opt-in, off by default, no dark patterns.
- src/components/AccountMenu.ts: a "Data & privacy" switch in the signed-in dropdown.
Painted OFF until the async read lands (never a false "on"); optimistic flip that
reverts if the server refuses. Exact microcopy: rating keeps only a numeric score,
never prompts/outputs, runs confidential/anonymous, off anytime.
- src/services/training-contribution.ts: get/set via the app's org-scoped authed
fetch (orgHeaders). Signed-out / error / unset all read false — privacy-safe.
- internal/world/handlers_training.go: same-origin proxy /v1/world/training-contribution
→ ai get-/update-training-contribution, forwarding the caller's bearer + X-Org-Id.
World holds NO copy; ai self-scopes to the caller's own org and is the sole authority.
Tests: proxy gates signed-out (401, no upstream call), forwards bearer + org, and
unwraps the casibase envelope to a bare {enabled} on both GET and POST.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
The dashboard defaulted to grid snap, so dragging or resizing one panel
reflowed all the others and resize jumped between coarse column/row spans.
CSS grid can't move a panel without disturbing siblings, so make FREE the
default layout: every panel owns its own {x,y,w,h}. The current grid
arrangement is frozen as the starting geometry (grid-config.applyDefaultLayout,
run once after panels+map lay out), so the flip is invisible; Grid stays a
dropdown choice and grid-config records an explicit pick so the default never
overrides it.
- Independent positioning (#1): free mode — moving/resizing one panel leaves
every sibling exactly where it was (proven: siblings drift 0px).
- Granular resize (#2): pixel-exact w/h from any edge/corner, not span jumps.
- Narrower panels (#3): free floors 160->96 (map 240->140); grid track floor
140->120. A panel drags to 96px, well under the old min.
- Viewport-flexible (#4): map + live-news resize freely, small to large.
Hidden panels no longer persist 0x0 rects; a panel shown while free seeds a
tidy default slot. Tests: +2 harness proofs (no-sibling-reflow, sub-160 width);
repaired two stale span-2 assertions to the fine-grid data-span reality.
Passively listening to a live stream in fullscreen fires no mousedown/keydown/scroll
(fullscreen video captures input), so the 5-min input-idle timer fired and pauseForIdle
→ destroyPlayer tore down the iframe — which force-exits fullscreen AND stops playback
(the reported "listening fullscreen and it exited + stopped"). Fix: treat fullscreen as
the strongest "actively watching" signal — never idle-kill when our player is the
fullscreen element — and count fullscreenchange as activity so entering fullscreen
resets the idle timer.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
Restores the dotted-land basemap (land shown as a lattice of glowing dots over a
black-ocean sphere — the abstract cybermap surface): new land-dots.ts (getLandDots()
samples /data/countries.geojson into a cached lat/long lattice, land points only),
GlobeNative renders it as a ScatterplotLayer on the 3D globe, variant.ts defaults
cloud + AI to the 'dot' style, and App opens cloud/AI on the 3D globe (the live-traffic
centerpiece). Recovered from a stashed WIP; DeckGLMap kept at current main (preserves
the shipped layer panel + control dropdowns), so this ports cleanly with no regression.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
The flagship Cloud view was defaulting to the grid dashboard, demoting the
full-viewport 3D dot map to a small corner tile. Restore it: when the visitor
hasn't explicitly picked a layout, the Cloud variant opens on the immersive
globe (map fills the viewport, panels float over it). An explicit grid choice
and the ?layout= URL param still win; other variants unchanged.
Ships 3d892d96: the AI analyst now folds a live Hanzo-cloud snapshot into its context
(traffic, platform 24h, Enso router cost-saved/reward/engine-share, flywheel ledger,
billing), so it can answer questions about the dashboard's real numbers — on top of
its existing 26-command control surface.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
The analyst's context fed panel names + visibility but NOT the live values, so it
couldn't answer "what's my spend / how many DO nodes / is Enso saving money". On the
cloud board it now also folds a real-data snapshot — live traffic (rps/req), platform
24h (requests/tokens/nodes/GPUs/regions/uptime), the Enso router (cost saved, reward
rate, engine share, models routed), the flywheel ledger, and the caller's billing
(spend/balance/events). Best-effort + parallel with the news feeds; real data only,
mirroring the panels. Pairs with the 26-command control surface — the analyst can now
both control the dashboard AND answer questions about what it shows.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
2.4.14 tagged before the build_secrets wiring landed, so it builds without the
Mapbox token. 2.4.15 = same tree + the KMS-sourced VITE_MAPBOX_TOKEN build-arg
(via hanzoai/ci v1.0.5), so satellite-streets/outdoors render in 2D and 3D
instead of the keyless dark fallback.
Root cause of the dark Sat/Terrain: no VITE_MAPBOX_TOKEN was baked at build, so
the token-gated Mapbox satellite-streets-v12 / outdoors-v12 styles silently fell
back to the keyless CartoDB dark basemap in both 2D and 3D. The publishable pk
token now lives in KMS (hanzo/deploy/VITE_MAPBOX_TOKEN, env=prod) and is sourced
by hanzoai/ci (v1.0.5) as a build_secret --build-arg into the Vite SPA.
Rides the pending main fixes (analyst reasoning recovery, AI Compute rate,
map-control dropdowns).
The analyst rendered "The analyst couldn't answer — empty response" whenever the
served model streamed its answer on the reasoning channel with an EMPTY content
channel — gpt-oss / harmony, deepseek-r1 and kin. The ONE completion path
(chatMessagesModel + chatMessagesModelStream) read only the content channel, so a
reasoning model's output was dropped and surfaced as a blank.
- Read the reasoning channel (message.reasoning_content / reasoning, and the SSE
delta.reasoning_content / reasoning) as a fallback when content is empty, and
forward reasoning deltas live so the replyExtractor shows them on the thinking line.
Content is always preferred; non-reasoning models (best/zen5) are unaffected.
- Replace the opaque "empty response" with an honest, model-named error
("the <model> model returned an empty answer"): never misreads a normal
completion's response id as an error code, never trips the 402 top-up CTA.
Tests: reasoning-channel recovery (buffered + SSE) and the honest empty-answer error.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
AI Compute (and cloud-pulse) rps/tps were dead flat: the usage series reports its
bucket width as a WORD enum ("hour"/"day"), but intervalSeconds parsed it with
time.ParseDuration — which fails on "hour" (needs "1h") — so usageRate always fell
back to total24h/86400, a constant. Resolve the enum (minute/hour/day/week) before
ParseDuration; the rate now reflects the freshest hour and moves. Regression tests
for intervalSeconds + usageRate.
My Usage & Bill: a real sub-cent inference charge (< $0.005) rounds to 0 integer
cents and rendered a bare "$0.00" that reads as "not billed". Show "< $0.01" for a
real row that rounded to zero — honest about a tiny real charge (the ledger only
carries integer cents on the wire).
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
Map controls:
- Projection (2D/3D), basemap (Dark/Sat/Terrain) and time-range each collapse into ONE
compact dropdown (trigger shows the active value + caret; options in a popover). The
top dock is now a single tidy row of three instead of two wrapping rows. Global
.deckgl-dd treatment (one way, every map host). e2e specs open the dropdown before
clicking an option via a shared clickMapControl helper.
Panel symmetry (real data, never padded):
- Live Traffic: added a real 4th tile (distinct countries with traffic) → 2×2.
- Model Improvement: split scored-events into its own tile → 2×2 (reward rate,
requests scored, active days, retrains).
- Cloud Overview / Global Platform: dropped the 'regions' tile (fleet carries region
detail) per CTO.
- New .cloud-stat-grid-2 (exactly two columns) for the symmetric 2×2 layout.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
The Hanzo-Cloud globe 'spazzed': data layers floated above the sphere, the
far hemisphere showed through, count badges hovered over the planet, terrain
striped at tile seams, and the live request-geo dots froze on the 3D view.
- GlobeNative seats every data layer against the depth-writing ocean sphere
(depth-TEST, no write) so points/icons/text/paths/arcs occlude on the GPU.
- occludeFarSide now faces the globe's OWN live camera (mapbox is parked/frozen
behind the native GlobeView) and culls TextLayer badges too, so a back-side
count badge disappears instead of floating over the limb. Camera is fed each
rebuild + on drag >2°; escape path (?globe=mapbox) keeps the live mapbox center.
- Draped ESRI terrain/satellite tiles stop writing depth (coplanar tiles were
z-fighting at seams → striping); the ocean sphere owns the depth buffer.
- Cloud /v1/world/cloud/* poll gates on tab visibility, not renderPaused, so the
realtime dots keep updating on the native 3D globe (the default view).
- Request-origin → serving-region arcs ON by default in Cloud (real feed, empty
when no traffic — never demo).
- e2e/globe-render.spec.ts asserts occlusion (dot + badge), 2D/3D parity,
terrain single-context, and live-dot updates.
Ships the unreleased 2.4.11..HEAD range (all UI): globe cables hug the sphere, map
controls in one top row + top-left Layers button (footer/scrollbar removed), one Fleet
panel with system RAM, BYO once, and the org logo in the account switcher.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
The account chip showed the USER avatar next to the org name and never the org's
own image — IAM's get-organizations returns each org's Casdoor `logo`, but listOrgs
parsed only name + displayName and dropped it, so the hanzo org logo never rendered.
Thread `logo` end-to-end (OrgInfo → OrgOption → scope) and render an ORG avatar in
the switcher trigger: the active org's logo when set, else org initials (rounded-
square so it reads as an org, not a person). The dropdown's user row keeps the user
avatar. Falls back to the user avatar only when there's no org scope (signed out).
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
Map controls (CTO revision of the bottom dock):
- 2D/3D toggle + Dark/Sat/Terrain switcher + time-range pills now sit in ONE row
across the map's TOP, side by side (was a bottom-border dock). Zoom stays top-right,
legend bottom.
- New top-left "Layers" button on the map toggles the floating layer panel (opens
just below it). Removed the redundant Layers button from the page footer dock —
layers now live on the map itself.
- Hid the .main-content right-edge scrollbar (wheel/trackpad scroll unchanged) — it
read as a stray "sidebar scroller" over the map. Same treatment header/switchers use.
Fleet:
- BYO nodes rendered TWICE — once as a "byo" provider group and again as the BYO GPU
workers list (same dbc/evo/spark). Managed providers (DO/GCP/AWS) now render as
provider groups; BYO renders once, as the richer workers list.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
The fleet machine rows showed GPUs · VRAM · vCPU · OS but never system RAM —
there was no memory field on the path at all. Thread `mem` end-to-end on the
world side: decode it from /v1/machines, carry it on fleetMachineRow, expose it
on FleetMachineRow, and render it in machineSpec (GPUs · VRAM · vCPU · RAM · OS)
only when present. The cloud service now populates `mem` (DO size-slug + upstream
memSize); world shows whatever is real and omits it otherwise — never fabricated.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
Globe (3D):
- Cables + pipelines (PathLayer) drew straight chords across the sphere — deck.gl
has no greatCircle for paths (only ArcLayer). Pre-densify each static path along
its great circle into <=4-deg hops so the many short segments hug the surface.
Flat map keeps the raw sparse vertices.
Map controls:
- The 2D/3D toggle, basemap switcher and time-range pills were scattered across the
wide empty map header (top-center, mid-left, top-right). Dock them into ONE tidy
strip along the map's bottom border via the existing controlsHost mechanism (App
passed none). The dock mounts on the map container — a SIBLING of the deck wrapper
— because 3D parks that wrapper (visibility:hidden) and renders via GlobeNative; a
dock inside it would vanish on the globe. Reuses the .world-dock inlining CSS.
Fleet:
- "Fleet & GPUs" (FleetPanel, all users) and "Fleet & clusters" (CloudFleetPanel,
admin) were duplicate platform-fleet renders. Collapse to ONE "Fleet & GPUs":
widen FleetPanel, port the utilNote line, derive the provider-scope label from the
providers actually reporting (not a hardcoded four), delete CloudFleetPanel.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
golang:1.26-alpine ships no git; go resolves the private indirect
github.com/hanzoai/csqlite 'direct', so 'go mod download' died with
git-not-found. Add git, set GOPRIVATE, mount the gh_token secret ci already
passes. Unbreaks world main builds (broken since the Go-base-1.26 drop-in).
go.mod now requires go >= 1.26.4 (github.com/hanzoai/sqlite). Bump the
builder base golang:1.25-alpine -> golang:1.26-alpine so the prod image
keeps building. The binary stays CGO-free (CGO_ENABLED=0), so hanzoai/sqlite
selects its pure-Go modernc backend — FTS5 built in, works. CGO test path
(no FTS5 without -tags sqlite_fts5 + libsqlcipher) is not what world ships.
.panel-wide kept grid-row: span 2 (the old 200px-grid assumption) after the move
to 16px rows — so all 7 wide panels (ai-compute, cloud-overview, live-news,
traffic-globe, enso-flywheel, model-improvement, hanzo-status) reserved only ~36px
of track while being 350px tall, overflowing their cell and painting over the
panels below. Fixed to span 18 (≈356px) with the shared 48px min-height floor.
Also: extract fetchLLMUsage (DRY) shared by cloud-pulse + ai-pulse, and fall back
to LLM observability in ai-pulse so AI Compute shows real usage (not zeros) for an
authorized operator.
Model Usage showed 'warming up' / nothing and usage totals were 0 for a signed-in
admin whose super-admin usage ledger (?org=all) is denied. Fixed: the admin path
now folds REAL platform usage from LLM observability (/v1/admin/o11y) with the
caller's own bearer — measured requests, tokens, and top models by REAL name (not
the opaque router arms) — clearing volumeModeled. Falls back honestly to empty if
the caller isn't authorized for platform observability upstream.
Private, competitive data (names competitor models + Fugu), so it is gated
server-side, never bundled into the public SPA:
- backend: GET /v1/world/enso-benchmarks reuses the exact requireAdmin gate
(401 no-token / 403 non-admin, IAM-introspection). Reshapes the SAME embedded
enso-bench snapshot the flywheel reads (one embed, two views) into the measured
head-to-head + verify-then-select ablation + agentic SWE-Bench Pro pilot +
Fugu-reported reference + server-authored honest caveats. No upstream forward.
- snapshot: refresh ensodata/summary.json to the full measured set (LCB+GPQA+HLE,
6 arms), ultra_ablation, and the committed agentic result folded in.
- frontend: EnsoBenchmarkPanel (admin-only; adminOnlyState when not admin) +
services/enso-benchmarks.ts (bearer, 403->null). Registered in the admin-only
Cloud panels. Theme-safe tables, scroll-in-box, no body h-scroll.
- privacy: no competitor names, arm ids, numbers, or our routing methodology
names are baked into the public bundle — all arrive only via the gated endpoint.
Honest framing: enso MATCHES the best single SOTA arm at a fraction of the cost
(LCB 91.4% @ $5.24 vs opus $39.43; GPQA ties top); does NOT beat every SOTA;
HLE is preflight-only; we are below Fugu's REPORTED numbers. Decisive wins are
cost-efficiency, the ablation (better AND cheaper), and agentic step-routing.
Tests: gate matrix (anon 401 / non-admin 403 / admin 200 + no-leak) + real-number
payload assertions green; existing enso-training + red admin-gate suites green.
Vertical resize snapped in coarse 200px tiers (120/200/400/600/800) and a hard
200px min-height blocked shrinking — the 'jump too much / can't fit content'
complaint. Now the grid is a fine 16px row unit (@hanzo/gui space-4): per-panel
resize snaps in ~20px steps, uncapped, and min-height drops to 48px so 200px is
just the DEFAULT for a new panel, not a floor. Tier presets (size menu + map) are
redefined on the same grid so they still work; the map resizes smoothly too.
Horizontal already fills width in 160px column steps.
The public router-stats by_model is relabeled to opaque arm-N ids on platform
scope (privacy), so it is NOT real model names. cloud-pulse wrongly surfaced it
as the model mix, so Model Usage showed 'arm-8, arm-7, arm-6'. Now the public
fallback carries real request rate + throughput only; the model mix comes solely
from the measured usage ledger (admin path) — otherwise Models stays empty and
the panel honestly shows 'warming up'.
The region breakdown dropped any machine whose region wasn't one of the 8
catalog cities (real DO regions like tor1, or GCP/AWS regions), so a fleet in
uncatalogued regions rendered regions:0. Now every real region is counted —
catalog coords/name when known, else the raw region string as its own region.
Fleet & GPUs now shows every real machine grouped by provider/region (with GPU
model, VRAM, vCPU, OS) plus BYO GPU workers with specs (Spark/Evo/dbc: hostname,
GPU, VRAM, capabilities) — one shared renderer (cloud-fleet-view) used by both
FleetPanel (flagship, admin) and CloudFleetPanel (deep). Backend adds vCPU from
visor MachineView.
Removes the last fabricated data: byo-gpu and traffic map layers are now
real-or-empty (no demoGPUs / demoTraffic), so a signed-out visitor sees honest
empty globe layers, never fake clusters/arcs.
Wires the one signups/growth gap that has a real source: IAM /v1/iam/global-users
(all users, global-admin only), read with the signed-in admin's OWN bearer and
folded into AGGREGATES ONLY (never PII): total users, signups 24h/7d, active-now
(isOnline), and a 14-day daily-signup series (from each user's createdTime).
Attached to the cloud-pulse admin path only (p.users, omitempty — the public
teaser never carries it). CloudOverviewPanel renders users / +N signups·24h /
active-now tiles + a new-users sparkline, live at the 30s poll.
Remaining gaps stay honestly empty — they have NO upstream source (verified against
the visor/o11y OpenAPI specs): GPU util/mem/temp (visor GpuView: "Live telemetry is
omitted"), disk (no field), historical retention cohorts (no login-events store).
Those need real instrumentation upstream, never faked.
go build/vet/test green (TestCloudPulseAdminBearer extended); tsc + vite clean.
App.ts untouched.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
Completes the "make it all real" pass on the cloud flagship:
- FleetPanel shows the REAL platform fleet (visor /v1/world/cloud/fleet) for an
admin — machines + GPUs across DO/GCP/AWS/BYO, rolled up by region, 30s refresh.
Removed the demo/sample-region fallback — never fabricates regions now.
- Globe BYO-GPU layer real for admins (handleCloudBYOGPU, caller bearer, no-store)
— real GPU clusters instead of the 8 demoGPUs dots. Anonymous keeps the
clearly-flagged demo sample (no public GPU inventory source).
- AiComputePanel: ai-pulse admin-aware; EventSource can't send a bearer, so the
panel uses an authenticated poll when signed in — z@hanzo.ai gets the full
measured compute pulse (tokens/s, req/s, spend, top models, fleet), own bearer.
Every admin/authed response is Vary: Authorization + private,no-store. Honest gaps
stay labeled (GPU util/mem/temp, disk, signups/cohorts need real collection
upstream). go build/vet/test green (TestBYOGPUAdminReal, TestAIPulseAdminPoll);
tsc + vite clean. App.ts untouched.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
cloud-pulse never looked at who was logged in (server service-token only), and the
real admin panels were gated to orgs {admin, built-in} — so z@hanzo.ai (org hanzo)
got 403 and saw only the public aggregate. Now:
- cloud-pulse is admin-aware (handlers_cloud.go): a signed-in admin's request is
served the FULL measured all-org aggregate fetched with THEIR OWN bearer
(get-cloud-usages?org=all + visor), Cache-Control: private,no-store + Vary:
Authorization so it never mixes with the public edge cache. No server-side
super-admin token needed — the admin's own login drives it. The ai gateway
independently re-checks ?org=all, so a non-global-admin degrades to real public
data, never fake (gateway is authoritative).
- Operator org recognized as admin (cloud_identity.go): {admin, built-in} +
operator org (default hanzo, override WORLD_ADMIN_ORGS) — unlocks the existing
real admin panels (services/llm/fleet/analytics) for z@hanzo.ai. Anonymous
short-circuits, so the public path stays cheap.
- Frontend forwards the bearer (cloud-pulse.ts) and the client admin gate
(iam.ts isAdmin/cachedIsAdmin) matches the server, so App.ts's existing
isAdmin() reveal works — App.ts untouched.
go build/vet/test green (added TestCloudPulseAdminBearer); tsc + vite clean.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
The flagship cloud dashboard showed modeled numbers under a "live" badge:
demoPulse() sine-wave rps/24h/tokens + sparklines, demoModels() mix, demoRegions()
per-city rates, and a hardcoded 99.98% uptime. Replaced with an honest source
ladder (producePulse in handlers_cloud.go):
- RequestsPerSec <- /v1/traffic/globe totals.rps_1m (the real ~0.62)
- Requests24h + sparkline <- /v1/router/stats?scope=platform
- Models mix <- ledger byModel (super-admin) else router-stats by_model
- UptimePct <- status.hanzo.ai Gatus up/total (0 -> tile dropped, never a const)
- Regions <- real visor fleet
Tokens24h/TokenSeries have no non-ledger source -> honest 0/empty, volumeModeled
stays true (UI renders "—"). demo:true now means only "nothing measured yet".
Frontend (CloudOverview/LiveActivity/ModelUsage) keys the live badge on
!demo && !volumeModeled — no modeled number wears a live badge; LiveActivity
regions show real visor node counts. TrafficGlobe filters garbage country codes
to ISO-3166. Dead admin-panel gate fixed (mountAdminCloudPanels !== 'saas' ->
'cloud', so Cloud Fleet + LLM panels mount for admins).
Full all-org volume + per-model tokens need a super-admin HANZO_CLOUD_PULSE_TOKEN
in KMS; until then honestly blank. go build/vet/test green; tsc + vite clean.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
- Header tabs reordered to Cloud, AI, Crypto, Finance, Tech, World (World last).
- Tab + status labels Title-case, not ALL CAPS (World/Crypto/Finance/Tech/Live;
AI kept as the acronym) across en + nl.
- Export (⬇) and historical-playback (⏪ ⏮ ◀ ▶ ⏭) emoji replaced with clean
16px stroke SVGs matching the header's search/theme icons.
tsc clean.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
* feat(cloud): Cloud-view polish — compact H + CLOUD tab, in-map controls, flywheel history, accurate legend
- Header: merge the redundant H-icon + "Hanzo" wordmark into one compact (16px)
canonical H mark (brand/home toggle); rename the flagship tab "Hanzo" → "Cloud"
(switcher: [Cloud | World | AI | Crypto | Finance | Tech]). Canonical variant id
renamed hanzo → cloud, with hanzo + saas kept as runtime aliases (links + stored
prefs keep working).
- Controls: dock the 2D/3D toggle, basemap switcher and time-range pills IN the map
(controlsHost = map) alongside the zoom, layer toggles and legend that already
overlay it, instead of floating them in the bottom toolbar; time pills move to the
bottom-left so they clear the top-left layer toggles; the empty dock slot collapses.
- Flywheel history: a same-origin GET /v1/world/cloud/router-history proxy (→ ai's
public /v1/router/history?scope=platform; honest empty, never demo) + a Model
Improvement panel — reward-rate line chart with retrain-version markers, a ticking
cumulative cost-saved hero, and an adoption sparkline. Charts start empty and GROW
with real data; nothing is seeded.
- Legend: add a Cloud data-class legend (request origin·volume, validator node, GPU
fleet, cloud region, datacenter) so it matches what the globe plots rather than the
geopolitical default.
tsc + vite build clean; world Go build + gofmt clean.
Claude-Session: https://claude.ai/code/session_015Z1iLf7QBrq1LhignJrzDw
* fix(cloud): kill the 3D-globe flicker — settle the globe, guard the cables layer
Root cause (per a deep read-only diagnosis): the 3D globe is a custom GlobeNative
deck.gl renderer whose idle auto-rotate (~2°/s) keeps thin coplanar on-sphere vector
features (borders/coastlines/markers) perpetually sub-pixel-moving, re-rasterized
every frame — steady-state limb shimmer on real GPUs. Fixes:
- GlobeNative created with autoRotate:false — the globe SETTLES, so the deck render
loop idles (verified: camera longitude does not drift and renders drop from ~40/s
to a couple of data-sync ticks). Drag-to-rotate still works; re-enable the idle spin
only with a coplanar depth/polygonOffset fix so it no longer z-fights the sphere.
- Guard createCablesLayer: filter UNDERSEA_CABLES to paths with >=2 finite [lon,lat]
vertices, so one malformed cable can no longer make the PathLayer assert at init and
silently drop the whole layer (the "[GlobeNative] cables-layer assertion").
- Cables OFF in the Cloud view (not a Hanzo Cloud data class; absent from the Cloud
legend) so the globe stays to the plotted classes and the legend matches exactly.
Playwright: a new cloud-mode test asserts the 3D globe settles (lonDelta<0.001,
renders<12), the legend shows the Cloud classes (not the geopolitical default), and no
cables assertion fires. tsc + the full cloud-mode suite green.
Claude-Session: https://claude.ai/code/session_015Z1iLf7QBrq1LhignJrzDw
* feat(cloud): CLOUD variant rename, compact H, in-map controls, flywheel panel + proxy
Completes the Cloud-view content (the prior checkpoint captured only the file renames):
- variant.ts: canonical variant renamed hanzo → cloud, with hanzo + saas as runtime
aliases; host-default cloud on world.hanzo.ai. isHanzoBrandHost gates the brand.
- App.ts: header merges the redundant H-icon + wordmark into one compact 16px
canonical H (brand/home toggle); first tab renamed Hanzo → ☁️ Cloud; createPanels
wires the cloud panel set incl. the Model Improvement panel; controlsHost = the map
so the 2D/3D toggle + basemap + time pills dock IN the map cluster.
- ModelImprovementPanel + router-history service + /v1/world/cloud/router-history
proxy: the flywheel-over-time story (reward-rate line + retrain markers, ticking
cost-saved hero, adoption sparkline) — honest empty until the ledger fills.
- variants/cloud.ts (mirror) + saas.ts alias; flywheel + in-map-controls CSS.
tsc + vite build clean.
Claude-Session: https://claude.ai/code/session_015Z1iLf7QBrq1LhignJrzDw
* feat(cloud): regional activity — traffic-origin tooltip shows task mix + volume
Hover a traffic origin on the globe (or flat map) → its request volume + the router
task mix (code/reasoning/chat/vision — what customers there are DOING), falling back
to the coarse service class until the per-geo byTask recording lands. Uses the new
TrafficGlobePoint.byTask from /v1/traffic/globe. Works in both 2D and 3D (GlobeNative
delegates tooltips to this handler).
Claude-Session: https://claude.ai/code/session_015Z1iLf7QBrq1LhignJrzDw
---------
Co-authored-by: hanzo-dev <dev@hanzo.ai>
* feat(hanzo): Hanzo mode — H-logo switcher + live-traffic globe (flagship default)
world.hanzo.ai now leads with the first-class `hanzo` variant. The H logo toggles
Hanzo mode, revealing the [Hanzo | World | AI | Crypto | Finance | Tech] switcher
(Hanzo first + default on the world.hanzo.ai host). The Hanzo entry + toggle appear
ONLY on hanzo brand hosts (white-label rule); off-brand/OSS hosts keep the plain
home-link logo + an always-on switcher. The former `saas` variant is folded in and
kept as a runtime alias (saas → hanzo, normalized in variant.ts).
Globe: a new native deck.gl `traffic` layer plots WHERE requests hit api.hanzo.ai
from — pulsing points sized/heated by count at country/US-state centroids, breathing
on the shared cloud-pulse clock. Fed by a same-origin proxy /v1/world/cloud/traffic-
globe → the ai backend's public /v1/traffic/globe (aggregates only, no IPs). The
existing traffic-arc layer now draws from the SAME native source (one source of
truth). Both degrade to an HONEST empty state — never demo — until the ai release
lands.
Side panels (the globe's companions): Live Traffic (native throughput totals + top
origins), Enso Live Training + Enso Flywheel (router stats), AI Compute, Model Usage
(model mix), Fleet, Cloud Overview, Hanzo Status (uptime), My Usage.
Tests: Go proxy passthrough + honest-empty state; tsc + vite build; a Playwright spec
driving the H toggle → switcher reveal (6 tabs, Hanzo first) + the globe layer toggle,
plus the TrafficGlobePanel live + empty states.
Claude-Session: https://claude.ai/code/session_015Z1iLf7QBrq1LhignJrzDw
* refactor(hanzo): lead the globe with the honest native points layer, arcs off by default
The trafficArcs feed carries a demo fallback (demoTraffic), so enabling it by
default on the flagship could paint fabricated flows when native data is absent. The
new native `traffic` points layer is ALWAYS honest (empty until real traffic lands,
never demo), so the hanzo default now leads with it and ships trafficArcs OFF — still
one toggle away for the animated flow lines (which are native-sourced once live).
Claude-Session: https://claude.ai/code/session_015Z1iLf7QBrq1LhignJrzDw
---------
Co-authored-by: hanzo-dev <dev@hanzo.ai>
The platform's own live compute/usage pulse and router-training flywheel
were gated to ?variant=ai — invisible on plain world.hanzo.ai. They are
front-page telemetry: registered in FULL_PANELS and instantiated for the
default (full) variant alongside ai.
Claude-Session: https://claude.ai/code/session_018PmFAHZvbBSTsuWyebwMra
Wire thumbs up/down + regenerate reward signals into the analyst chat,
keyed on the gateway response id, through a same-origin /v1/feedback BFF
proxy and the just-published @hanzo/ai@0.2.1 sendFeedback.
Backend:
- Thread the gateway response id out of every completion: chatResponse.ID
(non-stream) + the per-chunk SSE frame id (first non-empty wins);
runAnalystLoop returns it; the analyst done/JSON payloads carry "id".
- New same-origin BFF /v1/feedback (bare, matching the gateway path so the
SDK's baseUrl:'' reaches it). Forwards the caller's IAM bearer +
org/project selectors to a.base+"/feedback". Content-free is enforced
server-side: the body is WHITELISTED to exactly {request_id, signal,
rating?}; rating rides only with signal=="rating" and only for 1..3; a
dismiss or any non-rating signal never forwards a rating. No bearer,
bad signal, empty id, or malformed JSON => quiet 204 no-op (never 401).
Frontend:
- analyst-transport: surface id on AnalystResponse (JSON + SSE done).
- AnalystChat: store id + originating prompt on the assistant message; add
thumbs-up/down (mutually-exclusive pressed state) + regenerate to the
meta row, matching the copy button. emitFeedback helper calls the SDK
same-origin with a lazy IAM bearer; opt-out via VITE_HANZO_FEEDBACK.
- icons: thumbs-up, thumbs-down, refresh-cw (lucide).
Content-free is a HARD invariant in BOTH layers: the SDK FeedbackInput type
union and the Go BFF whitelist. No prompt/response/text ever transits.
Tests: handlers_feedback_test.go (whitelist, rating rules, no-op paths,
preflight) + analyst id propagation. go test ./internal/world/... green;
tsc --noEmit + vite build green.
Rollout: merge to main ships via the world operator Service CR
(universe: infra/k8s/operator/crs/world.yaml) through hanzoai/ci.
Claude-Session: https://claude.ai/code/session_015Z1iLf7QBrq1LhignJrzDw
Co-authored-by: hanzo-dev <dev@hanzo.ai>
/v1/world/cloud/router-stats raw-passed ai's {status,data:{…}} envelope, but the
Enso Live Training panel (and this proxy's own unavailable fallback) expect the
BARE RouterStats — so live data would never render (the panel sat on
"connecting…" because stats.window was undefined). Unwrap `data` on success;
a missing/null data or non-ok status degrades to the honest unavailable payload
(never 5xx). Test now feeds the enveloped upstream and asserts the unwrap.
Claude-Session: https://claude.ai/code/session_015Z1iLf7QBrq1LhignJrzDw
An "Enso Live Training" panel for the Hanzo Cloud (saas) variant of
world.hanzo.ai — live routing throughput, learned-engine agreement, per-arm
routing share (opaque arm-N), cumulative cost-saved, and the last-retrain gate
verdict — fed by the PUBLIC /v1/router/stats?scope=platform aggregate.
Data via the same-origin Go proxy /v1/world/cloud/router-stats (the established
/v1/world/cloud/* convention): hard-pins scope=platform (client can't request a
vendor-labeled scope), clamps hours to [1,168], degrades to 200 {unavailable}
on an unreachable upstream (never 5xx, never fabricated). API base configurable
via HANZO_API_BASE (default https://api.hanzo.ai).
Enso stays private: the public scope already relabels arms to opaque arm-N; the
panel labels them "Enso arm N" and an e2e test asserts no vendor token ever
reaches the DOM.
Reconciled with the parallel Enso Flywheel panel that landed on main (ai
variant, /v1/world/enso-training): distinct variant + audience (this is the
Cloud self-metrics economics view; the flywheel is the public eval-loop view).
The colliding services/enso-training.ts was split — theirs kept, mine moved to
services/router-stats.ts — so both panels coexist with no shared symbol. VERSION
bumped past the flywheel's 2.4.6 to 2.4.7.
Claude-Session: https://claude.ai/code/session_015Z1iLf7QBrq1LhignJrzDw
The router self-improvement loop made visible for the AI variant.
/v1/world/enso-training folds three honest sources: the routing-decision ledger
tail (export-routing-ledger ?since=, super-admin JSONL) → decision count,
engine-vs-heuristic mix, a routing-confidence histogram, task/model distributions;
the reward tail (export-routing-rewards) → rewarded count + mean reward; and the
latest enso-bench eval scores. Eval scores come from a committed snapshot of
enso-bench results/summary.json (go:embed), overridable live via ENSO_BENCH_URL —
so the panel is useful even signed-out; state (live/partial/demo) says which live
sources resolved. The payload is event-typed (Events[]) so retrain/deploy
milestones slot into the same timeline.
Frontend: enso-training.ts + EnsoFlywheelPanel registered for the ai variant,
reusing the existing cloud stat-tile / model-row / share-bar idioms.
Claude-Session: https://claude.ai/code/session_018PmFAHZvbBSTsuWyebwMra
Live Hanzo inference-plane telemetry for the AI variant. /v1/world/ai-pulse
reuses the analyst SSE plumbing + the shared cloud-usage seam: it polls the
platform usage ledger (get-cloud-usages allOrgs → tokens/s, req/s, spend, top
models) + visor (gpu/machine/region counts) on an interval and pushes typed
usage/fleet/status frames to EventSource clients, coalesced through cache.go so N
streams cause ~one upstream sweep per 10s. A plain GET returns one JSON snapshot —
the panel's poll fallback. No token ⇒ honest state:"unavailable".
Frontend: ai-pulse.ts (SSE stream + poll fallback, cloned from cloud-pulse.ts),
AiComputePanel registered for the ai variant (panels.ts AI_PANELS + variants/ai.ts
+ App.ts). Reuses the existing cloud-panel stat-tile / model-row idioms.
Shared usage helpers (usageRate/topModelsFromUsage) decomplected into
cloud_service.go so cloud-pulse and ai-pulse derive rate/top-models one way.
Claude-Session: https://claude.ai/code/session_018PmFAHZvbBSTsuWyebwMra
/v1/world/cloud-pulse now reads MEASURED 24h request/token volume + top models
from the ClickHouse-backed usage ledger (get-cloud-usages ?org=all, super-admin)
on top of the live model/node/GPU/region counts, dropping demo:true AND
volumeModeled:true. Without a token — or with a non-admin token that can't reach
the allOrgs ledger — it stays honestly demo/modeled; platform numbers are never
faked silently, and the service bearer never reaches the browser.
One credential, one accessor: serviceToken()/serviceAuth()/apiHost() are now the
single service-plane seam (cloud-pulse + cloud-map share it) reading the existing
KMS-injected HANZO_CLOUD_PULSE_TOKEN. New cloud_service.go holds the shared
get-cloud-usages decode + fetcher, reused by the forthcoming ai-pulse.
Claude-Session: https://claude.ai/code/session_018PmFAHZvbBSTsuWyebwMra
The analyst returned an opaque 'empty response' when a signed-in user's AI
credits were exhausted. The ONE AI gateway (hanzo/ai routers/filter_balance.go)
signals this as HTTP 402 {code: insufficient_balance}; the gateway can also
answer 2xx with an error envelope carrying no choices. World now detects EXACTLY
that one contract (no keyword heuristics, no invented codes — world stays a thin
client of api.hanzo.ai) and renders a wallet CTA: 'You're out of AI credits →
Add credits (console.hanzo.ai/billing) / View usage' instead of a dead bubble.
Adds failure logging on the analyst path (was silent). Placeholder set to the
canonical 'Ask anything. Update your world.'
The ~52-char placeholder wrapped at 14px inside the ~360px composer,
shifting the box when it did. The placeholder now renders at 12.5px; the
unitless 1.5 line-height stays pegged to the input's 14px row, so a
smaller placeholder never moves the row vertically — typed text stays
14px, the composer stays one line.
/v1/world/analyst with Accept: text/event-stream now emits round /
think / delta / tool / done events while the agentic loop runs; the
done event carries exactly the JSON path's payload, so deltas are
cosmetic and the final render is unchanged. Server: chatMessagesModelStream
reads upstream OpenAI SSE; replyExtractor incrementally decodes the
strict-JSON envelope's "reply" value (chunk-safe escapes, byte-at-a-time
tested) and routes a reasoning model's pre-envelope prose to think
events. Tool rounds stream silently; tools surface as live chips.
Client: streaming bubble with breathing caret, dim italic thinking that
real answer text supersedes; non-SSE responses (old backend, sign-in
gate, agent path) fall back to the JSON contract in the same call.
Proven live: 61 think + 41 delta frames then done, gpt-oss-120b.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
The composer pill (fixed Zen logo + native <select>) becomes a proper
listbox popover: rows carry per-family marks (Zen ring ONLY on zen*,
sparkles for the Best router, G/L/C monograms for gpt/llama/claude,
bot for agents), group headers, active check. Assistant avatars and the
thinking row now wear the SERVING model's mark. One mapping in
src/utils/model-marks.ts, mirrored server-side by modelGroup() so the
roster groups honestly (Auto/Zen/GPT/Llama/Claude). Menu is
body-mounted + fixed so panel overflow never clips it. npm run dev
gains a /v1 proxy (prod by default, VITE_DEV_API_PROXY override).
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
The stale guard demanded an html-free precache glob, contradicting the
committed vite.config (offline.html precached, index/settings excluded
via globIgnores). Assert the actual invariant instead.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
Yahoo closes come back float32-widened (17.209999084472656); Go re-emits
the noise verbatim, ~40-50% of sparkline payload bytes. One sparkline()
emit path (né tailN) rounds via strconv 'g'/7; scalars stay exact.
Ported from upstream worldmonitor seed-utils.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
News panels gated their first DOM write on enrichWithVelocityML — on a
cold browser that means a 65MB sentiment-model download before any
headline appears (15-25s after the feeds answered in <1s). Paint with
keyword velocity immediately, upgrade sentiment in place, guarded by
renderRequestId. Analyst context fetches now cap at 2.5s so a cold
endpoint can't delay the chat send.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
cachedJSON/passthrough now serve stale instantly and refresh in the
background (one coalesced flight per key; cold misses single-flighted).
gdelt-doc/gdelt-geo stalled every request ~10s on each 5-minute TTL
lapse; the analyst grounding + protests-layer keys are additionally
kept warm every ~4min, GDELT-paced.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
zen5 went dark when the inference plane's claim catalog shifted — an
unclaimed id falls through to a proxy that rejects every credential, so
every AI endpoint died with an opaque 402/401. 'best' is the gateway's
own routing alias (owned_by hanzo) and always resolves; non-2xx bodies
now surface their code (insufficient_balance, spend_cap_exceeded, …) in
the chat instead of a bare status.
Claude-Session: https://claude.ai/code/session_01SpMZ69ur3tjAXCiwaa7Wv2
The 3D globe launched arcs far off the silhouette: deck.gl ArcLayer arches each
arc by ~1x the straight-line CHORD, so long cross-continent arcs (displacement
flows, traffic pulses) ballooned into space — the 'off-globe' glitch in the
screenshots. On the globe (mode==='3d') set greatCircle:true + getHeight:0 so
arcs follow the great circle flat on the surface; the flat map keeps its arch.
Also: the analyst model selector was a wide bar (max-width 160px showing full
model ids) — capped to 104px + ellipsis so it reads as a compact chip.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
The analyst could show/hide/move panels and drive the map, but NOT change the
layout mode, add a topic to watch, or drive the Watch Queue — so 'reconfigure
the layout and all widgets from the chat' was not actually true.
New commands (each drives the SAME public API the UI does; no second path):
- set_layout_mode: grid / free / immersive
- set_immersive_background: map | video
- set_language
- add_monitor / remove_monitor — 'watch for X'; goes through the one monitor
path, so an AI-added monitor persists + matches server-side like a typed one
- queue_next / queue_prev — drive the Watch Queue
AppState snapshot now exposes layoutMode, immersiveBg, language, the user's
monitors, and queue depth — the model cannot drive what it cannot see.
Fixes a real WatchQueue restore bug the e2e caught: a persisted blob whose
currentId pointed at a non-'watching' item silently lost watch-tracking
(next() only finishes a 'watching' item). load() now restores the invariant.
e2e/analyst-full-control.spec.ts: AI really flips to immersive, really adds a
monitor that lands in storage, really advances the queue — 3/3. Full suite 56.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Monitors lived entirely in the browser: keywords in localStorage, matching done
client-side over whatever headlines that tab had loaded. So a monitor only ever
saw one client's slice of the corpus, and it ceased to exist when you closed the
tab or switched device.
- /v1/world/monitors (GET/PUT): the list is persisted PER IDENTITY, reusing the
existing per-identity store (namespaced 'monitors') — no new mechanism, no new
table. Bounded + normalised at the boundary (sanitizeMonitors).
- /v1/world/monitors/matches: matching runs against the LAKE — every item the
backend ingested — via the existing FTS search. FTS tokenising can over-match,
so each hit is re-checked against the same word-boundary rule the browser used
('ai' must never trip on 'train').
- store: Lake.Flush() — drains the write-behind buffer synchronously. Run() is
still the steady-state consumer; Flush is for callers that need the write to be
VISIBLE (deterministic tests, and a shutdown that wants its buffer on disk).
- services/monitors.ts: the ONE path to where monitors live. Signed in → server
(and localStorage as the offline mirror); signed out → localStorage, unchanged.
Local monitors are ADOPTED on first sign-in rather than silently dropped.
- MonitorPanel.renderServerMatches renders lake matches with the same markup (and
the data-ctx-* attrs, so Summarize-with-AI works on them too).
Tests run against the real SQLite lake (temp WORLD_DATA_DIR), not a mock:
per-identity round-trip + isolation (user-2 cannot see user-1's monitors),
matching against the lake, and the word-boundary guarantee. 4/4 pass.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
The browser was classifying threat level and inferring geo for every headline,
on every client, on every render — the data and the logic both lived in the
frontend. The Go backend is the product; the browser is a view.
- enrichdata/{threat-keywords,geo-hubs}.json: the tables are DATA now, one
source of truth (extracted by EVALUATING the TS, not transcribed).
- internal/world/enrich.go: Go classifier + geo inference. Two parity traps
handled deliberately — JS object key order IS match priority (tables stay
ordered slices, not maps), and JS sort is stable (sort.SliceStable).
- feeds-batch now returns threat + resolved geo (hub, lat/lon, name) per item;
parseFeedItems stays a pure XML parser, enrichFeedItems does the meaning.
- rss.ts consumes the server's enrichment; the browser no longer geo-locates
anything (the import is gone). Single feeds now batch too, so the per-feed
browser path survives only for feeds that aren't proxy-shaped.
PROOF, not a claim: enrich_test.go replays a 494-case golden corpus generated
from the real TypeScript (every keyword × both variants, every geo-hub keyword,
every exclusion, plus adversarial word-boundary cases) — 494×2 classifications
and all geo inferences are byte-identical to the code being replaced.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
- Summarize with AI: any item already annotated data-ctx-headline (news, custom
feeds) gets it as the FIRST context-menu entry. It routes into the ONE analyst
dock via a capability port (registerSummarizePort), mirroring the existing
map port — the menu never reaches into the AI. New AnalystChat.ask() /
AiAnalystDock.askInDock() drive the same send path as typing; no second AI.
- Header: drop the border-bottom. The header shares the app background, so the
rule just cut the page in half.
- Map: drop the visible drag pill (::after). The grip strip is still the drag
target (cursor:grab) — the map stays chromeless, drag is unchanged.
- Try Hanzo is an ACQUISITION CTA: it now disappears once identity resolves as
signed-in, driven by one new 'hanzo:auth' event emitted from the single place
auth resolves (AccountMenu.refresh).
FIXES A REAL BUG the e2e caught: .try-hanzo sets display:inline-flex, which
beats [hidden]'s UA rule — so hiding it would have silently done nothing.
e2e/ui-polish.spec.ts covers all four against real DOM/CSS: 4/4 pass.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
queue_video was dead in prod: no YOUTUBE_API_KEY is configured, so Data-API
search returned empty. Scrape the results page instead — the same keyless
pattern handleYouTubeLive already uses (browser UA + consent cookies, since
datacenter IPs get an interstitial). The Data API stays as the fast path when
a key exists; the scraper degrades to an honest empty list, never a fake hit.
Verified live: 'Milken Institute Jensen Huang' returns the real talks
(HT8-KPAjpiA = Global Conference 2025) as the top results.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Content the AI surfaced was ephemeral: a video it found (e.g. the Milken
Institute Jensen Huang talk) was lost on reload. Now everything consumable
lands in ONE persistent queue.
- WatchQueuePanel: stage (video/image/story) + list; prev / finish-and-next /
fullscreen; click a row to play, x to remove. Owns no state — a pure view
over watchQueue, so panel and immersive views can never disagree.
- queue_video app-command: the analyst resolves free text ('milken jensen
2025') to a real video and queues it — reachable over MCP like every other
command.
- /v1/world/youtube/search: Data-API search, cached, honest empty result when
no key is configured (never fakes hits).
- Queue persists to localStorage: survives reload, tracks watched/finished.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Unifies non-live news clips, AI-surfaced media, and story cards into ONE
ordered queue with tracked per-item state (queued/watching/watched), so
content can be consumed reliably instead of flashing by. Pure logic, no DOM;
persisted to localStorage so the queue + watched history survive reload.
Idempotent enqueue (dedup by id) + bounded size (prune oldest watched).
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Adds a fullscreen control next to mute/play that promotes the video player
container to true fullscreen (and toggles back). CSS makes the iframe fill
the fullscreen root edge-to-edge. Answers 'make the video bigger / go full
screen so I can enjoy streaming it'.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Both live-video panels ran a 5-min input-idle timer (mousedown/keydown/
scroll/touchstart on document). Passively watching a video fires none of
those, so after ~5 min the timer destroyed the player/iframes mid-watch —
exactly the 'video disappears after a few minutes' report. LiveNewsPanel
had no non-visibility resume path, so it stayed dead until a tab switch.
Fix: when the idle timer fires while the video is actively playing and the
tab is visible, reschedule instead of tearing down. The visibilitychange
handler (tab hidden) and the webcam IntersectionObserver (panel off-screen)
still cover the genuine 'user left' cases, so abandoned tabs still pause.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
The cutover from the hanzoai/static image to the cmd/world Go binary dropped
response compression and asset caching, so every cold load pulled the Vite
bundle raw: main.js 1.5MB, map.js 2.7MB — ~4.5MB of uncompressed JS.
- gzipStatic wraps ONLY the SPA handler (never /v1/world/*, which streams):
gzip for compressible types, Range + non-gzip clients pass through,
already-compressed media skipped. main.js -73%, map.js -72% on the wire.
- setCacheHeaders: content-hashed assets/ → immutable 1y; every unhashed file
(favicons, manifest, service worker) stays no-cache so SW updates aren't
stuck behind a stale cache.
- static_test.go: proves encoding, ratio, round-trip decode, cache policy.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Free mode gains NW/NE/SW corner grips alongside the existing SE one, so a
panel can be resized from any corner like a real window manager. The resize
is anchor-aware: dragging a corner pins the opposite corner — w/n edges shift
the panel's left/top in lock-step with the size, e/s grow from a fixed
top-left as before. One primitive (applyFreeResize over a {n,s,e,w} edge set)
drives every edge and corner handle; attachPanelCornerResize takes a `corner`
and only the SE corner also drives grid-mode span/column snapping (top/left
resize has no meaning in grid flow, so those grips are free-mode only, hidden
in grid via CSS). Header controls are raised above the grips so the close/info
buttons stay clickable where a top corner overlaps them.
Verified: e2e all-four-corners test asserts each corner resizes and pins its
opposite corner (14/14 panel-drag specs green); tsc clean.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
The datastore lake/settings DB (store.dbFile = "world.db", opened in WAL
mode) lands in the repo's data/ dir when WORLD_DATA_DIR points there in
local dev. Ignore world.db and its -wal/-shm sidecars alongside the
existing world-model runtime snapshots so they are never committed.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
conflict-impact, github-trending, hackernews (244 LOC): none imported
anywhere, none re-exported via services/index.ts, and every exported
symbol (correlateConflictImpact, fetchGitHubTrending, fetchHackerNews, …)
has zero references repo-wide. The github/HN panels now fetch through the
/v1/world/* URL registry in config/variants/base.ts, which superseded
these client-side fetchers. tsc + vite build green; bundle unaffected.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Neither is imported anywhere in src/, src-tauri/, or api/ (0 refs
repo-wide). Removing them drops 4 packages from the lockfile (the two
plus their now-orphaned transitives @bufbuild/protobuf and meriyah) and
~26 MB of install footprint (youtubei.js alone is 21 MB).
Proven inert: with all four physically removed from node_modules,
`tsc --noEmit` and `vite build` pass and the JS bundle is byte-identical
(6,139,678 B) — they were never bundled, so bundle-size delta is 0; the
win is pure supply-chain / install-surface reduction.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
introspectOwner and introspectIdentity were the same memoized userinfo
call under two cache-key namespaces ("cloud-owner:" vs "identity:"), so a
token used for both the admin gate and settings sync was fetched and
cached twice. Delete introspectOwner; route requireAdmin through
introspectIdentity and read the owner claim off it. One function, one
cache entry per token, one source of identity truth.
Fail-closed semantics unchanged: no token → 401, introspection error or
non-admin owner → 403. Admin-gate and settings tests green.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Five funcs unreachable even counting test callers, per
golang.org/x/tools/cmd/deadcode -test:
- strings.go fieldsCollapse — 0 refs
- mcp.go toolError — 0 refs
- model.Engine.History() — dead accessor; the /history route reads
e.history directly (comment was stale)
- model.Engine.Context() — superseded grounding path; AI handlers
ground via CountryContext + HistoryDigest
(comment "called by the AI handlers" was false)
- model.toneNote() — only called by the removed Context()
ftoa/signed/round2 verified still live (history.go, model.go). Build,
vet, and internal/world tests green.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Replace the signed-out header CTA with the hanzo.ai "Try" pattern: a white
.hz-cta pill "Try Hanzo ▾" that drops a monochrome menu of Hanzo products
(World/Chat/Dev/App/Cloud/Desktop), with world.hanzo.ai highlighted as the
current product. The former "Try Hanzo World" pill was the AccountMenu login
CTA — relabel it to a secondary "Sign in" so the header has one primary action
(hanzo.ai layout: [Try Hanzo ▾] [Sign in]).
- New self-contained stylesheet src/styles/try-hanzo.css (not main.css).
- Menu portaled to <body> + position:fixed so it never widens the page and is
never clipped by the header's mobile overflow-y:hidden tab scroller.
- Closes on click-away and Escape; icon-only trigger < 680px. No h-overflow at
1440/390; theme-aware (white pill dark / black pill light).
- All product URLs verified live (200): world.hanzo.ai, hanzo.chat,
hanzo.ai/code, hanzo.app, console.hanzo.ai, hanzo.ai/desktop.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
The dock chat is rebuilt to a native-@hanzo/ai, hanzo.chat-grade copilot in a
self-contained `hzc-` namespace + its own stylesheet (analyst-chat.css), so it
never touches main.css.
P0 fixes
- Markdown now renders: a tiny XSS-safe renderer (utils/markdown.ts) does
bold/italic/lists/headings/code/links/tables — no raw HTML passthrough, every
text run escaped once, every href run through sanitizeUrl.
- HTML entities decoded at the source: the backend HTML-escapes replies, so an
apostrophe arrived as the literal `'`. decodeEntities() runs first, then a
single re-escape on output — the browser shows `'`, never `'` and never the
double-escaped `&#39;`.
Brand + look
- Real Zen mark everywhere (the notched ring from zen/logo/dist/logo-mono.svg):
icon('zen') is now the scaled logo and a new zenLogo() drives the FAB, the
header, the assistant avatar, and the pulsing "thinking" state — no more
generic enso/sparkle.
- vercel-black monochrome, Geist, sentence case: clean user/assistant bubbles,
a card composer with an inline model selector, the model tag on each reply,
tidy action-log + tool-trace styling.
Window modes (expanded scope)
- Closed = a small Zen-mark FAB bottom-right. Open = dock / sidebar / split /
fullscreen, toggled from the header. Sidebar is the default and reflows #app
via `body.hzc-shift` + the `--hzc-width` var (the auto-fit grid reflows
cleanly); left-edge drag resizes; mode/width/open-state persist so a
variant-switch reload reopens the same rail.
App control + data
- New bulk commands hide_all + show_only in the command registry (composed from
existing AppHost methods, so they auto-propagate to the backend via
commandManifest()); "hide all panels / show news / flip to finance" now
visibly re-aligns the dashboard with an action-log receipt.
- MCP data-tool traces render inline as compact tables, not raw JSON.
Transport is still request/response (no SSE server yet), so slow zen5 shows a
live pulsing-Zen "thinking" state rather than a dead spinner; the SSE seam stays
in analyst-transport.ts for when a streaming cap lands.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
1. PANEL HEIGHT CAP (CTO bug): grid-mode height snapped to the span ladder and
hard-capped at span-4 (~800px); dragging the bottom edge further did nothing.
Now height snaps to the fine tier ladder [120,200,400,600,800] up to its top,
then grows CONTINUOUSLY with NO upper cap — each extra ~200px adds a whole row
(spanForHeight, shared by the bottom-edge + corner handles). Spans beyond the
CSS ladder (>4) are driven inline (grid-row + min-height; dataset.span is the
source of truth via currentSpan), so a panel can be made arbitrarily tall.
Free mode was already pixel-unbounded. MIN (120px) kept, no MAX. resetHeight
clears the inline geometry too.
Proof: grid 200→1628px, free 200→1700px — both persist across reload.
2. CJK glyph 403 spam: CartoDB's CJK glyph fonts (HanWang / NanumBarun / Noto
Sans CJK…) 403 on CORS whenever a CJK label is in view, flooding the console.
A map transformRequest repoints just those glyph fetches at the CORS-open Latin
font (Montserrat), so CJK labels fall back cleanly with zero failed requests.
Latin fonts (incl. "Noto Sans Regular") are untouched.
Proof (panned to East Asia): 0 font 403s; Montserrat fallback in use.
Gate: typecheck + build clean; e2e panel-drag 13/13; responsive 17/17.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Right-clicking the empty workspace (grid / content background, no panel or map
under the cursor) now opens a workspace menu: Grid / Free / Immersive layout, Add
widget, Reset layout. So even if the user hides every panel and the map, a
right-click always offers a way to reset and restore. Native menu still shows in
text inputs and over the chrome (header, dock, modals); panel/map right-clicks keep
their existing specific menus.
Proof: synthetic contextmenu on .main-content → menu with
[Grid layout, Free layout, Immersive layout, Add widget, Reset layout].
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Free-mode drag now tiles like macOS / FancyZones: dragging a panel near an EDGE
previews a HALF, near a CORNER previews a QUADRANT, the centre stays free
placement. A monochrome translucent target rectangle fades in during the drag and
glides between zones as the cursor moves; on drop the panel resizes + positions to
the zone (with a clean gutter). Escape cancels (restores the pre-drag position);
reduced-motion drops the glide. One mechanism: the zone's viewport rect converts to
the panel's grid-relative free geometry on commit — no new coordinate system.
Zones are computed against the visible workspace (grid ∩ scroll viewport), so
tiling always fills the on-screen area regardless of scroll. This is the "toss
panels around like a window manager" experience over the map/globe.
Proof: drag → left-half preview 720×810 → drop → panel = left half (708×798);
top-right corner → quadrant 720×405 → panel = top-right quarter. Screenshots of the
mid-drag highlight captured.
Follow-ups (noted, not in this commit): center/left/right THIRDS, and split-view
when dragging over another panel's half.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
mapbox-gl v3 refuses to paint ANY basemap without a token (the canvas goes fully
black even though the third-party CartoDB tiles fetch 200), which the CTO hit on
localhost and any deploy missing VITE_MAPBOX_TOKEN. Fix: when no token is present,
drape a keyless CartoDB raster basemap rendered by DECK (token-independent, same as
the data dots and the native ESRI globe) at the bottom of the overlaid deck stack —
theme-aware (dark_all / light_all), works in both the flat MapView and the globe
_GlobeView. Data layers draw on top. The token path is untouched: with a token the
mapbox vector basemap stands and no deck basemap is added.
Proof (VITE_MAPBOX_TOKEN unset): 2D = full CartoDB dark basemap w/ coastlines +
labels (60% non-black, was 0), 3D = basemap draped on the globe. Never a black void.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
- Item count moves inline after the title (left), so it never collides with the
hover-✕ (top-right); hidden entirely until count>0 (a loading "0" read as broken).
- Retire the "LIVE" text chip: it's all live by default and the static label
misled while a feed was loading/empty. Live state now shows only a subtle pulsing
dot (reduced-motion: static); cached/unavailable keep their informative labels.
- Summarize: the cryptic ✨ becomes a labelled "✨ Summarize" button (monochrome,
sentence case, aria-label), reserved clear of the ✕. Funded-key path unchanged.
Playwright: 0 LIVE text chips, 0 zero/empty counts shown, summarize reads
"✨ Summarize".
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
- Dock: replace the Immersive button + Grid/Free segmented control with a single
Layout dropdown (Grid / Free / Immersive). One control, one way.
- Decomplect background from snap: immersive is a BACKGROUND mode (map fills the
viewport, panels float on top); grid/free is the SNAP behaviour. Immersive
defaults to freestyle floating (free snap) over the FULL viewport — the old
narrow right-docked column is gone (it collapsed once free-mode made the cards
absolute). Map stays fixed full-viewport via !important over the inline free geom.
- Background Map/Video + collapse controls show only while immersive is active.
- NewsPanel category tags: drop inline color (threat colour as text failed WCAG);
text now var(--text), threat colour carried by border + translucent bg only.
Verified: dropdown drives grid→free→immersive→grid (map fixed full-viewport in
immersive, cards float visibly over it); 17/17 P0 checks still green.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Layered on the integrated layout engine (194e6657). The grid's auto-rows
collapsed to their 120px floor, so every span-2 panel (min-height 400) — the
map included — overflowed ~150px onto the row below and OVERLAPPED it
(43 overlaps across 41 panels). Root fix: definite 200px row unit
(grid-auto-rows: var(--panel-row,200px)) so span-N reserves exactly its height;
content that exceeds a panel scrolls inside it, never onto neighbours.
- Full width: auto-fill → auto-fit so real columns always stretch to consume
100% width, no dead right-hand gutter at any size (verified 9 widths).
- Map edge-to-edge: the 14px drag-grip is now an absolute overlay, not in flow,
so the canvas fills the panel top (black top strip 15px → 1px).
- ?maplogo=0 (+ localStorage) hides the basemap wordmark; a compact "ⓘ"
AttributionControl is always mounted so basemap ToS still holds.
- Mobile-view warning modal retired (layout is responsive to 390px).
- span-0 stays a genuine 120px short tier (top-aligned in its 200px track).
Proof: 17/17 playwright checks (full/crypto/saas × 1920–390): hOverflow=0,
0 overlaps, gutter=grid-padding only, no modal. Live-news video grows to full
width (353→1430px, 16:9, no cap). Dark/Sat/Terrain inline with 2D/3D in the dock.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Rebuilds the free-form + snap-to-grid layout engine on top of the advanced
origin/main (footer dock + responsive work) and unifies the two grid-cell
mechanisms into one:
- grid-config drives the dock's `--panel-col-min` (default 160px, range 140–360,
matching the base .panels-grid rule + the dock slider) — the duplicate
`--grid-cell` override is gone. One variable, one way. Default 160 = the grid
is byte-identical until the slider moves it, so no fixed-span panel shrinks.
- window.worldGrid exposes the engine to the footer dock's Grid⇄Free toggle +
cell-size slider (App.gridApi delegates to it).
- main.css layout-engine section is append-only after the dock CSS; only NEW
selectors (corner grip, snap overlay, free-mode absolute layout, mobile guard).
Live News (video) now resizes freely with the video filling at any size (CTO
ask): the panel carries id="live-news" so the intended `#live-news .panel-content`
fill CSS (padding:0 + flex-column) finally applies — the 16:9 `.live-news-player`
fills the container edge-to-edge (was 16px short under generic padding). With the
new right-edge + corner grips and the uncapped column snap (span up to full
width), Live News drags to 2-3 cols or full-width-top in grid mode and to any
pixel size in free mode; the width-driven video fills at every size.
Tests (13, all green; typecheck + build pass): 4 original drag/resize + 7
layout-engine (grid snap, corner resize, overlay, cell re-snap, free drag+resize
persist across reload, map participates, toggle) + 2 real-app live-news (grid
default→3col→full-width fill setup + resize, free arbitrary-size). Screenshot:
e2e/layout-shots/live-news-fullwidth.png (full-width-top, large video area).
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Make news/feeds INSTANT and give world one place to query everything.
Feed hot cache (two-tier, ONE way to read a feed body):
- L1 per-pod in-mem mirror (sub-ms hot reads) + L2 hanzo-kv (shared across
pods, survives restart). A body warmed by any pod is instantly served by
every pod; a restarted pod reads the still-warm shared cache instead of
cold-starting. Graceful degrade to L1-only when hanzo-kv is unreachable.
- Background warmer: on boot + every ~5m (jittered) fetches every warm feed,
write-throughs the body, and folds items into the lake. rss-proxy and
feeds-batch serve from the warm cache and never block on upstream while any
cached copy exists (stale-while-revalidate; revalidation runs in background).
- Demand-driven warm set (fleet-wide via hanzo-kv) + a curated seed of the
crypto / financial-regulation / crypto-news feeds so a brand-new pod warms
THOSE first. Cross-pod fetch dedupe via shared copy freshness.
Ingested-data lake (embedded SQLite, modernc.org/sqlite — pure Go, FTS5):
- One normalized `items` table (id, kind, source, ts, title, text, tickers,
country, geo, payload) fed by feed news AND world-model observations, so
everything is searchable/countable together.
- GET /v1/world/search (FTS5 bm25 ranking; filters kind/since/country/ticker)
and GET /v1/world/analytics (totals, by kind/source, top tickers). Rolling
retention window with an hourly prune job. Write-behind ingest (never on the
request path).
Per-identity settings (same SQLite DB, own table):
- GET/PUT /v1/world/settings, bearer-gated, keyed by (org, user_sub, project)
from IAM userinfo, so a signed-in dashboard syncs across devices. Anonymous
→ 401 (client keeps localStorage).
Everything degrades cleanly (never-5xx): no hanzo-kv → per-pod cache; no SQLite
→ empty search/analytics + settings say not-stored. Binary stays CGO-free
(CGO_ENABLED=0). go directive + Dockerfile bumped to 1.25 for modernc.
Latency (local, warm): feeds-batch warm 0.5-1.9ms vs 154ms cold; search 0.8-1.5ms.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
The analyst chat is paid usage: metered to the signed-in user's org via their
own IAM bearer. handleAnalyst used s.ai.bearerFor(r), which falls back to the
funded service key (HANZO_AI_KEY) when no user token is present — so an anonymous
chat silently ran on the shared key (proven live: an unauthenticated POST to
/v1/world/analyst returns a real 200 answer). That made the backend "Sign in to
chat" gate dead code and meant paid usage was never actually enforced or metered.
Use userBearer(r) for the chat path (user-only). The funded key now backs ONLY the
anonymous auto-insight endpoints (summarize/classify/country-intel), exactly the
intended pro model. The sign-in gate is now live server-side, and the frontend
gate gains defense-in-depth. Roster listing (handleModels) stays on bearerFor —
it is read-only, not metered inference.
Test: TestAnalystChatRequiresUserBearer pins the contract (funded key present,
anonymous chat still refused with the sign-in skip). TestAnalystDataToolLoop now
drives the chat as a signed-in caller (Authorization header), the real paid path.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
The native deck GlobeView now honors the 2D basemap switcher (dark | satellite |
terrain), reading the SAME localStorage key (hanzo-world-basemap-style) and
reacting live to a basemap-style-changed event DeckGLMap fires — no reload.
- dark: the existing monochrome vercel-black sphere.
- satellite: keyless ESRI World Imagery draped via a deck TileLayer/BitmapLayer,
back-faces culled so the far hemisphere never bleeds through; the near-black
ocean sphere underneath supplies depth + darkens the imagery so data dots stay
legible. Thin light country borders overlaid for intel orientation.
- terrain: keyless ESRI World Physical Map (natural relief).
Data layers (dots/arcs/borders) render ON TOP and keep picking; ocean sphere is
tinted dark slate-blue on bright styles so the Web-Mercator polar gap blends.
Since native uses keyless ESRI, the switcher's sat/terrain buttons no longer need
a Mapbox token when native is the 3D renderer (default), and setBasemapStyle
notifies the globe before the tokenless mapbox setStyle so a missing token can't
block the switch. Adds a style-switch e2e (single-context preserved, data still
picks) + harness hooks.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Live 1366×768 had 167px of horizontal page overflow (documentElement.scrollWidth
1533 vs innerWidth 1366): the header's action row (Copy Link / theme / Immersive /
PANELS / SOURCES + variant tabs) could not shrink and pushed the account CTA off the
right edge. The layers panel also rendered OPEN by default, floating over the map and
the panels beneath it.
CTO-approved restructure: the header is now IDENTITY + NAV only (Hanzo mark, variant
tabs, LIVE, search, theme, account). All operational controls move to a new slim,
monochrome, collapsible BOTTOM TOOLBAR DOCK:
- Map's 2D/3D toggle, basemap switcher (Dark/Sat/Terrain) and time-range pills mount
into the dock via a new optional DeckGLMap `controlsHost` (defaults to the map
container, so the e2e harnesses that drive the overlay controls are unchanged).
- Region select, Layers toggle, Immersive controls, Panels, Sources, Copy link and
Fullscreen relocate into the dock (same ids → existing handlers still bind).
- New controls: layout-mode (Grid ⇄ Free) + widget-size slider, wired through a
feature-checked adapter that prefers window.worldGrid (world-layoutengine) and
otherwise applies a self-contained fallback (CSS column-floor var + body data attr);
and "+ Add widget", a searchable palette over the panel registry that shows a panel
via the one setPanelEnabled() path — works in both grid and immersive layouts.
Fixes:
1. Horizontal overflow — scrollWidth === innerWidth at 1920/1440/1366/1024/768/390.
Header groups are min-width:0 shrinkable; variant tabs collapse to icons then scroll
INSIDE their own strip; the dock scrolls horizontally so the PAGE never scrolls.
2. Layers panel — hidden by default (`.open` reveals it via the dock's Layers button),
anchored top-left WITHIN the map box, height-capped with internal scroll, draggable.
3. Basemap switcher — verified switching Dark/Sat/Terrain in 2D (tiles load, deck data
layers survive setStyle, choice persists in localStorage). The token IS in the
runtime bundle. In native-3D-globe mode the deck GlobeView renders its own sphere,
so sat/terrain don't retexture it there — a pre-existing renderer limitation, noted.
4. Globe fit — the deck globe canvas fills the map container exactly (measured
1356×384 == map box; no letterboxing); CSS forces the wrappers/canvas to 100%.
Gates: typecheck ✓, vite build ✓, e2e globe-overlay 1/1 ✓, panel-drag 4/4 ✓.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
P1: on the Cloud (saas) dashboard the Chains and Cloud Overview widgets sat on a
loading spinner for 30s+. Root cause: /v1/world/cloud/chain-nodes took ~24s on a
cold cache because unreachable L1 RPCs (zoo/hanzo) hung until the request-wide
deadline, and CloudOverviewPanel compounded it by awaiting getChainNodes() in the
same Promise.all as the (instant) pulse. Crypto only looked fine because its cache
was already warm.
Fixes:
- Backend: bound each chain's telemetry fetch with a 4s perChainTimeout so one
dead L1 can't stall the whole response. Cold chain-nodes 24.0s -> 4.0s (verified
on fresh servers); live chains unaffected, dead chains honestly live:false.
- CloudOverviewPanel: primary render now depends only on pulse+models (both
instant); the chain-scale tiles fetch independently and fold in when ready —
the slow fetch can never gate first paint again. Reordered the two real
chain-scale tiles ("chains live", "total block height") ahead of the
demo-modeled nodes/GPUs/regions/uptime so they land in the panel's visible rows.
Polish — one honest-copy choke point (runtime.canConfigureKeys, used by
Panel.showConfigError and the FRED site): public web viewers now get the quiet
"No data available" line instead of raw "FINNHUB_API_KEY / NASA_FIRMS_API_KEY /
FRED_API_KEY not configured — add in Settings"; the specific, actionable copy
stays only for the desktop app / dev build where keys can actually be set.
Verified live via playwright (?variant=saas): Chains renders ETH 25,501,617 +
BTC 957,412 (count 5, LIVE); Cloud Overview populated incl. chain tiles (2-3
live / 26.46M total height); ?variant=full body scan for /_API_KEY/ returns null
across 91 panels; zero console errors. Gates: typecheck + build + go build/vet +
go test ./internal/world/ (17s) all green.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Context menus (panel-menu.ts): extend the single installed contextmenu
listener into a per-context system. Layers, most-specific first: text entry
→ native menu; component items via a data-ctx-* convention (news → Open/Copy
link + Copy headline; market/FX/commodity/yield rows → Copy value/symbol);
the map surface → Copy coordinates / Fly here / 2D-3D toggle through a narrow
MapContextPort (registered by MapContainer); then the panel baseline (Hide,
Move to top, five Size presets, Reset layout). A stationary right-click on the
map opens the menu; a rotate-drag still bails so maplibre keeps the gesture.
Size presets reuse Panel's setSpanClass/savePanelSpan; Move to top routes
through App.movePanelInGrid via a new panel-move-request event — one way each.
Panels only annotate their DOM (quoteRow, MarketPanel, NewsPanel,
CustomFeedPanel); DeckGLMap gains a read-only screenToLngLat.
Analyst data tools (handlers_analyst.go + mcp/): bind the 7 world MCP tools as
DATA tools in the analyst loop. mcp.CallTool executes them IN-PROCESS via the
same dispatcher tools/call uses (one path, no self-HTTP); mcp.ToolSpecs feeds a
derived tool contract into the system prompt. The handler runs an agentic loop
(≤3 tool rounds) — the model requests tools in a "tools" array, results are fed
back, then it answers citing live data. Tool traces ride the response and render
as collapsed "🔧 tool(...)" lines in the chat (AnalystChat). App-commands stay
auth-gated (unchanged); data tools dispatch anonymously.
Gates: go build+test (incl. new agentic-loop + parse/contract tests), tsc,
vite build, playwright (news/map menus + stubbed analyst tool round-trip) — all
green.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Layout/style batch:
1. Bigger default widgets — the 160px column FLOOR stays (fine map column-resize),
but auto-placed panels now default to `grid-column: span 2` (~324px) instead of a
160px sliver. Map keeps full-width; height tiers untouched (still shrink to span-0).
2. Immersive mode (?layout=immersive, persisted) — body.immersive promotes the map to
a fixed full-viewport background; panels float above as a translucent (#0a0a0acc +
backdrop-blur) column docked right, still drag/resize/close-able. One background
slot = map | video: the video option mounts the muted live-news YouTube embed as a
fixed BG. Collapse-to-edge affordance slides the column off so the globe breathes.
Escape / the header toggle returns to the grid; state survives reload. New service
src/services/immersive.ts owns the state; App only wires header chrome to it.
3. Layers selector UX — the collapsed layer list is now a draggable panel (header grip,
position persisted) with pointer drag-and-drop reordering of its entries (order
persisted, purely cosmetic). Contained in DeckGLMap.
4. Calmer globe — AUTO_ROTATE_DEG_PER_SEC halved 4 -> 2 in BOTH renderers (GlobeNative,
the active 3D globe, and the mapbox DeckGLMap globe — kept identical by design).
5. Nicer basemaps — a small dark/satellite/terrain switcher under the 2D/3D toggle
(2D mapbox renderer). One setStyle path re-adds fog/atmosphere/projection + drapes
terrain over the Mapbox DEM (exaggeration 1.4) on style.load; deck overlay survives.
satellite/terrain require VITE_MAPBOX_TOKEN — gated (disabled) without one so the map
never breaks. Data dots gain a thin dark halo over bright rasters (deck-canvas only).
Gates: typecheck + build green; e2e globe-overlay + panel-drag green; globe.spec
auto-rotate band updated to the new 2°/s. Deliverable screenshots under e2e/screens/.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Follow-up: the merged status-page proxy assumed the source repo's Gatus route
(/v1/status/endpoints/statuses), but the LIVE status.hanzo.ai deployment serves
the upstream-standard /api/v1/endpoints/statuses — so the endpoint returned
available:false/total:0. Probe both routes and use whichever answers with a
non-empty board (correct against either deployment). Live now yields 25 services,
24 up, 1 incident (AI/Chat).
Visual: replace the HanzoStatusPanel iframe embed of status.hanzo.ai with a
NATIVE monochrome board fed by the same /v1/world/cloud/status-page summary —
services grouped by group (AI/Apps/Commerce/…), per-service up/down dot + response
time, active-incidents section on top. One data source, one style, no iframe
sizing/scroll jank. Extract the shared "since" formatter to cloud-format.fmtAgo
(used by both status panels — no duplication).
Gates: typecheck + build + go build/vet + go test ./internal/world/ all green;
live JSON verified end-to-end through the running server.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
With several releases a day the SW precache served an old index.html that pointed
at hashed bundles a newer deploy had already replaced: the app flashed the old
build, then went black as old and new chunks mixed. Aligns SW behaviour with the
deploy caching policy (index.html no-store, /assets/* immutable).
vite.config PWA (generateSW):
- navigateFallback: null — disable the plugin's default 'index.html' fallback. A
precache-served fallback is cache-first and re-introduces the stale shell; it was
also generating createHandlerBoundToURL("index.html") against a non-precached URL.
- globIgnores += index.html, settings.html — keep the HTML shell out of the
precache so navigations resolve to the network-first route, not a stale copy.
Only immutable, content-hashed assets stay precached, so an old-HTML/new-chunk
mix is impossible. offline.html remains the sole offline shell.
- runtimeCaching: NetworkOnly passthrough for /v1/world/* and /.well-known/*
registered before the navigate handler, so the data plane and discovery docs are
never intercepted or cached; the navigate handler stays NetworkFirst.
- skipWaiting + clientsClaim + registerType autoUpdate retained: a new SW activates
immediately and (via workbox-window) reloads the tab exactly once on a real
update — never on first install, never in a loop.
Client (src/services/sw-update.ts, wired from main.ts):
- Extract SW lifecycle into one module. Register via vite-plugin-pwa autoUpdate and
call registration.update() on visibilitychange->visible (plus an hourly backstop),
so a long-lived backgrounded tab learns about a deploy the instant it is looked at
again — before the user triggers a lazy chunk import the server may have purged.
Verified: tsc + vite build clean; dist/sw.js contains self.skipWaiting()+clientsClaim(),
no NavigationRoute, index.html/settings.html absent from precache, /v1/world &
/.well-known are NetworkOnly. Playwright upgrade sim (Go server on :8193, build A ->
swap shell -> build B): tab lands on build B via network-first HTML with no
hard-refresh and no chunk-load errors (9/9).
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
- DeckGLMap: cull far-side point markers by alpha on the 3D globe (overlaid
deck.gl shares no depth buffer with mapbox's globe, so back-hemisphere dots
projected through the front and looked like they floated off the sphere).
One DRY helper clones Scatterplot/Icon layers with camera-facing color
accessors, re-evaluated each buildLayers (move debounced ~10Hz).
- LiveNews video: width-driven 16:9 (flex:0 0 auto + aspect-ratio) so the embed
scales with its container instead of being stretched by the flex parent.
- Mobile: keep the centered map legend inside the viewport (wrap + max-width).
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Clicking a country now opens a proper fullscreen application view instead of a
centered card overlay: intel content (instability index, brief, signals,
timeline, prediction markets, infrastructure) breathes across the canvas on the
left, with a docked AI analyst chat as a right sidebar.
- CountryBriefPage: fullscreen flex layout (header + cb-view = cb-main | resize
handle | cb-analyst-sidebar). Docks the ONE AnalystChat component by
composition — model picker + command registry intact — seeded with the country
as the active region so the first question is grounded. Resizable divider,
collapsible sidebar with a reopen pill; on mobile the sidebar is a bottom sheet
(no horizontal scroll), intel leads by default.
- App: browser Back closes the view (opened via pushState), Forward re-opens it;
?country= deep link restores it. Escape/✕ still restore the dashboard. Passes
the analyst host into the page. Replaced a fragile typeof-this type query with
a stable class index access.
- main.css: append-only fullscreen layout + analyst sidebar styling (monochrome,
Geist, sentence case), mobile bottom-sheet, print-flow restore.
- e2e/country-view.spec.ts: desktop fullscreen + docked chat + URL + Escape,
collapse/reopen, mobile 390px bottom sheet, ?country=FR deep link. 4 passing.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Add GlobeNative — a pure deck.gl _GlobeView renderer for the 3D globe that
replaces the mapbox-gl globe + deck overlay (two cameras, two contexts). One
Deck, one canvas, one WebGL context: perfect data registration by construction,
proper far-side occlusion (depth-writing ocean sphere), monochrome vercel-black
basemap from the bundled /data/countries.geojson.
Data layers, tooltips and clicks are reused verbatim from DeckGLMap via an
additive asGlobeSource() bridge (no builder duplication; heatmap->scatter swap
comes free). Native is now the default 3D renderer; ?globe=mapbox is the escape
hatch. 2D stays mapbox mercator, untouched. Clean create/destroy on the 2D<->3D
toggle (no leaked contexts). Idle auto-rotate + fly-to + reduced-motion +
tab-hidden gates ported from the mapbox globe.
Adds e2e/globe-native.spec.ts (flag activation, single context, GlobeViewport,
pick-on-sphere) + a standalone harness.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Expand the Cloud dashboard with live, honest platform telemetry across four
fronts. Every path keeps the layer's never-5xx + explicit-flag contracts.
1. Cloud Overview +2 real tiles: "chains live / N tracked" and summed "total
block height", computed from the live chain-nodes feed. Omitted (not zeroed)
when the feed is unavailable.
2. More chains: the chain catalog now carries three fetch strategies (luxfi/node,
public EVM JSON-RPC, plain-integer height). Adds Hanzo (api/rpc.hanzo.network
failover), Ethereum (eth_blockNumber/eth_chainId via ethereum-rpc.publicnode.com)
and Bitcoin (blockchain.info height) — each SSRF-allowlisted, per-chain live
flag, no invented heights. BlockchainPanel hides peers/chainId where not
meaningful (public reference chains).
3. status.hanzo.ai integration: GET /v1/world/cloud/status-page proxies the
Gatus board (/v1/status/endpoints/statuses), distilled to a per-service
up/down board + active-incidents list (latest-by-timestamp, incident dated
from last UNHEALTHY event). Surfaced in CloudServicesPanel as a public health
section fused with the admin RED metrics. Honest available:false when down.
4. OpenSky OAuth2: when OPENSKY_CLIENT_ID/SECRET are present, fetch+cache a
client-credentials bearer and attach it to the states/all proxy; anonymous
path is byte-identical otherwise.
Tests: chain catalog + allowlist + hex parse, status-page summarize + live
degrade, OpenSky anonymous + invalid-cred fallback. All gates green.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Clean-room MCP apps surface for Hanzo World — a thin, read-only projection
of the existing public /v1/world/* routes. Ideas-only from the upstream
study; no code copied.
- internal/world/mcp: JSON-RPC 2.0 over streamable HTTP. 7 tools
(world_brief, country_instability, model_history, market_quotes,
chain_status, traffic_map, feeds) each dispatched IN-PROCESS through the
same world mux via an httptest recorder — one impl per data path, the
model.Engine's unexported handlers reached through the mux composition
point, zero edits to existing handlers.
- Two MCP apps (ui:// resources): world-brief + market-radar. resources/read
returns a DATA-FREE static HTML shell (text/html;profile=mcp-app, strict
CSP, textContent-only, no innerHTML) that receives data later via a
tools/call over the host postMessage bridge (quota-safe two-phase).
- Discovery: root server.json (MCP registry manifest) + public/.well-known/
mcp/server-card.json (tool + app inventory), both generated by
cmd/gencard and digest-stable like the agent-skills index.
- feeds tool exposes a curated category enum (never raw URLs) over
allowlisted RSS hosts — no SSRF surface.
- Anonymous public access; gate() attachment point noted per the
internal/world/model gate() pattern for later pro-tier gating.
Tests: JSON-RPC initialize/tools.list/tools.call/resources round-trip over
live wiring, server-card + server.json drift guards, shell digest pins, and
a data-free/no-innerHTML shell-safety grep. Verified end-to-end against the
compiled binary with the official @modelcontextprotocol/inspector CLI.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
P0 perf — the CTO's Chromium froze on 2.9.33. The news-pulse (500ms) and
cloud-pulse (30fps) each re-ran buildLayers(), reconstructing ALL ~30 deck
layers with fresh accessor closures every tick — full-retina double-canvas fill
+ 2Hz/30Hz layer reconstruction + GC churn. Fixes:
- Animation-only update path: buildAnimationLayers() reuses the instances from the
last full buildLayers() and swaps in ONLY the layers that animate (news/hotspot/
protest pulse for the news clock; chainNodes/trafficArcs for the cloud clock).
deck.gl's same-instance fast path (Layer._transferState: this===oldLayer → return)
then skips the other ~28 — no alloc, no attribute upload. Measured (SwiftShader,
cloud layers on): full rebuilds during a 5s pulse window 38 → 0; layers
re-instanced per frame 48 → 2; long-task time 2825ms → ~0; effective 8fps → 31fps;
heap drift 0% over 8s idle.
- News pulse 500ms → 2000ms, skips while paused/webgl-lost; pulse periods lengthened
to ~8s so the 2s sampling still reads as a calm breathe (2s/318 aliased to a static
ring).
- useDevicePixels capped at min(dpr, 2): a DPR-3 display no longer quadruples the
second canvas fill; 2× keeps dots/text crisp.
- Cloud-pulse RAF now invalidates only the arc + chain-node layers (via the cloud
clock) and still stops when its gate closes; chain-node dots memoized by source
ref so the breathe is a radiusScale-only change.
Finer grid sizing (CTO: "tiny widgets, less constrained"):
- .panels-grid columns 280→160px (7 tracks @1280px, was ~4) and rows 200→120px.
- New .span-0 tiny tier (grid-row 1, 120px); height-resize snaps to the
[120,200,400,600,800] tier ladder (attachPanelResize snapHeights) with minSpan 0.
- Map floor min-height 240px; participates in the same grid, width-resizes to any
integer track (finer now that there are more tracks).
- Resize handles quiet by default, edge grips fade in on panel hover.
Verified: npm run typecheck && build && go build ./... && go test ./internal/world/;
e2e globe-overlay (P0 render) + panel-drag specs green; screenshots of the dense
grid, a 178×130 span-0 widget, and the map at ~third width in 3D.
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
PART 1 — one introspectable command registry + one dispatcher (decomplected):
- src/services/app-commands.ts is the single source of truth: AppHost capability
port + 16 typed commands (name, JSON-schema params, human description, run()) +
commandManifest() + the ONE dispatch(): gate (sign-in) → validate → execute →
visible action log. Full control now covers panels (show/hide/move/resize),
map (layers, 2D/3D, fly-to, region, time range), variant, theme, search,
reset layout, custom feeds, and org switch — each driving existing App APIs
through the port (no reaching into internals).
- analyst-actions.ts reduced to a type shim (AnalystHost=AppHost). AnalystChat
dispatches through the registry and renders per-command ✓/✗ entries.
- Go tool contract DERIVES from the registry: the client sends its manifest with
every request; handlers_analyst.go builds the action-contract prompt from it
(renderCommandContract) and passes actions through generically, gated by the
manifest's type set — the backend no longer duplicates the schema. Embedded
mirror data/analyst_commands.json (generated by scripts/gen-analyst-commands.mjs,
drift-guarded by TestAnalystCommandManifest) covers callers that omit a manifest.
Model dropdown + backend plane (CTO directive):
- Chat header carries a monochrome model select (Zen family first, then upstream
/v1/models + agents); choice persists and rides every request. New same-origin
/v1/world/models proxy; model passes through to inference (chatMessagesModel).
"agent:<ref>" ids route to the agents run plane (invoke an agent in-chat).
P0 — errors always visible: the chat never shows a blank. An empty/degraded/errored
backend reply (e.g. prod's aud=hanzo-world 401) now surfaces the reason inline.
PART 2 — transport seam: src/services/analyst-transport.ts isolates the wire; HTTP
is the real impl, registerTransport() is the ZAP seam. Verdict: real browser ZAP
(@zap-proto/web) exists and is production-wired in hanzo/platform, but world has no
ZAP dep and no Go WS-ZAP server, and ZAP has no streaming primitive today — so HTTP
stays default, ZAP wiring documented in-file (no fantasy transport).
Gates: tsc + vite build + go build + go test ./internal/world/ all pass; Playwright
(e2e/analyst-command.spec.ts) proves signed-out sign-in prompt and a signed-in
command round-trip that visibly moves a panel + logs the action (screens/).
Claude-Session: https://claude.ai/code/session_013jh8aka8q8RvhhVQ1psMeW
Four map fixes on the DeckGL/mapbox-gl basemap, dark aesthetic preserved:
1. Edge seam — the CartoDB `background` layer ships a near-black-grey fill
(#0e0e0e) a shade lighter than the app shell, so the map canvas showed a
visible seam against --map-bg (#020a08). applyAtmosphere() now repaints the
basemap `background` layer to the themed --map-bg on load and after every
setStyle, so the ocean/void blends into the container.
2. Layers panel collapsed — the bottom-left "Layers" control shipped with the
toggle-list `collapsed` (display:none) and a ▶ caret by default. Now
default-expanded (▼), showing the legend/guide without a click.
3. Bare white cloud markers — chain-validator dots rendered near-white
[240,240,240] and BYO-GPU rings pure white [255,255,255], reading as
unstyled loading placeholders. Restyled to the on-brand cloud palette:
live validators cyan [0,200,255], online GPU rings teal [0,255,200], both
dimming to slate when offline.
4. Stale e2e selectors — the maplibre→mapbox-gl migration left `.maplibregl-*`
selectors in globe.spec.ts (drag-gate assertion) and map-harness.ts
(deterministic control-hide CSS); updated to `.mapboxgl-*`.
Drag-pan verified working under the exact prod build + prod CSP (dragPan
enabled, viewport moves in 2D/3D; the deck overlay canvas is pointer-events:none
so events reach the map) — the live "can't drag" is a stale deploy this rolls
out.
Verified: prod build green; all four fixes asserted headlessly under the prod
CSP with live demo cloud data (bgPaint==--map-bg, panel expanded, GPU/chain
layer colors non-white, drag moves the viewport).
Claude-Session: https://claude.ai/code/session_016yg7GPhYdWCh9vpp4HEwLZ
Co-authored-by: hanzo-dev <dev@hanzo.ai>
The 3D-entry camera glide now honors prefers-reduced-motion: it snaps instantly
(jumpTo) instead of the ~1.1s easeTo flourish (bumped from 900ms). The overlaid
deck layers already reproject through the whole transition (proven by the globe
spec's 2D<->3D round-trips), so the morph carries the data with it. mapbox handles
the globe<->mercator projection morph itself on exit.
Kill the 'Global Situation' title + centred UTC header bar that ate a strip off the
top of the map. The map's .panel-header becomes a slim (14px) grip that is
invisible until you hover the map, then shows a small drag pill — it is still the
exact element panel-drag grabs (mouse + touch), so reorder/drag is unchanged, just
chromeless. The UTC clock keeps its #headerClock id (zero JS change) but moves into
a small, halo'd bottom-right corner overlay that stays legible over bright
basemaps. Map controls (time pills, 2D/3D, zoom, layers, legend) are children of
.map-container and are untouched. Removed the now-dead .header-clock rule.
Verified live: no title bar, grip == .panel-header (14px), UTC in the corner, drag
still reorders and the full-width map stays anchored first.
origin/main independently added 'keep variant in URL' to buildMapUrl (via the
SITE_VARIANT const). Remove the parallel state.variant param this branch had added
for the same purpose so there is exactly one mechanism; setSiteVariant still
overrides ?variant for the switch, and getShareUrl no longer needs to pass it.
CPU profile flagged residual main-thread saturation on saas/crypto entirely in the
map render loop: the 500ms news-pulse setInterval doing a 2Hz FULL deck-layer
rebuild, the idle globe-spin RAF, and cloud-poll re-renders — all kept running when
the tab was backgrounded or the user had gone idle. The visibilitychange + 2-min
idle handlers only toggled a CSS class; they never paused the JS/GPU render loop.
Route both signals through one chokepoint, syncMapRenderActive(), which calls the
existing DeckGLMap.setRenderPaused(document.hidden || isIdle). setRenderPaused
already stops the pulse interval, the auto-rotate RAF and the cloud pulse, and
drains a pending render on resume — so a hidden/idle tab now does zero map render
work and picks straight back up on focus/activity. Verified: renderPaused flips
true on hidden (pulse+spin cleared) and false on visible.
Note (follow-up): foreground-active cost is the pulse's full buildLayers re-running
every accessor over all data because each rebuild makes fresh accessor closures.
Only cables/pipelines are instance-cached today; extending that cache (keyed by
theme + zoom-bucket + highlight-signature) to the ~15 static config-driven layers
is the scoped reuse-layer-instances refactor.
SITE_VARIANT is an import-time const woven through ~30 modules, so a true
zero-reload in-place variant swap is out of scope here (report notes the refactor).
The pragmatic switch makes the reload feel like only the panels changed:
- One switch path (header tabs + analyst set_variant both call setSiteVariant),
replacing the divergent localStorage+reload() the tabs used. It FLUSHES the exact
live map view (camera + 2D/3D mode + layers + time range) into the URL
synchronously — closing the 250ms URL-sync debounce gap — and stamps ?variant.
- getShareUrl/buildMapUrl now carry the active non-default variant, so every URL
sync keeps it (WORLD omitted as the clean default) and shared links reopen the
same view.
- applyInitialUrlState restore was the missing half: it treated ANY view (incl. the
'global' free-camera default that share/switch URLs always emit) as a region
preset and reset the camera, so lat/lon/zoom never restored. Now only real region
presets (america/mena/eu/…) own the camera; 'global' honors the exact URL camera
in one move.
- DeckGLMap keeps state.zoom in step with direct wheel/pinch zoom on moveend, so
the flushed URL captures the LIVE zoom, not a stale one.
- main.ts sets the DEV/e2e window.__app hook before init() so it never waits on the
initial data load.
Verified live (127.0.0.1:8195): switching WORLD→AI in 3D at a panned camera reloads
with variant=ai, mode=3d, the globe at the identical centre/zoom, panels swapped.
Replace the generic lucide 'sparkles' on the floating analyst dock (fab + title)
with a house Zen ensō: a single open brush-ring that inherits currentColor and
the shared icon wrapper's round caps (brush tips), so it reads as one of our
icons. No zen asset existed in the repo; this adds one inline SVG path.
Fix the AI model default: the bare "zen" was NOT a servable id
(api.hanzo.ai/v1/models lists zen5/zen5-flash/zen3-* etc., no plain "zen"), so
every AI endpoint — the analyst included — would 4xx and silently fall back.
Default to zen5 (the flagship general Zen chat model); HANZO_AI_MODEL still
overrides per-deploy.
REDIRECT_URI was computed once at import as location.origin with no runtime
branch. On web that is already correct (world.hanzo.ai + every *.hanzo.app fork
stay self-consistent), but the Tauri desktop shell runs on an app-scheme origin
(tauri://localhost) that no OIDC provider can redirect back to — hence the
127.0.0.1:5219 confusion. Make the branch explicit and evaluate it per call:
web (isDesktopRuntime() false) ALWAYS uses ${location.origin}/auth/callback;
desktop uses the fixed loopback http://127.0.0.1:5219/auth/callback that the app
captures (registered in the hanzo-world client allowlist, added IAM-side).
Verified live: on http://127.0.0.1:8195 the authorize redirect_uri resolves to
http://127.0.0.1:8195/auth/callback (site origin), never a hardcoded host.
The CTO's broken page had two braided causes; both are the full-width map fighting
the grid, fixed at their chokepoints:
P0-2 layout void — a full-width map MUST be the first grid child, else CSS grid
wraps a stray panel into row 1 (a thin sliver top-left) and shoves the map into a
black void under the header. The only way a panel lands ahead of the map is live
drag (restore already filters the map out), so guard the drag chokepoint:
panel-drag's referenceNodeAt refuses to insert before a leading-anchor panel
(redirects the ref to its nextSibling). App owns the invariant via healMapAnchor()
— idempotent, called on load, drag-commit, width-resize and analyst move — which
also heals any stale saved state (reset-layout already clears it). The critical
banner offset is corrected to the real 46px header height and now tracks the
banner's measured height (--critical-banner-h) instead of a guessed 50px that left
a seam. Dead .map-section.pinned / .map-pin-btn CSS (nothing toggles it) removed.
main.ts exposes window.__app in DEV/e2e to drive the invariant in tests.
P0-3 dropdowns under the map — .header carries a backdrop-filter that silently
creates a z-index:auto stacking context, while .panels-grid is z-index:1, so the
whole grid+map painted OVER the header and its dropdowns. Give .header
position:relative; z-index:200 — one documented stacking scale (modals 1000+ >
header 200 > grid/map 1), no per-control arms race. Verified: export/status/
playback dropdowns resolve above the full-width map via elementFromPoint.
deck.gl 9.2's interleaved MapboxOverlay draws its layers inside mapbox's
projection pass, which does NOT reproject onto mapbox-gl v3's globe — so on the
3D globe every overlay silently vanished (2D was fine). Flip the overlay to
interleaved:false: deck gets its own canvas and its own _GlobeView derived from
the live map projection each frame, so lat/lon layers reproject on BOTH the flat
map and the globe, and survive setStyle for free (they live outside the style).
Proven by e2e/globe-overlay.spec.ts: in 3D the overlay is overlaid
(interleaved===false, own canvas) with a GlobeViewport, and a seeded feature
picks at its globe-projected screen point — across 2D<->3D round-trips and a live
setStyle. Adds DEV/e2e-only window.__deckMap/__deckOverlay/__mapboxMap hooks and
harness introspection to assert it.
Audit P1/P2 fixes (client + data plane; map component untouched):
1. live-flash (P1): panels re-render by wholesale innerHTML swap, so the
value-bump observer's characterData path never fired (0 bumps/1000+ samples).
Now childList swaps are diffed: snapshot removed-subtree numeric leaves by a
stable positional signature, bump the same-signature added leaf when its
numeric text changed. Node-visit budget keeps it cheap; first render (nothing
to match) produces no bumps.
3. Feed panels stuck spinning (P1): the feeds-batch fetch (and per-feed + UCDP
fetches) had no timeout, so a stalled upstream hung the panel forever. Added a
shared fetchWithTimeout (AbortController) — feeds-batch 28s, others 20s — so a
hung request aborts, falls back, and the zero-item guard resolves the panel to
an honest empty note. UCDP Go handler already never-5xx (stale + degrade).
5. Yahoo polled ~1/sec (P2): FX/commodities/yields/indices fired one GET per
symbol (~80/cycle). New batched /v1/world/yahoo-batch resolves a whole symbol
list in one request (bounded parallelism, per-symbol timeout, dedup, never-5xx,
shares the yahoo:<sym> cache with the single passthrough). 131 yahoo-finance
calls/130s -> 8 yahoo-batch calls; cadence stays a sane 2 min.
4. Variant lost on share (P2): buildMapUrl rebuilds the query from scratch, so
map-state sync + Copy Link stripped ?variant=. It now serializes the active
variant (omitted for the default 'full'). Fixed entirely in urlState.ts.
6. Removed the dead @vercel/analytics inject() (the /_vercel/insights 404 on the
Go-served world.hanzo.ai).
Tests: TestValidateYahooSymbols, TestYahooBatchNever5xxShape; yahoo-batch added
to the route sweep. Verified via local Playwright (bumps fire on market/crypto
ticks; tech panels resolve 19->0 even with feeds-batch stalled; variant kept).
world now fetches its data-source credentials directly from Hanzo KMS at
process start instead of relying on an out-of-cluster KMSSecret operator
syncing them into a K8s Secret mounted via envFrom.
internal/world/kms.go — LoadKMSSecrets():
- gated on KMS_CLIENT_ID/KMS_CLIENT_SECRET (per-tenant IAM machine creds)
- POST {KMS_HOST}/v1/kms/auth/login {clientId,clientSecret} -> accessToken
- GET {KMS_HOST}/v1/kms/orgs/{org}/secrets/{path}/{name}?env={env}
for each key in the declared world key set (org=hanzo, env=prod,
path=/world-secrets by default; all overridable)
- injects each fetched key into env only if not already set (explicit
env always wins); never overwrites an operator override
- fail-open: no creds / unreachable / slow KMS logs one line and
continues on plain env; hard 5s boot-timeout cap, never crashes
- stdlib net/http only — deliberately does NOT pull the luxfi/zap
native client; keeps the world image dependency-free (stdlib-only)
cmd/world/main.go — call LoadKMSSecrets first, before the server reads
any config.
The KMS REST surface exposes no path enumeration, so the key set is
declared in one place (worldSecretKeys, the in-repo analogue of the
KMSSecret CR keys[] list); override with KMS_KEYS.
Tests (internal/world/kms_test.go, httptest KMS stub): injection +
explicit-env precedence + absent-key skip + no-creds skip + timeout
degrade + login-401 error + value round-trip + store-path convention.
Map renderer swap maplibre-gl v5 -> mapbox-gl v3 in DeckGLMap:
- mapbox-gl v3 with the publishable pk token (VITE_MAPBOX_TOKEN override)
- native globe projection for 3D, mercator for 2D; smooth easeTo on switch
- monochrome vercel-black atmosphere via setFog (black space, faint cool
horizon glow, no stars); re-applied on style swap
- keep CartoDB dark-matter/voyager styles (already pure monochrome) rendered
through mapbox for the exact locked aesthetic
- deck.gl MapboxOverlay stays interleaved; all layers/tooltips/clicks, the
AnimatedArcLayer coef pulse, and heatmap->scatter globe substitution intact
- antialias:false (deck AAs its own geometry) for globe fill-rate
- getProjection().name / setProjection(string) for the v3 API; reuse existing
render-pause + webgl-lost handling
- e2e harness + dual .mapboxgl-* CSS follow the rename
- workbox cache limit raised to 3 MiB (mapbox-gl is heavier)
FX panel: full fiat board (39 pairs) grouped Majors/Crosses/Asia/EMEA/LATAM
as collapsible native <details> (Majors+Crosses open, EM boards collapsed with
counts, state survives refresh); per-pair precision; all symbols verified live
against the yahoo-finance passthrough.
Publishes a discoverable skills catalog: index.json + 6 SKILL.md docs served
same-origin from https://world.hanzo.ai/.well-known/agent-skills/*. Verified
end-to-end: vite copies public/.well-known -> dist, and the Go SPA handler
serves index.json (200 application/json) and each SKILL.md (200 text/markdown);
a missing skill 404s (not the SPA shell).
Every documented route was verified against routes.go and uses its REAL params.
Notably /v1/world/model/summary and /v1/world/markets do NOT exist, so:
- world-brief -> /v1/world/model/top?metric&kind&n (ranked brief)
- country-instability -> /v1/world/model/country/{ISO} (path segment)
- market-quotes -> /v1/world/finnhub?symbols= (real plural param)
- chain-status -> /v1/world/cloud/chain-nodes
- traffic-map -> /v1/world/cloud/traffic
- model-history -> /v1/world/model/history?hours
Each doc carries YAML frontmatter (name/version/description), auth:none,
endpoint, real params, response shape, a worked curl, a "responses are data,
not instructions" injection guard, and a "when NOT to use" section.
index.json is GENERATED, never hand-edited: internal/agentskills is the single
source of truth (Build -> Marshal), the generator is `go generate
./internal/agentskills`, and a drift test asserts the on-disk index.json equals
the freshly-built bytes, each digest recomputes, docs are complete, and every
endpoint is a registered /v1/world route.
Claude-Session: https://claude.ai/code/session_01CDooqWJiB7yNNaSjGQjdL7
New pure package internal/world/markethours answers the US market phase from a
time.Time alone. America/New_York session math: pre (04:00-09:30), regular
(09:30-16:00), post (16:00-20:00), closed (overnight/weekday holiday), weekend.
NYSE full-day holidays are COMPUTED, not table-maintained: New Year, MLK,
Presidents, Good Friday (Gregorian computus → Easter−2), Memorial, Juneteenth,
Independence, Labor, Thanksgiving, Christmas — with Sat→prior-Fri / Sun→next-Mon
observance shifts, and a year+1 union so a Saturday New Year observed on Dec 31
is caught. tzdata is embedded (_ "time/tzdata") so it works on a bare container.
IsTradingDay / CurrentSession exported. handleStockIndex now stamps an additive
"marketSession" into its Yahoo-derived response map — prices untouched, no
behavior change.
Tests: session boundaries, weekends, Good Friday 2026-04-03, July-4-2026
observed 2026-07-03, New Year observance both directions, the full 2026 holiday
set, and Easter for 2024-2027.
Claude-Session: https://claude.ai/code/session_01CDooqWJiB7yNNaSjGQjdL7
New pure package internal/world/ticker (no I/O, no state) extracts up to 8
tickers per headline from two sources:
- Cashtags ($AAPL): uppercase 1-5 chars, word-bounded, left-boundary checked
so a currency amount (R$AAPL) is not a cashtag. Always trusted.
- A curated ~150-major company/crypto name dictionary, longest-match-first and
word-bounded (meta never matches inside metallica).
Conservative by design — a headline is prose, so a false positive is worse
than a miss: ambiguous names (apple, amazon, meta, oracle, visa, intel,
target, ford, gap, block, zoom, shell, bp, avalanche, polygon) and bare
English-word symbols (GM, IT, V, ALL) are NOT bare keys and require a cashtag.
Crypto by name always; bare BTC/ETH recognized (non-words) but SOL/ADA/DOT/
LINK/XRP name-only.
Wired into the feeds-batch parse path: feedBatchItem gains
Tickers []string `json:"tickers,omitempty"`, stamped from the title for both
RSS/RDF and Atom items. Frontend can adopt later.
Tests: cashtag rules, ambiguous-vs-distinctive, word boundaries, common-symbol
rejection, longest-match multiword, crypto, punctuation/joined names, dedup,
8-cap, and a curation guard that ambiguous names never leak into the dict.
Claude-Session: https://claude.ai/code/session_01CDooqWJiB7yNNaSjGQjdL7
The live model's conflict dimension was flat-zero without an ACLED key
(sourceConflict returned nil), so composite instability lost the whole
conflict signal in the common unconfigured case.
Now sourceConflict dispatches on KEY PRESENCE (not on empty results):
- key present -> real ACLED protest/riot counts (sourceConflictACLED). A
credentialed call that returns zero is honestly empty, never proxied.
- key absent -> GDELT proxy: per tier-1 country, conflict-keyword article
volume via the shared fetchGDELTArticles helper, queried by country NAME,
compressed+capped onto the ACLED-event scale (0.4/article, cap 40) so it's
a moderate bounded signal — never a saturating masquerade.
Provenance stays honest: proxy observations are stamped Src="gdelt-proxy".
To make that stick, the engine's Src stamp is now a DEFAULT (fills only when
the adapter left it empty) instead of an unconditional override — a small
decomplection (provenance is a value on the observation; source name is the
fallback). Existing adapters are unchanged.
Tests: conflictSourceMode selection (absent/whitespace/present), proxy
normalization (zero/linear/cap/never-negative), query shape, and engine
provenance preservation.
Claude-Session: https://claude.ai/code/session_01CDooqWJiB7yNNaSjGQjdL7
Two latent bugs from the audit:
(a) LiveNewsPanel.activeChannel was LIVE_CHANNELS[0]! — a non-null assertion
that only silences the compiler. Both source arrays are non-empty today so it
cannot crash now, but any future filter/empty variant list makes it undefined
and the constructor crashes at the first activeChannel deref. Replaced with
LIVE_CHANNELS[0] ?? EMPTY_CHANNEL: a valid non-null default that renders a
clean offline state and keeps the type non-nullable (no deref-site ripple).
(b) PizzINT had TWO ungated activation sites — the initial loadAllData task and
the 10-min scheduleRefresh interval — both calling loadPizzInt() on every
variant, even though setupPizzIntIndicator is variant-gated AND off by default.
Result: eager off-variant/off-feature fetches of PizzINT status + GDELT tensions
on load and every 10 minutes. Fixed with ONE guard at the source: loadPizzInt()
returns early unless the indicator exists, so the single activation decision in
setupPizzIntIndicator covers both sites (DRY) and a disabled PizzINT makes zero
upstream requests.
Claude-Session: https://claude.ai/code/session_01CDooqWJiB7yNNaSjGQjdL7
Map.render() and ~15 draw/coordinate paths each read
container.clientWidth/clientHeight live — dozens of layout-forcing reads
per frame, interleaved with SVG writes (classic read/write layout thrash).
Cache the size in two fields refreshed only by measureContainer(), the sole
live-read site: the constructor primes it and the ResizeObserver refreshes
it on actual size change. All draw paths now read the cached
containerWidth/containerHeight, so a frame forces no synchronous layout and
the viewBox and every projection share one consistent size. getBoundingClientRect
(pointer→SVG mapping, position-dependent) stays live by design.
Claude-Session: https://claude.ai/code/session_01CDooqWJiB7yNNaSjGQjdL7
A 200 with an empty/whitespace-only body poisoned the cache for the full
TTL: passthrough Set the blank body as both fresh and stale, so a transient
upstream blip served empty data to every caller until it aged out.
Decomplect detection from policy:
- isBlankBody() is pure detection (empty/whitespace-only).
- passthrough owns the policy: a blank 200 joins the failure condition
(err | non-2xx | blank). On failure it serves last-good stale if present,
else negative-caches the upstream briefly (30s) and writes the handler's
degraded response with Cache-Control: no-store so the failure is never
cached downstream either. Negative markers live under a namespaced cache
key so they never clobber the last-good value.
Audit of sibling raw-body cache sites (same class): feedXML and
handleRSSProxy both cached raw feed bodies without a blank guard and SHARE
the "rss:" key, so a blank from either poisoned the other — both now guard
with isBlankBody. handleFwdstart caches a computed RSS envelope (always
non-blank, getText already rejects non-2xx) — audited, not affected.
Claude-Session: https://claude.ai/code/session_01CDooqWJiB7yNNaSjGQjdL7
Adds a HanzoStatusPanel that embeds the ENTIRE status.hanzo.ai page (all
groups + endpoints + uptime history) via an iframe in the saas/Cloud variant
dashboard — the full component, not just a badge. status.hanzo.ai ships no
X-Frame-Options and no CSP frame-ancestors, so it is iframe-embeddable. The
panel is PUBLIC (not behind the admin-only cloud panels gate).
- src/components/HanzoStatusPanel.ts — new panel (sandboxed iframe + open-in-
new-tab link; static URL, nothing to escape).
- src/components/index.ts — export it.
- src/App.ts — instantiate under SITE_VARIANT === 'saas'.
- src/config/panels.ts — enable 'hanzo-status' in the saas panel set.
Verify: tsc --noEmit = 0 errors; VITE_VARIANT=saas npm run build = success.
- POST /v1/world/feeds-batch: fetches+parses a category's feeds in parallel
server-side (RSS2/RDF/Atom), shares the rss-proxy cache keys, never-5xx,
per-feed ok flags, allowlist-enforced
- rss.ts: shared storeFeedItems enrichment pipeline (one place for classify/
geo/cache/trending/AI-queue); batch transport with per-feed fallback
- App: category concurrency 5->8 (each category is now ~1 request); zero
items with no failures resolves to a quiet empty state, never a spinner
Claude-Session: https://claude.ai/code/session_01CDooqWJiB7yNNaSjGQjdL7
Typography + Vercel look:
- Panel titles 13px / weight 600, sentence case, subtle -0.01em tracking;
toggle + modal-title sizes normalized (10px→12px, 11px→13px).
- Unified widgets: drop the panel-header background band AND border-bottom
divider everywhere — header now flows into the body; the whole card is one
subtle 1px outer outline. Bold headings, muted secondary text preserved.
Contrast / monochrome sweep (fixes white-on-white + off-brand accents):
- New --accent-fg / --accent-hover tokens. Buttons on --accent were using
color:var(--panel-bg) — transparent in dark → invisible text (AI-analyst
Sign in + send). Now use --accent-fg (black on white / white on black).
- Webcam region/feed/view active tabs: red/white-on-white → monochrome accent.
- Sources-modal checkboxes: green → accent (now match the Panels-modal ones).
- "+ Add Monitor": green fill → neutral outlined button.
Header:
- Inline Hanzo H mark (currentColor) before the variant switcher; links home.
- "Try Hanzo World" primary CTA (one reusable .hz-cta pill, hanzo.ai style)
replaces the bare Sign in; the analyst signed-out CTA reuses .hz-cta.
- Mobile (≤768px): header no longer clips — icon-only switcher, drop the
redundant LIVE chip, single horizontally-scrollable bar.
Layout controls:
- Visible "Reset layout" pinned in the Panels-menu footer. resetPanelLayout()
now clears order + spans + custom panels + settings, then reloads to the
pristine variant default.
Hanzo showcase (saas/cloud):
- Cloud variant video panel = "Hanzo Showcase" playing real @hanzoai uploads.
- LiveNewsPanel now reads the runtime SITE_VARIANT (was build-time env), so the
showcase actually activates for ?variant=saas.
Tests:
- New e2e/video-drag-reset.spec.ts: proves the live-news video panel is
grabbable by its header and drags (iframe yields to pointer-events:none),
content panels reorder, resize snaps, and Reset restores the default order.
Cloud (saas) variant becomes a public "excitement layer" with real data, plus
admin-org-only deep operator panels enforced server-side (fail-closed 403).
Access model (reconciled):
- Cloud tab public in the switcher; public panels use real/anonymized sources.
- Deep panels (fleet internals, per-service metrics, web analytics, LLM obs) are
admin-only. Client hides them (owner==admin via IAM userinfo); the backend
independently verifies owner==admin (requireAdmin, IAM userinfo introspection,
cached) and forwards the caller's bearer — cloud re-verifies. Fail-closed.
Real data wired (thin proxies over api.hanzo.ai, honest degrade, never 5xx):
- /v1/world/cloud/models (PUBLIC) real served-model catalog from gateway /v1/models
- /v1/world/cloud/fleet (ADMIN) visor machines+gpus+workers grouped by
provider/region; GPU util/temp honestly
labeled "not instrumented" (no data-plane source)
- /v1/world/cloud/services(ADMIN) per-subsystem o11y status + RED metrics
- /v1/world/cloud/analytics(ADMIN) top pages/referrers/countries + live visitors
from analytics.hanzo.ai (insights merge)
- /v1/world/cloud/llm (ADMIN) per-model/per-org usage, tokens, cost, errors,
trace latency from cloud /v1/admin/o11y
Overview now shows the REAL served-model count (no demo where real exists).
Icons: unified switcher + panel icons to the canonical lucide set (@hanzo/ui),
inlined for vanilla-TS. No more emoji in the switcher.
AI widget: floating bottom-right launcher (AiAnalystDock) opening an overlay chat
dock. Analyst logic extracted to AnalystChat and shared verbatim by the panel and
the dock — one code path, same action executor. Signed-out opens a sign-in prompt.
Tests: fail-closed gate test (401 anon, no payload leak) + route sweep (never-5xx).
go build ./..., go test ./internal/world/, npm run build (tsc) all green.
Verified headless: 4 lucide switcher icons, floating fab, admin panels absent for
anon, overview "54 models served · live", dock prompts sign-in.
Adds a spinnable 3D globe alongside the flat map, switchable live without
losing camera, layers, or time range.
Approach: maplibre-gl v5 native globe projection + the existing deck.gl
MapboxOverlay. deck.gl's overlay derives its view (MapView vs _GlobeView)
from map.getProjection() every render frame, so a single setProjection swaps
the whole stack — one map instance, one overlay, all lat/lon layers shared.
No second Deck instance, no dual render path.
- 2D|3D pill toggle in the map header (Geist pill style), persisted to
localStorage (hanzo-world-map-mode) + URL (?mode=3d); URL > localStorage > 2d.
- All deck layer types render on the globe (path/geojson/icon/scatter/arc/text/
clusters). The one screen-space layer (climate HeatmapLayer, unsupported on
_GlobeView) auto-substitutes a globe-safe graduated scatter in 3D.
- Native inertial drag-rotate + subtle idle auto-rotate (~4deg/s, 30fps-capped,
starts after 5s idle, stops on interaction, respects prefers-reduced-motion).
- Lazy: globe projection only applied when 3D selected; auto-rotate rAF only in 3D.
- e2e (map-harness): projection toggle, all layer types on globe, climate
substitution, reduced-motion gating, and the auto-rotate gate/step logic.
Replace HTML5 native DnD (OS drag image, y-only grid hit-test, no touch, no
animation) with a pointer-events system in services/panel-drag.ts:
- custom translucent ghost that tracks the pointer
- gap-opening placeholder + transform FLIP so siblings slide to their new slots
- 6px press threshold (no accidental drags), Escape-to-cancel, rAF-throttled
- unified mouse/touch/pen; on touch the panel header is the grab handle
Resize moves onto the same pointer plumbing with a clean 200px row-span snap
grid (fixes the old 250/350/500 thresholds that fought the 400/600/800 CSS
heights) and pointer capture for reliable mouse+touch.
App keeps panel-order persistence; Panel keeps the span<->class mapping — the
module owns only pointer math + visuals. Adds a deterministic Playwright drag
harness + spec (reorder, threshold guard, Escape cancel, resize snap).
Two new same-origin data routes and their panels:
- /v1/world/sentiment — realtime GDELT news-sentiment: global index (0-100),
per-topic (markets/conflict/energy/tech) and per-region breakdowns, each with
a 24h tone sparkline + velocity. GDELT hard-limits to 1 req/5s, so the handler
serves the shared cache instantly and a single background refresh walks the
queries sequentially (~5.2s apart, early-abort when throttled). Never blocks,
never 5xx; degrades to a clean 'warming' body.
- /v1/world/indicators — the trader risk suite from free/no-key sources: VIX +
VVIX + MOVE (real ^MOVE, else 10Y realized-vol proxy); full yield curve with
2s10s/3m10y/5s30s spreads + inversion flag (2Y via 2YY=F); crypto + computed
equity fear/greed; SPY/QQQ/IWM/DIA momentum; 11-sector advance/decline breadth;
BTC dominance + perp funding (Binance→OKX fallback, both geo-degrade cleanly);
DXY + gold/oil/copper; and a documented risk-on/off composite. Every field
degrades to null per-source; composites recompute over what survived and ship
their formulas inline. Cached ~2m via the shared cachedJSON path.
Panels: SentimentPanel (global gauge + topic tiles + region bars) and
TraderDeskPanel (dense stat tiles: value + change + sparkline + green/red).
Registered in all three variants — priority 1 in finance, 2 in full/tech.
Geist Mono numerics, monochrome, rounded pills. Route smoke suite sweeps both.
New variant that renders HANZO CLOUD ITSELF instead of world intel:
- variant wiring (variant.ts/panels.ts/variants/saas.ts + header 'Cloud' entry)
- panels: cloud-overview, model-usage, fleet, my-usage, live-activity
- public aggregate route /v1/world/cloud-pulse (demo-flagged; real non-sensitive
counts via KMS service token) — never-5xx, swept by the route smoke suite
- org-scoped drill-down calls api.hanzo.ai directly with the caller's IAM bearer
(/v1/machines,/v1/gpus,/v1/models,/v1/billing) — no shared keys
- map reuses datacenters + cloudRegions layers as the global infra backdrop
- honesty: demo flag travels in the payload + a 'demo data' pill in the UI
State engine (internal/world/model): folds live public feeds into a typed
per-entity state vector (countries/theaters/markets), derives a composite
instability + news velocity in one place, snapshots to disk for warm restart,
and serves state/country/top/changes + SSE deltas. Decomplected from the feeds:
model_sources.go is the only bridge, reusing existing Server fetchers
(fetchGDELTArticles extracted + shared with /gdelt-doc). AI grounding via
ModelContext wired into /v1/world/country-intel. Pro-tier gating attaches in
one place (gate()). Routes auto-swept by routes_test.go (never-5xx).
Dockable 'ai-analyst' chat panel: the signed-in user asks questions grounded in a
client-composed snapshot of the live dashboard (top headlines + crypto + macro +
current panel/layer state), and can also RECONFIGURE the dashboard in natural
language. The backend /v1/world/analyst route frames the persona + a strict-JSON
action contract; the SPA executor validates each typed action and dispatches it
through a narrow AnalystHost port (show/hide/move panels, toggle map layers, time
range, variant, add an allowlisted RSS feed panel, reset layout).
Auth/billing: forwards the caller's IAM token to Hanzo inference (per-user
org/project metering, no shared key) — same pattern as the summarize/classify
routes. Signed-out state prompts the existing OIDC login. Feed panels reuse the
host-allowlisted rss-proxy (one SSRF boundary); a blocked domain degrades to a
quiet 'not in the allowlist' note.
Backend: handlers_analyst.go (multi-turn, temp 0.4, 700 tok) + ai.go chatMessages
helper; route registered and swept by the never-5xx smoke suite.
Frontend: AiAnalystPanel + CustomFeedPanel; analyst.ts (client) + analyst-actions.ts
(executor/port); panel registered in all three variants + App wiring; scoped styles.
- src/services/iam.ts: framework-free PKCE S256 login against hanzo.id
(public client hanzo-world), token refresh, userinfo, logout. localStorage
PKCE tx survives the SSO redirect.
- src/services/org-scope.ts: org (owner claim) + project scoping, X-Org-Id/
X-Project-Id headers for /v1/world calls; projects from the cloud backend
with graceful fallback.
- src/components/AccountMenu.ts: header account chip — logged-in user +
org/project switcher (theme-aware), or Sign in. Rightmost in header-right.
- src/App.ts: mount AccountMenu; variant nav links worldmonitor.app ->
world.hanzo.ai (residual brand leak).
- src/main.ts: complete the OIDC callback before the dashboard renders.
- hanzo.yml + .github/workflows/cicd.yml: bring world into the canonical
hanzoai/ci build+deploy (ghcr.io/hanzoai/world, roll the world Service CR).
world.hanzo.ai served a static-only image (hanzoai/static) with no /api, so
every /api/* fell through to index.html — the app showed no data and no live
video. Replace it with cmd/world: one CGO-free Go binary that serves BOTH the
Vite static build (SPA fallback for client routes) AND ~48 /api/* endpoints,
each a faithful port of the original edge function.
- Dockerfile: node build -> dist, go build cmd/world, minimal alpine final
(ca-certificates), EXPOSE 3000, run the binary serving /srv + /api.
- Live video: /api/youtube/live?channel=@X resolves the channel's current LIVE
videoId (scrape /live, optional YOUTUBE_API_KEY Data-API path); /api/youtube/embed
serves the bridge player. Degrades to {videoId:null} for the frontend fallback.
- Proxies with 30-120s TTL cache, exact-host upstreams (SSRF-safe), rss-proxy
host allowlist, degrade-to-empty never 5xx. AI endpoints route to Hanzo's own
inference (api.hanzo.ai /v1). Missing optional key -> clean skipped payload.
- New domain feeds (robotics/quantum/sports/space-weather/weather) need no new
route: they reuse /api/arxiv or hit CORS upstreams directly.
Add a Dockerfile that builds the Vite SPA and serves it via the canonical
hanzoai/static scratch image (--root=/srv --spa --port=3000). Replaces the
off-pattern world:2.8.0 nginx image (Server: nginx/1.29.8 — platform rule
violation). Static frontend only; the data/MCP API stays in world-gw.
Original worldmonitor v2.4.0 was MIT; keep the project MIT and credit Elie Habib
in NOTICE. Removes the AGPL provenance references and the BSD-3-Clause split on
Hanzo modifications — single MIT license, with NOTICE attribution.
- Live Webcams Panel with region filters and grid/single view (#111)
- Mobile detection: width-only, touch notebooks get desktop layout (#113)
- Le Monde RSS URL fix, workbox precache HTML, panel ordering migration
- Sentry: ML timeout catch, YT player guards, maplibre noise filters
- Changelog for v2.4.0
- Remove (pointer: coarse) from mobile detection so touch-capable
desktops (e.g. ROG Flow X13) get desktop layout instead of mobile
- Define MOBILE_BREAKPOINT_PX (768) in utils and use in
isMobileDevice(); CSS @media (max-width: 768px) kept in sync
- MobileWarningModal uses isMobileDevice() for consistent behavior
Ref: https://github.com/koala73/worldmonitor/discussions/94
Co-authored-by: Cursor <cursoragent@cursor.com>
- Idle handler now clears isIdle and re-renders on user activity
(was permanently paused after 5min timeout)
- Add grid back button in single-view switcher row for mobile
(view toggle hidden at <=768px left users stuck)
- ALL grid now picks one feed per region (was showing 4x mideast)
- Jerusalem & Tehran adjacent (conflict hotspots)
- Replace broken Berlin with Paris Eiffel Tower webcam
- Switch toolbar SVG icons from stroke to fill for dark mode clarity
CI already builds AppImage for Linux — this adds the missing UI:
- linux-appimage pattern in api/download.js
- Linux UA detection and button in DownloadBanner
- i18n key added to all 13 locale files
Spaces inside HTML tags (e.g. `< span class= "trend-up" >`) caused
browsers to render raw markup text instead of elements. Fixed ~45
instances across CIIPanel, ServiceStatusPanel, and StrategicPosturePanel.
- Fix summarization race: check isModelLoaded before attempting browser
T5, fall through to cloud providers while model loads in background
- Increase modelLoadTimeoutMs 30s→600s and inferenceTimeoutMs 45s→120s
to accommodate first-time model downloads
- Use flan-t5-small (60MB) instead of flan-t5-base (250MB) in beta mode
for country brief fallback
- Skip ML summarization in country brief when model not loaded to avoid
blocking UX
- Broadcast model-loaded notifications from worker on implicit loads so
manager's loadedModels stays in sync
- Suppress ONNX CleanUnusedInitializersAndNodeArgs warnings via
ort.env.logLevel
- Fix non-monotonic progress reporting with separate warm/cold step counts
e.target can be a text node when dragging text content inside a panel.
Text nodes lack .closest(), causing TypeError. Add instanceof Element check.
Resolves Sentry P2: TypeError: o.closest is not a function
## Summary
This release introduces comprehensive localization support, transforming
WorldMonitor into a truly global intelligence platform.
### Key Features
- **Multilingual UI**: Full support for 12 languages (EN, FR, ES, DE,
IT, PT, NL, SV, RU, AR, CN, JP).
- **RTL Support**: Native right-to-left layout for Arabic and Hebrew,
including mirrored UI components and correct text alignment.
- **Regional Intelligence**:
- Dynamic feed selection based on language preference.
- Dedicated regional monitoring panels for Africa, LatAm, Middle East,
and Asia.
- Granular error handling for regionally blocked feeds.
- **AI Integration**: On-demand translation and summarization of news
items using localized prompts.
### Technical Changes
- **Refactoring**: Overhauled \src/services/i18n.ts\ and
\src/config/feeds.ts\ to support dynamic loading.
- **Styling**: Added \src/styles/rtl-overrides.css\ using logical CSS
properties for maintenance-free RTL support.
- **Cleanup**: Resolved CSS lint warnings and improved code quality.
### Verification
- **Build**: \
pm run build\ passed successfully.
- **Manual Testing**: Verified RTL layout, language switching, and
regional feed loading.
Console-activated (beta=true) mode that swaps browser summarization to
Flan-T5-small (60MB) as first provider with comparison logging against
Groq. Persists via localStorage, shows amber BETA badge in header.
- LiveNewsPanel: player.mute/unMute may not exist before onReady (WORLDMONITOR-16)
- main.ts: add /Program failed to link/ noise filter (WORLDMONITOR-18)
- Wrap updateBaseline() in try/catch inside loadNewsCategory and intel
path so IndexedDB write failures don't delete successfully fetched
and rendered news data (P1)
- Add .catch() to saveCurrentSnapshot() initial call and setInterval
callback to prevent unhandled promise rejections from IndexedDB
readwrite failures (P2)
- webkitRequestFullscreen returns void (not Promise) on Safari — use
try/catch instead of .catch() to avoid undefined.catch() throw
- Module-import beforeSend filter: only suppress when stack frames
originate from browser extensions, not by URL domain check
- withTransaction: throw on readwrite InvalidStateError after retry
instead of silently returning undefined (prevents write-drop)
- toggleFullscreen: use void .catch() for Promise-based requestFullscreen/
exitFullscreen + webkit prefix fallback for iOS Safari (WORLDMONITOR-11/13)
- Narrow /^TypeError: Failed to fetch/ to exact match (was suppressing real
API failures). Move module-import-failed to beforeSend with extension/
webview context check instead of blanket ignore (WORLDMONITOR-15)
- Guard classList?.contains and target.closest?. on event targets that may
not be Elements (WORLDMONITOR-Z/10)
- Add noise filters: Fullscreen request denied, requestFullscreen,
vc_text_indicators_context (WORLDMONITOR-12)
Prevent getProjection null crash when WebGL context is lost by tracking
webglLost flag and skipping all setProps/layer rebuild calls until restored.
Add ignoreErrors for IndexedDB iOS kills, Twitter WebView injection, and
CSP unsafe-eval from extensions.
Browser extensions intercept window.fetch causing "Failed to fetch
(gamma-api.polymarket.com)" to leak as unhandled rejection. Remove
the $ anchor so the pattern matches any suffix.
- withTransaction now returns undefined instead of throwing when
InvalidStateError persists after retry (transient browser event)
- Add .catch() to fire-and-forget cleanOldSnapshots() call
- Add NotAllowedError, InvalidAccessError, importScripts to Sentry ignoreErrors
- Add global unhandledrejection handler for YouTube IFrame API autoplay blocks
- Add onError handler to deck.gl MapboxOverlay for internal render-cycle races
- storage.ts: add withTransaction() retry wrapper for IndexedDB InvalidStateError on iOS/Safari tab backgrounding
- usa-spending.ts: add 20s AbortController timeout to prevent Safari "Load failed" on stalled POST
- App.ts: add catch to runGuarded() to prevent unhandled rejections from task runner
- main.ts: add Sentry ignoreErrors for WebGL context loss and ResizeObserver loop
- DeckGLMap.ts: add webglcontextlost/restored handlers for graceful GPU recovery
- feeds.ts: route rsshub.app feeds (NHK, MIIT, MOFCOM) through Railway proxy, switch Nikkei Asia and ECFR to Google News proxy
- finance.ts: switch Nikkei Asia to Google News proxy, remove unused railwayRss helper
Resolve instead of reject when the script fails to load (ad blocker,
network issue). Guard initializePlayer against missing YT.Player.
Prevents noisy unhandled rejection errors in Sentry.
Initializes @sentry/browser early in main.ts with environment
detection (production/preview/development). Disabled on localhost
and Tauri desktop. Traces sampled at 10%.
## Summary
- PR #97 hid the badge but the `SignalModal` kept auto-opening on new
signals — this is what the reporter was still seeing
- Gates all 5 automatic `this.signalModal?.show()` calls behind
`this.findingsBadge?.isEnabled()` so disabling Intelligence Findings
also suppresses the full-screen popup overlay and sounds
- Signal history is still recorded (`addToSignalHistory`) even when
popup is suppressed, so re-enabling the toggle shows them
Closes#89
## Test plan
- [x] Disable Intelligence Findings via PANELS toggle or right-click
- [x] Wait for signal refresh cycle — no full-screen popup should appear
- [x] Re-enable → popups resume on next signal detection
- [x] Build succeeds with no type errors
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
- Adds [La Silla Vacía](https://www.lasillavacia.com) RSS feed (`/rss`)
to the `latam` feed category
- Adds source tier entry (Tier 3 — specialty/investigative)
- Colombian independent outlet covering political power structures,
governance, and armed conflict
Ref #96
## Test plan
- [ ] Verify feed loads in LATAM news panel (content is in Spanish)
- [ ] Confirm no duplicate or broken entries in feed list
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Add lasillavacia.com RSS feed to improve Latin American political
coverage. Independent Colombian investigative outlet covering governance,
armed conflict, and regional power dynamics.
Ref #96
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR #97 only hid the badge itself but the SignalModal kept auto-opening
on new signals. Gate all 5 automatic signalModal.show() calls behind
findingsBadge.isEnabled() so disabling Intelligence Findings also
suppresses the full-screen popup overlay.
Closes#89
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
- Full i18n system with 14 locales: en, fr, de, es, it, pl, pt, nl, sv,
ru, ar (RTL), zh, ja — all at 1132-key parity
- Eliminated ~110 hardcoded English strings across 50+ source files,
replaced with `t()` calls
- RTL support for Arabic with proper regional code normalization (ar-SA
→ ar)
- Dead English fallback literals (`t() || 'English'`) removed from all
components
- Community discussion floating widget (localized)
- Linux AppImage desktop build support
- Proper noun heuristic fallback for trending keywords when ML
unavailable
## Key changes
- **New**: `src/services/i18n.ts` — i18next setup with language
detection, RTL, locale switching
- **New**: 13 locale JSON files (1132 keys each) in `src/locales/`
- **New**: `src/styles/rtl-overrides.css` +
`src/styles/lang-switcher.css`
- **Modified**: 50+ components/services to use `t()` instead of
hardcoded strings
- **Modified**: `.github/workflows/build-desktop.yml` — Linux CI matrix
- **Modified**: `scripts/desktop-package.mjs` + `download-node.sh` —
Linux target support
## Test plan
- [ ] Verify language switcher shows all 14 languages
- [ ] Switch to Arabic — confirm `dir="rtl"` on `<html>`, layout mirrors
- [ ] Switch to Japanese — confirm all panel labels, tooltips, popups
render in Japanese
- [ ] Switch to French — confirm no English leaks in panels, modals, map
legend
- [ ] Verify `{{count}}` interpolation works in timeAgo strings
- [ ] Verify `tsc --noEmit` passes (confirmed locally)
- [ ] Test community widget dismiss/localStorage persistence
- CommunityWidget: add DOM check to prevent duplicate widgets on repeated loadNews() calls
- RuntimeConfigPanel: compare t() result against key path to suppress missing help translations
Main variant: NHK World + Nikkei Asia in asia category.
Finance variant: Nikkei Asia in markets category.
Added asia.nikkei.com to RSS proxy allowlist.
Main variant: NHK World + Nikkei Asia in asia category.
Finance variant: Nikkei Asia in markets category.
Added asia.nikkei.com to RSS proxy allowlist.
- Fix relative-time {{count}} placeholders in all 12 locales (5m ago, not m ago)
- Localize CommunityWidget: replace 3 hardcoded English strings with t() calls
- Add Linux AppImage to tech/finance Tauri configs, CI matrix (ubuntu-22.04), packaging scripts, and download-node.sh
- Fix language code normalization: add supportedLngs/nonExplicitSupportedLngs to i18next, normalize getCurrentLanguage()
- Translate ~240 untranslated English strings across 11 locale files (ru/ar/zh now 100% translated)
- Add components.community section to all 12 locales
- All 12 locales at 1130-key parity, 0 placeholder mismatches
Replace hardcoded English text with t() calls across 12 source files:
signal context descriptions, alert titles/summaries, intel topic
names, map legend labels, asset/threat labels, UCDP panel headers,
stablecoin/status panel sections. Add ~114 new keys to en.json and
translate to all 11 locales (fr/de/es/it/pl/pt/nl/sv/ru/ar/zh).
Remove dead #timeDisplay span that was never updated by JS.
All 12 locales at 1127-key parity with 0 placeholder mismatches.
- Add ar.json and zh.json translations (1013 keys each, full parity)
- RTL support: dir="rtl" on <html>, targeted CSS overrides in rtl-overrides.css
- Font fallbacks: system Arabic/CJK fonts via --font-body CSS variable
- i18n.ts: RTL_LANGUAGES set, applyDocumentDirection(), isRTL(), getLocale()
- story-renderer.ts: locale-aware date formatting via getLocale()
- Type declarations for both new locales
P1: Start-of-headline matches (idx=0) no longer count against the
capitalization ratio. When all matches are headline-start only,
falls back to acronym check instead of returning false.
P2: Removed dead [A-Z]{2,} shortcut — terms are lowercased by
asDisplayTerm() before reaching isLikelyProperNoun.
Floating pill badge (bottom-right) with pulsing dot, "Join the
Discussion" text, and CTA linking to GitHub Discussions #94.
Shows 15s after initial data load. "Don't show again" persists
dismissal to localStorage (wm-community-dismissed).
Shows a compact pill badge (bottom-right) inviting users to the GitHub
Discussions page. Includes pulsing dot indicator, CTA button, close
button, and "Don't show again" option that persists to localStorage.
When the NER model isn't loaded, isSignificantTerm was returning true
for all terms, letting common words like "here" trigger keyword spike
findings. Now falls back to a capitalization heuristic: single-word
terms must appear capitalized (mid-sentence) in >50% of headlines to
be treated as significant. Also added ~45 missing English stopwords.
When the NER model isn't loaded, isSignificantTerm was returning true
for all terms, letting common words like "here" trigger keyword spike
findings. Now falls back to a capitalization heuristic: single-word
terms must appear capitalized (mid-sentence) in >50% of headlines to
be treated as significant. Also added ~45 missing English stopwords.
Audit found missing keys in all non-EN locales (62-90 per file) and 25
stale keys in PT/NL/SV. Fixes gaps, removes stale keys, corrects FR
infra key misplacement, and adds complete Russian (ru) translation with
1013/1013 keys matching the English reference.
"here" and other basic English words (pronouns, prepositions, adverbs)
were not in SUPPRESSED_TRENDING_TERMS, causing false keyword spike
findings for common words.
"here" and other basic English words (pronouns, prepositions, adverbs)
were not in SUPPRESSED_TRENDING_TERMS, causing false keyword spike
findings for common words.
These CSS custom properties were used in 14 places but never defined,
causing transparent backgrounds on playback panel, toggle button,
header flash animation, select options, and offline retry button.
## Summary
- Adds ability to hide the Intelligence Findings badge for passive/TV
viewing — stops polling, sounds, and pulse animations when disabled
- Two ways to toggle: right-click context menu on the badge, or the
PANELS settings modal
- Preference persists in `localStorage` across page reloads
Closes#89
## Test plan
- [ ] Load app — badge shows by default (no regression)
- [ ] Right-click badge → "Hide Intelligence Findings" → badge
disappears, no polling/sounds
- [ ] Open PANELS → "Intelligence Findings" toggle shows as disabled →
click to re-enable → badge reappears
- [ ] Refresh page → preference persists
- [ ] Build succeeds with no type errors
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Allow users to hide the Intelligence Findings badge for passive viewing
(e.g. TV displays). The badge can be disabled via right-click context
menu or the PANELS settings modal. Preference persists in localStorage.
Closes#89
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Middle East panel: BBC Persian (direct RSS), Iran International and
Fars News (Google News fallbacks — no public RSS endpoints)
- Asia panel: MIIT and MOFCOM China government feeds via RSSHub
- Guard against Invalid Date in RSS parser — malformed pubDate strings
from IAEA/CrisisWatch feeds caused RangeError on toISOString()
- Replace 13 dead/blocked RSS feed URLs (403/404/500) with Google News
site-scoped fallbacks: Politico, RUSI, Kyiv Independent, War Zone,
MEI, Wilson Center, GMF, CNAS, Lowy, Arms Control, Bulletin, EU ISS
## Summary
- Detects user's OS via `navigator.userAgent` and shows only the
relevant download button (Windows, macOS Apple Silicon, or macOS Intel)
- Uses WebGL renderer info to distinguish Apple Silicon from Intel Macs
where possible
- Adds a "Show all platforms" toggle so users can still access other
platform downloads
- Falls back to showing all 3 buttons if OS can't be detected
## Test plan
- [x] Open the web app on a Mac — should see only the macOS (Apple
Silicon) button
- [x] Open on Windows — should see only the Windows button
- [x] Click "Show all platforms" — all 3 buttons should appear
- [x] Click "Show less" — should collapse back to the detected platform
- [x] Spoof an unrecognized user agent — all 3 buttons should show (no
toggle)
Replaces #91Closes#90🤖 Generated with [Claude Code](https://claude.com/claude-code)
Add 'macos' platform type for Macs where WebGL can't determine
Apple Silicon vs Intel. Shows both Mac download buttons without
the irrelevant Windows option.
WebGL renderer check now also detects Intel GPUs to return macos-x64.
When architecture can't be determined, return unknown so users see
both Apple Silicon and Intel download buttons instead of defaulting
to the wrong binary.
setStyle() replaces all map sources/layers. Country boundaries were
only loaded once (guarded by countryGeoJsonLoaded) so they vanished
after theme toggle with no way to re-add them.
- Reset countryGeoJsonLoaded in switchBasemap so loadCountryBoundaries
can re-run after the new style loads
- Listen for style.load before re-adding country source/layers
- Guard setupCountryHover with countryHoverSetup flag to prevent
duplicate mousemove/mouseout listeners on re-load
- Apply theme-correct paint values after layer creation
Switch from CARTO dark_all raster tiles (which showed continent names
in local languages like 亚洲, AFRIKA, أفريقيا) to CARTO Dark Matter
vector style with English labels. Set renderWorldCopies: false to
prevent horizontal map duplication. Use interleaved deck.gl overlay
so basemap labels render above data layers.
Closes#81
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Polymarket and other proxied requests from finance variant were blocked
by CORS because the Railway relay only allowed worldmonitor.app and
tech.worldmonitor.app origins.
772-line database of Saudi Arabia and UAE foreign direct investments in
global critical infrastructure (ports, energy, data centers, railways, etc.)
with filterable/sortable table panel.
Originally contributed as standalone 'infra' variant in PR #61.
- Add centered header-clock in GLOBAL SITUATION panel header
- Remove map-timestamp from Map.ts and DeckGLMap.ts
- Clock ticks every second with monospace font
- Add goldman, sachs, off, and all 12 month names to SUPPRESSED_TRENDING_TERMS
- Add stripSourceAttribution() to remove feed name suffixes (e.g. "- Wall Street Journal") from RSS titles before keyword extraction
- vercel.json: add no-cache headers for / and /index.html so CDN never serves stale HTML
- main.ts: add vite:preloadError listener to auto-reload once on chunk 404s
- vite.config.ts: remove HTML from SW precache, add NetworkFirst for navigation requests
- Restructure popups keys in de/es/it/pl/nl/pt/sv to match en.json nested structure
- Move orphaned top-level keys (earthquake, base, protest, flight, nuclear, cable, pipeline, etc.) into popups.* namespace
- Add full native translations for ~200 popup keys per locale
- Ensures i18next resolves popups.* keys correctly for all languages
Now that the header has a sun/moon dark/light toggle, the Appearance
section in the settings modal is redundant. Revert modal title from
"Settings" back to "Panels" and remove theme radio buttons and CSS.
Remove the duplicate time display from the header bar and add a
theme toggle button (sun/moon icon) that hooks into the existing
theme-manager. Bidirectional sync with settings modal radios.
## Summary
Adds a complete dual-theme system to WorldMonitor. Users can toggle
between dark and light mode in Settings, with the preference persisted
across sessions. Every panel, the map, charts, and all chrome are fully
themed.
**41 files changed, +1,591 / -1,128 lines**
## What's included
### CSS Foundation (Phase 1)
- 124+ hardcoded colors centralized into CSS custom properties
- Theme colors separated from semantic colors (threat reds, DEFCON,
status badges stay identical)
- `getCSSColor()` runtime utility with theme-aware cache for dynamic TS
components
### Theme Core (Phase 2)
- `ThemeManager` module with `get/set/apply` theme functions and
localStorage persistence
- FOUC prevention inline scripts in both HTML entry points
- Dark/Light radio toggle in Settings → Appearance section
### Map & Visualization Theming (Phase 3)
- Map auto-swaps between dark CARTO and light Voyager tiles (no flash)
- Deck.GL overlay layers adjust opacity/saturation per theme for
readability
- D3 CountryTimeline chart colors converted to theme-aware
`getCSSColor()`
### Polish & Accessibility (Phase 4)
- Smooth 200ms CSS transitions for all theme switching
- WCAG AA contrast compliance (`--text-muted` #767676, 4.54:1 ratio)
- Both build variants (full geopolitical + tech/startup) verified
working
## Demo
### Toggle dark ↔ light
https://github.com/user-attachments/assets/ce8d3bbe-fb5e-49cb-9bbd-5da5d900a15a
### Dark mode (unchanged)
<img width="5120" height="2648" alt="image"
src="https://github.com/user-attachments/assets/6a055ae6-e9a9-4d85-b328-a035b7bcd165"
/>
### Light mode
<img width="5120" height="2650" alt="image"
src="https://github.com/user-attachments/assets/3531c952-a801-43a5-8ef3-68c160fb85b8"
/>
## Review focus areas
1. **`src/styles/main.css`** — Bulk of the CSS variable conversion (~12K
lines). Check that `var()` references and `[data-theme="light"]`
overrides look correct.
2. **`src/utils/theme-manager.ts`** — New module. Simple but critical:
localStorage read/write, event dispatch, FOUC prevention.
3. **`src/utils/theme-colors.ts`** — `getCSSColor()` cache utility.
Verify cache invalidation on theme change is correct.
4. **`src/components/DeckGLMap.ts`** — Basemap tile swap logic and
`getOverlayColors()` per-theme adjustments.
5. **`index.html` / `settings.html`** — Inline FOUC prevention scripts.
CSP updated with `unsafe-inline` for script-src.
6. **`src/App.ts`** — Theme toggle UI wiring in settings modal. Check
event listener patterns.
## What NOT to worry about
- ~99 remaining hardcoded colors are documented acceptable exclusions
(canvas drawing, console.log, deprecated constants with migration paths,
category identifier colors)
- Dark mode is visually identical to before — no regressions
## Test plan
- [x] Run `npm run build` — builds without errors for both variants
- [x] Open app — dark mode loads by default, no FOUC
- [x] Go to Settings → Appearance → select Light — all panels, header,
sidebar switch smoothly
- [x] Verify map switches from dark CARTO to light Voyager tiles (no
blank flash)
- [x] Refresh page — light mode persists, no FOUC
- [x] Switch back to dark — everything returns to original look
- [x] Check semantic colors (threat dots, DEFCON badges, LIVE
indicators) identical in both themes
- [x] Open DevTools → inspect text contrast in light mode (should be
≥4.5:1)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
overlay-heavy is rgba(255,255,255,0.2) in dark mode (lightening tint),
but 11 places originally used rgba(0,0,0,x) (darkening tint). This
caused visible regressions in dark mode (channel bar, CII cards, etc).
Adds --darken-light/medium/heavy CSS variables that stay black-tinted
in both themes, and applies them to the affected selectors.
Add light-mode overrides for 12 semantic CSS variables that had
terrible contrast on light backgrounds (as low as 1.25:1 for #44ff88).
Uses Tailwind's light-friendly palette (green-600/700, amber-600,
orange-600/700, yellow-600, sky-600).
Also darken DeckGL overlay marker colors (startup hub, accelerator,
nuclear, datacenter) and their legend SVGs in light mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
These are local development/planning files that should be gitignored.
Already covered by .gitignore entries for .planning/ and .idea/.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- index.html and settings.html FOUC scripts add no-transition class to <html>
- src/main.ts removes no-transition after first paint via requestAnimationFrame
- src/settings-main.ts removes no-transition after first paint via requestAnimationFrame
- Prevents transition sweep from dark-to-light on page load while enabling smooth theme toggles
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add wildcard 200ms ease transition on background-color, color, border-color, box-shadow
- Add .no-transition suppression rule with !important for FOUC prevention
- Add canvas/maplibregl/deck-canvas transition exclusion to prevent rendering artifacts
- Fix --text-muted in light theme from #8a8a8a to #767676 (WCAG AA 4.54:1 vs #f8f9fa)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace static COLORS constant with getOverlayColors() function
- Refresh COLORS at top of buildLayers() on each render cycle
- Conflict zone fills more transparent in light mode (alpha 60 vs 100)
- Conflict zone line color reduced alpha in light mode (120 vs 180)
- Displacement arc colors deeper/more saturated on light backgrounds
- Threat dots remain identical in both modes (user locked decision)
- Infrastructure markers unchanged (semantic category colors)
- Update --map-country to warm cream (#f0e8d8) for Voyager-style land
- Update --map-stroke to warm border (#c8b8a8) complementing cream land
- Subtler blue grid (#b0c8d8) for light mode
- Add theme-changed event listener in Map.ts resetting baseRendered flag
- Dark theme map values unchanged
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add .theme-toggle-section with border-bottom separator
- Add .modal .section-label for APPEARANCE/PANELS headings (scoped to modal)
- Add .theme-toggle-group flex layout with gap
- Add .theme-option radio button labels with hover/active states
- Hide radio inputs, use :has(input:checked) for active state
- Override .section-label padding inside .theme-toggle-section to avoid double-padding
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Import setTheme/getCurrentTheme from ThemeManager
- Add APPEARANCE section with Dark/Light radio buttons to settings modal
- Wire theme toggle change listener calling setTheme()
- Add theme-changed listener to re-render map on theme switch
- Rename modal title from 'Panel Settings' to 'Settings'
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add inline FOUC prevention script to index.html and settings.html <head>
- Update CSP script-src to allow 'unsafe-inline' for FOUC prevention
- Import and call applyStoredTheme() in src/main.ts before App init
- Import and call applyStoredTheme() in src/settings-main.ts before loadDesktopSecrets
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add src/utils/theme-manager.ts with getStoredTheme, getCurrentTheme, setTheme, applyStoredTheme
- Export Theme type and all 4 functions from src/utils/index.ts barrel
- setTheme invalidates color cache, updates meta theme-color, dispatches theme-changed event
- applyStoredTheme is a lightweight pre-mount theme applicator (no events/cache invalidation)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CountryBriefPage: 2 aria-label='Close' + 3 export labels (Image/JSON/CSV)
- Added exportImage key to en.json and fr.json
- Expanded common section with ~48 native-translated keys in:
de.json (German), es.json (Spanish), it.json (Italian), pl.json (Polish),
nl.json (Dutch), pt.json (Portuguese), sv.json (Swedish)
- Also translated base nl/pt/sv common keys that were still in English
- Add StockExchangePopupData, FinancialCenterPopupData, CentralBankPopupData,
CommodityHubPopupData to PopupData.data union, removing unsafe double casts
(as unknown as) in MapPopup.ts
- Dynamically create NewsPanel instances for any FEEDS category that doesn't
already have a hardcoded panel, so finance-specific categories (forex, bonds,
centralbanks, derivatives, fintech, regulation, institutional, analysis, etc.)
get their own dedicated panels instead of being silently dropped
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace hardcoded feed category list with Object.entries(FEEDS) so that
variant-specific feed categories (e.g. finance variant's markets, forex,
bonds, commodities, crypto, etc.) are automatically discovered and loaded
instead of being silently skipped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a new 'finance' site variant (finance.worldmonitor.app) following the
same pattern as the existing tech variant. Includes:
- Finance-specific RSS feeds: markets, forex, bonds, commodities, crypto,
central banks, economic data, IPOs/M&A, derivatives, fintech, regulation,
institutional investors, and market analysis (all free/open RSS sources)
- Finance-focused panels with trading-themed labels (Market Headlines,
Live Markets, Forex & Currencies, Fixed Income, etc.)
- Geographic data for stock exchanges (30+), financial centers (20+),
central banks (14), and commodity hubs (10) worldwide
- Four new map layers: stockExchanges, financialCenters, centralBanks,
commodityHubs with tier-based icons and zoom-dependent labels
- Map popup rendering for all finance marker types
- Variant switcher updated with FINANCE tab in header
- Search modal with finance-specific sources and icons
- Vite HTML variant plugin metadata for SEO
- Build scripts (dev:finance, build:finance, test:e2e:finance)
- Tauri desktop config for Finance Monitor app
https://claude.ai/code/session_01CCmkws2EYuUHjYDonzXEtY
- Add getThreatColor() function with CSS variable reads (threat-classifier)
- Deprecate THREAT_COLORS constant (kept for backward compat)
- Convert getTrendColor to --semantic-* vars (oil-analytics)
- Convert getSeverityColor to --semantic-* vars (weather)
- Convert getSeverityColor for climate anomalies (climate)
- Convert getStatusColor to --semantic-* and --text-* vars (data-freshness)
- Convert getDisplacementBadge to --semantic-* vars (unhcr)
- Add getPipelineStatusColor function using CSS vars (pipelines)
- Document MONITOR_COLORS and PIPELINE_COLORS as fixed category colors
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add 01-02-SUMMARY.md with conversion metrics and deviation documentation
- Update STATE.md with decisions and resolved blocker
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The pattern /worldmonitor.*\.vercel\.app/ matched any Vercel deployment
with a "worldmonitor" prefix, allowing unauthorized origins to bypass
CORS and consume server-side API keys. The owner-scoped pattern on the
previous line already covers all legitimate preview deployments.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix 24 incorrect var(--shadow-color) background uses: map high-opacity (0.6-0.88) to var(--bg), low-opacity (0.15-0.35) to var(--overlay-heavy)
- Fix 50 white rgba text colors incorrectly mapped to var(--overlay-heavy): map to proper text hierarchy (--text-muted, --text-faint, --text-ghost, --text-dim)
- Fix 2 translucent white text mapped to var(--accent): remap to var(--text-secondary)
- Convert remaining white/black rgba values in lines 6001+
- All 223 remaining rgba are semantic color tints (intentionally excluded)
- All 42 remaining hex are country/flag/decorative colors (intentionally excluded)
- Build passes with valid CSS
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace 12 hardcoded hex/rgba definitions in .settings-shell with var(--*) references to global theme variables
- Convert ~13 remaining hardcoded rgba() calls to var(--overlay-*), var(--text-*), and color-mix() references
- Convert hardcoded #fff and #0b6fdb in button styles to var(--accent) and color-mix()
- Result: 0 hex values, 0 rgba values, 88 var(--) references
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add src/utils/theme-colors.ts with getCSSColor() and invalidateColorCache()
- getCSSColor reads CSS custom properties via getComputedStyle with caching
- Cache auto-invalidates when data-theme attribute changes
- Export both functions from src/utils/index.ts barrel
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Expand :root from 14 to 48 CSS custom properties in two namespaces
- Add theme colors (backgrounds, borders, text, overlays, scrollbar, input, map)
- Add semantic colors (severity, threat, DEFCON, status indicators)
- Define [data-theme="light"] selector overriding only theme variables
- Preserve all original dark-mode values for zero visual regression
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add CREATE_NO_WINDOW (0x08000000) creation flag to the sidecar
Command::new() spawn on Windows. Without this, node.exe inherits
a visible console window that overlays the Tauri GUI.
- Add Panel.showConfigError() with amber styling and desktop Settings link
- Propagate `skipped` flag from Finnhub and FIRMS API responses
- Show "API key not configured" on Markets/Heatmap/Commodities/FIRMS panels
when sidecar returns skipped (missing API key)
- ETF, Stablecoin, MacroSignals panels detect upstream API unavailability
and show retry message instead of generic "Failed to fetch"
- RuntimeConfigPanel auto-hides when all features are configured
- Bump version to 2.3.5
Tauri resource_dir() on Windows returns \\?\ extended-length paths that
Node.js module resolution cannot handle, causing EISDIR: lstat 'C:'.
Strip the prefix before passing to Node.js, set current_dir to the
sidecar directory, and add package.json with "type": "module" to prevent
ESM scope walk-up to drive root.
keyring v3 ships with NO default platform backends — API keys were
stored in-memory only, lost on every app restart. Add apple-native
and windows-native features to use real OS credential stores.
MD012 / no-multiple-blanks Multiple consecutive blank lines
MD022 / blanks-around-headings Headings should be surrounded by blank
lines
MD032 / blanks-around-lists Lists should be surrounded by blank lines
MD012 / no-multiple-blanks Multiple consecutive blank lines
MD022 / blanks-around-headings Headings should be surrounded by blank lines
MD032 / blanks-around-lists Lists should be surrounded by blank lines
- Save keys that pass verification even when others fail (was all-or-nothing)
- Capture un-blurred input values before render to prevent loss on checkbox toggle
- Fix missing isDisallowedOrigin import in PIZZINT endpoints
- Show "Staged" status/pill for buffered secrets instead of "Missing"
- Add macOS Edit menu (Cmd+C/V/X/Z) for WKWebView clipboard support
- Raise settings window when main gains focus (prevent hide-behind)
- Fix Cloudflare verification to probe Radar API (not token/verify)
- Fix EIA verification URL to valid v2 endpoint
- Force IPv4 globally: monkey-patch fetch() to avoid IPv6 ETIMEDOUT
on government APIs (EIA, NASA FIRMS) with broken AAAA records
- Soft-pass on network errors during secret verification (don't block save)
- Add desktopRequiredSecrets to skip relay URLs on desktop
- Cross-window sync for secrets and feature toggles via localStorage events
- Add @tauri-apps/cli devDependency
Common English verbs like "say" pass frequency thresholds and generate
false intelligence findings. Now runs browser NER (BERT) on spike
headlines before alerting — terms not recognized as named entities
(PER/ORG/LOC/MISC) are suppressed. Falls back gracefully when NER
is unavailable. Also adds ~55 base verb forms to the suppressed list
as hardened fallback for low-spec devices.
Major security hardening: CORS enforcement on all API endpoints,
sidecar auth bypass fix, postMessage origin validation, CSP
tightening, and service worker stale cache fix.
Add skipWaiting, clientsClaim, and cleanupOutdatedCaches to workbox
config. Fixes NS_ERROR_CORRUPTED_CONTENT / MIME type errors when
users have a cached service worker serving old HTML that references
asset hashes no longer on the CDN after redeployment.
- Expand SUPPRESSED_TRENDING_TERMS from 13 to ~170 entries to filter
common English words (department, state, news, etc.) from intelligence
findings
- Move sidecar admin endpoints (debug-toggle, traffic-log, env-update,
local-status) before LOCAL_API_TOKEN auth gate — settings window sends
bare fetch without token, causing silent 401 failures
- Restore Market Radar and Economic Indicators panels to tech variant
- Remove stale Documentation section from README
- Clean up .env.example cyber threat keys (handled internally)
- Bump v2.2.6
Replace Access-Control-Allow-Origin: * with shared getCorsHeaders()
across 20 API edge functions to restrict access to worldmonitor.app,
tech.worldmonitor.app, and authorized Vercel preview URLs.
Version bump to 2.2.5 across package.json, tauri.conf.json, Cargo.toml.
Replace Access-Control-Allow-Origin: * with an allowlist of our domains
(worldmonitor.app, tech.worldmonitor.app, localhost dev ports, Tauri,
and *.vercel.app previews). Prevents third parties from using the relay
as a free proxy.
Desktop Configuration panel was leaking to web due to being in
FULL_PANELS/TECH_PANELS configs. Removed from static configs (injected
programmatically for Tauri) and filtered from toggle UI on web.
World Bank API returns 403 to Vercel edge IPs. Added /worldbank proxy
route to Railway relay with full response transformation and 30-min cache.
Client tries Railway first, falls back to Vercel.
Polymarket gamma-api suffers Cloudflare JA3 blocking from Vercel.
Added /polymarket proxy route to Railway relay with 2-min cache.
Client fallback chain: browser-direct → Tauri → Railway → Vercel → production.
Update README with 5 new How It Works sections and 10 inline updates
reflecting implemented features. Data layer count 29→30+, data sources
14→22, new Tech Stack and Roadmap entries.
Bring Climate Anomalies, Population Exposure, UCDP Conflict Events,
and UNHCR Displacement panels up to dashboard design standards
matching Fires/Markets/Stablecoins panels.
- Replace div-based layouts with proper <table> structures
- Add scoped <style> blocks (no inline style= attributes)
- CSS class-based severity badges, death colors, displacement badges
- tabular-nums on all numeric columns, right-aligned
- Row hover effects, text-overflow ellipsis for long text
- Stat grid for UNHCR global totals, total deaths summary for UCDP
- Custom tab styling for UCDP and Displacement panels
- Tooltip: show "Cyber Threat" + severity + country instead of raw source names
- Popup header: "Cyber Threat" title instead of raw IP address
- Popup: show all 5 source labels and add malware family field
- Increase dot size for better visibility at global zoom level
ipwho.is returns 403 from Node.js/Edge ("CORS not supported on Free plan")
and ipapi.co rate-limits aggressively (429). Both providers only work from
browser-side requests.
Switch to ipinfo.io (HTTPS, 50K/mo free, works from Edge runtime) as primary
with freeipapi.com as fallback.
Validated end-to-end: 20 C2 IPs geo-enriched in <1s with real coordinates.
Expands from 2 to 5 threat intel sources:
- C2IntelFeeds (free, no auth): ~500 active C2 server IPs (CobaltStrike, Metasploit)
- AlienVault OTX (optional API key): community-sourced IOCs with tags
- AbuseIPDB (optional API key): high-confidence abuse reports with geo
- Feodo: now includes recently-offline entries (still threat-relevant)
All sources fetched in parallel. Graceful degradation when API keys missing.
Plot live botnet C2 servers, malware distribution nodes, and malicious IPs
on the globe using free abuse.ch APIs (Feodo Tracker + URLhaus).
- Vercel edge API with triple-layer caching (Redis → memory → stale fallback)
- IP geolocation via ipwho.is + ipapi.co (HTTPS-compatible with Edge runtime)
- Severity-based color coding (critical=red, high=orange, medium=amber, low=yellow)
- Feature-gated behind VITE_ENABLE_CYBER_LAYER=true env var
- Frontend circuit breaker, data sanitization, 10min auto-refresh
- Tauri desktop support: 3 new secret keys (URLHAUS, OTX, AbuseIPDB)
- Full test suite (6 unit tests), e2e harness updates, popup + tooltip rendering
- Add 60s startup cooldown to suppress pulse during initial data hydration
- Consolidate pulse lifecycle into syncPulseAnimation() across all setters
- Replace static predicates (h.level=high, c.maxSeverity=high) with dynamic-only
checks: recent news, recent ACLED riots (<2h), breaking hotspots
- Exclude GDELT riots from pulse gating (noisy, not actionable)
- Reduce pulse interval from 250ms to 500ms (halves layer rebuild rate)
- Pause pulse when render is paused (country brief overlay)
- Add latestRiotEventTimeMs to MapProtestCluster with raw protest fallback
- Add Playwright tests for startup cooldown and lifecycle start/stop/GDELT exclusion
The runtime fetch patch previously returned local 4xx/5xx responses
as-is. Now falls back to worldmonitor.app cloud API on any non-OK
local response, matching the existing network-error fallback behavior.
- Full-page Country Brief replacing modal overlay
- Tightened headline relevance with negative country filter
- Top News section replacing redundant Evidence/Sources
- Shared country-geometry service for local-first detection
- BR/AE added to tier-1, deep-link name resolution
- Prediction Markets moved to right column for balanced layout
GeoJSON uses "United States of America" but COUNTRY_TAG_MAP and
variant matching expect "United States". Normalize at entry point
so polymarket, search terms, and all downstream lookups work.
- Remove Evidence/Sources section (duplicate of Top News)
- Top News now shows 8 items with citation anchor IDs
- AI brief [N] citations link to Top News cards with scroll+highlight
- Move Prediction Markets to right column for balanced layout
- Left: Risk + Brief + Top News | Right: Signals + Timeline + Markets + Infra
- Add Brazil and UAE to TIER1_COUNTRIES, BASELINE_RISK, EVENT_MULTIPLIER,
COUNTRY_KEYWORDS, COUNTRY_BOUNDS, COUNTRY_ALIASES, and Polymarket maps
- Use Intl.DisplayNames to resolve non-tier ISO codes in deep-link URLs
- Guard raw 2-letter codes from becoming overly broad search terms
- Fix military timeline to use same either/or logic as getCountrySignals
- Add negative country filter: headlines where another country is mentioned
first in the title are excluded (fixes Venezuela brief showing for US)
- Filtered headlines used for both evidence section AND LLM context
- Add Top News section (5 items) between Intelligence Brief and Markets
- Limit prediction markets to max 3, tighter padding
- RuntimeConfigPanel only created when isDesktopApp is true (was leaking
full desktop config UI including secret key inputs to web visitors)
- Fix polymarket sub-market selection: when event title doesn't match
country but a sub-market does, restrict candidates to matching
sub-markets instead of picking highest-volume globally (was showing
Xi Jinping market for France because Macron sub-market triggered match)
Shield.io badges for Windows, macOS ARM, and macOS Intel desktop downloads
pointing to the new /api/download redirect. Web app links restyled as badges.
Edge function that redirects /api/download?platform=macos-arm64 to
the matching asset from the latest GitHub release. Falls back to
the releases page when no match is found.
- Rename variant default from 'world' to 'full' across config files
- Replace all startups.worldmonitor.app references with tech.worldmonitor.app
- Add CARTO basemap tile caching to Workbox runtime config (basemaps.cartocdn.com)
- Make open_settings_window_command async to prevent WebView2 deadlock on Windows
- Create settings window with visible(false) to avoid white flash before content loads
- Remove menu bar from settings window on Windows/Linux (macOS uses screen-level menu)
- Frontend calls plugin:window|show + set_focus after init completes
Switch ARM64 build from cross-compiling on macos-latest (Intel) to
native compilation on macos-14 (M1). Updates platform checks to
use contains() so signing works on both macOS runner types.
Gracefully handle missing/invalid Apple Developer certificates instead
of failing the entire build. Detects secret availability, validates
certificate file size, makes import failures non-fatal, and splits
each variant into signed/unsigned conditional paths.
| WORLDMONITOR-1G | `Error: ML request unload-model timed out after 120000ms` | 30 | 27 | Wrapped `unloadModel()` in try/catch; timeout no longer leaks as unhandled rejection. Cleans up `loadedModels` set on failure. |
| WORLDMONITOR-1F | `Error: ML request unload-model timed out after 120000ms` | 9 | 9 | Same root cause as 1G (different release build hash). |
| WORLDMONITOR-1K | `TypeError: this.player.playVideo is not a function` | 1 | 1 | Added optional chaining (`playVideo?.()`, `pauseVideo?.()`) in `LiveNewsPanel.ts`. YT IFrame API player object may not have methods ready during initialization race. |
### NOISE — Filtered
| ID | Title | Events | Users | Filter |
|---|---|---|---|---|
| WORLDMONITOR-1J | `InternalError: too much recursion` | 1 | 1 | i18next internal `translate -> extractFromKey` cycle on Firefox 147. Added `/too much recursion/` to `ignoreErrors`. |
| WORLDMONITOR-1H | `TypeError: Cannot read properties of null (reading 'id')` | 1 | 1 | maplibre-gl internal render crash (`_drawLayers -> renderLayers`). Extended `beforeSend` regex to suppress null `id`/`type` when stack is in map chunk. |
## Files Modified
| File | Change |
|---|---|
| `src/services/ml-worker.ts` | `unloadModel()`: try/catch around `this.request()`, clean `loadedModels` on failure |
| `src/components/LiveNewsPanel.ts` | Optional chaining on `playVideo?.()` and `pauseVideo?.()` |
| `src/main.ts` | Added `/too much recursion/` to `ignoreErrors`; extended maplibre `beforeSend` filter for null `id`/`type` |
## Sentry Status
All 5 issues marked **resolved (in next release)** via API. They will auto-reopen if errors recur after deployment.
All notable changes to World Monitor are documented here.
## [2.4.19]
### Fixed
- **Moving/resizing a panel no longer shifts the others** (the "when I shift the 3D map it shifts all other components" report). The dashboard now DEFAULTS to the free layout: every panel owns its own {x,y,w,h}, so a drag or resize leaves every sibling exactly where it was — no more grid reflow. The switch is invisible: the current grid arrangement is frozen as the starting geometry (`grid-config.applyDefaultLayout`), and Grid stays one dropdown-click away for anyone who wants snap-to-grid back.
- **Panels resize granularly** — pixel-exact width AND height from any edge or corner, instead of jumping between coarse column/row spans.
- **Panels can be made much narrower/smaller** ("constrained on min width"): the free-mode floors drop to 96×40px (map 140px), and the grid widget-size slider now reaches a 120px column track (was 140).
### Changed
- A panel shown from the Panels menu / "+ Add widget" while in free mode seeds a tidy default slot below the placed panels (never a full-width block under its absolute siblings). Hidden panels no longer persist 0×0 geometry.
## [2.4.13]
### Fixed
- **Globe overlays now sit ON the sphere with correct occlusion** (the "map spazzes" report). On the native deck.gl GlobeView, data layers (shared with the 2D map) carried no depth parameters, so the far hemisphere showed through and count badges floated above the limb. GlobeNative now seats every data layer against the depth-writing ocean sphere (test, don't write — no inter-marker z-fight), and the far-side billboard cull (`occludeFarSide`) faces the globe's OWN live camera instead of the parked (frozen) mapbox center — extended to `TextLayer` so a back-side "36" count badge disappears instead of hovering over the planet.
- **Terrain striping on the globe**: the draped ESRI imagery tiles each wrote depth, so coplanar neighbours z-fought at the tile seams (horizontal stripes / partial render). Tiles now depth-TEST only; the ocean sphere owns the depth buffer.
- **Live request-geo dots kept polling on the 3D globe**: the `/v1/world/cloud/*` poll was gated on `renderPaused`, which the native GlobeView sets on activation — so the realtime dots froze the moment you entered the default 3D view. It now gates on tab visibility, so the dots stay live (and fade) on the globe.
### Changed
- **Request-origin → serving-region arcs on by default** in the Cloud view: the `/cloud/traffic` arcs derive from the same real native request-geo points as the dots (origin country → nearest Hanzo region) and degrade to empty — never demo.
## [2.4.6]
### Added
- **Enso Flywheel panel (AI variant)**: the router self-improvement loop made visible. New `/v1/world/enso-training` folds the routing-decision ledger tail + reward tail (`export-routing-ledger` / `export-routing-rewards`, super-admin) into ledger growth, engine-vs-heuristic mix, a routing-confidence histogram, and task/model distributions, alongside the latest enso-bench eval scores (an embedded `results/summary.json` snapshot, overridable live via `ENSO_BENCH_URL`). The response is event-typed so future retrain/deploy milestones slot into the same timeline. Eval scores render even signed-out; `state` (live/partial/demo) says which live sources resolved.
## [2.4.5]
### Added
- **AI Compute panel (AI variant)**: live Hanzo inference-plane telemetry over SSE — tokens/s, requests/s, 24h spend, top models by real spend, and the serving fleet (GPUs, machines online, models served). New `/v1/world/ai-pulse` pushes typed `usage`/`fleet`/`status` frames (EventSource) and answers a plain GET with one JSON snapshot as the poll fallback. Honest "connecting"/"unavailable" states — never a zero dressed up as live; the service bearer stays server-side.
## [2.4.4]
### Changed
- **Cloud Pulse is real when a service token is wired**: `/v1/world/cloud-pulse` now folds MEASURED platform-wide 24h request/token volume from the ClickHouse-backed usage ledger (`get-cloud-usages ?org=all`, super-admin) on top of the live model/node/GPU/region counts — dropping both `demo:true` and `volumeModeled:true`. Top models come from real ledger spend. Without a token, or with a non-admin token, it stays honestly demo/modeled — platform numbers are never faked silently. The service bearer stays server-side (never sent to the browser).
## [2.4.2]
### Added
- **Streaming analyst**: answers flow in live over SSE — reasoning shows as dim thinking text, tool calls appear as chips the moment they run, the reply types itself in; the final render and command dispatch are unchanged (done event = the old JSON contract)
- **Model menu**: the composer pill opens a grouped popover (Auto / Zen / GPT / Llama / Claude / Agents) with per-family marks and an active check
### Fixed
- **Model identity**: the Zen ring appears only on zen* models — gpt-oss/llama/claude get their own marks on the pill, menu rows, avatars, and the thinking row
- **Dev server**: `npm run dev` proxies /v1 to production by default (VITE_DEV_API_PROXY overrides)
## [2.4.1]
### Added
- **Western Pacific cyclones**: cross-agency tropical-cyclone attribution (GDACS + HKO warnings via new `/v1/world/hko-warnings` proxy) with per-agency wind observations, canonical dedup, and map popup detail rows
- **Model roster**: `Best (auto)` leads the analyst model picker — the gateway routing alias that always resolves
### Changed
- **AI default model**: `zen5` → `best`; a pinned family id goes dark when the inference plane's claim catalog shifts, the routing alias never does
- **Server cache is now stale-while-revalidate**: `cachedJSON`/`passthrough` serve stale instantly and refresh in the background (single-flight); GDELT/theater-posture no longer stall requests ~10s on TTL expiry
- **GDELT cache warmers**: hot keys (analyst grounding, protests layer) refreshed every ~4min so no user ever eats a cold miss
- **Sparkline payloads**: close arrays rounded to 7 significant digits (float32-widening noise stripped, ~40% smaller; scalars untouched)
### Fixed
- **News first paint**: panels no longer gate their first DOM write on a 65MB ML sentiment model download — headlines paint immediately, sentiment refines in place
- **Analyst grounding snapshot**: context fetches bounded at 2.5s so a cold endpoint can't hold the chat send hostage
- **AI errors are honest**: upstream error codes (`insufficient_balance`, `spend_cap_exceeded`, …) surface in the chat instead of a bare `status 402`
## [2.4.0] - 2026-02-19
### Added
- **Live Webcams Panel**: 2x2 grid of live YouTube webcam feeds from global hotspots with region filters (Middle East, Europe, Asia-Pacific, Americas), grid/single view toggle, idle detection, and full i18n support (#111)
- **Linux download**: added `.AppImage` option to download banner
### Changed
- **Mobile detection**: use viewport width only for mobile detection; touch-capable notebooks (e.g. ROG Flow X13) now get desktop layout (#113)
- **Webcam feeds**: curated Tel Aviv, Mecca, LA, Miami; replaced dead Tokyo feed; diverse ALL grid with Jerusalem, Tehran, Kyiv, Washington
### Fixed
- **Le Monde RSS**: English feed URL updated (`/en/rss/full.xml` → `/en/rss/une.xml`) to fix 404
- **Workbox precache**: added `html` to `globPatterns` so `navigateFallback` works for offline PWA
- **Panel ordering**: one-time migration ensures Live Webcams follows Live News for existing users
- **Light mode readability**: Darkened neon semantic colors and overlay backgrounds for light mode contrast
## [2.3.6] - 2026-02-16
### Fixed
- **Windows console window**: Hide the `node.exe` console window that appeared alongside the desktop app on Windows
## [2.3.5] - 2026-02-16
### Changed
- **Panel error messages**: Differentiated error messages per panel so users see context-specific guidance instead of generic failures
- **Desktop config auto-hide**: Desktop configuration panel automatically hides on web deployments where it is not relevant
## [2.3.4] - 2026-02-16
### Fixed
- **Windows sidecar crash**: Strip `\\?\` UNC extended-length prefix from paths before passing to Node.js — Tauri `resource_dir()` on Windows returns UNC-prefixed paths that cause `EISDIR: lstat 'C:'` in Node.js module resolution
- **Windows sidecar CWD**: Set explicit `current_dir` on the Node.js Command to prevent bare drive-letter working directory issues from NSIS shortcut launcher
- **Sidecar package scope**: Add `package.json` with `"type": "module"` to sidecar directory, preventing Node.js from walking up the entire directory tree during ESM scope resolution
## [2.3.3] - 2026-02-16
### Fixed
- **Keychain persistence**: Enable `apple-native` (macOS) and `windows-native` (Windows) features for the `keyring` crate — v3 ships with no default platform backends, so API keys were stored in-memory only and lost on restart
- **Settings key verification**: Soft-pass network errors during API key verification so transient sidecar failures don't block saving
- **Resilient keychain reads**: Use `Promise.allSettled` in `loadDesktopSecrets` so a single key failure doesn't discard all loaded secrets
- **Settings window capabilities**: Add `"settings"` to Tauri capabilities window list for core plugin permissions
- **Input preservation**: Capture unsaved input values before DOM re-render in settings panel
- **Meta tag validation**: Validate URL query params with regex allowlist in `parseStoryParams()`
### Fixed
- **Service worker stale assets**: Add `skipWaiting`, `clientsClaim`, and `cleanupOutdatedCaches` to workbox config — fixes `NS_ERROR_CORRUPTED_CONTENT` / MIME type errors when users have a cached SW serving old HTML after redeployment
## [2.2.6] - 2026-02-14
### Fixed
- Filter trending noise and fix sidecar auth
- Restore tech variant panels
- Remove Market Radar and Economic Data panels from tech variant
### Docs
- Add developer X/Twitter link to Support section
- Add cyber threat API keys to `.env.example`
## [2.2.5] - 2026-02-13
### Security
- Migrate all Vercel edge functions to CORS allowlist
- Restrict Railway relay CORS to allowed origins only
**NEVER merge or push to a different branch without explicit user permission.**
- If on `beta`, only push to `beta` - never merge to `main` without asking
- If on `main`, stay on `main` - never switch branches and push without asking
- NEVER merge branches without explicit request
- Pushing to the CURRENT branch after commits is OK when continuing work
## Critical: RSS Proxy Allowlist
When adding new RSS feeds in `src/config/feeds.ts`, you **MUST** also add the feed domains to the allowlist in `api/rss-proxy.js`.
### Why
The RSS proxy has a security allowlist (`ALLOWED_DOMAINS`) that blocks requests to domains not explicitly listed. Feeds from unlisted domains will return HTTP 403 "Domain not allowed" errors.
### How to Add New Feeds
1. Add the feed to `src/config/feeds.ts`
2. Extract the domain from the feed URL (e.g., `https://www.ycombinator.com/blog/rss/` → `www.ycombinator.com`)
3. Add the domain to `ALLOWED_DOMAINS` array in `api/rss-proxy.js`
4. Deploy changes to Vercel
### Example
```javascript
// In api/rss-proxy.js
constALLOWED_DOMAINS=[
// ... existing domains
'www.ycombinator.com',// Add new domain here
];
```
### Debugging Feed Issues
If a panel shows "No news available":
1. Open browser DevTools → Console
2. Look for `HTTP 403` or "Domain not allowed" errors
3. Check if the domain is in `api/rss-proxy.js` allowlist
## Site Variants
Two variants controlled by `VITE_VARIANT` environment variable:
**Real-time global intelligence dashboard** — AI-powered news aggregation, geopolitical monitoring, and infrastructure tracking in a unified situational awareness interface.
| **Finance Monitor** | [finance.worldmonitor.app](https://finance.worldmonitor.app) | Global markets, trading, central banks, Gulf FDI |
Both variants run from a single codebase — switch between them with one click.
All three variants run from a single codebase — switch between them with one click via the header bar (🌍 WORLD | 💻 TECH | 📈 FINANCE).
---
## Key Features
### Interactive Global Map
- **25+ data layers** — conflicts, military bases, nuclear facilities, undersea cables, pipelines, satellite fire detection, protests, natural disasters, datacenters, and more
- **Smart clustering** — markers intelligently group at low zoom, expand on zoom in
- **Progressive disclosure** — detail layers (bases, nuclear, datacenters) appear only when zoomed in; zoom-adaptive opacity prevents clutter at world view
- **RTL Support** — Native right-to-left layout support for Arabic (`ar`) and Hebrew.
- **Localized News Feeds** — Region-specific RSS selection based on language preference (e.g., viewing the app in French loads Le Monde, Jeune Afrique, and France24).
- **AI Translation** — Integrated LLM translation for news headlines and summaries, enabling cross-language intelligence gathering.
- **Regional Intelligence** — Dedicated monitoring panels for Africa, Latin America, Middle East, and Asia with local sources.
### Interactive 3D Globe
- **WebGL-accelerated rendering** — deck.gl + MapLibre GL JS for smooth 60fps performance with thousands of concurrent markers. Switchable between **3D globe** (with pitch/rotation) and **flat map** mode via `VITE_MAP_INTERACTION_MODE`
- **35+ data layers** — conflicts, military bases, nuclear facilities, undersea cables, pipelines, satellite fire detection, protests, natural disasters, datacenters, displacement flows, climate anomalies, cyber threat IOCs, stock exchanges, financial centers, central banks, commodity hubs, Gulf investments, and more
- **Smart clustering** — Supercluster groups markers at low zoom, expands on zoom in. Cluster thresholds adapt to zoom level
- **Progressive disclosure** — detail layers (bases, nuclear, datacenters) appear only when zoomed in; zoom-adaptive opacity fades markers from 0.2 at world view to 1.0 at street level
- **Label deconfliction** — overlapping labels (e.g., multiple BREAKING badges) are automatically suppressed by priority, highest-severity first
- **8 regional presets** — Global, Americas, Europe, MENA, Asia, Africa, Oceania, Latin America
- **URL state sharing** — map center, zoom, active layers, and time range are encoded in the URL for shareable views (`?view=mena&zoom=4&layers=conflicts,bases`)
### AI-Powered Intelligence
- **World Brief** — LLM-synthesized summary of top global developments (Groq Llama 3.1, Redis-cached)
- **Hybrid Threat Classification** — instant keyword classifier with async LLM override for higher-confidence results
- **Focal Point Detection** — correlates entities across news, military activity, protests, outages, and markets to identify convergence
- **Country Instability Index** — real-time stability scores for 20 monitored nations using weighted multi-signal blend
- **Country Instability Index** — real-time stability scores for 22 monitored nations using weighted multi-signal blend
- **Trending Keyword Spike Detection** — 2-hour rolling window vs 7-day baseline flags surging terms across RSS feeds, with CVE/APT entity extraction and auto-summarization
- **Strategic Posture Assessment** — composite risk score combining all intelligence modules with trend detection
- **Country Brief Pages** — click any country for a full-page intelligence dossier with CII score ring, AI-generated analysis, top news with citation anchoring, prediction markets, 7-day event timeline, active signal chips, infrastructure exposure, and stock market index — exportable as JSON, CSV, or image
### Real-Time Data Layers
<details>
<summary><strong>Geopolitical</strong></summary>
- Active conflict zones with escalation tracking
- Active conflict zones with escalation tracking (UCDP + ACLED)
- 92 global stock exchanges — mega (NYSE, NASDAQ, Shanghai, Euronext, Tokyo), major (Hong Kong, London, NSE/BSE, Toronto, Korea, Saudi Tadawul), and emerging markets — with market caps and trading hours
- 19 financial centers — ranked by Global Financial Centres Index (New York #1 through offshore centers: Cayman Islands, Luxembourg, Bermuda, Channel Islands)
- 13 central banks — Federal Reserve, ECB, BoJ, BoE, PBoC, SNB, RBA, BoC, RBI, BoK, BCB, SAMA, plus supranational institutions (BIS, IMF)
- Gulf FDI investment layer — 64 Saudi/UAE foreign direct investments plotted globally, color-coded by status (operational, under-construction, announced), sized by investment amount
</details>
### Live News & Video
- **100+ RSS feeds** across geopolitics, defense, energy, tech
- **Live video streams** — Bloomberg, Sky News, Al Jazeera, CNBC, and more
- **Custom monitors** — Create keyword-based alerts for any topic
- **150+ RSS feeds** across geopolitics, defense, energy, tech, and finance — domain-allowlisted proxy prevents CORS issues. Each variant loads its own curated feed set: ~25 categories for geopolitical, ~20 for tech, ~18 for finance
- **8 live video streams** — Bloomberg, Sky News, Al Jazeera, Euronews, DW, France24, CNBC, Al Arabiya — with automatic live detection that scrapes YouTube channel pages every 5 minutes to find active streams
- **Desktop embed bridge** — YouTube's IFrame API restricts playback in native webviews (error 153). The dashboard detects this and transparently routes through a cloud-hosted embed proxy with bidirectional message passing (play/pause/mute/unmute/loadVideo)
- **Idle-aware playback** — video players pause and are removed from the DOM after 5 minutes of inactivity, resuming when the user returns. Tab visibility changes also suspend/resume streams
- **Custom monitors** — Create keyword-based alerts for any topic, color-coded with persistent storage
- **Virtual scrolling** — news panels render only visible DOM elements, handling thousands of items without browser lag
### Signal Aggregation & Anomaly Detection
- **Multi-source signal fusion** — internet outages, military flights, naval vessels, protests, AIS disruptions, and satellite fires are aggregated into a unified intelligence picture with per-country and per-region clustering
- **Multi-source signal fusion** — internet outages, military flights, naval vessels, protests, AIS disruptions, satellite fires, and keyword spikes are aggregated into a unified intelligence picture with per-country and per-region clustering
- **Temporal baseline anomaly detection** — Welford's online algorithm computes streaming mean/variance per event type, region, weekday, and month over a 90-day window. Z-score thresholds (1.5/2.0/3.0) flag deviations like "Military flights 3.2x normal for Thursday (January)" — stored in Redis via Upstash
- **Regional convergence scoring** — when multiple signal types spike in the same geographic area, the system identifies convergence zones and escalates severity
### Story Sharing & Social Export
- **Shareable intelligence stories** — generate country-level intelligence briefs with CII scores, threat counts, theater posture, and related prediction markets
- **Multi-platform export** — custom-formatted sharing for Twitter/X, LinkedIn, WhatsApp, Telegram, Reddit, and Facebook with platform-appropriate formatting
- **Deep links** — every story generates a unique URL (`/story?c=<country>&t=<type>`) with dynamic Open Graph meta tags for rich social previews
- **Canvas-based image generation** — stories render as PNG images for visual sharing, with QR codes linking back to the live dashboard
### Desktop Application (Tauri)
- **Native desktop app** for macOS and Windows — packages the full dashboard with a local Node.js sidecar that runs all 60+ API handlers locally
- **OS keychain integration** — API keys stored in the system credential manager (macOS Keychain, Windows Credential Manager), never in plaintext files
- **Token-authenticated sidecar** — a unique session token prevents other local processes from accessing the sidecar on localhost. Generated per launch using randomized hashing
- **Cloud fallback** — when a local API handler fails or is missing, requests transparently fall through to the cloud deployment (worldmonitor.app) with origin headers stripped
- **Settings window** — dedicated configuration UI (Cmd+,) for managing 17 API keys with validation, signup links, and feature-availability indicators
- **Verbose debug mode** — toggle traffic logging with persistent state across restarts. View the last 200 requests with timing, status codes, and error details
- **DevTools toggle** — Cmd+Alt+I opens the embedded web inspector for debugging
### Progressive Web App
- **Installable** — the dashboard can be installed to the home screen on mobile or as a standalone desktop app via Chrome's install prompt. Full-screen `standalone` display mode with custom theme color
- **Offline map support** — MapTiler tiles are cached using a CacheFirst strategy (up to 500 tiles, 30-day TTL), enabling map browsing without a network connection
- **Smart caching strategies** — APIs and RSS feeds use NetworkOnly (real-time data must always be fresh), while fonts (1-year TTL), images (7-day StaleWhileRevalidate), and static assets (1-year immutable) are aggressively cached
- **Auto-updating service worker** — checks for new versions every 60 minutes. Tauri desktop builds skip service worker registration entirely (uses native APIs instead)
- **Offline fallback** — a branded fallback page with retry button is served when the network is unavailable
### Additional Capabilities
- Signal intelligence with "Why It Matters" context
- Infrastructure cascade analysis with proximity correlation
- Maritime & aviation tracking with surge detection
- Prediction market integration (Polymarket) as leading indicators
- Service status monitoring (cloud providers, AI services)
- Shareable map state via URL parameters (view, zoom, coordinates, time range, active layers)
- Data freshness monitoring across 14 data sources with explicit intelligence gap reporting
- Per-feed circuit breakers with 5-minute cooldowns to prevent cascading failures
- Browser-side ML worker (Transformers.js) for NER and sentiment analysis without server dependency
- **Cmd+K search** — fuzzy search across news headlines, countries, and entities
- **Virtual scrolling** — news panels render only visible DOM elements, handling thousands of items without browser lag
- **Cmd+K search** — fuzzy search across 20+ result types: news headlines, countries (with direct country brief navigation), hotspots, markets, military bases, cables, pipelines, datacenters, nuclear facilities, tech companies, and more
- **Historical playback** — dashboard snapshots are stored in IndexedDB. A time slider allows rewinding to any saved state, with live updates paused during playback
- **Mobile detection** — screens below 768px receive a warning modal since the dashboard is designed for multi-panel desktop use
- **UCDP conflict classification** — countries with active wars (1,000+ battle deaths/year) receive automatic CII floor scores, preventing optimistic drift
- **HAPI humanitarian data** — UN OCHA humanitarian access metrics feed into country-level instability scoring
- **HAPI humanitarian data** — UN OCHA humanitarian access metrics and displacement flows feed into country-level instability scoring with dual-perspective (origins vs. hosts) panel
- **Idle-aware resource management** — animations pause after 2 minutes of inactivity and when the tab is hidden, preventing battery drain. Video streams are destroyed from the DOM and recreated on return
- **Country-specific stock indices** — country briefs display the primary stock market index with 1-week change (S&P 500 for US, Shanghai Composite for China, etc.) via the `/api/stock-index` endpoint
- **Climate anomaly panel** — 15 conflict-prone zones monitored for temperature/precipitation deviations against 30-day ERA5 baselines, with severity classification feeding into CII
- **Country brief export** — every brief is downloadable as structured JSON, flattened CSV, or rendered PNG image, enabling offline analysis and reporting workflows
- **Print/PDF support** — country briefs include a print button that triggers the browser's native print dialog, producing clean PDF output
- **Oil & energy analytics** — WTI/Brent crude prices, US production (Mbbl/d), and inventory levels via the EIA API with weekly trend detection
- **Population exposure estimation** — WorldPop density data estimates civilian population within event-specific radii (50–100km) for conflicts, earthquakes, floods, and wildfires
- **Trending keywords panel** — real-time display of surging terms across all RSS feeds with spike severity, source count, and AI-generated context summaries
- **Download banner** — persistent notification for web users linking to native desktop installers for their detected platform
- **Download API** — `/api/download?platform={windows-exe|windows-msi|macos-arm64|macos-x64}` redirects to the matching GitHub Release asset, with fallback to the releases page
- **Non-tier country support** — clicking countries outside the 22 tier-1 list opens a brief with available data (news, markets, infrastructure) and a "Limited coverage" badge; country names for non-tier countries resolve via `Intl.DisplayNames`
- **Feature toggles** — 14 runtime toggles (AI/Groq, AI/OpenRouter, FRED economic, EIA energy, internet outages, ACLED conflicts, threat intel feeds, AIS relay, OpenSky, Finnhub, NASA FIRMS) stored in `localStorage`, allowing administrators to enable/disable data sources without rebuilding
- **AIS chokepoint detection** — the relay server monitors 8 strategic maritime chokepoints (Strait of Hormuz, Suez Canal, Malacca Strait, Bab el-Mandeb, Panama Canal, Taiwan Strait, South China Sea, Turkish Straits) and classifies transiting vessels by naval candidacy using MMSI prefixes, ship type codes, and name patterns
- **AIS density grid** — vessel positions are aggregated into 2°×2° geographic cells over 30-minute windows, producing a heatmap of maritime traffic density that feeds into convergence detection
- **Panel resizing** — drag handles on panel edges allow height adjustment (span-1 through span-4 grid rows), persisted to localStorage. Double-click resets to default height
---
## Regression Testing
Map overlay behavior is validated in Playwright using the map harness (`/map-harness.html`).
Map overlay behavior is validated in Playwright using the map harness (`/tests/map-harness.html`).
- Cluster-state cache initialization guard:
-`updates protest marker click payload after data refresh`
@@ -170,6 +256,37 @@ Map overlay behavior is validated in Playwright using the map harness (`/map-har
## How It Works
### Country Brief Pages
Clicking any country on the map opens a full-page intelligence dossier — a single-screen synthesis of all intelligence modules for that country. The brief is organized into a two-column layout:
**Left column**:
- **Instability Index** — animated SVG score ring (0–100) with four component breakdown bars (Unrest, Conflict, Security, Information), severity badge, and trend indicator
- **Intelligence Brief** — AI-generated analysis (Groq Llama 3.1) with inline citation anchors `[1]`–`[8]` that scroll to the corresponding news source when clicked
- **Top News** — 8 most relevant headlines for the country, threat-level color-coded, with source and time-ago metadata
**Right column**:
- **Active Signals** — real-time chip indicators for protests, military aircraft, naval vessels, internet outages, earthquakes, displacement flows, climate stress, conflict events, and the country's stock market index (1-week change)
- **7-Day Timeline** — D3.js-rendered event chart with 4 severity-coded lanes (protest, conflict, natural, military), interactive tooltips, and responsive resizing
- **Prediction Markets** — top 3 Polymarket contracts by volume with probability bars and external links
- **Infrastructure Exposure** — pipelines, undersea cables, datacenters, military bases, nuclear facilities, and ports within a 600km radius of the country centroid, ranked by distance
**Headline relevance filtering**: each country has an alias map (e.g., `US → ["united states", "american", "washington", "pentagon", "biden", "trump"]`). Headlines are filtered using a negative-match algorithm — if another country's alias appears earlier in the headline title than the target country's alias, the headline is excluded. This prevents cross-contamination (e.g., a headline about Venezuela mentioning "Washington sanctions" appearing in the US brief).
**Export options**: briefs are exportable as JSON (structured data with all scores, signals, and headlines), CSV (flattened tabular format), or PNG image. A print button triggers the browser's native print dialog for PDF export.
### Local-First Country Detection
Map clicks resolve to countries using a local geometry service rather than relying on network reverse-geocoding (Nominatim). The system loads a GeoJSON file containing polygon boundaries for ~200 countries and builds an indexed spatial lookup:
1.**Bounding box pre-filter** — each country's polygon(s) are wrapped in a bounding box (`[minLon, minLat, maxLon, maxLat]`). Points outside the bbox are rejected without polygon intersection testing.
2.**Ray-casting algorithm** — for points inside the bbox, a ray is cast from the point along the positive x-axis. The number of polygon edge intersections determines inside/outside status (odd = inside). Edge cases are handled: points on segment boundaries return `true`, and polygon holes are subtracted (a point inside an outer ring but also inside a hole is excluded).
3.**MultiPolygon support** — countries with non-contiguous territories (e.g., the US with Alaska and Hawaii, Indonesia with thousands of islands) use MultiPolygon geometries where each polygon is tested independently.
This approach provides sub-millisecond country detection entirely in the browser, with no network latency. The geometry data is preloaded at app startup and cached for the session. For countries not in the GeoJSON (rare), the system falls back to hardcoded rectangular bounding boxes, and finally to network reverse-geocoding as a last resort.
### Threat Classification Pipeline
Every news item passes through a two-stage classification pipeline:
@@ -181,7 +298,7 @@ This hybrid approach means the UI is never blocked waiting for AI — users see
### Country Instability Index (CII)
Each monitored country receives a real-time instability score (0–100) computed from:
22 tier-1 countries receive continuous monitoring: US, Russia, China, Ukraine, Iran, Israel, Taiwan, North Korea, Saudi Arabia, Turkey, Poland, Germany, France, UK, India, Pakistan, Syria, Yemen, Myanmar, Venezuela, Brazil, and UAE. Each receives a real-time instability score (0–100) computed from:
| Component | Weight | Details |
|-----------|--------|---------|
@@ -224,6 +341,7 @@ Nine operational theaters are continuously assessed for military posture escalat
| Black Sea | ISR flights, naval movements |
Posture levels escalate from NORMAL → ELEVATED → CRITICAL based on a composite of:
- **Aircraft count** in theater (both resident and transient)
- **Strike capability** — the presence of tankers + AWACS + fighters together indicates strike packaging, not routine training
- **Naval presence** — carrier groups and combatant formations
@@ -271,6 +390,20 @@ When a news event is geo-located, the system automatically identifies critical i
A 74-hub strategic location database infers geography from headlines via keyword matching. Hubs span capitals, conflict zones, strategic chokepoints (Strait of Hormuz, Suez Canal, Malacca Strait), and international organizations. Confidence scoring is boosted for critical-tier hubs and active conflict zones, enabling map-driven news placement without requiring explicit location metadata from RSS feeds.
### Entity Index & Cross-Referencing
A structured entity registry catalogs countries, organizations, world leaders, and military entities with multiple lookup indices:
| Index Type | Purpose | Example |
|-----------|---------|---------|
| **ID index** | Direct entity lookup | `entity:us` → United States profile |
| **Alias index** | Name variant matching | "America", "USA", "United States" → same entity |
| **Keyword index** | Contextual detection | "Pentagon", "White House" → United States |
Entity matching uses word-boundary regex to prevent false positives (e.g., "Iran" matching "Ukraine"). Confidence scores are tiered by match quality: exact name matches score 1.0, aliases 0.85–0.95, and keyword matches 0.7. When the same entity surfaces across multiple independent data sources (news, military tracking, protest feeds, market signals), the system identifies it as a focal point and escalates its prominence in the intelligence picture.
### Temporal Baseline Anomaly Detection
Rather than relying on static thresholds, the system learns what "normal" looks like and flags deviations. Each event type (military flights, naval vessels, protests, news velocity, AIS gaps, satellite fires) is tracked per region with separate baselines for each weekday and month — because military activity patterns differ on Tuesdays vs. weekends, and January vs. July.
@@ -285,6 +418,115 @@ The algorithm uses **Welford's online method** for numerically stable streaming
A minimum of 10 historical samples is required before anomalies are reported, preventing false positives during the learning phase. Anomalies are ingested back into the signal aggregator, where they compound with other signals for convergence detection.
### Trending Keyword Spike Detection
Every RSS headline is tokenized into individual terms and tracked in per-term frequency maps. A 2-hour rolling window captures current activity while a 7-day baseline (refreshed hourly) establishes what "normal" looks like for each term. A spike fires when all conditions are met:
| **Cooldown** | 30 minutes since last spike for the same term |
The tokenizer extracts CVE identifiers (`CVE-2024-xxxxx`), APT/FIN threat actor designators, and 12 compound terms for world leaders (e.g., "Xi Jinping", "Kim Jong Un") that would be lost by naive whitespace splitting. A configurable blocklist suppresses common noise terms.
Detected spikes are auto-summarized via Groq (rate-limited to 5 summaries/hour) and emitted as `keyword_spike` signals into the correlation engine, where they compound with other signal types for convergence detection. The term registry is capped at 10,000 entries with LRU eviction to bound memory usage. All thresholds (spike multiplier, min count, cooldown, blocked terms) are configurable via the Settings panel.
### Cyber Threat Intelligence Layer
Five threat intelligence feeds provide indicators of compromise (IOCs) for active command-and-control servers, malware distribution hosts, phishing campaigns, and malicious URLs:
Each IP-based IOC is geo-enriched using ipinfo.io with freeipapi.com as fallback. Geolocation results are Redis-cached for 24 hours. Enrichment runs concurrently — 16 parallel lookups with a 12-second timeout, processing up to 250 IPs per collection run.
IOCs are classified into four types (`c2_server`, `malware_host`, `phishing`, `malicious_url`) with four severity levels, rendered as color-coded scatter dots on the globe. The layer uses a 10-minute cache, a 14-day rolling window, and caps display at 500 IOCs to maintain rendering performance.
### Natural Disaster Monitoring
Three independent sources are merged into a unified disaster picture, then deduplicated on a 0.1° geographic grid:
GDACS events carry color-coded alert levels (Red = critical, Orange = high) and are filtered to exclude low-severity Green alerts. EONET wildfires are filtered to events within 48 hours to prevent stale data. Earthquakes from EONET are excluded since USGS provides higher-quality seismological data.
The merged output feeds into the signal aggregator for geographic convergence detection — e.g., an earthquake near a pipeline triggers an infrastructure cascade alert.
### Dual-Source Protest Tracking
Protest data is sourced from two independent providers to reduce single-source bias:
1.**ACLED** (Armed Conflict Location & Event Data) — 30-day window, tokenized API with Redis caching (10-minute TTL). Covers protests, riots, strikes, and demonstrations with actor attribution and fatality counts.
2.**GDELT** (Global Database of Events, Language, and Tone) — 7-day geospatial event feed filtered to protest keywords. Events with mention count ≥5 are included; those above 30 are marked as `validated`.
Events from both sources are **Haversine-deduplicated** on a 0.5° grid (~50km) with same-day matching. ACLED events take priority due to higher editorial confidence. Severity is classified as:
- **High** — fatalities present or riot/clash keywords
- **Medium** — standard protest/demonstration
- **Low** — default
Protest scoring is regime-aware: democratic countries use logarithmic scaling (routine protests don't trigger instability), while authoritarian states use linear scoring (every protest is significant). Fatalities and concurrent internet outages apply severity boosts.
### Climate Anomaly Detection
15 conflict-prone and disaster-prone zones are continuously monitored for temperature and precipitation anomalies using Open-Meteo ERA5 reanalysis data. A 30-day baseline is computed, and current conditions are compared against it to determine severity:
| Severity | Temperature Deviation | Precipitation Deviation |
| **Normal** | Within expected range | Within expected range |
Anomalies feed into the signal aggregator, where they amplify CII scores for affected countries (climate stress is a recognized conflict accelerant). The Climate Anomaly panel surfaces these deviations in a severity-sorted list.
### Displacement Tracking
Refugee and displacement data is sourced from the UN OCHA Humanitarian API (HAPI), providing population-level counts for refugees, asylum seekers, and internally displaced persons (IDPs). The Displacement panel offers two perspectives:
- **Origins** — countries people are fleeing from, ranked by outflow volume
- **Hosts** — countries absorbing displaced populations, ranked by intake
Crisis badges flag countries with extreme displacement: > 1 million displaced (red), > 500,000 (orange). Displacement outflow feeds into the CII as a component signal — high displacement is a lagging indicator of instability that persists even when headlines move on.
### Population Exposure Estimation
Active events (conflicts, earthquakes, floods, wildfires) are cross-referenced against WorldPop population density data to estimate the number of civilians within the impact zone. Event-specific radii reflect typical impact footprints:
| Event Type | Radius | Rationale |
|-----------|--------|-----------|
| **Conflicts** | 50 km | Direct combat zone + displacement buffer |
| **Earthquakes** | 100 km | Shaking intensity propagation |
| **Floods** | 100 km | Watershed and drainage basin extent |
| **Wildfires** | 30 km | Smoke and evacuation perimeter |
API calls to WorldPop are batched concurrently (max 10 parallel requests) to handle multiple simultaneous events without sequential bottlenecks. The Population Exposure panel displays a summary header with total affected population and a per-event breakdown table.
### Strategic Port Infrastructure
83 strategic ports are cataloged across six types, reflecting their role in global trade and military posture:
Ports are ranked by throughput and weighted by strategic importance in the infrastructure cascade model: oil/LNG terminals carry 0.9 criticality, container ports 0.7, and naval bases 0.4. Port proximity appears in the Country Brief infrastructure exposure section.
### Browser-Side ML Pipeline
The dashboard runs a full ML pipeline in the browser via Transformers.js, with no server dependency for core intelligence. This is automatically disabled on mobile devices to conserve memory.
@@ -304,18 +546,30 @@ News velocity is tracked per cluster — when multiple Tier 1–2 sources conver
All real-time data sources feed into a central signal aggregator that builds a unified geospatial intelligence picture. Signals are clustered by country and region, with each signal carrying a severity (low/medium/high), geographic coordinates, and metadata. The aggregator:
1.**Clusters by country** — groups signals from diverse sources (flights, vessels, protests, fires, outages) into per-country profiles
1.**Clusters by country** — groups signals from diverse sources (flights, vessels, protests, fires, outages, `keyword_spike`) into per-country profiles
2.**Detects regional convergence** — identifies when multiple signal types spike in the same geographic corridor (e.g., military flights + protests + satellite fires in Eastern Mediterranean)
3.**Feeds downstream analysis** — the CII, hotspot escalation, focal point detection, and AI insights modules all consume the aggregated signal picture rather than raw data
### Data Freshness & Intelligence Gaps
A singleton tracker monitors 14 data sources (GDELT, RSS, AIS, military flights, earthquakes, weather, outages, ACLED, Polymarket, economic indicators, NASA FIRMS, and more) with status categorization: fresh (<15 min), stale (1h), very_stale (6h), no_data, error, disabled. It explicitly reports **intelligence gaps** — what analysts can't see — preventing false confidence when critical data sources are down or degraded.
A singleton tracker monitors 22 data sources (GDELT, RSS, AIS, military flights, earthquakes, weather, outages, ACLED, Polymarket, economic indicators, NASA FIRMS, cyber threat feeds, trending keywords, oil/energy, population exposure, and more) with status categorization: fresh (<15 min), stale (1h), very_stale (6h), no_data, error, disabled. It explicitly reports **intelligence gaps** — what analysts can't see — preventing false confidence when critical data sources are down or degraded.
### Prediction Markets as Leading Indicators
Polymarket geopolitical markets are queried using tag-based filters (Ukraine, Iran, China, Taiwan, etc.) with 5-minute caching. Market probability shifts are correlated with news volume: if a prediction market moves significantly before matching news arrives, this is flagged as a potential early-warning signal.
**Cloudflare JA3 bypass** — Polymarket's API is protected by Cloudflare TLS fingerprinting (JA3) that blocks all server-side requests. The system uses a 3-tier fallback:
Once browser-direct succeeds, the system caches this state and skips fallback tiers on subsequent requests. Country-specific markets are fetched by mapping countries to Polymarket tags with name-variant matching (e.g., "Russia" matches titles containing "Russian", "Moscow", "Kremlin", "Putin").
Markets are filtered to exclude sports and entertainment (100+ exclusion keywords), require meaningful price divergence from 50% or volume above $50K, and are ranked by trading volume. Each variant gets different tag sets — geopolitical focus queries politics/world/ukraine/middle-east tags, while tech focus queries ai/crypto/business tags.
### Macro Signal Analysis (Market Radar)
The Market Radar panel computes a composite BUY/CASH verdict from 7 independent signals sourced entirely from free APIs (Yahoo Finance, mempool.space, alternative.me):
@@ -340,6 +594,23 @@ VWAP = Σ(price × volume) / Σ(volume) for last 30 trading days
The **Mayer Multiple** (BTC price / SMA200) provides a long-term valuation context — historically, values above 2.4 indicate overheating, while values below 0.8 suggest deep undervaluation.
### Gulf FDI Investment Database
The Finance variant includes a curated database of 64 major foreign direct investments by Saudi Arabia and the UAE in global critical infrastructure. Investments are tracked across 12 sectors:
| Sector | Examples |
|--------|---------|
| **Ports** | DP World's 11 global container terminals, AD Ports (Khalifa, Al-Sokhna, Karachi), Saudi Mawani ports |
| **Energy** | ADNOC Ruwais LNG (9.6 mtpa), Aramco's Motiva Port Arthur refinery (630K bpd), ACWA Power renewables |
| **Renewables** | Masdar wind/solar (UK Hornsea, Zarafshan 500MW, Gulf of Suez), NEOM Green Hydrogen (world's largest) |
| **Megaprojects** | NEOM THE LINE ($500B), Saudi National Cloud ($6B hyperscale datacenters) |
| **Telecoms** | STC's 9.9% stake in Telefónica, PIF's 20% of Telecom Italia NetCo |
Each investment records the investing entity (DP World, Mubadala, PIF, ADNOC, Masdar, Saudi Aramco, ACWA Power, etc.), target country, geographic coordinates, investment amount (USD), ownership stake, operational status, and year. The Investments Panel provides filterable views by country (SA/UAE), sector, entity, and status — clicking any row navigates the map to the investment location.
On the globe, investments appear as scaled bubbles: ≥$50B projects (NEOM) render at maximum size, while sub-$1B investments use smaller markers. Color encodes status: green for operational, amber for under-construction, blue for announced.
### Stablecoin Peg Monitoring
Five major stablecoins (USDT, USDC, DAI, FDUSD, USDe) are monitored via the CoinGecko API with 2-minute caching. Each coin's deviation from the $1.00 peg determines its health status:
@@ -352,6 +623,19 @@ Five major stablecoins (USDT, USDC, DAI, FDUSD, USDe) are monitored via the Coin
The panel aggregates total stablecoin market cap, 24h volume, and an overall health status (HEALTHY / CAUTION / WARNING). The `coins` query parameter accepts a comma-separated list of CoinGecko IDs, validated against a `[a-z0-9-]+` regex to prevent injection.
### Oil & Energy Analytics
The Oil & Energy panel tracks four key indicators from the U.S. Energy Information Administration (EIA) API:
Trend detection flags week-over-week changes exceeding ±0.5% as rising or falling, with flat readings within the threshold shown as stable. Results are cached client-side for 30 minutes. The panel provides energy market context for geopolitical analysis — price spikes often correlate with supply disruptions in monitored conflict zones and chokepoint closures.
### BTC ETF Flow Estimation
Ten spot Bitcoin ETFs are tracked via Yahoo Finance's 5-day chart API (IBIT, FBTC, ARKB, BITB, GBTC, HODL, BRRR, EZBC, BTCO, BTCW). Since ETF flow data requires expensive terminal subscriptions, the system estimates flow direction from publicly available signals:
@@ -364,6 +648,25 @@ This is an approximation, not a substitute for official flow data, but it captur
---
## Tri-Variant Architecture
A single codebase produces three specialized dashboards, each with distinct feeds, panels, map layers, and branding:
**Build-time selection** — the `VITE_VARIANT` environment variable controls which configuration is bundled. A Vite HTML plugin transforms meta tags, Open Graph data, PWA manifest, and JSON-LD structured data at build time. Each variant tree-shakes unused data files — the finance build excludes military base coordinates and APT group data, while the geopolitical build excludes stock exchange listings.
**Runtime switching** — a variant selector in the header bar (🌍 WORLD | 💻 TECH | 📈 FINANCE) navigates between deployed domains on the web, or sets `localStorage['worldmonitor-variant']` in the desktop app to switch without rebuilding.
---
## Architecture Principles
| Principle | Implementation |
@@ -372,10 +675,14 @@ This is an approximation, not a substitute for official flow data, but it captur
| **Assume failure** | Per-feed circuit breakers with 5-minute cooldowns. AI fallback chain: Groq → OpenRouter → browser-side T5. Redis cache failures degrade gracefully. Every edge function returns stale cached data when upstream APIs are down. |
| **Show what you can't see** | Intelligence gap tracker explicitly reports data source outages rather than silently hiding them. |
| **Local-first geolocation** | Country detection uses browser-side ray-casting against GeoJSON polygons rather than network reverse-geocoding. Sub-millisecond response, zero API dependency, works offline. Network geocoding is a fallback, not the primary path. |
| **Multi-signal correlation** | No single data source is trusted alone. Focal points require convergence across news + military + markets + protests before escalating to critical. |
| **Geopolitical grounding** | Hard-coded conflict zones, baseline country risk, and strategic chokepoints prevent statistical noise from generating false alerts in low-data regions. |
| **Defense in depth** | CORS origin allowlist, domain-allowlisted RSS proxy, server-side API key isolation, input sanitization with output encoding, IP rate limiting on AI endpoints. |
| **Cache everything, trust nothing** | Three-tier caching (in-memory → Redis → upstream) with versioned cache keys and stale-on-error fallback. Every API response includes `X-Cache` header for debugging. |
| **Defense in depth** | CORS origin allowlist, domain-allowlisted RSS proxy, server-side API key isolation, token-authenticated desktop sidecar, input sanitization with output encoding, IP rate limiting on AI endpoints. |
| **Cache everything, trust nothing** | Three-tier caching (in-memory → Redis → upstream) with versioned cache keys and stale-on-error fallback. Every API response includes `X-Cache` header for debugging. CDN layer (`s-maxage`) absorbs repeated requests before they reach edge functions. |
| **Bandwidth efficiency** | Gzip compression on all relay responses (80% reduction). Content-hash static assets with 1-year immutable cache. Staggered polling intervals prevent synchronized API storms. Animations and polling pause on hidden tabs. |
| **Baseline-aware alerting** | Trending keyword detection uses rolling 2-hour windows against 7-day baselines with per-term spike multipliers, cooldowns, and source diversity requirements — surfacing genuine surges while suppressing noise. |
| **Run anywhere** | Same codebase produces three specialized variants (geopolitical, tech, finance) and deploys to Vercel (web), Railway (relay), Tauri (desktop), and PWA (installable). Desktop sidecar mirrors all cloud API handlers locally. Service worker caches map tiles for offline use while keeping intelligence data always-fresh (NetworkOnly). |
---
@@ -396,7 +703,7 @@ Feeds also carry a **propaganda risk rating** and **state affiliation flag**. St
## Edge Function Architecture
World Monitor uses 45+ Vercel Edge Functions as a lightweight API layer. Each edge function handles a single data source concern — proxying, caching, or transforming external APIs. This architecture avoids a monolithic backend while keeping API keys server-side:
World Monitor uses 60+ Vercel Edge Functions as a lightweight API layer. Each edge function handles a single data source concern — proxying, caching, or transforming external APIs. This architecture avoids a monolithic backend while keeping API keys server-side:
- **RSS Proxy** — domain-allowlisted proxy for 100+ feeds, preventing CORS issues and hiding origin servers. Feeds from domains that block Vercel IPs are automatically routed through the Railway relay.
- **AI Pipeline** — Groq and OpenRouter edge functions with Redis deduplication, so identical headlines across concurrent users only trigger one LLM call. The classify-event endpoint pauses its queue on 500 errors to avoid wasting API quota.
@@ -404,30 +711,41 @@ World Monitor uses 45+ Vercel Edge Functions as a lightweight API layer. Each ed
- **Market Intelligence** — macro signals, ETF flows, and stablecoin monitors compute derived analytics server-side (VWAP, SMA, peg deviation, flow estimates) and cache results in Redis
- **Temporal Baseline** — Welford's algorithm state is persisted in Redis across requests, building statistical baselines without a traditional database
- **Custom Scrapers** — sources without RSS feeds (FwdStart, GitHub Trending, tech events) are scraped and transformed into RSS-compatible formats
- **Finance Geo Data** — stock exchanges (92), financial centers (19), central banks (13), and commodity hubs (10) are served as static typed datasets with market caps, GFCI rankings, trading hours, and commodity specializations
All edge functions include circuit breaker logic and return cached stale data when upstream APIs are unavailable, ensuring the dashboard never shows blank panels.
---
## Dual-Deployment Architecture
## Multi-Platform Architecture
World Monitor runs on two platforms that work together:
All three variants run on three platforms that work together:
```
┌─────────────────────────────────────┐
│ Vercel (Edge) │
│ 45+ edge functions · static SPA │
│ 60+ edge functions · static SPA │
│ CORS allowlist · Redis cache │
│ AI pipeline · market analytics │
└──────────────┬──────────────────────┘
│ https:// (server-side)
│ wss:// (client-side)
▼
│ CDN caching (s-maxage) · PWA host │
└──────────┬─────────────┬────────────┘
│ │ fallback
│ ▼
│ ┌───────────────────────────────────┐
│ │ Tauri Desktop (Rust + Node) │
│ │ OS keychain · Token-auth sidecar │
│ │ 60+ local API handlers · gzip │
│ │ Cloud fallback · Traffic logging │
│ └───────────────────────────────────┘
│
│ https:// (server-side)
│ wss:// (client-side)
▼
┌─────────────────────────────────────┐
│ Railway (Relay Server) │
│ WebSocket relay · OpenSky OAuth2 │
│ RSS proxy for blocked domains │
│ AIS vessel stream multiplexer │
│ AIS vessel stream · gzip all resp │
└─────────────────────────────────────┘
```
@@ -439,6 +757,102 @@ World Monitor runs on two platforms that work together:
The Vercel edge functions connect to Railway via `WS_RELAY_URL` (server-side, HTTPS) while browser clients connect via `VITE_WS_RELAY_URL` (client-side, WSS). This separation keeps the relay URL configurable per deployment without leaking server-side configuration to the browser.
All Railway relay responses are gzip-compressed (zlib `gzipSync`) when the client accepts it and the payload exceeds 1KB, reducing egress by ~80% for JSON and XML responses.
---
## Desktop Application Architecture
The Tauri desktop app wraps the dashboard in a native window with a local Node.js sidecar that runs all API handlers without cloud dependency:
API keys are stored in the operating system's credential manager (macOS Keychain, Windows Credential Manager) — never in plaintext config files. At sidecar launch, all 17 supported secrets are read from the keyring, trimmed, and injected as environment variables. Empty or whitespace-only values are skipped.
Secrets can also be updated at runtime without restarting the sidecar: saving a key in the Settings window triggers a `POST /api/local-env-update` call that hot-patches `process.env` and clears the module cache so handlers pick up the new value immediately.
### Sidecar Authentication
A unique 32-character hex token is generated per app launch using randomized hash state (`RandomState` from Rust's standard library). The token is:
1. Injected into the sidecar as `LOCAL_API_TOKEN`
2. Retrieved by the frontend via the `get_local_api_token` Tauri command (lazy-loaded on first API request)
3. Attached as `Authorization: Bearer <token>` to every local request
The `/api/service-status` health check endpoint is exempt from token validation to support monitoring tools.
### Cloud Fallback
When a local API handler is missing, throws an error, or returns a 5xx status, the sidecar transparently proxies the request to the cloud deployment. Endpoints that fail are marked as `cloudPreferred` — subsequent requests skip the local handler and go directly to the cloud until the sidecar is restarted. Origin and Referer headers are stripped before proxying to maintain server-to-server parity.
### Observability
- **Traffic log** — a ring buffer of the last 200 requests with method, path, status, and duration (ms), accessible via `GET /api/local-traffic-log`
- **Verbose mode** — togglable via `POST /api/local-debug-toggle`, persists across sidecar restarts in `verbose-mode.json`
- **Dual log files** — `desktop.log` captures Rust-side events (startup, secret injection counts, menu actions), while `local-api.log` captures Node.js stdout/stderr
- **DevTools** — `Cmd+Alt+I` toggles the embedded web inspector
---
## Bandwidth Optimization
The system minimizes egress costs through layered caching and compression across all three deployment targets:
### Vercel CDN Headers
Every API edge function includes `Cache-Control` headers that enable Vercel's CDN to serve cached responses without hitting the origin:
| Data Type | `s-maxage` | `stale-while-revalidate` | Rationale |
Static assets use content-hash filenames with 1-year immutable cache headers. The service worker file (`sw.js`) is never cached (`max-age=0, must-revalidate`) to ensure update detection.
### Railway Relay Compression
All relay server responses pass through `gzipSync` when the client accepts gzip and the payload exceeds 1KB. This applies to OpenSky aircraft JSON, RSS XML feeds, UCDP event data, AIS snapshots, and health checks — reducing wire size by approximately 80%.
### Frontend Polling Intervals
Panels refresh at staggered intervals to avoid synchronized API storms:
All animations and polling pause when the tab is hidden or after 2 minutes of inactivity, preventing wasted requests from background tabs.
---
## Caching Architecture
@@ -467,13 +881,15 @@ The AI summarization pipeline adds content-based deduplication: headlines are ha
| Layer | Mechanism |
|-------|-----------|
| **CORS origin allowlist** | Only `worldmonitor.app`, `startups.worldmonitor.app`, and `localhost:*` can call API endpoints. All others receive 403. Implemented in `api/_cors.js`. |
| **CORS origin allowlist** | Only `worldmonitor.app`, `tech.worldmonitor.app`, `finance.worldmonitor.app`, and `localhost:*` can call API endpoints. All others receive 403. Implemented in `api/_cors.js`. |
| **RSS domain allowlist** | The RSS proxy only fetches from explicitly listed domains (~90+). Requests for unlisted domains are rejected with 403. |
| **Railway domain allowlist** | The Railway relay has a separate, smaller domain allowlist for feeds that need the alternate origin. |
| **API key isolation** | All API keys live server-side in Vercel environment variables. The browser never sees Groq, OpenRouter, ACLED, Finnhub, or other credentials. |
| **Input sanitization** | User-facing content passes through `escapeHtml()` (prevents XSS) and `sanitizeUrl()` (blocks `javascript:` and `data:` URIs). URLs use `escapeAttr()` for attribute context encoding. |
| **Query parameter validation** | API endpoints validate input formats (e.g., stablecoin coin IDs must match `[a-z0-9-]+`, bounding box params are numeric). |
| **IP rate limiting** | AI endpoints use Upstash Redis-backed rate limiting to prevent abuse of Groq/OpenRouter quotas. |
| **Desktop sidecar auth** | The local API sidecar requires a per-session `Bearer` token generated at launch. The token is stored in Rust state and injected into the sidecar environment — only the Tauri frontend can retrieve it via IPC. Health check endpoints are exempt. |
| **OS keychain storage** | Desktop API keys are stored in the operating system's credential manager (macOS Keychain, Windows Credential Manager), never in plaintext files or environment variables on disk. |
| **No debug endpoints** | The `api/debug-env.js` endpoint returns 404 in production — it exists only as a disabled placeholder. |
---
@@ -485,7 +901,7 @@ The AI summarization pipeline adds content-based deduplication: headlines are ha
vercel dev # Runs frontend + all 45+ API edge functions
vercel dev # Runs frontend + all 60+ API edge functions
```
Open [http://localhost:3000](http://localhost:3000)
@@ -518,7 +934,7 @@ See [`.env.example`](./.env.example) for the complete list with registration lin
## Self-Hosting
World Monitor relies on **45+ Vercel Edge Functions** in the `api/` directory for RSS proxying, data caching, and API key isolation. Running `npm run dev` alone starts only the Vite frontend — the edge functions won't execute, and most panels (news feeds, markets, AI summaries) will be empty.
World Monitor relies on **60+ Vercel Edge Functions** in the `api/` directory for RSS proxying, data caching, and API key isolation. Running `npm run dev` alone starts only the Vite frontend — the edge functions won't execute, and most panels (news feeds, markets, AI summaries) will be empty.
### Option 1: Deploy to Vercel (Recommended)
@@ -529,7 +945,7 @@ npm install -g vercel
vercel # Follow prompts to link/create project
```
Add your API keys in the Vercel dashboard under **Settings → Environment Variables**, then visit your deployment URL. The free Hobby plan supports all 45+ edge functions.
Add your API keys in the Vercel dashboard under **Settings → Environment Variables**, then visit your deployment URL. The free Hobby plan supports all 60+ edge functions.
### Option 2: Local Development with Vercel CLI
@@ -580,31 +996,20 @@ Set `WS_RELAY_URL` (server-side, HTTPS) and `VITE_WS_RELAY_URL` (client-side, WS
# World Monitor Roadmap: Intelligence Correlation Enhancements
This document outlines the top 5 features a geopolitical intelligence analyst would want, focusing on **correlation between existing data points** and leveraging **free APIs/RSS feeds only**.
---
## Current Correlation Capabilities
### What We Already Do Well
| Signal Type | Description | Data Sources |
|------------|-------------|--------------|
| **Convergence** | 3+ source types report same story within 30min | News feeds |
| **Silent Divergence** | Market moves 2%+ with minimal related news | Yahoo/Finnhub + News |
| **Flow/Price Divergence** | Energy price spike without pipeline news | Markets + News |
| **Related Assets** | News stories enriched with nearby infrastructure | Hotspots + All assets |
| **GDELT Tensions** | Country-pair tension scores with 7-day trends | GDELT GPR API |
### What's Missing
1.**No cross-layer correlation** - Protests, military movements, and economic data don't talk to each other
2.**No temporal pattern detection** - Can't detect "unusual for this time of year"
3.**No geographic clustering** - Multiple event types in same region not flagged
4.**No country-level aggregation** - No unified risk view per country
5.**No infrastructure dependency mapping** - Don't show cascade effects
---
## Top 5 Priority Features
### 1. Multi-Signal Geographic Convergence
**What:** When 3+ independent data types converge on the same geographic region within 24-48 hours, generate a high-priority alert.
**Why:** The most valuable I&W (Indications & Warning) signals come from multiple independent sources detecting activity in the same area. A protest + military flight activity + shipping disruption in the same region is far more significant than any single event.
**Data Sources (Already Have):**
- Protests (ACLED/GDELT) → lat/lon
- Military flights (OpenSky) → lat/lon
- Military vessels (AIS) → lat/lon
- Earthquakes/natural events → lat/lon
- News hotspots → lat/lon
- Chokepoint congestion → lat/lon
- Pipeline incidents → lat/lon (inferred)
**Implementation:**
```
1. Define 50km grid cells globally
2. Each refresh cycle, tag events to grid cells
3. Track event counts by type per cell over 24h window
4. Alert when: cell has events from 3+ distinct data types
- **World Bank Governance Indicators** (annual, free API)
- **UN Refugee Data** (UNHCR, RSS feeds)
- **Election proximity** (static calendar)
**Implementation:**
```
1. Map all events to ISO country codes
2. Maintain rolling 7-day and 30-day baselines per country
3. Calculate Z-scores for each component
4. Weight and sum to 0-100 index
5. Track index changes for trend detection
```
**UI:**
- Choropleth map layer showing index by color
- Sortable country list panel
- Click country → drill-down to component breakdown
- Alert when country moves 10+ points in 24h
---
### 3. Trade Route Risk Scoring
**What:** Real-time risk assessment for major shipping routes, showing which supply chains are most vulnerable right now.
**Why:** Supply chain disruptions are the primary economic consequence of geopolitical events. An analyst needs to quickly assess "if X happens, what trade is affected?"
**Major Routes to Score:**
| Route | Chokepoints | Commodities |
|-------|-------------|-------------|
| Asia → Europe (Suez) | Suez, Bab el-Mandeb, Malacca | Containers, oil |
| Asia → US West Coast | Malacca, Taiwan Strait, Panama | Containers, electronics |
| Middle East → Europe | Hormuz, Suez, Bosphorus | Oil, LNG |
| Russia → Europe | Baltic, Bosphorus | Oil, gas, grain |
| South America → Asia | Panama, Magellan | Commodities, grain |
**Risk Components:**
| Factor | Source | Notes |
|--------|--------|-------|
| Chokepoint congestion | AIS density | Real-time |
| Sanctions impact | Config | Which ports blocked |
| Port delays | Inferred from AIS | Real-time |
**Implementation:**
```
1. Define route polylines with chokepoint waypoints
2. For each chokepoint, calculate: density_change + gap_rate + weather_alerts + conflict_distance
3. Weight by chokepoint criticality (Hormuz > Malacca > Panama for oil)
4. Sum to 0-100 risk score per route
5. Compare to 30-day baseline for trend
```
**UI:**
- Route lines on map colored by risk (green → yellow → red)
- Panel showing route rankings with trends
- Click route → show chokepoint breakdown
- Alert when route risk jumps 20+ points
---
### 4. Infrastructure Cascade Visualization
**What:** When you click any infrastructure asset, show what depends on it and what would be affected by its disruption.
**Why:** Critical infrastructure is interconnected. A submarine cable fault affects countries downstream. A pipeline disruption affects refineries and ports. Analysts need to see the "so what."
**Dependency Mappings:**
**Ports → dependent on:**
- Pipelines (oil/LNG terminals)
- Submarine cables (data for port operations)
- Nearby naval bases (protection)
- Chokepoints (access routes)
**Cables → serve:**
- Countries (list from cable data)
- Data centers (proximity)
- Financial centers (criticality)
**Pipelines → connect:**
- Origin countries
- Transit countries
- Destination ports/refineries
- Alternate routes
**Implementation:**
```
1. Build static dependency graph in config
2. For cables: map landing points to countries
3. For pipelines: map to origin/transit/destination
4. For ports: map to pipelines that terminate there
5. On asset click: traverse graph, highlight dependents on map
6. Show impact panel: "Disruption would affect: X countries, Y trade volume"
- **Global Energy Monitor** pipeline database (public)
- **UN COMTRADE** for trade flow volumes (free API)
---
### 5. Temporal Anomaly Detection
**What:** Detect when current activity levels deviate significantly from historical norms for the same time period (day of week, month, season).
**Why:** "Unusual activity" only makes sense in context. Military flights on a Tuesday might be normal; the same level on a Sunday might be significant. Activity in December might be normal for end-of-year exercises but unusual in March.
**What to Track:**
| Data Type | Baseline Period | Anomaly Threshold |
| Multi-Signal Geographic Convergence | Medium | Very High | 1 |
| Country Instability Index | Medium | High | 2 |
| Temporal Anomaly Detection | Medium | High | 3 |
| Trade Route Risk Scoring | High | High | 4 |
| Infrastructure Cascade Viz | High | Medium | 5 |
**Recommended approach:** Implement features 1-3 first as they primarily leverage existing data with new correlation logic. Features 4-5 require additional data mapping and UI work.
The most valuable enhancements for a geopolitical analyst focus on **correlation, not accumulation**. The dashboard already aggregates vast amounts of data; the next step is making that data talk to each other.
Priority 1 (Geographic Convergence) alone would significantly elevate the tool's I&W capability by detecting when multiple independent signals point to the same location—the hallmark of significant events.
All proposed features use **existing data sources** or **free APIs/RSS feeds**, keeping with the project's accessible, open-source philosophy.
returnnewResponse(JSON.stringify({error:'Payload too large'}),{
status:413,
headers:{'Content-Type':'application/json'},
});
}
try{
const{country,code,context}=awaitrequest.json();
@@ -109,7 +131,8 @@ Rules:
- 5-6 paragraphs, 300-400 words.
- No speculation beyond what the data supports.
- Use plain language, not jargon.
- If military assets are 0, don't speculate about military presence — say monitoring shows no current military activity.`;
- If military assets are 0, don't speculate about military presence — say monitoring shows no current military activity.
- When referencing a specific headline from the numbered list, cite it as [N] where N is the headline number (e.g. "tensions escalated [3]"). Only cite headlines you directly reference.`;
@@ -115,6 +144,9 @@ export default async function handler(request) {
constisTechVariant=variant==='tech';
constdateContext=`Current date: ${newDate().toISOString().split('T')[0]}.${isTechVariant?'':' Donald Trump is the current US President (second term, inaugurated Jan 2025).'}`;
// Language instruction
constlangInstruction=lang&&lang!=='en'?`\nIMPORTANT: Output the summary in ${lang.toUpperCase()} language.`:'';
if(mode==='brief'){
if(isTechVariant){
// Tech variant: focus on startups, AI, funding, product launches
@@ -126,7 +158,7 @@ Rules:
- IGNORE political news, trade policy, tariffs, government actions unless directly about tech regulation
- Lead with the company/product/technology name
- Start directly: "OpenAI announced...", "A new $50M Series B...", "GitHub released..."
- No bullet points, no meta-commentary`;
- No bullet points, no meta-commentary${langInstruction}`;
}else{
// Full variant: geopolitical focus
systemPrompt=`${dateContext}
@@ -138,7 +170,7 @@ Rules:
- Start directly with the subject: "Iran's regime...", "The US Treasury...", "Protests in..."
- CRITICAL FOCAL POINTS are the main actors - mention them by name
- If focal points show news + signals convergence, that's the lead
- No bullet points, no meta-commentary`;
- No bullet points, no meta-commentary${langInstruction}`;
}
userPrompt=`Summarize the top story:\n${headlineText}${intelSection}`;
}elseif(mode==='analysis'){
@@ -166,10 +198,19 @@ Rules:
userPrompt=isTechVariant
?`What's the key tech trend or development?\n${headlineText}${intelSection}`
:`What's the key pattern or risk?\n${headlineText}${intelSection}`;
}elseif(mode==='translate'){
consttargetLang=variant;// In translate mode, variant param holds the target language code (e.g., 'fr', 'es')
systemPrompt=`You are a professional news translator. Translate the following news headlines/summaries into ${targetLang}.
Rules:
- Maintain the original tone and journalistic style.
- Do NOT add any conversational filler (e.g., "Here is the translation").
- Output ONLY the translated text.
- If the text is already in ${targetLang}, return it as is.`;
userPrompt=`Translate to ${targetLang}:\n${headlines[0]}`;
}else{
systemPrompt=isTechVariant
?`${dateContext}\n\nSynthesize tech news in 2 sentences. Focus on startups, AI, funding, products. Ignore politics unless directly about tech regulation.`
:`${dateContext}\n\nSynthesize in 2 sentences max. Lead with substance. NEVER start with "Breaking news" or "Tonight" - just state the insight directly. CRITICAL focal points with news-signal convergence are significant.`;
?`${dateContext}\n\nSynthesize tech news in 2 sentences. Focus on startups, AI, funding, products. Ignore politics unless directly about tech regulation.${langInstruction}`
:`${dateContext}\n\nSynthesize in 2 sentences max. Lead with substance. NEVER start with "Breaking news" or "Tonight" - just state the insight directly. CRITICAL focal points with news-signal convergence are significant.${langInstruction}`;
@@ -116,6 +145,9 @@ export default async function handler(request) {
constisTechVariant=variant==='tech';
constdateContext=`Current date: ${newDate().toISOString().split('T')[0]}.${isTechVariant?'':' Donald Trump is the current US President (second term, inaugurated Jan 2025).'}`;
// Language instruction
constlangInstruction=lang&&lang!=='en'?`\nIMPORTANT: Output the summary in ${lang.toUpperCase()} language.`:'';
if(mode==='brief'){
if(isTechVariant){
// Tech variant: focus on startups, AI, funding, product launches
@@ -127,7 +159,7 @@ Rules:
- IGNORE political news, trade policy, tariffs, government actions unless directly about tech regulation
- Lead with the company/product/technology name
- Start directly: "OpenAI announced...", "A new $50M Series B...", "GitHub released..."
- No bullet points, no meta-commentary`;
- No bullet points, no meta-commentary${langInstruction}`;
}else{
// Full variant: geopolitical focus
systemPrompt=`${dateContext}
@@ -139,7 +171,7 @@ Rules:
- Start directly with the subject: "Iran's regime...", "The US Treasury...", "Protests in..."
- CRITICAL FOCAL POINTS are the main actors - mention them by name
- If focal points show news + signals convergence, that's the lead
- No bullet points, no meta-commentary`;
- No bullet points, no meta-commentary${langInstruction}`;
}
userPrompt=`Summarize the top story:\n${headlineText}${intelSection}`;
}elseif(mode==='analysis'){
@@ -167,10 +199,18 @@ Rules:
userPrompt=isTechVariant
?`What's the key tech trend or development?\n${headlineText}${intelSection}`
:`What's the key pattern or risk?\n${headlineText}${intelSection}`;
}elseif(mode==='translate'){
consttargetLang=variant;// In translate mode, variant param holds the target language code
systemPrompt=`You are a professional news translator. Translate the following news headlines/summaries into ${targetLang}.
Rules:
- Maintain the original tone and journalistic style.
- Do NOT add any conversational filler.
- Output ONLY the translated text.`;
userPrompt=`Translate to ${targetLang}:\n${headlines[0]}`;
}else{
systemPrompt=isTechVariant
?`${dateContext}\n\nSynthesize tech news in 2 sentences. Focus on startups, AI, funding, products. Ignore politics unless directly about tech regulation.`
:`${dateContext}\n\nSynthesize in 2 sentences max. Lead with substance. NEVER start with "Breaking news" or "Tonight" - just state the insight directly. CRITICAL focal points with news-signal convergence are significant.`;
?`${dateContext}\n\nSynthesize tech news in 2 sentences. Focus on startups, AI, funding, products. Ignore politics unless directly about tech regulation.${langInstruction}`
:`${dateContext}\n\nSynthesize in 2 sentences max. Lead with substance. NEVER start with "Breaking news" or "Tonight" - just state the insight directly. CRITICAL focal points with news-signal convergence are significant.${langInstruction}`;
- **Middle East / MENA** - Al Jazeera, BBC ME, Guardian ME, Al Arabiya, Times of Israel
- **Africa** - BBC Africa, News24, Google News aggregation (regional & Sahel coverage)
@@ -213,6 +223,7 @@ Multi-source RSS aggregation across categories:
The **📡 SOURCES** button in the header opens a global source management modal, enabling fine-grained control over which news sources appear in the dashboard.
**Capabilities:**
- **Search**: Filter the source list by name to quickly find specific outlets
- **Individual Toggle**: Click any source to enable/disable it
- **Bulk Actions**: "Select All" and "Select None" for quick adjustments
@@ -220,12 +231,14 @@ The **📡 SOURCES** button in the header opens a global source management modal
- **Persistence**: Settings are saved to localStorage and persist across sessions
**Use Cases:**
- **Noise Reduction**: Disable high-volume aggregators (Google News) to focus on primary sources
- **Regional Focus**: Enable only sources relevant to a specific geographic area
- **Source Quality**: Disable sources with poor signal-to-noise ratio
- **Bias Management**: Balance coverage by enabling/disabling sources with known editorial perspectives
**Technical Details:**
- Disabled sources are filtered at fetch time (not display time), reducing bandwidth and API calls
- Affects all news panels simultaneously—disable BBC once, it's gone everywhere
- Panels with all sources disabled show "All sources disabled" message
@@ -260,6 +273,7 @@ Embedded YouTube live streams from major news networks with channel switching:
| **Al Jazeera** | Middle East & international news |
**Core Features:**
- **Channel Switcher** - One-click switching between networks
The system maintains 48-point history (24 hours at 30-minute intervals) per hotspot:
- **Linear regression** calculates slope of recent scores
- **Rising**: Slope > +0.1 points per interval
- **Falling**: Slope < -0.1 points per interval
@@ -838,6 +866,7 @@ The system maintains 48-point history (24 hours at 30-minute intervals) per hots
**Signal Generation**
Escalation signals (`hotspot_escalation`) are emitted when:
1. Final score exceeds threshold (typically 60)
2. At least 2 hours since last signal for this hotspot (cooldown)
3. Trend is rising or score is critical (>80)
@@ -998,6 +1027,7 @@ The floor applies *after* the standard calculation—if the computed score excee
### Trend Detection
The CII tracks 24-hour changes to identify trajectory:
- **Rising**: Score increased by ≥5 points (escalating situation)
- **Stable**: Change within ±5 points (steady state)
- **Falling**: Score decreased by ≥5 points (de-escalation)
@@ -1013,15 +1043,18 @@ Beyond the base component scores, several contextual factors can boost a country
| **Focal Point** | 8 | AI focal point detection on country | Multi-source convergence indicator |
**Hotspot Boost Calculation**:
- Hotspot activity (0-100) scaled by 1.5× then capped at 10
- Zero boost for countries with no associated hotspot activity
**News Urgency Boost Tiers**:
- Information ≥70: +5 points
- Information ≥50: +3 points
- Information <50: +0 points
**Focal Point Boost Tiers**:
- Critical urgency: +8 points
- Elevated urgency: +4 points
- Normal urgency: +0 points
@@ -1039,6 +1072,7 @@ On dashboard startup, the CII system enters **Learning Mode**—a 15-minute cali
**Note**: Server-side pre-computation now provides immediate scores to new users—Learning Mode primarily affects client-side dynamic adjustments and alert generation rather than initial score display.
**Why 15 minutes?** Real-world testing showed that CII scores stabilize after approximately 10-20 minutes of data collection. The 15-minute window provides sufficient time for:
- Multiple refresh cycles across all data sources
- Trend detection to establish direction (rising/stable/falling)
- Cross-source correlation to normalize bias
@@ -1073,6 +1107,7 @@ This ensures users understand that early scores are provisional while preventing
- Residential IP ranges (not blocked like cloud providers)
- WebSocket support for persistent connections
- Global edge deployment for low latency
@@ -1614,22 +1661,26 @@ Vessels within 50km of these bases are flagged, enabling detection of unusual ac
Military aircraft are tracked via the OpenSky Network using ADS-B data. OpenSky blocks unauthenticated requests from cloud provider IPs (Vercel, Railway, AWS), so aircraft tracking requires a relay server with credentials.
**Authentication**:
- Register for a free account at [opensky-network.org](https://opensky-network.org)
- Create an API client in account settings to get `OPENSKY_CLIENT_ID` and `OPENSKY_CLIENT_SECRET`
- The relay uses **OAuth2 client credentials flow** to obtain Bearer tokens
- Tokens are cached (30-minute expiry) and automatically refreshed
**Identification Methods**:
- **Callsign matching**: Known military callsign patterns (RCH, REACH, DUKE, etc.)
- **ICAO hex ranges**: Military aircraft use assigned hex code blocks by country
3. EONET geometry provides more precise coordinates
@@ -2817,6 +2908,7 @@ When both GDACS and EONET report the same event:
### Filtering Logic
To prevent map clutter, natural events are filtered:
- **Wildfires**: Only events < 48 hours old (older fires are either contained or well-known)
- **Earthquakes**: M4.5+ globally, lower threshold for populated areas
- **Storms**: Only named storms or those with warnings
@@ -2928,6 +3020,7 @@ Nine geographic theaters are monitored continuously, each with custom thresholds
Beyond raw counts, the system assesses whether forces in a theater constitute an **offensive strike package**—the combination of assets required for sustained combat operations.
This design prioritizes geographic awareness over label density—users can quickly scan for markers and then interact for context.
### Panel Management
- **Drag panels** - Reorder layout
- **Settings (⚙)** - Toggle panel visibility
### Shareable Links
The current view state is encoded in the URL, enabling:
- **Bookmarking**: Save specific views for quick access
- **Sharing**: Send colleagues a link to your exact map position and layer configuration
- **Deep linking**: Link directly to a specific region or feature
@@ -3429,9 +3529,11 @@ Values are validated and clamped to prevent invalid states.
## Data Sources
### News Feeds
Aggregates **70+ RSS feeds** from major news outlets, government sources, and specialty publications with source-tier prioritization. Categories include world news, MENA, Africa, Latin America, Asia-Pacific, energy, technology, AI/ML, finance, government releases, defense/intel, think tanks, and international crisis organizations.
### Geospatial Data
- **Hotspots**: 30+ global intelligence hotspots with keyword correlation (including Sahel, Haiti, Horn of Africa)
- **Conflicts**: 10+ active conflict zones with involved parties
- **Military Bases**: 220+ installations from US, NATO, Russia, China, and allies
@@ -3443,6 +3545,7 @@ Aggregates **70+ RSS feeds** from major news outlets, government sources, and sp
@@ -3460,30 +3563,36 @@ Aggregates **70+ RSS feeds** from major news outlets, government sources, and sp
This project uses data from the following sources. Please respect their terms of use.
### Aircraft Tracking
Data provided by [The OpenSky Network](https://opensky-network.org). If you use this data in publications, please cite:
> Matthias Schäfer, Martin Strohmeier, Vincent Lenders, Ivan Martinovic and Matthias Wilhelm. "Bringing Up OpenSky: A Large-scale ADS-B Sensor Network for Research". In *Proceedings of the 13th IEEE/ACM International Symposium on Information Processing in Sensor Networks (IPSN)*, pages 83-94, April 2014.
### Conflict & Protest Data
- **ACLED**: Armed Conflict Location & Event Data. Source: [ACLED](https://acleddata.com). Data must be attributed per their [Attribution Policy](https://acleddata.com/attributionpolicy/).
- **GDELT**: Global Database of Events, Language, and Tone. Source: [The GDELT Project](https://www.gdeltproject.org/).
### Financial Data
- **Stock Quotes**: Powered by [Finnhub](https://finnhub.io/) (primary), with [Yahoo Finance](https://finance.yahoo.com/) as backup for indices and commodities
- **Cryptocurrency**: Powered by [CoinGecko API](https://www.coingecko.com/en/api)
- **Economic Indicators**: Data from [FRED](https://fred.stlouisfed.org/), Federal Reserve Bank of St. Louis
The application fetches news via `src/services/rss.ts`.
- **Mechanism**: Direct HTTP requests (via proxy) to RSS/Atom XML feeds.
- **Processing**: `DOMParser` parses XML client-side.
- **Storage**: Items are stored in-memory in `App.ts` (`allNews`, `newsByCategory`).
## The Challenge
Legacy RSS feeds are static XML files in their original language. There is no built-in "negotiation" for language. To display French news, we must either:
1. Fetch French feeds.
2. Translate English feeds on the fly.
## Proposed Solutions
### Option 1: Localized Feed Discovery (Recommended for "Major" Support)
Instead of forcing translation, we switch the *source* based on the selected language.
- **Implementation**:
- In `src/config/feeds.ts`, change the simple URL string to an object: `url: { en: '...', fr: '...' }` or separate constant lists `FEEDS_EN`, `FEEDS_FR`.
- **Pros**: Zero latency, native content quality, no API costs.
- **Cons**: Hard to find equivalent feeds for niche topics (e.g., specific mil-tech blogs) in all languages.
- **Strategy**: Creating a curated list of international feeds for major categories (World, Politics, Finance) is the most robust & scalable approach.
### Option 2: On-Demand Client-Side Translation
Add a "Translate" button to each news card.
- **Implementation**:
- Click triggers a call to a translation API (Google/DeepL/LLM).
- Store result in a local cache (Map).
- **Pros**: Low cost (only used when needed), preserves original context.
- **Quality**: Short headlines often translate poorly without context.
## Recommendation
**Hybrid Approach**:
1. **Primary**: Source localized feeds where possible (e.g., Le Monde for FR, Spiegel for DE). This requires a community effort to curate `feeds.json` for each locale.
2. **Fallback**: Keep English feeds for niche tech/intel sources where no alternative exists.
3. **Feature**: Add a "Summarize & Translate" button using the existing LLM worker. The prompt to the LLM (currently used for summaries) can be adjusted to "Summarize this in [Current Language]".
## Next Steps
1. Audit `src/config/feeds.ts` to structure it for multi-language support.
2. Update `rss.ts` to select the correct URL based on `i18n.language`.
Validated desktop build readiness for the World Monitor Tauri app by checking frontend compilation, TypeScript integrity, and Tauri/Rust build execution.
## Preflight checks before desktop validation
@@ -25,6 +26,7 @@ If any of these checks fail, treat downstream desktop build failures as environm
5. `cargo check` (from `src-tauri/`) — failed because the environment blocks downloading crates from `https://index.crates.io` (`403 CONNECT tunnel failed`).
## Assessment
- The web app portion compiles successfully.
- Full Tauri desktop validation in this run is blocked by an **external environment outage/restriction** (registry access denied with HTTP 403).
- No code/runtime defects were observed in project sources during this validation pass.
@@ -42,6 +44,7 @@ Use these labels in future reports so outcomes are actionable:
- Action: provision offline artifacts/mirror config first, enable offline override (`config.local.toml` or CLI `--config`), then rerun.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.