* feat: harness engineering P0 - linting, testing, architecture docs
Add foundational infrastructure for agent-first development:
- AGENTS.md: agent entry point with progressive disclosure to deeper docs
- ARCHITECTURE.md: 12-section system reference with source-file refs and ownership rule
- Biome 2.4.7 linter with project-tuned rules, CI workflow (lint-code.yml)
- Architectural boundary lint enforcing forward-only dependency direction (lint-boundaries.mjs)
- Unit test CI workflow (test.yml), all 1083 tests passing
- Fixed 9 pre-existing test failures (bootstrap sync, deploy-config headers, globe parity, redis mocks, geometry URL, import.meta.env null safety)
- Fixed 12 architectural boundary violations (types moved to proper layers)
- Added 3 missing cache tier entries in gateway.ts
- Synced cache-keys.ts with bootstrap.js
- Renamed docs/architecture.mdx to "Design Philosophy" with cross-references
- Deprecated legacy docs/Docs_To_Review/ARCHITECTURE.md
- Harness engineering roadmap tracking doc
* fix: address PR review feedback on harness-engineering-p0
- countries-geojson.test.mjs: skip gracefully when CDN unreachable
instead of failing CI on network issues
- country-geometry-overrides.test.mts: relax timing assertion
(250ms -> 2000ms) for constrained CI environments
- lint-boundaries.mjs: implement the documented api/ boundary check
(was documented but missing, causing false green)
* fix(lint): scan api/ .ts files in boundary check
The api/ boundary check only scanned .js/.mjs files, missing the 25
sebuf RPC .ts edge functions. Now scans .ts files with correct rules:
- Legacy .js: fully self-contained (no server/ or src/ imports)
- RPC .ts: may import server/ and src/generated/ (bundled at deploy),
but blocks imports from src/ application code
* fix(lint): detect import() type expressions in boundary lint
- Move AppContext back to app/app-context.ts (aggregate type that
references components/services/utils belongs at the top, not types/)
- Move HappyContentCategory and TechHQ to types/ (simple enums/interfaces)
- Boundary lint now catches import('@/layer') expressions, not just
from '@/layer' imports
- correlation-engine imports of AppContext marked boundary-ignore
(type-only imports of top-level aggregate)
* ci: upgrade GitHub Actions to Node.js 24-compatible versions
Node.js 20 actions are deprecated and will be forced to Node.js 24
starting June 2, 2026. Bump all affected actions:
- actions/checkout v4 -> v6
- actions/setup-node v4 -> v6
- actions/cache v4 -> v5
- actions/upload-artifact v4 -> v6
- docker/setup-qemu-action v3 -> v4
- docker/setup-buildx-action v3 -> v4
- docker/login-action v3 -> v4
- docker/metadata-action v5 -> v6
- docker/build-push-action v6 -> v7
* ci: pin all GitHub Actions to full commit SHAs
Replace floating @vN tags with immutable SHA refs for supply-chain
security. Tags can be moved; SHAs cannot. Each ref includes a # vN
comment for readability.
Also pins actions/cache@v5, actions/setup-go@v5, and all docker/*
actions that were previously using floating tags.
* ci: add proto generation freshness check
Add a new CI workflow that verifies proto-generated code is up to date.
The workflow installs buf and the sebuf protoc plugins, runs
`make generate`, and fails if any files in src/generated/ or docs/api/
differ from what is committed. This prevents proto definitions from
drifting out of sync with the generated TypeScript and OpenAPI output.
Closes#200
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(ci): detect untracked generated files and self-trigger workflow
- Add untracked file check via git ls-files after git diff to catch
new files created by make generate that weren't committed (P1)
- Add proto-check.yml to paths filter so the workflow triggers when
the workflow itself is modified (P2)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* perf(ci): cache ~/go/bin for proto generation workflow
Add actions/cache step for Go binaries (buf CLI, protoc plugins) keyed
on Makefile hash. Saves ~30-60s on repeat runs by skipping reinstall.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Elie Habib <elie.habib@gmail.com>
Co-authored-by: Sebastien Melki <sebastien@anghami.com>
* fix(csp): add commodity variant to CSP and fix iframe variant navigation
- Add commodity.worldmonitor.app to frame-src and frame-ancestors in
vercel.json and index.html CSP — was missing while all other variants
were listed
- Open variant links in new tab when app runs inside an iframe to prevent
sandbox navigation errors ("This content is blocked")
- Add allow-popups and allow-popups-to-escape-sandbox to pro page iframe
sandbox attribute
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(csp): add missing variant subdomains to tauri.conf.json frame-src
Sync tauri.conf.json CSP with index.html and vercel.json by adding
finance, commodity, and happy worldmonitor.app subdomains to frame-src.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add PR screenshots for CSP fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Elie Habib <elie.habib@gmail.com>
* feat(ui): add close buttons on panels and Add Panel block
Add hover-visible close (×) buttons to panel headers that disable the
panel via the existing toggle infrastructure, and an "Add Panel" card
at the end of the grid that opens the Settings → Panels tab.
- Close button on all panels except Live News and Live Webcams
- Button always positioned far-right via CSS order: 999
- Panel count badges and action buttons pushed right with margin-left: auto
- World Clock gear icon shifted to avoid overlap with close button
- Styled icon-btn class for Airline Intelligence refresh button
- i18n keys added for closePanel and addPanel
- wm:panel-close custom event handled in event-handlers.ts
Closes#1347
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add PR screenshots for panel controls feature
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(ui): address PR review — move inline styles to CSS, store event listener ref
- Move inline marginLeft from MarketPanel and AirlineIntelPanel to CSS
- Store wm:panel-close listener as boundPanelCloseHandler with cleanup in destroy()
- Close button now extends .icon-btn (shared base styles, 5 overrides instead of 15)
- Scope .live-news-settings-btn margin-left to .panel-header context only
- Add gap: 8px to .panel-header for uniform spacing
- Center LIVE badge and sparkle btn between title and count/close via auto margins
- Fix close button hover/touch specificity by scoping to .panel-header
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(ui): consolidate margin-left auto, fix close btn icon and hover color
- Replace scattered margin-left: auto with single .panel-header-left + *
selector to correctly push the first right-aligned element
- Use multiplication X (U+2715) instead of multiplication sign (U+00D7)
for the close button icon
- Use color-mix with --semantic-critical for close hover background
instead of hardcoded rgba
- Convert wc-settings-btn from absolute positioning to flex flow,
removing the fragile right: 30px magic number
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Elie Habib <elie.habib@gmail.com>
Railway auto-detects Dockerfiles at repo root and uses them for ALL
services, even those set to NIXPACKS. This caused all seed services
(ais-relay, seed-gpsjam, etc.) to build nginx-only containers with
no node binary, breaking every Railway service.
Move Dockerfile and related files to docker/ subdirectory. Railway
only checks the repo root for Dockerfiles, so this prevents
accidental detection. GHA workflow updated with explicit file: path.
* feat: publish official Docker image on release #1260 solved
* docker image changes
* fixes fix
* all fixes
* things changed according to suggestions
* fixed
* feat(infrastructure): expand submarine cables to 86 via TeleGeography API seed
- Add `seed-submarine-cables.mjs` Railway cron script fetching 86 strategic
cables from TeleGeography API (was 19 hand-curated)
- Update `geo.ts` static baseline with full cable data (routes, landing points,
owners, RFS year, regions)
- Update `get-cable-health.ts` cable name/landing mappings for new slug-based IDs
- Add `data?.cables?.length` to `_seed-utils.mjs` record count heuristic
- Update `map-harness.ts` cable ID references
- Remove GitHub Actions workflows for UCDP and WB indicators (Railway cron only)
* fix(infrastructure): cable route matching, name false positives, validation threshold
- Fix route geometry: only strip numeric suffix when result matches a known
cable slug, preventing seamewe-6→seamewe, farice-1→farice, etc.
- Fix name matching: use word-boundary regex instead of substring includes;
disambiguate short names (ACE→ACE CABLE, SAFE→SAFE CABLE, PEACE→PEACE CABLE,
TEAMS→TEAMS CABLE) to prevent false matches on common NGA words
- Raise validation threshold from 50 to 75 (88% success required) to prevent
heavily partial upstream results from overwriting good cached data
* fix(infrastructure): tie validation threshold to 90% of configured cable count
Dynamic threshold based on CABLE_REGIONS length instead of a hardcoded number.
Currently requires >= 78 of 86 cables (90%).
- Wrap all 4 behavioral it() blocks in try/finally so clearAllCircuitBreakers()
always runs on assertion failure (P2 — leaked breaker state between tests)
- Add assert.ok(fnStart !== -1) guards for fetchHapiSummary, fetchPositiveGdeltArticles,
and fetchGdeltArticles so renames produce a clear diagnostic (P2 — silent false-positives)
- Fix misleading comment in seed-wb-indicators.mjs: WLD/EAS are 3-char codes and
aren't filtered by iso3.length !== 3 (P3)
- Add timeout-minutes: 10 and permissions: contents: read to seed GHA workflow (P3)
* feat(tech-readiness): bootstrap hydration via Railway seed + bootstrap key
Add pre-computed TechReadiness rankings to the bootstrap payload so the
panel renders immediately on first load instead of waiting for 4 slow
World Bank RPC calls (which can trip circuit breakers on cold starts,
causing persistent "No data available" until the 5-min cooldown expires).
- scripts/seed-wb-indicators.mjs: new Railway seed script that fetches
IT.NET.USER.ZS / IT.CEL.SETS.P2 / IT.NET.BBND.P2 / GB.XPD.RSDV.GD.ZS
for all countries, computes rankings (same weights as the frontend
getTechReadinessRankings), and writes economic:worldbank-techreadiness:v1
to Redis with a 7-day TTL
- api/bootstrap.js: register techReadiness key in BOOTSTRAP_CACHE_KEYS
and SLOW_KEYS (s-maxage=3600, appropriate for annual WB data)
- src/services/economic/index.ts: fast-path in getTechReadinessRankings()
returns getHydratedData('techReadiness') immediately on first page load;
country-specific comparison requests still use live RPCs
* ci: add weekly GHA workflow for WB tech readiness seed
* feat(conflict): wire UCDP API access token across full stack
UCDP API now requires an `x-ucdp-access-token` header. Renames the
stub `UC_DP_KEY` to `UCDP_ACCESS_TOKEN` (matching ACLED convention)
and wires it through Rust keychain, sidecar allowlist + verification,
handler fetch headers, feature toggles, and desktop settings UI.
- Rename UC_DP_KEY → UCDP_ACCESS_TOKEN in type system and labels
- Add ucdpConflicts feature toggle with required secret
- Add UCDP_ACCESS_TOKEN to Rust SUPPORTED_SECRET_KEYS (24→25)
- Add sidecar ALLOWED_ENV_KEYS entry + validation with dynamic GED version probing
- Handler sends x-ucdp-access-token header when token is present
- UC_DP_KEY fallback in handler for one-release migration window
- Update .env.example, desktop-readiness, and docs
* feat(conflict): pre-fetch UCDP events via Railway cron + Redis cache
Replace the 228-line edge handler that fetched UCDP GED API on every
request with a thin Redis reader. The heavy fetch logic (version
discovery, paginated backward fetch, 1-year trailing window filter)
now runs as a setInterval loop in the Railway relay (ais-relay.cjs)
every 6 hours, writing to Redis key conflict:ucdp-events:v1.
Changes:
- Add UCDP seed loop to ais-relay.cjs (6h interval, 6 pages, 2K cap)
- Rewrite list-ucdp-events.ts as thin Redis reader (35 lines)
- Add conflict:ucdp-events:v1 to bootstrap batch keys
- Protect key from cache-purge via durable data prefix
- Add manual-only seed-ucdp-events workflow + standalone script
- Rename panel "UCDP Events" → "Armed Conflict Events" in locale
- Add 24h TTL + 25h staleness check as safety nets
Tauri's `bundleMediaFramework: true` causes linuxdeploy to bundle Ubuntu's
Mesa libEGL, libGLESv2, libwayland-*, and DRI drivers. On Arch-based distros
with NVIDIA GPUs on Wayland, these override the host GPU drivers via
LD_LIBRARY_PATH, causing EGL_BAD_ALLOC / EGL_BAD_PARAMETER → black screen.
Post-build step extracts the AppImage, strips 14 GPU/Wayland lib patterns
plus Mesa DRI drivers (all on the official AppImage excludelist), repackages
with appimagetool 1.9.1 (SHA256-verified), verifies no banned libs remain,
and re-uploads the stripped AppImage to the GitHub Release.
* fix(desktop): route register-interest to cloud when sidecar lacks CONVEX_URL
The waitlist registration endpoint needs Convex (cloud-only dependency).
The sidecar handler returned 503 without cloud fallback, and
getRemoteApiBaseUrl() returned '' on desktop (VITE_WS_API_URL unset),
so the settings window fetch resolved to tauri://localhost → 404.
Three-layer fix:
1. Sidecar: tryCloudFallback() when CONVEX_URL missing (proxies to
https://worldmonitor.app via remoteBase)
2. runtime.ts: getRemoteApiBaseUrl() defaults to https://worldmonitor.app
on desktop when VITE_WS_API_URL is unset
3. CI: add VITE_WS_API_URL=https://worldmonitor.app to all 4 desktop
build steps
* chore(deps): bump posthog-js to fix pre-push typecheck
* perf: bootstrap endpoint + polling optimization (phases 3-4)
Replace 15+ individual RPC calls on startup with a single /api/bootstrap
batch call that fetches pre-cached data from Redis. Consolidate 6 panel
setInterval timers into the central RefreshScheduler for hidden-tab
awareness (10x multiplier) and adaptive backoff (up to 4x for unchanged
data). Convert IntelligenceGapBadge from 10s polling to event-driven
updates with 60s safety fallback.
* fix(bootstrap): inline Redis + cache keys in edge function
Vercel Edge Functions cannot resolve cross-directory TypeScript imports
from server/_shared/. Inline getCachedJsonBatch and BOOTSTRAP_CACHE_KEYS
directly in api/bootstrap.js. Add sync test to ensure inlined keys stay
in sync with the canonical server/_shared/cache-keys.ts registry.
* test: add Edge Function module isolation guard for all api/*.js files
Prevents any Edge Function from importing from ../server/ or ../src/
which breaks Vercel builds. Scans all 12 non-helper Edge Functions.
* fix(bootstrap): read unprefixed cache keys on all environments
Preview deploys set VERCEL_ENV=preview which caused getKeyPrefix() to
prefix Redis keys with preview:<sha>:, but handlers only write to
unprefixed keys on production. Bootstrap is a read-only consumer of
production cache — always read unprefixed keys.
* fix(bootstrap): wire sectors hydration + add coverage guard
- Wire getHydratedData('sectors') in data-loader to skip Yahoo Finance
fetch when bootstrap provides sector data
- Add test ensuring every bootstrap key has a getHydratedData consumer
— prevents adding keys without wiring them
* fix(server): resolve 25 TypeScript errors + add server typecheck to CI
- _shared.ts: remove unused `delay` variable
- list-etf-flows.ts: add missing `rateLimited` field to 3 return literals
- list-market-quotes.ts: add missing `rateLimited` field to 4 return literals
- get-cable-health.ts: add non-null assertions for regex groups and array access
- list-positive-geo-events.ts: add non-null assertion for array index
- get-chokepoint-status.ts: add required fields to request objects
- CI: run `typecheck:api` (tsconfig.api.json) alongside `typecheck` to catch
server/ TS errors before merge
AppImage only bundled gst-plugins-base and gst-plugins-good, missing
H.264/AAC (gst-libav), x264 (plugins-ugly), AV1 (plugins-bad), and
GL video sink (gst-gl). YouTube's MSE player checks codec support
via MediaSource.isTypeSupported() — WebKitGTK delegates to GStreamer
and reports no compatible decoders, showing "can't play this video".
Add plugins-bad, plugins-ugly, gst-libav, and gst-gl to CI install
so bundleMediaFramework includes them in the AppImage.
Both ubuntu-24.04 (x64) and ubuntu-24.04-arm (ARM64) upload a smoke
test screenshot with the static name `linux-smoke-test-screenshot`.
upload-artifact@v4 rejects duplicate names with 409 Conflict.
Append matrix.label so each gets a unique artifact name.
CI log proof: run 22452393753 shows the ARM64 "Upload smoke test
screenshot" step failing on the exact artifact name collision.
* ci(linux): add AppImage smoke test to desktop build
Launch the built AppImage under Xvfb after the Linux build to catch
startup crashes and render failures automatically. Uploads a screenshot
artifact for visual inspection.
* Optimize Wingbits API usage and panel polling
* fix(ci): use weston+XWayland for Linux smoke test instead of pure Wayland
Previous attempt used GDK_BACKEND=wayland which caused GTK init panic
(tao requires X11). Now: weston headless with XWayland provides X11
through a real compositor. Falls back to Xvfb if weston fails.
Also uploads weston/app logs as artifacts for debugging.
* refactor(ci): use xwfb-run for Linux smoke test display
xwfb-run (from xwayland-run package) is purpose-built for this:
Xwayland on a headless Wayland compositor, replaces xvfb-run.
Falls back to plain Xvfb if xwfb-run is unavailable.
Uploads display-server and app logs as artifacts.
The squash merge of #413 put smoke test steps into build-desktop.yml
instead of the intended standalone workflow. This commit:
- Reverts build-desktop.yml to its original state
- Adds test-linux-app.yml as a separate Linux-only smoke test workflow
Launch the built AppImage under Xvfb after the Linux build to catch
startup crashes and render failures automatically. Uploads a screenshot
artifact for visual inspection.
- Sidecar calls Convex HTTP API directly (Vercel Attack Challenge Mode
blocks server-side proxy). CONVEX_URL read from env, not hardcoded.
- Rust injects CONVEX_URL into sidecar via option_env! (CI) / env var (dev)
- GitHub Actions passes CONVEX_URL secret to all 4 build steps
- Tighten WM tab CSS spacing so all content fits in one viewport
- 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
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.
Builds macOS (ARM64 + x64) and Windows via tauri-apps/tauri-action.
Supports full/tech variants, optional code signing, and automatic
GitHub Release creation on tag push.