mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
a969a9e3a38a7c581ee05573a2d49cdc58399e92
2723 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a969a9e3a3 |
feat(auth): integrate clerk.dev (#1812)
* feat(auth): integrate better-auth with @better-auth/infra dash plugin Wire up better-auth server config with the dash() plugin from @better-auth/infra, and the matching sentinelClient() on the client side. Adds BETTER_AUTH_API_KEY to .env.example. * feat(auth): swap @better-auth/infra for @convex-dev/better-auth [10-01 task 1] Install @convex-dev/better-auth@0.11.2, remove @better-auth/infra, delete old server/auth.ts skeleton, rewrite auth-client.ts to use crossDomainClient + convexClient plugins. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(auth): create Convex auth component files [10-01 task 2] Add convex.config.ts (register betterAuth component), auth.config.ts (JWT/JWKS provider), auth.ts (better-auth server with Convex adapter, crossDomain + convex plugins), http.ts (mount auth routes with CORS). Uses better-auth/minimal for lighter bundle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(auth): add admin, organization, and dash plugins [10-01] Re-install @better-auth/infra for dash() plugin to enable dash.better-auth.com admin dashboard. Add admin() and organization() plugins from better-auth/plugins for user and org management. Update both server (convex/auth.ts) and client (auth-client.ts). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(auth): drop @better-auth/infra (Node.js deps incompatible with Convex V8) Keep admin() and organization() from better-auth/plugins (V8-safe). @better-auth/infra's dash() transitively imports SAML/SSO with node:crypto, fs, zlib — can't run in Convex's serverless runtime. Dashboard features available via admin plugin endpoints instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(11-01): create auth-state.ts with OTT handler and session subscription - Add initAuthState() for OAuth one-time token verification on page load - Add subscribeAuthState() reactive wrapper around useSession nanostore atom - Add getAuthState() synchronous snapshot getter - Export AuthUser and AuthSession types for UI consumption Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(11-01): add Google OAuth provider and wire initAuthState into App.ts - Add socialProviders.google with GOOGLE_CLIENT_ID/SECRET to convex/auth.ts - Add all variant subdomains to trustedOrigins for cross-subdomain CORS - Call initAuthState() in App.init() before panelLayout.init() - Add authModal field to AppContext interface (prepares for Plan 02) - Add authModal: null to App constructor state initialization Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(11-02): create AuthModal with Sign In/Sign Up tabs and Google OAuth - Sign In tab: email/password form calling authClient.signIn.email() - Sign Up tab: name/email/password form calling authClient.signUp.email() - Google OAuth button calling authClient.signIn.social({ provider: 'google', callbackURL: '/' }) - Auto-close on successful auth via subscribeAuthState() subscription - Escape key, overlay click, and X button close the modal - Loading states, error display, and client-side validation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(11-02): add AuthHeaderWidget, mount in header, add auth CSS - AuthHeaderWidget: reactive header widget showing Sign In button (anonymous) or avatar + dropdown (authenticated) - User dropdown: name, email, Free tier badge, Sign Out button calling authClient.signOut() - setupAuthWidget() in EventHandlerManager creates modal + widget, mounts at authWidgetMount span - authWidgetMount added to panel-layout.ts header-right, positioned before download wrapper - setupAuthWidget() called from App.ts after setupUnifiedSettings() - Full auth CSS: modal styles, tabs, forms, Google button, header widget, avatar, dropdown Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(11-02): add localhost:3000 to trustedOrigins for local dev CORS Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(auth): remove admin/organization plugins that break Convex adapter validator The admin() plugin adds banned/role fields to user creation data, but the @convex-dev/better-auth adapter validator doesn't include them. These plugins are Phase 12 work — will re-add with additionalFields config when needed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(12-01): add Resend email transport, verification + reset callbacks, role field - Install resend SDK for transactional email - Add emailVerification with sendOnSignUp:true and fire-and-forget Resend callbacks - Add sendResetPassword callback with 1-hour token expiry - Add user.additionalFields.role (free/pro, input:false, defaultValue:free) - Create userRoles fallback table in schema with by_userId index - Create getUserRole query and setUserRole mutation in convex/userRoles.ts - Lazy-init Resend client to avoid Convex module analysis error Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(12-01): enhance auth-state with emailVerified and role fields - Add emailVerified (boolean) and role ('free' | 'pro') to AuthUser interface - Fetch role from Convex userRoles table via HTTP query after session hydration - Cache role per userId to avoid redundant fetches - Re-notify subscribers asynchronously when role is fetched for a new user - Map emailVerified from core better-auth user field (default false) - Derive Convex cloud URL from VITE_CONVEX_SITE_URL env var Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(12-01): add Convex generated files from deployment - Track convex/_generated/ files produced by npx convex dev --once Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(12-03): create panel-gating service with auth-aware showGatedCta - Add PanelGateReason enum (NONE/ANONYMOUS/UNVERIFIED/FREE_TIER) - Add getPanelGateReason() computing gating from AuthSession + premium flag - Add Panel.showGatedCta() rendering auth-aware CTA overlays - Add Panel.unlockPanel() to reverse locked state - Extract lockSvg to module-level const shared by showLocked/showGatedCta - Add i18n keys: signInToUnlock, signIn, verifyEmailToUnlock, resendVerification, upgradeDesc, upgradeToPro Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(12-02): add forgot password flow, password reset form, and token detection - Widen authModal interface in app-context.ts to support reset-password mode and setResetToken - AuthModal refactored with 4 views: signin, signup, forgot-password, reset-password - Forgot password view sends reset email via authClient.requestPasswordReset - Reset password form validates matching passwords and calls authClient.resetPassword - auth-state.ts detects ?token= param from email links, stores as pendingResetToken - App.ts routes pending reset token to auth modal after UI initialization - CSS for forgot-link, back-link, and success message elements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(12-02): add email verification banner to AuthHeaderWidget and tier badge - Show non-blocking verification banner below header for unverified users - Banner has "Resend" button calling authClient.sendVerificationEmail - Banner is dismissible (stored in sessionStorage, reappears next session) - Tier badge dynamically shows Free/Pro based on user.role - Pro badge has gradient styling distinct from Free badge - Dropdown shows unverified status indicator with yellow dot - Banner uses fixed positioning, does not push content down - CSS for banner, pro badge, and verification status indicators Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(12-03): wire reactive auth-based gating into panel-layout - Add WEB_PREMIUM_PANELS Set (stock-analysis, stock-backtest, daily-market-brief) - Subscribe to auth state changes in PanelLayoutManager.init() - Add updatePanelGating() iterating panels with getPanelGateReason() - Add getGateAction() returning CTA callbacks per gate reason - Remove inline showLocked() calls for web premium panels - Preserve desktop _lockPanels for forecast, oref-sirens, telegram-intel - Clean up auth subscription in destroy() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(13-01): create auth-token utility and inject Bearer header in web fetch redirect - Add src/services/auth-token.ts with getSessionBearerToken() that reads session token from localStorage - Add WEB_PREMIUM_API_PATHS Set for the 4 premium market API paths - Inject Authorization: Bearer header in installWebApiRedirect() for premium paths when session exists - Desktop installRuntimeFetchPatch() left unchanged (API key only) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(13-01): create server-side session validation module - Add server/auth-session.ts with validateBearerToken() for Vercel edge gateway - Validates tokens via Convex /api/auth/get-session with Better-Auth-Cookie header - Falls back to userRoles:getUserRole Convex query for role resolution - In-memory cache with 60s TTL and 100-entry cap - Network errors not cached to allow retry on next request Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(13-02): add bearer token fallback auth for premium API endpoints - Dynamic import of auth-session.ts when premium endpoint + API key fails - Valid pro session tokens fall through to route handler - Non-pro authenticated users get 403 'Pro subscription required' - Invalid/expired tokens get 401 'Invalid or expired session' - Non-premium endpoints and static API key flow unchanged Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(auth): sign-in button invisible in dark theme — white on white --accent is #fff in dark theme, so background: var(--accent) + color: #fff was invisible. Changed to transparent background with var(--text) color. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(auth): add premium panel keys to full and finance variant configs stock-analysis, stock-backtest, and daily-market-brief were defined in the shared panels.ts but missing from variant DEFAULT_PANELS, causing shouldCreatePanel() to return false and panel gating CTAs to never render. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(auth): add Playwright smoke tests for auth UI (phases 12-13) 6 tests covering: Sign In button visibility, auth modal opening, modal views (Sign In/Sign Up/Forgot Password), premium panel gating for anonymous users, and auth token absence when logged out. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(auth): remove role additionalField that breaks Convex component validator The betterAuth Convex component has a strict input validator for the user model that doesn't include custom fields. The role additionalField caused ArgumentValidationError on sign-up. Roles are already stored in the separate userRoles table — no data loss. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(auth): use Authorization Bearer header for Convex session validation Better-Auth-Cookie header returned null — the crossDomain plugin's get-session endpoint expects Authorization: Bearer format instead. Confirmed via curl against live Convex deployment. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(auth): use verified worldmonitor.app domain for auth emails Was using noreply@resend.dev (testing domain) which can't send to external recipients. Switched to noreply@worldmonitor.app matching existing waitlist/contact emails. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(auth): await Resend email sends — Convex kills dangling promises void (fire-and-forget) causes Convex to terminate the fetch before Resend receives it. Await ensures emails actually get sent. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: update Convex generated auth files after config changes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(auth): guard against undefined VITE_CONVEX_SITE_URL in auth-state The Convex cloud URL derivation crashed the entire app when VITE_CONVEX_SITE_URL wasn't set in the build environment (Vercel preview). Now gracefully defaults to empty string and skips role fetching when the URL is unavailable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(auth): add dash + organization plugins, remove Google OAuth, fix dark mode button - Add @better-auth/infra dash plugin for hosted admin dashboard - Add organization plugin for org management in dashboard - Add dash.better-auth.com to trustedOrigins - Remove Google OAuth (socialProviders, button, divider, CSS) - Fix auth submit button invisible in dark mode (var(--accent) → #3b82f6) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(auth): replace dash plugin with admin — @better-auth/infra incompatible with Convex V8 @better-auth/infra imports SSO/SAML libraries requiring Node.js built-ins (crypto, fs, stream) which Convex's V8 runtime doesn't support. Replaced with admin plugin from better-auth/plugins which provides user management endpoints (set-role, list-users, ban, etc.) natively. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: remove stale Convex generated files after plugin update Convex dev regenerated _generated/ — the per-module JS files (auth.js, http.js, schema.js, etc.) are no longer emitted. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(auth): remove organization plugin — will add in subsequent PR Organization support (team accounts, invitations, member management) is not wired into any frontend flow yet. Removing to keep the auth PR focused on email/password + admin endpoints. Will add back when building the org/team feature. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add authentication & panel gating guide Documents the auth stack, panel gating configuration, server-side session enforcement, environment variables, and user roles. Includes step-by-step guide for adding new premium panels. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(test): stub panel-gating in RuntimeConfigPanel test harness Panel.ts now imports @/services/panel-gating, which wasn't stubbed — causing the real runtime.ts (with window.location) to be bundled, breaking Node.js tests with "ReferenceError: location is not defined". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(auth): allow Vercel preview origins in Convex trustedOrigins * fix(auth): broaden Convex trustedOrigins to cover *.worldmonitor.app previews * fix(auth): use hostonly wildcard pattern for *.worldmonitor.app in trustedOrigins * fix(auth): add Convex site origins to trustedOrigins * fix(ci): add convex/ to vercel-ignore watched paths * fix(auth): remove admin() plugin — adds banned/role fields rejected by Convex validator * fix(auth): remove admin() plugin — injects banned/role fields rejected by Convex betterAuth validator * feat(auth): replace email/password with email OTP passwordless flow - Replace emailAndPassword + emailVerification with emailOTP plugin - Rewrite AuthModal: email entry -> OTP code verification (no passwords) - Remove admin() plugin (caused Convex schema validation errors) - Remove email verification banner and UNVERIFIED gate reason (OTP inherently verifies email) - Remove password reset flow (forgot/reset password views, token handling) - Clean up unused CSS (tabs, verification banner, success messages) - Update docs to reflect new passwordless auth stack Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(quick-2): harden Convex userRoles and add role cache TTL - P0: Convert setUserRole from mutation to internalMutation (not callable from client) - P2: Add 5-minute TTL to role cache in auth-state.ts - P2: Add localStorage shape warning on auth-token.ts - P3: Document getUserRole public query trade-off - P3: Fix misleading cache comment in auth-session.ts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(quick-2): auth widget teardown, E2E test rewrite, gateway comment - P2: Store authHeaderWidget on AppContext, destroy in EventHandlerManager.destroy() - P2: Also destroy authModal in destroy() to prevent leaked subscriptions - P1: Rewrite E2E tests for 2-view OTP modal (email input + submit button) - P1: Remove stale "Sign Up" and "Forgot Password" test assertions - P2: Replace flaky waitForTimeout(5000) with Playwright auto-retry assertion - P3: Add clarifying comment on premium bearer-token fallback in gateway Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(header): restructure header/footer, add profile editing, pro-gate playback/export - Remove version, @eliehabib, GitHub link, and download button from header - Move version + @eliehabib credit to footer brand line; download link to footer nav - Move auth widget (profile avatar) to far right of header (after settings gear) - Add default generic SVG avatar for users with no image and no name - Add profile editing in auth dropdown: display name + avatar URL with Save/Cancel - Add Settings shortcut in auth dropdown (opens UnifiedSettings) - Gate Historical Playback and Export controls behind pro role (hidden for free users) - Reactive pro-gate: subscribes to auth state changes, stores unsub in proGateUnsubscribers[] - Clean up proGateUnsubscribers on EventHandlerManager.destroy() to prevent leaks - Fix: render Settings button unconditionally (hidden via style), stable DOM structure - Fix: typed updateUser call with runtime existence check instead of (any) cast - Make initFooterDownload() private to match class conventions * feat(analytics): add Umami auth integration and event tracking - Wire analytics.ts facade to Umami (port from main #1914): search, country, map layers, panels, LLM, theme, language, variant switch, webcam, download, findings, deeplinks - Add Window.umami shim to vite-env.d.ts - Add initAuthAnalytics() that subscribes to auth state and calls identifyUser(id, role) / clearIdentity() on sign-in/sign-out - Add trackSignIn, trackSignUp, trackSignOut, trackGateHit exports - Call initAuthAnalytics() from App.ts after initAuthState() - Track sign-in/sign-up (via isNewUser flag) in AuthModal OTP verify - Track sign-out in AuthHeaderWidget before authClient.signOut() - Track gate-hit for export, playback (event-handlers) and pro-banner * feat(auth): professional avatar widget with colored initials and clean profile edit - Replace white-circle avatar with deterministic colored initials (Gmail/Linear style) - Avatar color derived from email hash across 8-color palette - Dropdown redesigned: row layout with large avatar + name/email/tier info - Profile edit form: name-only (removed avatar URL field) - Remove Settings button from dropdown (gear icon in header is sufficient) - Discord community widget: single CTA link, no redundant text label - Add all missing CSS for dropdown interior, profile edit form, menu items * fix(auth): lock down billing tier visibility and fix TOCTOU race P1: getUserRole converted to internalQuery — billing tier no longer accessible via any public Convex client API. Exposed only through the new authenticated /api/user-role HTTP action which validates the session Bearer token before returning the role. P1: subscribeAuthState generation counter + AbortController prevents rapid sign-in/sign-out from delivering stale role for wrong user. P2: typed RawSessionUser/RawSessionValue interfaces replace any casts at the better-auth nanostore boundary. fetchUserRole drops userId param — server derives identity from Bearer token only. P2: isNewUser heuristic removed from OTP verify — better-auth emailOTP has no reliable isNewUser signal. All verifications tracked as trackSignIn. OTP resend gets 30s client-side cooldown. P2: auth-token.ts version pin comment added (better-auth@1.5.5 + @convex-dev/better-auth@0.11.2). Gateway inner PREMIUM_RPC_PATHS comment clarified to explain why it is not redundant. Adds tests/auth-session.test.mts: 11 tests covering role fallback endpoint selection, fail-closed behavior, and CORS origin matching. * feat(quick-4): replace better-auth with Clerk JS -- packages, Convex config, browser auth layer - Remove better-auth, @convex-dev/better-auth, @better-auth/infra, resend from dependencies - Add @clerk/clerk-js and jose to dependencies - Rewrite convex/auth.config.ts for Clerk issuer domain - Simplify convex/convex.config.ts (remove betterAuth component) - Delete convex/auth.ts, convex/http.ts, convex/userRoles.ts - Remove userRoles table from convex/schema.ts - Create src/services/clerk.ts with Clerk JS init, sign-in, sign-out, token, user metadata, UserButton - Rewrite src/services/auth-state.ts backed by Clerk (same AuthUser/AuthSession interface) - Delete src/services/auth-client.ts (better-auth client) - Delete src/services/auth-token.ts (localStorage token scraping) - Update .env.example with Clerk env vars, remove BETTER_AUTH_API_KEY Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(quick-4): UI components, runtime fetch, server-side JWT, CSP, and tests - Delete AuthModal.ts, create AuthLauncher.ts (thin Clerk.openSignIn wrapper) - Rewrite AuthHeaderWidget.ts to use Clerk UserButton + openSignIn - Update event-handlers.ts to use AuthLauncher instead of AuthModal - Rewrite runtime.ts enrichInitForPremium to use async getClerkToken() - Rewrite server/auth-session.ts for jose-based JWT verification with cached JWKS - Update vercel.json CSP: add *.clerk.accounts.dev to script-src and frame-src - Add Clerk CSP tests to deploy-config.test.mjs - Rewrite e2e/auth-ui.spec.ts for Clerk UI - Rewrite auth-session.test.mts for jose-based validation - Use dynamic import for @clerk/clerk-js to avoid Node.js test breakage Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(auth): allow Clerk Pro users to load premium data on web The data-loader gated premium panel loading (stock-analysis, stock-backtest, daily-market-brief) on WORLDMONITOR_API_KEY only, which is desktop-only. Web users with Clerk Pro auth were seeing unlocked panels stuck on "Loading..." because the requests were never made. Added hasPremiumAccess() helper that checks for EITHER desktop API key OR Clerk Pro role, matching the migration plan Phase 7 requirements. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(auth): address PR #1812 review — all 4 merge blockers + 3 gaps Blockers: 1. Remove stale Convex artifacts (http.js, userRoles.js, betterAuth component) from convex/_generated/api.d.ts 2. isProUser() now checks getAuthState().user?.role === 'pro' alongside legacy localStorage keys 3. Finance premium refresh scheduling now fires for Clerk Pro web users (not just API key holders) 4. JWT verification now validates audience: 'convex' to reject tokens scoped to other Clerk templates Gaps: 5. auth-session tests: 10 new cases (valid pro/free, expired, wrong key/audience/issuer, missing sub/plan, JWKS reuse) using self-signed keys + local JWKS server 6. premium-stock-gateway tests: 4 new bearer token cases (pro→200, free→403, invalid→401, public unaffected) 7. docs/authentication.mdx rewritten for Clerk (removed all better-auth references, updated stack/files/env vars/roles sections) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address P1 reactive Pro UI + P2 daily-market-brief + P3 stale env vars P1 — In-session Pro UI changes no longer require a full reload: - setupExportPanel: removed early isProUser() return, always creates and relies on reactive subscribeAuthState show/hide - setupPlaybackControl: same pattern — always creates, reactive gate - Custom widget panels: always loaded regardless of Pro status - Pro add-panel and MCP add-panel blocks: always rendered, shown/hidden reactively via subscribeAuthState callback - Flight search wiring: always wired, checks Pro status inside callback so mid-session sign-ins work immediately P2 — daily-market-brief added to hasPremiumAccess() block in loadAllData() so Clerk Pro web users get initial data load (was only primed in primeVisiblePanelData, missing from the general reload path) P3 — Removed stale CONVEX_SITE_URL and VITE_CONVEX_SITE_URL from docs/authentication.mdx env vars table (neither is referenced in codebase) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add isProUser import, populate PREMIUM_RPC_PATHS, and fix bearer token auth flow - Added missing isProUser import in App.ts (fixes typecheck) - Populated PREMIUM_RPC_PATHS with stock analysis endpoints - Restructured gateway auth: trusted browser origins bypass API key for premium endpoints (client-side isProUser gate), while bearer token validation runs as a separate step for premium paths when present Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(gateway): require credentials for premium paths + defer free-tier enforcement until auth ready P0: Removed trusted-origin bypass for premium endpoints — Origin header is spoofable and cannot be a security boundary. Premium paths now always require either an API key or valid bearer token. P1: Deferred panel/source free-tier enforcement until auth state resolves. Previously ran in the constructor before initAuthState(), causing Clerk Pro users to have their panels/sources trimmed on every startup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(auth): apply WorldMonitor design system to Clerk modal Theme-aware appearance config passed to clerk.load(), openSignIn(), and mountUserButton(). Dark mode: dark bg (#111), green primary (#44ff88), monospace font. Light mode: white bg, green-600 primary (#16a34a). Reads document.documentElement.dataset.theme at call time so theme switches are respected. * fix(auth): gate Clerk init and auth widget behind BETA_MODE Clerk auth initialization and the Sign In header widget are now only activated when localStorage `worldmonitor-beta-mode` is set to "true", allowing silent deployment for internal testing before public rollout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(auth): gate Clerk init and auth widget behind isProUser() Clerk auth initialization and the Sign In header widget are now only activated when the user has wm-widget-key or wm-pro-key in localStorage (i.e. isProUser() returns true), allowing silent deployment for internal testing before public rollout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(data-loader): replace stale isProUser() with hasPremiumAccess() loadMarketImplications() still referenced the removed isProUser import, causing a TS2304 build error. Align with the rest of data-loader.ts which uses hasPremiumAccess() (checks both API key and Clerk auth). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(auth): address PR #1812 review — P1 security fixes + P2 improvements P1 fixes: - Add algorithms: ['RS256'] allowlist to jwtVerify (prevents alg:none bypass) - Reset loadPromise on Clerk init failure (allows retry instead of permanent breakage) P2 fixes: - Extract PREMIUM_RPC_PATHS to shared module (eliminates server/client divergence risk) - Add fail-fast guard in convex/auth.config.ts for missing CLERK_JWT_ISSUER_DOMAIN - Add 50s token cache with in-flight dedup to getClerkToken() (prevents concurrent races) - Sync Clerk CSP entries to index.html and tauri.conf.json (previously only in vercel.json) - Type clerkInstance as Clerk instead of any Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(auth): clear cached token on signOut() Prevents stale token from being returned during the ≤50s cache window after a user signs out. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Sebastien Melki <sebastien@anghami.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Sebastien Melki <sebastienmelki@gmail.com> |
||
|
|
4bb1ac8023 | fix(panels): wire 6 new panels into primeTask and scheduleRefresh (#2290) | ||
|
|
ece8b77adf | fix(hormuz): coerce Power BI series values to number (#2289) | ||
|
|
87b6a525cf |
fix(feeds+seeds): graceful ACLED skip + USNI Google News proxy (#2288)
ACLED no longer throws when credentials are absent — graceful log+return instead of crashing the entire seed on Railway envs without ACLED creds. USNI direct feed (news.usni.org/feed) was returning 403 in production; switched to Google News proxy (same pattern as Janes and CSIS) to stop the relay backoff from saturating error logs. |
||
|
|
316295404e |
feat(iran-events): add Kafr Qasim, Taybad, Sweihan to LOCATION_COORDS (#2287)
New locations referenced in March 2026 Iran conflict events. |
||
|
|
bcf525de4f |
fix(health): bump iranEvents maxStaleMin 10080→20160 (14d) (#2286)
Manual seed from LiveUAMap runs weekly at best. 10080min (7d) = 1× interval with zero buffer — any delay over the exact cadence fires STALE_SEED. 20160 = 14 days = 2× interval, absorbs one missed week without alerting. |
||
|
|
0169245f45 |
feat(seo): BlogPosting schema, FAQPage JSON-LD, extensible author system (#2284)
* feat(seo): BlogPosting schema, FAQPage JSON-LD, author system, AI crawler welcome Blog structured data: - Change @type Article to BlogPosting for all blog posts - Author: Organization to Person with extensible default (Elie Habib) - Add per-post author/authorUrl/authorBio/modifiedDate frontmatter fields - Auto-extract FAQPage JSON-LD from FAQ sections in all 17 posts - Show Updated date when modifiedDate differs from pubDate - Add author bio section with GitHub avatar and fallback Main app: - Add commodity variant to middleware VARIANT_HOST_MAP and VARIANT_OG - Add commodity.worldmonitor.app to sitemap.xml - Shorten index.html meta description to 136 chars (was 161) - Remove worksFor block from index.html author JSON-LD - Welcome all bots in robots.txt (removed per-bot blocks, global allows) - Update llms.txt: five variants listed, all 17 blog post URLs added * fix(seo): scope FAQ regex to section boundary, use author-aware avatar - extractFaqLd now slices only to the next ## heading (was: to end of body) preventing bold text in post-FAQ sections from being mistakenly extracted - Avatar src now derived from DEFAULT_AUTHOR_GITHUB constant (koala73) only when using the default author; custom authors fall back to favicon so multi-author posts show a correct image instead of the wrong profile |
||
|
|
5b08ce3788 |
fix(simulation): surface market_cascade path in UI — increase uiTheaters topPaths cap 2→3 (#2285)
The simulation produces 3 paths (escalation, containment, market_cascade) but writeSimulationOutcome was slicing to 2, silently dropping market_cascade before writing the Redis pointer. The UI and ForecastPanel never saw the economic cascade path. |
||
|
|
7709c6f302 |
fix(seed-cot): unblock CFTC fetch — switch to publicreporting.cftc.gov Socrata API (#2283)
* fix(seed-cot): switch from blocked www.cftc.gov to publicreporting.cftc.gov Socrata API www.cftc.gov/dea/newcot/c_disaggrt.txt returns HTTP 403 from Railway container IPs (same IP-blocking pattern as api.bls.gov before PR #2238). Replace with two CFTC Socrata endpoints that allow programmatic access: - yw9f-hn96 (TFF Combined): ES, NQ, ZN, ZT, EC, JY - rxbv-e226 (Disaggregated All Combined): GC, CL Market name patterns updated to match current Socrata naming. Output shape unchanged. * fix(seed-cot): truncate ISO timestamp in parseDate; restore envId filter in railway script parseDate fell through for Socrata ISO timestamps (2026-03-17T00:00:00.000), storing the full string instead of YYYY-MM-DD. Fix: slice(0, 10) as fallback. railway-set-watch-paths: environmentId filter was accidentally dropped from serviceInstances query, risking wrong-environment instance selection. |
||
|
|
e234a962d1 |
fix(sentry): broaden ss_bootstrap_config filter + 2 new noise patterns (#2275)
- Widen Surfly filter from Safari-only ("Can't find variable:") to also
cover Chrome ("is not defined") by matching on /ss_bootstrap_config/
- Add /Can only call Window\.setTimeout on instances of Window/ for iOS
Safari cross-frame setTimeout errors from 3rd-party injected scripts
- Add /^Can't find variable: _G$/ for browser extension/userscript _G
global injection (33 events / 18 users, all "global code" frames)
|
||
|
|
d5a754e478 |
fix(circuit-breaker): evict invalid cached data when shouldCache fails — unblocks BIS/BLS tabs (#2274)
* fix(circuit-breaker): evict invalid cached data when shouldCache predicate fails
Circuit breakers with persistCache=true would serve stale empty data (e.g.
[] or { rates: [] }) indefinitely within the 15-30min TTL window. This
caused the Central Banks and Labor tabs in the Macro Stress panel to never
appear even after the underlying seeders started returning real data.
- circuit-breaker.ts: when shouldCache option is provided, evict cached
entries that fail the predicate before checking fresh/SWR paths. This
forces a live fetch instead of serving known-invalid cached data.
- economic/index.ts: add shouldCache guards to blsBreaker (r.length > 0)
and all three BIS breakers (rates/entries length > 0) so empty responses
are never written to persistent cache and existing empty entries are
evicted on next call.
* fix(circuit-breaker): address Greptile P2 comments on PR #2274
- Use local shouldCache variable instead of options.shouldCache directly
(the default () => true means the condition is always false without an
explicit predicate — redundant guard removed, local var is cleaner)
- Document the fire-and-forget race window in comment
- Add 3 tests for the new shouldCache eviction path:
evicts invalid cached data and fetches fresh, skips eviction for valid
data, and preserves existing behavior when no predicate is provided
|
||
|
|
85317dfbb4 |
feat(seed): switch economic calendar from Finnhub to FRED API (#2272)
* feat(seed): switch economic calendar from Finnhub to FRED API Finnhub /calendar/economic requires a $3,500/mo premium subscription; free tier returns HTTP 403 on every call (confirmed in production logs). FRED (St. Louis Fed) provides the same government-scheduled release dates for free using FRED_API_KEY, already present in Railway env for seed-economy and seed-bls-series. No new API key or cost required. Sources now used: - Release 10: CPI (BLS) - Release 50: Nonfarm Payrolls (BLS) - Release 53: GDP (BEA) - Release 54: PCE / Personal Income (BEA) - Release 9: Retail Sales (Census Bureau) - Hardcoded: FOMC rate decision dates (2026, Fed annual schedule) FRED tracks the full year schedule in advance via include_release_dates_with_no_data=true. Tested locally: 11 events seeded to Redis successfully. * fix(seed): add User-Agent header and FOMC expiry warning Addresses Greptile review comments on PR #2272: - P2: re-import CHROME_UA and pass as User-Agent on FRED fetch (project convention from AGENTS.md) - P1: warn when FOMC_DATES_2026 has no upcoming dates so operators know to update the constant for the new year |
||
|
|
3c8c5bf64a | fix(panels): remove description blob from AI Market Implications; refresh every 3h (#2270) | ||
|
|
0e1714e559 |
fix(seed): write seed-meta when validateFn rejects empty data (#2273)
* feat(seed): switch economic calendar from Finnhub to FRED API Finnhub /calendar/economic requires a $3500/mo premium subscription. FRED (St. Louis Fed) provides official government-scheduled release dates for free using the existing FRED_API_KEY already in Railway env. Sources: - Release 10: CPI (BLS) - Release 50: Nonfarm Payrolls (BLS) - Release 53: GDP (BEA) - Release 54: PCE / Personal Income (BEA) - Release 9: Retail Sales (Census Bureau) - Hardcoded: FOMC rate decision dates (Fed, published annually) FRED tracks the full year schedule in advance via include_release_dates_with_no_data=true. No new API key needed. * fix(panels): remove description blob from AI Market Implications; refresh every 3h * fix(seed): write seed-meta even when validateFn rejects empty data When a seed runs but finds no publishable data (e.g. no earnings in the next 14-day window, no econ events scheduled), runSeed calls extendExistingTtl which only extends keys that already exist. If seed-meta was never written (first run or expired), health sees seedStale=true → STALE_SEED warn even though the seeder is healthy. Fix: call writeFreshnessMetadata(count=0) in the skipped path so health can distinguish 'seeder ran, nothing to publish' from 'seeder stopped running'. * fix(seed): add User-Agent to FRED fetch; make FOMC dates year-keyed not hardcoded Greptile P2s from PR #2273: - Missing User-Agent: CHROME_UA added to fetchFredReleaseDates per AGENTS.md - FOMC_DATES_2026 constant would silently return empty FOMC list from Jan 2027; restructured as FOMC_DATES_BY_YEAR map, buildFomcEvents merges current + next year so there is always a lookahead window until next year's dates are added |
||
|
|
d07f8a6c99 |
feat(cii): wire earthquakes + sanctions into CII score; fix 3 brief context gaps (#2269)
* feat(cii): wire earthquakes + sanctions into CII score; add displacement/climate/tier1 to AI brief context Gap 1 — displacement outflow, climate stress, isTier1 now included in buildBriefContextSnapshot() so the AI brief sees displacement/climate data for relevant countries. Gap 2 — ingestEarthquakesForCII(): M5.5–6.4 = +2, M6.5–7.4 = +5, M7.5+ = +10, capped at +25. Earthquakes showed in the brief signal count but contributed zero to the CII score. Gap 3 — ingestSanctionsForCII(): tiered boost (3/5/8/12) by designation count + +2 escalation for new entries. Sanctions showed in the brief context but didn't affect the instability score. Both ingests called in data-loader after their respective cache writes, consistent with all other ingest functions. * fix(cii): add 7-day recency filter to earthquake CII ingest Without a time window, all M5.5+ quakes in the USGS cache (up to 30 days) accumulated — permanently pinning Japan, Indonesia, Philippines etc. at the +25 earthquake boost cap even during quiet periods. Added a 7-day lookback cutoff on eq.occurredAt. Also moved processedCount increment after the recency check so stats count only contributing events. Also passes now= for testability (consistent with other CII calc functions). |
||
|
|
54cc8d6cc7 |
fix(panels): allow drag from anywhere in panel body, prevent text selection (#2271)
Removes .panel-content from the mousedown exclusion list so panels can be dragged from any non-interactive area inside them (not just the header). Adds user-select: none to .panel-content to prevent text getting highlighted when initiating a drag — avoids the "select the time" problem on World Clock. |
||
|
|
41d964265e |
fix(map): sanctions layer invisible on WebGL map (#2267)
* fix(health): extend EMPTY_DATA_OK_KEYS check to bootstrap loop
Greptile correctly identified the fix was a no-op: EMPTY_DATA_OK_KEYS was
only consulted in the STANDALONE_KEYS loop, not BOOTSTRAP_KEYS. All three
calendar keys are bootstrap keys, so critCount was still incremented.
Mirror the same seedStale branching already present in the standalone loop
into the bootstrap loop so EMPTY_DATA_OK members get OK/STALE_SEED instead
of EMPTY/EMPTY_DATA/critCount++.
* fix(map): implement sanctions choropleth layer in DeckGLMap
The sanctions layer toggle did nothing on the WebGL map — DeckGLMap had
no rendering logic, only a help text label. Only Map.ts (2D SVG) had the
updateCountryFills() implementation.
Add createSanctionsChoroplethLayer() as a GeoJsonLayer using
SANCTIONED_COUNTRIES_ALPHA2 (new export) since countriesGeoJsonData keys
by ISO3166-1-Alpha-2, not the numeric IDs used by Map.ts/TopoJSON.
Wire it into the layer pipeline after the CII choropleth.
Alpha values match Map.ts: severe=89, high=64, moderate=51 (0-255).
* Revert "fix(health): extend EMPTY_DATA_OK_KEYS check to bootstrap loop"
This reverts commit
|
||
|
|
c37bba279e |
fix(health): calendar keys are OK when empty (between seasons) (#2266)
* fix(health): add earningsCalendar, econCalendar, cotPositioning to EMPTY_DATA_OK_KEYS Calendar data is legitimately empty between seasons (no earnings scheduled, no econ events, COT release pending). Treating 0-record state as CRIT causes 503s when seeds run but publish nothing due to non-empty validateFn. With EMPTY_DATA_OK_KEYS: empty key → OK (seed fresh) or STALE_SEED (warn). Stops the 3-CRIT → DEGRADED → 503 flap when calendars have no upcoming events. * fix(health): extend EMPTY_DATA_OK_KEYS check to bootstrap loop Greptile correctly identified the fix was a no-op: EMPTY_DATA_OK_KEYS was only consulted in the STANDALONE_KEYS loop, not BOOTSTRAP_KEYS. All three calendar keys are bootstrap keys, so critCount was still incremented. Mirror the same seedStale branching already present in the standalone loop into the bootstrap loop so EMPTY_DATA_OK members get OK/STALE_SEED instead of EMPTY/EMPTY_DATA/critCount++. |
||
|
|
a7c7daa318 |
fix(outages): null description crashes popup, silencing all click interactions (#2265)
Cloudflare Radar annotations can have null description. The popup renderer called description.slice(0, 250) unconditionally — TypeError was silently swallowed by DeckGL's onClick, so no popup ever appeared on click. Also fixes tooltip to show outage.title (always set in seed) instead of obj.asn which doesn't exist on InternetOutage, and normalizes null descriptions to '' in the seed for future robustness. |
||
|
|
b0af1ad84f |
feat(simulation): geographic theater diversity + market_cascade economic paths (#2264)
* feat(simulation): geographic theater diversity + market_cascade economic paths Three improvements to the MiroFish simulation pipeline: 1. Geographic deduplication: adds THEATER_GEO_GROUPS constant mapping CHOKEPOINT_MARKET_REGIONS values to macro-groups (MENA, AsiaPacific, EastEurope, etc.). buildSimulationPackageFromDeepSnapshot now skips candidates whose macro-group is already represented, preventing Red Sea + Middle East (both MENA) from appearing as separate theaters. 2. Label cleanup: strips trailing (stateKind) parenthetical from theater labels before writing to selectedTheaters, so "Black Sea maritime disruption state (supply_chain)" becomes "Black Sea maritime disruption state" in the UI. 3. market_cascade path: renames spillover → market_cascade across 4 sites (evaluationTargets, Round 1 prompt + JSON template, Round 2 prompt + JSON template, tryParseSimulationRoundPayload expectedIds). The market_cascade path instructs the LLM to model 2nd/3rd order economic consequences: energy price direction ($/bbl), freight rate delta, downstream sector impacts, and FX stress on import-dependent economies. Tests: 176 pass (3 net new — geo-dedup, label cleanup, market_cascade prompt; plus updated entity-collision and path-validation tests). 🤖 Generated with Claude Sonnet 4.6 via Claude Code (https://claude.ai/claude-code) + Compound Engineering v2.49.0 Co-Authored-By: Claude Sonnet 4.6 (200K context) <noreply@anthropic.com> * docs: fix markdownlint MD032 in simulation diversity plan --------- Co-authored-by: Claude Sonnet 4.6 (200K context) <noreply@anthropic.com> |
||
|
|
2939b1f4a1 |
feat(finance-panels): add 7 macro/market panels + Daily Brief context (issues #2245-#2253) (#2258)
* feat(fear-greed): add regime state label, action stance badge, divergence warnings Closes #2245 * feat(finance-panels): add 7 new finance panels + Daily Brief macro context Implements issues #2245 (F&G Regime), #2246 (Sector Heatmap bars), #2247 (MacroTiles), #2248 (FSI), #2249 (Yield Curve), #2250 (Earnings Calendar), #2251 (Economic Calendar), #2252 (COT Positioning), #2253 (Daily Brief prompt extension). New panels: - MacroTilesPanel: CPI YoY, Unemployment, GDP, Fed Rate tiles via FRED - FSIPanel: Financial Stress Indicator gauge (HYG/TLT/VIX/HY-spread) - YieldCurvePanel: SVG yield curve chart with inverted/normal badge - EarningsCalendarPanel: Finnhub earnings calendar with BMO/AMC/BEAT/MISS - EconomicCalendarPanel: FOMC/CPI/NFP events with impact badges - CotPositioningPanel: CFTC disaggregated COT positioning bars - MarketPanel: adds sorted bar chart view above sector heatmap grid New RPCs: - ListEarningsCalendar (market/v1) - GetCotPositioning (market/v1) - GetEconomicCalendar (economic/v1) Seed scripts: - seed-earnings-calendar.mjs (Finnhub, 14-day window, TTL 12h) - seed-economic-calendar.mjs (Finnhub, 30-day window, TTL 12h) - seed-cot.mjs (CFTC disaggregated text file, TTL 7d) - seed-economy.mjs: adds yield curve tenors DGS1MO/3MO/6MO/1/2/5/30 - seed-fear-greed.mjs: adds FSI computation + sector performance Daily Brief: extends buildDailyMarketBrief with optional regime, yield curve, and sector context fed to the LLM summarization prompt. All panels default enabled in FINANCE_PANELS, disabled in FULL_PANELS. 🤖 Generated with Claude Sonnet 4.6 via Claude Code (https://claude.ai/claude-code) + Compound Engineering v2.40.0 Co-Authored-By: Claude Sonnet 4.6 (200K context) <noreply@anthropic.com> * fix(finance-panels): address code review P1/P2 findings P1 - Security/Correctness: - EconomicCalendarPanel: add escapeHtml on all 7 Finnhub-sourced fields - EconomicCalendarPanel: fix panel contract (public fetchData():boolean, remove constructor self-init, add retry callbacks to all showError calls) - YieldCurvePanel: fix NaN in xPos() when count <= 1 (divide-by-zero) - seed-earnings-calendar: move Finnhub API key from URL to X-Finnhub-Token header - seed-economic-calendar: move Finnhub API key from URL to X-Finnhub-Token header - seed-earnings-calendar: add isMain guard around runSeed() call - health.js + bootstrap.js: register earningsCalendar, econCalendar, cotPositioning keys - health.js dataSize(): add earnings + instruments to property name list P2 - Quality: - FSIPanel: change !resp.fsiValue → resp.fsiValue <= 0 (rejects valid zero) - data-loader: fix Promise.allSettled type inference via indexed destructure - seed-fear-greed: allowlist cnnLabel against known values before writing to Redis - seed-economic-calendar: remove unused sleep import - seed-earnings-calendar + econ-calendar: increase TTL 43200 → 129600 (36h = 3x interval) - YieldCurvePanel: use SERIES_IDS const in RPC call (single source of truth) * fix(bootstrap): remove on-demand panel keys from bootstrap.js earningsCalendar, econCalendar, cotPositioning panels fetch via RPC on demand — they have no getHydratedData consumer in src/ and must not be in api/bootstrap.js. They remain in api/health.js BOOTSTRAP_KEYS for staleness monitoring. * fix(compound-engineering): fix markdown lint error in local settings * fix(finance-panels): resolve all P3 code-review findings - 030: MacroTilesPanel: add `deltaFormat?` field to MacroTile interface, define per-tile delta formatters (CPI pp, GDP localeString+B), replace fragile tile.id switch in tileHtml with fmt = deltaFormat ?? format - 031: FSIPanel: check getHydratedData('fearGreedIndex') at top of fetchData(); extract fsi/vix/hySpread from headerMetrics and render synchronously; fall back to live RPC only when bootstrap absent - 032: All 6 finance panels: extract lazy module-level client singletons (EconomicServiceClient or MarketServiceClient) so the client is constructed at most once per panel module lifetime, not on every fetchData - 033: get-fred-series-batch: add BAMLC0A0CM and SOFR to ALLOWED_SERIES (both seeded by seed-economy.mjs but previously unreachable via RPC) * fix(finance-panels): health.js SEED_META, FSI calibration, seed-cot catch handler - health.js: add SEED_META entries for earningsCalendar (1440min), econCalendar (1440min), cotPositioning (14400min) — without these, stopped seeds only alarm CRIT:EMPTY after TTL expiry instead of earlier WARN:STALE_SEED - seed-cot.mjs: replace bare await with .catch() handler consistent with other seeds - seed-fear-greed.mjs: recalibrate FSI thresholds to match formula output range (Low>=1.5, Moderate>=0.8, Elevated>=0.3; old values >=0.08/0.05/0.03 were calibrated for [0,0.15] but formula yields ~1-2 in normal conditions) - FSIPanel.ts: fix gauge fillPct range to [0, 2.5] matching recalibrated thresholds - todos: fix MD022/MD032 markdown lint errors in P3 review files --------- Co-authored-by: Claude Sonnet 4.6 (200K context) <noreply@anthropic.com> |
||
|
|
5a24e8d60c |
fix(thermal): bump TTL 3h->6h and maxStaleMin 240->360 to stop 503 flapping (#2263)
Root cause: thermalEscalation cron runs every 2h but CACHE_TTL was 3h (1.5x). Any Railway delay caused the key to expire before the next run, triggering EMPTY -> CRIT -> 503. Confirmed via health:last-failure Redis snapshot: thermalEscalation:EMPTY(183min) at 2026-03-26T00:04:17Z (and 04:04, 06:03) - seed-thermal-escalation.mjs: TTL 3h -> 6h (3x the 2h cron interval) - health.js: maxStaleMin 240 -> 360 (3x interval, consistent with pattern) |
||
|
|
01d18a2774 |
fix(economic): BIS seed — write exchange/credit keys via afterPublish (Central Banks tab fix) (#2257)
* fix(economic): write BIS exchange/credit keys via afterPublish, not .then()
runSeed() calls process.exit(0) internally so .then() is unreachable.
The exchange and credit keys were never written to Redis, leaving
economic:bis:eer:v1 and economic:bis:credit:v1 empty.
Also fixes the canonical key shape: publishTransform now stores only
{ rates: [...] } at economic:bis:policy:v1 instead of the compound
{ policy, exchange, credit } object, matching what getBisPolicyRates
expects. Previously hasBis was always false → Central Banks tab hidden.
* fix(economic): fix validate() shape mismatch and restore exit(1) on fatal
validateFn receives post-transform data { rates: [...] }, not the raw
{ policy, exchange, credit } shape. The old check always returned falsy,
causing atomicPublish to skip every write.
Also restores process.exit(1) on fatal error so Railway alerts on seed
failures instead of silently exiting clean.
|
||
|
|
d851921fe1 |
fix(relay): refresh seed-meta on empty NWS response to prevent false STALE_SEED (#2256)
Root cause: seedWeatherAlerts() had two early-return paths that skipped the seed-meta upstash write — alerts.length===0 (quiet weather) and !resp.ok. After a transient NWS fetch failure, subsequent successful-but-empty runs never bumped fetchedAt, causing health.js to see the old timestamp grow stale. - Update seed-meta on alerts.length===0 (NWS OK, just no active alerts) - Keep !resp.ok path as-is (prolonged NWS outage should still alert) - Add weatherAlerts to EMPTY_DATA_OK_KEYS (0 alerts = valid quiet state) |
||
|
|
6dc2e97465 |
fix(forecasts): clean up NEXUS prob table layout (#2254)
* fix(forecasts): clean up NEXUS prob table — hide Analysis toggles until hover, remove empty nexus wrapper * fix(forecasts): match NEXUS prob table to playground — correct columns, header row, theater card sizing * fix(forecasts): resolve PR review — remove duplicate css rule, restore calibration in detail, touch fallback for Analysis toggle |
||
|
|
df04fecbca | fix(health): bump weatherAlerts maxStaleMin 30→45 (3× 15min relay interval) (#2255) | ||
|
|
45469fae3b |
feat(forecasts): NEXUS panel redesign + simulation theater data via theaterSummariesJson (#2244)
Redesigns ForecastPanel with a theater-first NEXUS layout that surfaces simulation outcomes alongside forecast probabilities in a unified view. Adds the theaterSummariesJson field to the GetSimulationOutcome proto so the server can return pre-condensed UI data from a single Redis read without any additional R2 fetches. - proto: add theater_summaries_json = 9 to GetSimulationOutcomeResponse - seed-forecasts.mjs: embed condensed uiTheaters array in Redis pointer at write time - get-simulation-outcome.ts: serialize pointer.uiTheaters → theaterSummariesJson in RPC response - src/services/forecast.ts: add fetchSimulationOutcome() returning theaterSummariesJson string - src/app/data-loader.ts: load simulation outcome alongside forecasts, call updateSimulation() - ForecastPanel.ts: full NEXUS redesign with SVG circular gauges, expandable theater detail, compact prob table, CSS custom property for per-theater accent color, race condition guard (skip render when forecasts array still empty on simulation arrival) |
||
|
|
b46878db79 |
fix(layers): unhide sanctions layer on flat map (#2243)
* fix(layers): unhide sanctions layer on flat map Sanctions was registered with renderers:[] making it invisible in the layers panel. Changed to ['flat'] and added to VARIANT_LAYER_ORDER for full/finance/commodity variants. Removed now-redundant SVG_ONLY_LAYERS workaround. * fix(layers): remove dead SVG_ONLY_LAYERS constant and no-op loop SVG_ONLY_LAYERS was emptied but not fully removed. Delete the constant and simplify getAllowedLayerKeys to a one-liner since the loop was a permanent no-op. |
||
|
|
c4ada136a4 | fix(simulation): hoist VALID_RUN_ID_RE above _isDirectRun to fix TDZ crash on enqueue (#2241) | ||
|
|
6e04a22ef4 |
fix(forecasts): semantic dedup + maritime gate + self-critique on cards (#2240)
* fix(forecasts): cross-state semantic dedup + maritime eligibility gate + card counter-thesis
Problem: AI Forecasts panel showed 7 near-identical "Inflation from [sea]" bets
(Baltic Sea 66%, Red Sea 66%, Persian Gulf 66%, Hormuz 70%...) with no insight,
plus irrelevant bets (Brazil maritime energy, Cuba inflation). No self-critique
visible at card level.
Root cause: deriveStateDrivenForecasts iterates every maritime state and generates
one forecast per bucket — each state gets a unique solo:${id} family so the
cross-family cap never fires. Result: N seas × 1 bucket = N identical bets.
Fix 1 — Cross-state semantic dedup (seed-forecasts.mjs):
After generating all derived forecasts, group by (bucketId x stateKind). Keep at
most 2 per group, requiring >=6% probability spread between kept forecasts.
7 "Inflation from maritime_disruption" bets -> max 2, differentiated.
Fix 2 — Maritime eligibility gate (seed-forecasts.mjs):
energy/freight supply_chain buckets now require source stateKind to be maritime
(maritime_disruption, port_disruption, shipping_disruption, chokepoint_closure,
naval_blockade, piracy_escalation). Blocks Brazil security escalation -> maritime
energy and Cuba infrastructure -> inflation bets with no causal path.
Fix 3 — Self-critique on card (ForecastPanel.ts):
Surface contrarianCase or first counterEvidence summary as a small red italic
note directly on the forecast card, visible without clicking into Analysis.
* fix(forecasts): add transport_pressure to maritime gate allowlist
* fix(forecasts): close maritime gate falsy bypass + fix dedup sort monotonicity
* fix(forecasts): hoist MARITIME set to module scope + typed caseFile cast
* fix(simulation): enqueue simulation task after package write completes
|
||
|
|
a2fac1d404 |
fix(panels): show radar error state on fetch failure across 22 panels (#2228)
* fix(panels): show radar error state on fetch failure across 22 panels
Add showError() call in catch blocks for 21 data-loader loaders so all
panels display the red radar error state instead of staying on "Loading..."
when upstream fetches fail. Also wraps ConsumerPricesPanel.fetchData() in
try/catch with showError + retry since it self-fetches.
Panels covered: stock-analysis, stock-backtest, tech-events, weather,
infra-outages, cyber-threats, protests, webcams, flight-delays,
energy-complex, trade-policy, supply-chain, satellite-fires,
security-advisories, sanctions-pressure, radiation-watch,
thermal-escalation, displacement, climate, oref-sirens,
population-exposure, consumer-prices.
* fix(panels): correct wrong panel IDs and fix ConsumerPrices error state
data-loader.ts:
- tech-events → events (actual registered panel ID)
- infra-outages → internet-disruptions (actual registered panel ID)
- Remove callPanel showError for weather/cyber-threats/protests/webcams/
flight-delays — these are map layers with no panel registration, so
those calls were no-ops
ConsumerPricesPanel.ts:
- Check overview.upstreamUnavailable after Promise.all and call showError()
with retry, since service functions swallow transport failures and return
the emptyOverview default (upstreamUnavailable:true) rather than rejecting
— the try/catch alone was dead code for this failure mode
* fix(panels): remove events showError from map-only catch, fix all-markets path
- Remove callPanel('events', 'showError') from loadTechEvents catch —
that loader runs a map-specific query (conference/mappable/90d) and
TechEventsPanel manages its own independent data fetch; a map failure
must not blank a healthy panel
- Add upstreamUnavailable check in ConsumerPricesPanel fetchData()
all-markets branch — the default view (market='all') called
fetchAllMarketsOverview() without checking the result, so all users
on the default view still got "Pending data" rows instead of the
radar error state
* fix(panels): remove update([], 0) after showError in satellite-fires catch
update() always re-renders, so calling it after showError() was
immediately replacing the radar error state with the empty dataset UI.
* revert(panels): drop ConsumerPricesPanel error state changes
upstreamUnavailable:true is set by the server on both cache miss
(seed not yet populated) and transport failure — the two cases are
indistinguishable from the client. Treating it as a hard error would
show the radar error screen on fresh deploys before the seeder has run.
The existing seeding placeholder is accurate for both states.
ConsumerPricesPanel is out of scope for this PR.
* fix(panels): remove dead showError calls from cache-fallback paths; add retry callbacks
stock-analysis / stock-backtest catch blocks had callPanel('showError')
at the top, immediately followed by a cache-fallback fetch that calls
renderAnalyses/renderBacktests (setContent) on success — wiping the
error state. Remove the premature callPanel calls; the explicit
panel.showError() at the end of each catch already handles the
no-cache path correctly.
Add onRetry callbacks for panels with long refresh intervals so
transient failures show a retry button instead of a permanent error
state lasting up to 6 hours:
- energy-complex (6h interval): → () => this.loadOilAnalytics()
- trade-policy (~1h): → () => this.loadTradePolicy()
- supply-chain (~1h): → () => this.loadSupplyChain()
|
||
|
|
54eda4b54b |
fix(bls): replace BLS API with FRED to unblock Labor tab in Macro Stress panel (#2238)
api.bls.gov rejects HTTPS CONNECT tunnels from Railway container IPs even through Decodo proxy (gate.decodo.com returns 403 on CONNECT for .gov domains). FRED mirrors the national BLS series with identical data and no IP restrictions. - seed-bls-series.mjs: rewrite to fetch USPRIV + ECIALLCIV from FRED (api.stlouisfed.org works through Railway, confirmed by existing seed-economy). Converts FRED date format to BLS observation shape (year/period/periodName/value) so the existing handler and frontend parsing are unchanged. Metro-area LAUMT* series dropped — no FRED equivalent available. - get-bls-series.ts: update KNOWN_SERIES_IDS allowlist to USPRIV + ECIALLCIV - economic/index.ts: update BLS_SERIES IDs and clear BLS_METRO_IDS This unblocks the Labor tab in the Macro Stress panel which has shown blank since the seeder was deployed (every BLS fetch = timeout/fetch failed). |
||
|
|
75d3d29bcd | feat(contact): include submitter IP and country in enterprise notification email (#2239) | ||
|
|
81b8bc5bc6 |
fix(fear-greed): correct M2SL YoY window (12→52 weeks), WALCL WoW→MoM, VIX9D gate (#2236)
* fix(panels): show radar error state on fetch failure across 22 panels
Add showError() call in catch blocks for 21 data-loader loaders so all
panels display the red radar error state instead of staying on "Loading..."
when upstream fetches fail. Also wraps ConsumerPricesPanel.fetchData() in
try/catch with showError + retry since it self-fetches.
Panels covered: stock-analysis, stock-backtest, tech-events, weather,
infra-outages, cyber-threats, protests, webcams, flight-delays,
energy-complex, trade-policy, supply-chain, satellite-fires,
security-advisories, sanctions-pressure, radiation-watch,
thermal-escalation, displacement, climate, oref-sirens,
population-exposure, consumer-prices.
* fix(panels): correct wrong panel IDs and fix ConsumerPrices error state
data-loader.ts:
- tech-events → events (actual registered panel ID)
- infra-outages → internet-disruptions (actual registered panel ID)
- Remove callPanel showError for weather/cyber-threats/protests/webcams/
flight-delays — these are map layers with no panel registration, so
those calls were no-ops
ConsumerPricesPanel.ts:
- Check overview.upstreamUnavailable after Promise.all and call showError()
with retry, since service functions swallow transport failures and return
the emptyOverview default (upstreamUnavailable:true) rather than rejecting
— the try/catch alone was dead code for this failure mode
* fix(panels): remove events showError from map-only catch, fix all-markets path
- Remove callPanel('events', 'showError') from loadTechEvents catch —
that loader runs a map-specific query (conference/mappable/90d) and
TechEventsPanel manages its own independent data fetch; a map failure
must not blank a healthy panel
- Add upstreamUnavailable check in ConsumerPricesPanel fetchData()
all-markets branch — the default view (market='all') called
fetchAllMarketsOverview() without checking the result, so all users
on the default view still got "Pending data" rows instead of the
radar error state
* fix(panels): remove update([], 0) after showError in satellite-fires catch
update() always re-renders, so calling it after showError() was
immediately replacing the radar error state with the empty dataset UI.
* revert(panels): drop ConsumerPricesPanel error state changes
upstreamUnavailable:true is set by the server on both cache miss
(seed not yet populated) and transport failure — the two cases are
indistinguishable from the client. Treating it as a hard error would
show the radar error screen on fresh deploys before the seeder has run.
The existing seeding placeholder is accurate for both states.
ConsumerPricesPanel is out of scope for this PR.
* fix(panels): remove dead showError calls from cache-fallback paths; add retry callbacks
stock-analysis / stock-backtest catch blocks had callPanel('showError')
at the top, immediately followed by a cache-fallback fetch that calls
renderAnalyses/renderBacktests (setContent) on success — wiping the
error state. Remove the premature callPanel calls; the explicit
panel.showError() at the end of each catch already handles the
no-cache path correctly.
Add onRetry callbacks for panels with long refresh intervals so
transient failures show a retry button instead of a permanent error
state lasting up to 6 hours:
- energy-complex (6h interval): → () => this.loadOilAnalytics()
- trade-policy (~1h): → () => this.loadTradePolicy()
- supply-chain (~1h): → () => this.loadSupplyChain()
* fix(fear-greed): correct M2SL YoY window (12→52 weeks), WALCL MoM (1→4 weeks), VIX9D gate
M2SL converted to weekly in 2021. fredNMonthsAgo(m2Obs, 12) = 12 weeks ≈ 3
months, not 12 months. True YoY requires 52 weekly observations. m2Score
was systematically understating expansionary conditions.
WALCL (Fed balance sheet) is weekly. fredNMonthsAgo(walclObs, 1) = 1 week
(WoW noise from settlements), not MoM. Use 4 observations for a cleaner
~1-month signal.
VIX term structure condition gated on both vix9d and vix3m, but vix9d was
unused in the actual score. If ^VIX9D failed on Yahoo, the entire term
structure fell back to neutral 50 even when ^VIX3M was available. Now
gates on vix3m only.
|
||
|
|
13549a3907 |
fix(fear-greed): expand sector coverage, remove bogus C:ISSU, fix WALCL cadence (#2237)
* fix(fear-greed): expand sector coverage, remove bogus C:ISSU, fix WALCL cadence - Add 7 missing S&P sectors to YAHOO_SYMBOLS and momentum sectorCloses: XLY, XLP, XLI, XLB, XLU, XLRE, XLC (was only 4: XLK, XLF, XLE, XLV) Bias toward tech+financials was understating sector RSI in defensive rallies - Remove C:ISSU advance/decline proxy — C: prefix is Yahoo crypto pair notation, not a real A/D symbol; silent corruption risk if Yahoo returns a stale price - WALCL momentum: fredNMonthsAgo(walclObs, 1→4) — 1 observation on weekly WALCL data is 1 week (noise), 4 observations = 1 month (meaningful trend) * fix(cmd-k): strip double Panel: prefix from new commands and economic-correlation resolveCommandLabel already prepends 'Panel: ' via i18n. Commands without a matching panels.* i18n key fall back to cmd.label, causing double prefix when cmd.label itself started with 'Panel: '. Affects all 23 new commands added in the previous commit plus the pre-existing panel:economic-correlation. Fix: label field now holds the bare panel name as fallback (no prefix). |
||
|
|
3085e154b1 |
fix(grocery-index): canola oil, tighter caps, outlier gate, sticky col, wow badge (#2234)
* fix(grocery-index): switch oil to canola, tighten caps, fix scroll + wow badge - Change oil item query from "sunflower cooking oil" to "canola oil 1L" and lower ITEM_USD_MAX.oil from 15 to 10 (canola tops ~$7 globally) - Evict all *:oil learned routes on seed startup since product changed - Tighten ITEM_USD_MAX caps: sugar 8->3.5, pasta 4->3.5, milk 8->5, flour 8->4.5, bread 8->6, salt 5->2.5 to prevent organic/bulk mismatches - Add 4x median cross-country outlier gate with Redis learned route eviction (catches France sugar $6.94 which was 4.75x median $1.46) - Strengthen validateFn: reject seed if <5 countries have >=40% coverage - Fix sticky first column in gb-scroll so item names don't scroll under prices - Add missing .gb-wow CSS rule so WoW badges render in total row * fix(grocery-index): one-time oil migration guard, WoW version gate, main.css dedupe P1: Gate oil route eviction behind a Redis migration sentinel (_migration:canola-oil-v1) so it only fires once and learned canola routes persist across subsequent weekly runs. P1: Add BASKET_VERSION=2 constant; suppress WoW when prevSnapshot.basketVersion differs to prevent bogus deltas on the first canola seed run comparing against a sunflower baseline. P2: Update main.css gb-item-col, gb-item-name, and gb-wow rules to match panels.css intent. The more-specific .gb-table selectors and later cascade position caused main.css to override sticky positioning, min-width: 110px, and gb-wow sizing back to old values. |
||
|
|
01f6057389 |
feat(simulation): MiroFish Phase 2 — theater-limited simulation runner (#2220)
* feat(simulation): MiroFish Phase 2 — theater-limited simulation runner Adds the simulation execution layer that consumes simulation-package.json and produces simulation-outcome.json for maritime chokepoint + energy/logistics theaters, closing the WorldMonitor → MiroFish handoff loop. Changes: - scripts/seed-forecasts.mjs: 2-round LLM simulation runner (prompt builders, JSON extractor, runTheaterSimulation, writeSimulationOutcome, task queue with NX dedup lock, runSimulationWorker poll loop) - scripts/process-simulation-tasks.mjs: standalone worker entry point - proto: GetSimulationOutcome RPC + make generate - server/worldmonitor/forecast/v1/get-simulation-outcome.ts: RPC handler - server/gateway.ts: slow tier for get-simulation-outcome - api/health.js: simulationOutcomeLatest in STANDALONE + ON_DEMAND keys - tests: 14 new tests for simulation runner functions * fix(simulation): address P1/P2 code review findings from PR #2220 Security (P1 #018): - sanitizeForPrompt() applied to all entity/seed fields interpolated into Round 1 prompt (entityId, class, stance, seedId, type, timing) - sanitizeForPrompt() applied to actorId and entityIds in Round 2 prompt - sanitizeForPrompt() + length caps applied to all LLM array fields written to R2 (dominantReactions, stabilizers, invalidators, keyActors, timingMarkers) Validation (P1 #019): - Added validateRunId() regex guard - Applied in enqueueSimulationTask() and processNextSimulationTask() loop Type safety (P1 #020): - Added isOutcomePointer() and isPackagePointer() type guards in TS handlers - Replaced unsafe as-casts with runtime-validated guards in both handlers Correctness (P2 #022): - Log warning when pkgPointer.runId does not match task runId Architecture (P2 #024): - isMaritimeChokeEnergyCandidate() accepts both flat and nested topBucketId - Call site simplified to pass theater directly Performance (P2 #025): - SIMULATION_ROUND1_MAX_TOKENS raised 1800 to 2200 - Added max 3 initialReactions instruction to Round 1 prompt Maintainability (P2 #026): - Simulation pointer keys exported from server/_shared/cache-keys.ts - Both TS handlers import from shared location Documentation (P2 #027): - Strengthened runId no-op description in proto and OpenAPI spec * fix(todos): add blank lines around lists in markdown todo files * style(api): reformat openapi yaml to match linter output * test(simulation): add flat-shape filter test + getSimulationOutcome handler coverage Two tests identified as missing during PR #2220 review: 1. isMaritimeChokeEnergyCandidate flat-shape tests — covers the || candidate.topBucketId normalization added in the P1/P2 review pass. The existing tests only used the nested marketContext.topBucketId shape; this adds the flat root-field shape that arrives from the simulation-package.json JSON (selectedTheaters entries have topBucketId at root). 2. getSimulationOutcome handler structural tests — verifies the isOutcomePointer guard, found:false NOT_FOUND return, found:true success path, note population on runId mismatch, and redis_unavailable error string. Follows the readSrc static-analysis pattern used elsewhere in server-handlers.test.mjs (handler imports Redis so full integration test would require a test Redis instance). |
||
|
|
8f27a871f5 | fix(health): bump usniFleet maxStaleMin 480→720 (2× 6h relay interval) (#2235) | ||
|
|
08cdf25865 |
fix(panels): Hormuz — remove summary text, per-chart colors, interactive tooltip (#2231)
* fix(panels): Hormuz — remove summary text, per-chart colors, interactive tooltip
- Remove summary text blob (user-facing text from WTO page)
- Per-chart colors: crude=#e67e22, LNG=#1abc9c, fertilizer=#9b59b6, agriculture=#27ae60
- Zero-value bars in red to signal disruption
- Interactive bar hover: fixed-position tooltip shows date + value + unit
- Event delegation on stable this.element so setContent debounce does not break listeners
* fix(panels): label-based unit detection, deduplicate bar data attrs, clamp tooltip
- Revert unit from index-based (idx===0) to label-based (chart.label.includes('crude_oil'))
so unit stays correct if seed CHART_CONFIGS order ever changes
- Remove redundant data attrs from visible rects; only hit rects need them
- Clamp tooltip top to Math.max(8, ...) to prevent viewport overflow on top charts
|
||
|
|
8465810167 |
fix(health+seed): hormuz — accurate record count and stronger validateFn (#2232)
health.js dataSize() was falling back to Object.keys count for the hormuz data shape (no known array field matched), reporting records=8 (top-level keys) while charts[] was empty. Added 'charts' to the known-fields list. seed-hormuz validateFn now requires at least one chart with actual series data, so a run where Power BI fails (empty charts) will skip publishing and extend the existing TTL instead of overwriting good data with empty charts. |
||
|
|
b785281a11 |
fix(cross-source-signals): use locationName not location object in radiation summary (#2233)
a.location is the proto LatLng object {latitude, longitude}, not a string.
This was producing "Radiation anomaly: [object Object] – 28 reading" in
the Cross-Source Signal Aggregator panel. Use a.locationName (the string
field) with fallbacks to stationName, country, region.
|
||
|
|
e33f856388 |
fix(seed): change hormuz chart window from 24h to 30 days (#2229)
24h cutoff filtered out all Power BI data points (which are daily shipping statistics, always older than 24h), resulting in empty charts. Panel should show 30 days of trend data. |
||
|
|
c6a4d11c64 |
fix(health): bump wildfires maxStaleMin 120→360 for FIRMS NRT midnight reset (#2227)
FIRMS NRT data resets at midnight UTC. All three VIIRS satellite sources (SNPP, NOAA20, NOAA21) return 0 records for 3-6h after the cutover as new passes accumulate. The seed correctly skips writing and extends TTL, but health was falsely alerting STALE_SEED for this expected window. |
||
|
|
80d747730a |
fix(seed): skip writing empty per-key flow entries in afterPublish (#2223)
A second Comtrade seed run (hitting API quota limits) was overwriting previously good per-key Redis entries with empty flows arrays. afterPublish now guards against writing zero-record entries, preserving valid historical data from prior successful runs. |
||
|
|
e964f9d43b |
fix(fear-greed): fix SMA200 null (range=1y) and VIX neutral calibration (#2224)
Two more scoring bugs in addition to the Credit/Liquidity fixes in #2222. SMA200 always null (range=3mo → range=1y): Yahoo was fetched with range=3mo (~63 trading days). SMA200 requires 200 bars so it was ALWAYS null. The trend formula falls back to dist200=0 → score=25 (below-average-MA penalty capped). With 1y data, SPX is still above its 200dMA despite the current correction, so aboveCount rises from 0→1 and dist200 reflects the actual buffer above the long-term trend (score ~47-53 instead of 25). VIX neutral point shifted (range 12–40 → 12–35): Old range put neutral at VIX=26, but the historical long-run average is ~19-20. VIX=26.57 scored 48 (neutral). With the corrected 12–35 range, neutral is at ~23.5 and VIX=26.57 scores ~37 (mild fear) — more consistent with CNN F&G=15, AAII Bear=52%, and the VIX backwardation term structure observed simultaneously. |
||
|
|
537ff8c2c6 |
fix(cross-source-signals): show radar error state when seeder hasn't run or fetch fails (#2221)
- Panel: call showError() with radar UI when evaluatedAt=0 (seeder never ran yet) - Panel: add showFetchError() method for network failure path - data-loader: call showFetchError() on catch instead of silently logging - Seeder: log names of missing Redis source keys (was only showing count) |
||
|
|
b947b25e0a |
fix(fear-greed): recalibrate Credit and Liquidity scoring formulas (#2222)
Three calibration bugs were producing inflated Credit (88) and Liquidity (69) scores even while VIX>26, CNN F&G=15, and AAII Bear=52% signaled market stress. Credit fixes: - HY OAS baseline was 3.0% (near all-time tights), causing scores ≈100 in normal conditions. Recalibrated to range 2.0%–10.0% (long-run avg ~5.0%). New score at 3.19%: 85 vs 96 previously. - IG OAS similarly recalibrated to range 0.4%–3.0% (long-run avg ~1.3%). - Trend now uses fredNTradingDaysAgo(obs, 20) (~1 calendar month for daily series) instead of fredNMonthsAgo(obs, 1) which was stepping back only 1 observation (= yesterday) on daily FRED data — comparing Friday→Monday noise, not a real 1-month trend. Liquidity fix: - M2 YoY multiplier reduced from 10x to 5x. With 10x, perfectly normal 5% annual M2 growth pegged the score at 100 (max greed), masking SOFR's restrictive signal. With 5x, 5% growth ≈ 75 (moderately accommodative). Net effect at current market conditions: - Credit: 88 → 68 (HY spreads still tight, but widening trend now reflected) - Liquidity: 69 → 59 (M2 growth acknowledged but not dominant) |
||
|
|
e548e6cca5 |
feat(intelligence): cross-source signal aggregator with composite escalation (#2143) (#2164)
* feat(intelligence): cross-source signal aggregator with composite escalation (#2143) Adds a threshold-based signal aggregator seeder that reads 15+ already-seeded Redis keys every 15 minutes, ranks cross-domain signals by severity, and detects composite escalation when >=3 signal categories co-fire in the same theater. * fix(cross-source-signals): wire panel data loading, inline styles, seeder cleanup - New src/services/cross-source-signals.ts: fetch via IntelligenceServiceClient with circuit breaker - data-loader.ts: add loadCrossSourceSignals() + startup batch entry (SITE_VARIANT !== 'happy' guard) - App.ts: add primeVisiblePanelData entry + scheduleRefresh at 15min interval - base.ts: add crossSourceSignals: 15 * 60 * 1000 to REFRESH_INTERVALS - CrossSourceSignalsPanel.ts: replace all CSS class usage with inline styles (MarketImplicationsPanel pattern) - seed-cross-source-signals.mjs: remove dead isMain var, fix afterPublish double-write, deterministic signal IDs, GDELT per-topic tone keys (military/nuclear/maritime) with 3-point declining trend + < -1.5 threshold per spec, bundled topics fallback * fix(cross-source-signals): complete bootstrap wiring, seeder fixes, cmd-k entry - cache-keys.ts: add crossSourceSignals to BOOTSTRAP_CACHE_KEYS + BOOTSTRAP_TIERS (slow) - bootstrap.js: add crossSourceSignals key + SLOW_KEYS entry - cross-source-signals.ts: add getHydratedData('crossSourceSignals') bootstrap hydration - seed script: fix isDeclinig typo, maritime theater ternary (Global->Indo-Pacific), displacement year dynamic - commands.ts: add panel:cross-source-signals to cmd-k * feat(cross-source-signals): redesign panel — severity bars, filled badges, icons, theater pills - 4px severity accent bar on all signal rows (scannable without reading badges) - Filled severity badges: CRITICAL=solid red/white, HIGH=faint red bg, MED=faint yellow bg - Type badge emoji prefix: ⚡ composite, 🔴 geo-physical, 📡 EW, ✈️ military, 📊 market, ⚠️ geopolitical - Composite card: full glow (box-shadow) instead of 3px left border only - Theater pill with inline age: "Middle East · 8m ago" - Contributor pills: individual chips instead of dot-separated string - Pulsing dot on composite escalation banner * fix(cross-source-signals): code review fixes — module-level Sets, signal cap, keyframe scoping, OREF expansion - Replace per-call Array literals in list-cross-source-signals.ts with module-level Set constants for O(1) lookups - Add index-based fallback ID in normalizeSignal to avoid undefined ids - Remove unused military:flights:stale:v1 from SOURCE_KEYS - Add MAX_SIGNALS=30 cap before writing to Redis - Expand extractOrefAlertCluster to any "do not travel" advisory (not just Israel) - Add BASE_WEIGHT inline documentation explaining scoring scale - Fix animation keyframe: move from setContent() <style> block to constructor (injected once), rename to cross-source-pulse-dot - Fix GDELT extractor to read per-topic gdelt:intel:tone:{topic} keys with correct decline logic - Fix isDeclinig typo, maritime dead ternary, and displacement year reference |
||
|
|
f87c8c71c4 |
feat(forecast): Phase 2 simulation package read path (#2219)
* feat(forecast): Phase 2 simulation package read path (getSimulationPackage RPC + Redis existence key)
- writeSimulationPackage now writes forecast:simulation-package:latest to Redis after
successful R2 write, containing { runId, pkgKey, schemaVersion, theaterCount, generatedAt }
with TTL matching TRACE_REDIS_TTL_SECONDS (60 days)
- New getSimulationPackage RPC handler reads Redis key, returns pointer metadata without
requiring an R2 fetch (zero R2 cost for existence check)
- Wired into ForecastServiceHandler and server/gateway.ts cache tier (medium)
- Proto: GetSimulationPackage RPC + get_simulation_package.proto message definitions
- api/health.js: simulationPackageLatest added to STANDALONE_KEYS + ON_DEMAND_KEYS
- Tests: SIMULATION_PACKAGE_LATEST_KEY constant + writeSimulationPackage null-guard test
Closes todo #017 (Phase 2 prerequisites for MiroFish integration)
* chore(generated): regenerate proto types for GetSimulationPackage RPC
* fix(simulation-rpc): distinguish Redis failure from not-found; signal runId mismatch
- Add `error` field to GetSimulationPackageResponse: populated with
"redis_unavailable" on Redis errors so callers can distinguish a
healthy not-found (found=false, error="") from a Redis failure
(found=false, error="redis_unavailable"). Adds console.warn on error.
- Add `note` field: populated when req.runId is supplied but does not
match the latest package's runId, signalling that per-run filtering
is not yet active (Phase 3).
- Add proto comment on run_id: "Currently ignored; reserved for Phase 3"
- Add milliseconds annotation to generated_at description.
- Simplify handler: extract NOT_FOUND constant, remove SimulationPackagePointer
interface, remove || '' / || 0 guards on guaranteed-present fields.
- Regenerate all buf-generated files.
Fixes todos #018 (runId silently ignored) and #019 (error indistinguishable
from not-found). Also resolves todos #022 (simplifications) and #023
(OpenAPI required fields / generatedAt unit annotation).
* fix(simulation-rpc): change cache tier from medium to slow (aligns with deep-run update frequency)
* fix(simulation-rpc): fix key prefixing, make Redis errors reachable, no-cache not-found
Three P1 regressions caught in external review:
1. Key prefix bug: getCachedJson() applies preview:<sha>: prefix in non-production
environments, but writeSimulationPackage writes the raw key via a direct Redis
command. In preview/dev the RPC always returned found:false even when the package
existed. Fix: new getRawJson() in redis.ts always uses the unprefixed key AND throws
on failure instead of swallowing errors.
2. redis_unavailable unreachable: getCachedJson swallows fetch failures and missing-
credentials by returning null, so the catch block for redis_unavailable was dead
code. getRawJson() throws on HTTP errors and missing credentials, making the
error: "redis_unavailable" contract actually reachable.
3. Negative-cache stampede: slow tier caches every 200 GET. A request before any deep
run wrote a package returned { found:false } which the CDN cached for up to 1h,
breaking post-run discovery. Fix: markNoCacheResponse() on both not-found and
error paths so they are served fresh on every request.
|
||
|
|
ded27047a6 |
feat(forecast-panel): prediction market layout (Option A) (#2218)
Replace the verbose analysis card layout with a Kalshi/Polymarket-style prediction market UI: - YES/NO outcome pills with large probability percentages - Color-coded: green ≥60%, yellow 40-59%, red <40% - Category tag per domain with domain accent color - Region + trend indicator (↑ ↓ →) on bottom row - 2-col auto-fill grid (min 240px per card), 6px gap, 4px border-radius - Removed: probability bar, time-horizon projections row - Kept: Analysis / Signals collapsible toggles, full detail section |