diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index bb482ea..3362806 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,3 +1,4 @@ +{"_type":"issue","id":"islandflow-9w7","title":"Allow local dev origins on hosted API","description":"Local bun run dev:web and desktop-local point at the hosted API, but browser requests from http://127.0.0.1:3000 are blocked because the API omits CORS headers and returns 404 for OPTIONS preflight. Add API-side CORS handling, validate local web/desktop browser access, and deploy the API fix.","acceptance_criteria":"API responses include Access-Control-Allow-Origin for allowed local/dev origins; OPTIONS preflight succeeds; bun run dev:web reaches hosted REST/WS endpoints from a browser; bun run dev:desktop local mode reaches the backend through the local web UI; tests/build pass; fix is deployed to api.flow.deltaisland.io.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-13T15:04:19Z","created_by":"dirtydishes","updated_at":"2026-06-13T15:29:42Z","started_at":"2026-06-13T15:04:26Z","closed_at":"2026-06-13T15:29:42Z","close_reason":"Hosted API now reflects allowed local dev origins and handles OPTIONS preflight; local web and desktop dev runners both reach https://api.flow.deltaisland.io; API tests, typecheck, and web build passed.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-xkq","title":"Rebuild production dashboard options news around mock9 aesthetic","description":"Reconstruct the production web UI for Dashboard, Options, and News around the mock9 through mock12 dense terminal aesthetic while preserving production data subscriptions, drawers, virtualization, route helpers, redirects, and validation.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-13T14:07:34Z","created_by":"dirtydishes","updated_at":"2026-06-13T14:26:46Z","started_at":"2026-06-13T14:07:53Z","closed_at":"2026-06-13T14:26:46Z","close_reason":"Rebuilt Dashboard, Options, and News around the dense mock9 to mock12 production aesthetic; tests and build passed, and Browser visual inspection was documented as blocked by the unavailable in-app browser backend.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-u45","title":"Patch CVE-related dependency and Docker image findings","description":"Address Forgejo issues #15, #18, and #19 by upgrading the vulnerable tmp dependency resolution and moving Bun Docker images off the vulnerable oven/bun:1.3.11 base image with patched OpenSSL packages during image build.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-12T23:21:29Z","created_by":"dirtydishes","updated_at":"2026-06-12T23:23:27Z","started_at":"2026-06-12T23:22:16Z","closed_at":"2026-06-12T23:23:27Z","close_reason":"Patched Forgejo #15/#18 tmp CVE by resolving tmp@0.2.7, updated Bun Docker images and OpenSSL package upgrade layers for #19, and validated with bun audit, tests, web build, docker workspace check, and replacement image manifest inspection.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-hut","title":"Fix tmp path traversal audit finding","description":"bun audit reports GHSA-ph9p-34f9-6g65 through workspace:@islandflow/desktop via @electron-forge/cli. Update dependency resolution so tmp is at a non-vulnerable version and verify bun audit passes.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-12T22:50:18Z","created_by":"dirtydishes","updated_at":"2026-06-12T22:58:59Z","started_at":"2026-06-12T22:58:33Z","closed_at":"2026-06-12T22:58:59Z","close_reason":"Fixed by bumping the root tmp override to ^0.2.6, refreshing bun.lock to tmp@0.2.7, and validating with bun audit plus bun test. Forgejo issue listing was inaccessible from this environment, so the branch targets the active audit finding visible on current main.","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -30,6 +31,7 @@ {"_type":"issue","id":"islandflow-ayo","title":"Drop stale backlog events from live fanout","description":"Follow-up to live freshness rollout: /ws/live was still fanning out stale backlog events for freshness-gated channels, which kept tape panes in Live feed behind despite active synthetic ingest. Gate fanout and cache ingest by freshness for options/nbbo/equities/flow.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T21:26:39Z","created_by":"dirtydishes","updated_at":"2026-04-28T21:26:44Z","started_at":"2026-04-28T21:26:44Z","closed_at":"2026-04-28T21:26:44Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-0v6","title":"Fix tape freshness, NBBO coverage, pause controls, and filter popup","description":"Implement the tape fixes requested for synthetic options notional sizing, strict live freshness, live-mode pause/resume behavior, stronger NBBO snapshot coverage, and moving flow filters behind a popup. Includes server-side live cache changes, web terminal state/UI changes, and tests for synthetic pricing, live snapshot freshness/NBBO retention, and live pause/filter interactions.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T21:02:52Z","created_by":"dirtydishes","updated_at":"2026-04-28T21:13:38Z","started_at":"2026-04-28T21:02:57Z","closed_at":"2026-04-28T21:13:38Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-e4r","title":"Implement smart-money flow filtering and synthetic firehose modes","description":"Implement the approved multi-surface plan for named synthetic market profiles, options raw-vs-signal filtering, live/API filter contracts, Tape page client-side flow filters, firehose-readiness improvements, tests, and README updates.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T20:10:49Z","created_by":"dirtydishes","updated_at":"2026-04-28T20:29:29Z","started_at":"2026-04-28T20:10:53Z","closed_at":"2026-04-28T20:29:29Z","close_reason":"Implemented synthetic market profiles, options signal-path filtering, signal-aware API/replay contracts, Tape page filters, tests, and README updates. Follow-up tracked in islandflow-biq.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"islandflow-cq6","title":"consolidate deploy script prompts","description":"Add a more robust consolidated deploy script that can prompt for runtime, branch/ref, and deploy pieces while preserving non-interactive CLI usage.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-13T15:12:51Z","created_by":"dirtydishes","updated_at":"2026-06-13T15:28:45Z","started_at":"2026-06-13T15:28:18Z","closed_at":"2026-06-13T15:28:45Z","close_reason":"Implemented guided deploy prompts, named branch deploys, explicit piece selection, docs, validation, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-9gb","title":"Rename news route to Newswire","description":"Follow-up to the mock9 production terminal rebuild: rename the /news route title from Wire Control to Newswire and keep the visual verification/docs aligned with the latest user-facing label.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-13T14:33:30Z","created_by":"dirtydishes","updated_at":"2026-06-13T14:37:01Z","started_at":"2026-06-13T14:33:42Z","closed_at":"2026-06-13T14:37:01Z","close_reason":"Renamed the /news route to Newswire, updated the design record and turn document, decoded common provider HTML entities in news text, and validated with focused web tests, production build, and Helium fitted/narrow inspection.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-iil","title":"Replace overview with dashboard command page","description":"Turn the mock9 Market Command concept into the production root dashboard, rename the visible route from Home to Dashboard, and keep the layout dense with a chart-first command surface.","acceptance_criteria":"Root page displays Dashboard instead of Home; dashboard includes command metrics, chart area, decision levels, priority board, live context, feed health, dark context, and replay context; web tests and production build pass.","notes":"Implemented from the mock9 direction while preserving the existing / URL and using the existing ChartPane until proper chart implementation lands.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-06-13T07:37:56Z","created_by":"dirtydishes","updated_at":"2026-06-13T07:43:44Z","started_at":"2026-06-13T07:38:02Z","closed_at":"2026-06-13T07:43:44Z","close_reason":"dashboard replacement implemented, validated, and documented","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-7l2","title":"Configure local web and desktop to use hosted Islandflow API","description":"Local web development and the Electron desktop shell are not connecting to the VPS-hosted API reliably after a recent endpoint change. Verify the active Delta Island API hostname, update local/default configuration so bun run dev:web and desktop development target it correctly, and validate the relevant web/desktop paths.","status":"closed","priority":2,"issue_type":"bug","owner":"dishes@dpdrm.com","created_at":"2026-06-13T07:32:28Z","created_by":"dirtydishes","updated_at":"2026-06-13T07:38:19Z","closed_at":"2026-06-13T07:38:19Z","close_reason":"Configured local web and desktop development to use https://api.flow.deltaisland.io as the hosted API origin, updated docs and local ignored env, verified the API host from the VPS, passed focused tests, public API route checks, and web build. Dev-web smoke confirmed the corrected API origin but port 3000 was already occupied.","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/.env.example b/.env.example index 0d59497..da449ef 100644 --- a/.env.example +++ b/.env.example @@ -67,6 +67,7 @@ NEXT_PUBLIC_LIVE_HOT_WINDOW=600 NEXT_PUBLIC_LIVE_HOT_WINDOW_OPTIONS=1200 NEXT_PUBLIC_PINNED_EVIDENCE_TTL_MS=1200000 NEXT_PUBLIC_PINNED_EVIDENCE_MAX_ITEMS=4000 +NEXT_ALLOWED_DEV_ORIGINS= ROLLING_WINDOW_SIZE=50 ROLLING_TTL_SEC=86400 CLASSIFIER_SWEEP_MIN_PREMIUM=40000 diff --git a/README.md b/README.md index 227fbbc..969ece4 100644 --- a/README.md +++ b/README.md @@ -412,6 +412,7 @@ Default `smart-money` policy rejects lower-information prints and keeps higher-c | `NEXT_PUBLIC_LIVE_HOT_WINDOW_OPTIONS` | `1200` | Dedicated max hot-window items retained for options prints. | | `NEXT_PUBLIC_NBBO_MAX_AGE_MS` | `1000` | Frontend NBBO staleness threshold. | | `NEXT_PUBLIC_FLOW_FILTER_PRESET` | `smart-money` | Default flow filter preset: `smart-money`, `balanced`, or `all`. | +| `NEXT_ALLOWED_DEV_ORIGINS` | empty, plus auto-detected local IPv4 addresses | Optional comma-separated extra hostnames/IPs allowed to load Next.js dev resources when local browser tooling reaches the dev server through a nonstandard local interface. | ### Replay and testing controls diff --git a/apps/web/app/terminal.tsx b/apps/web/app/terminal.tsx index a61bd29..ad8d046 100644 --- a/apps/web/app/terminal.tsx +++ b/apps/web/app/terminal.tsx @@ -542,6 +542,87 @@ const readErrorDetail = async (response: Response): Promise => { } }; +const OPTION_PRINT_LOOKUP_BATCH_SIZE = 100; +const FLOW_PACKET_LOOKUP_BATCH_SIZE = 12; + +const isAbortLikeError = (error: unknown): boolean => { + return ( + typeof error === "object" && + error !== null && + "name" in error && + (error as { name?: unknown }).name === "AbortError" + ); +}; + +const uniqueNonEmpty = (items: string[]): string[] => { + return Array.from(new Set(items.map((item) => item.trim()).filter(Boolean))); +}; + +const chunkItems = (items: T[], size: number): T[][] => { + const chunks: T[][] = []; + for (let index = 0; index < items.length; index += size) { + chunks.push(items.slice(index, index + size)); + } + return chunks; +}; + +const fetchFlowPacketsByIds = async ( + packetIds: string[], + signal?: AbortSignal +): Promise => { + const packets: FlowPacket[] = []; + for (const batch of chunkItems(uniqueNonEmpty(packetIds), FLOW_PACKET_LOOKUP_BATCH_SIZE)) { + if (signal?.aborted) { + break; + } + const batchPackets = await Promise.all( + batch.map(async (packetId) => { + const response = await fetch(buildApiUrl(`/flow/packets/${encodeURIComponent(packetId)}`), { + signal + }); + if (!response.ok) { + throw new Error(await readErrorDetail(response)); + } + const payload = (await response.json()) as { data?: FlowPacket | null }; + return payload.data ?? null; + }) + ); + for (const packet of batchPackets) { + if (packet) { + packets.push(packet); + } + } + } + return packets; +}; + +const fetchOptionPrintsByTraceIds = async ( + traceIds: string[], + signal?: AbortSignal +): Promise => { + const prints: OptionPrint[] = []; + for (const batch of chunkItems(uniqueNonEmpty(traceIds), OPTION_PRINT_LOOKUP_BATCH_SIZE)) { + if (signal?.aborted) { + break; + } + const url = new URL(buildApiUrl("/option-prints/by-trace")); + for (const traceId of batch) { + url.searchParams.append("trace_id", traceId); + } + const response = await fetch(url.toString(), { signal }); + if (!response.ok) { + throw new Error(await readErrorDetail(response)); + } + const payload = (await response.json()) as { data?: OptionPrint[] }; + for (const item of payload.data ?? []) { + if (item?.trace_id) { + prints.push(item); + } + } + } + return prints; +}; + type WsStatus = "connecting" | "connected" | "disconnected" | "stale"; type TapeMode = "live" | "replay"; @@ -4515,7 +4596,7 @@ const CandleChart = ({ url.searchParams.set("underlying_id", ticker); url.searchParams.set("start_ts", Math.floor(startTs).toString()); url.searchParams.set("end_ts", Math.floor(endTs).toString()); - url.searchParams.set("limit", "2500"); + url.searchParams.set("limit", "1000"); const response = await fetch(url.toString(), { signal: abort.signal }); if (!response.ok) { @@ -6350,8 +6431,10 @@ const useTerminalState = () => { } let cancelled = false; + const abort = new AbortController(); void fetch(buildApiUrl("/lookup/options-support"), { method: "POST", + signal: abort.signal, headers: { "content-type": "application/json" }, body: JSON.stringify({ trace_ids: uniqueTraceIds, @@ -6417,11 +6500,15 @@ const useTerminalState = () => { } }) .catch((error) => { + if (cancelled || abort.signal.aborted || isAbortLikeError(error)) { + return; + } console.warn("Failed to hydrate option row support", error); }); return () => { cancelled = true; + abort.abort(); }; }, [ mode, @@ -6526,35 +6613,26 @@ const useTerminalState = () => { return; } + const abort = new AbortController(); const missingPacketIds = selectedSmartMoneyEvent.packet_ids.filter( (id) => !resolvedFlowPacketMap.has(id) ); if (missingPacketIds.length > 0) { incrementRetentionMetric("pinnedFetchMisses", missingPacketIds.length); - void Promise.all( - missingPacketIds.map(async (packetId) => { - const response = await fetch( - buildApiUrl(`/flow/packets/${encodeURIComponent(packetId)}`) - ); - if (!response.ok) { - throw new Error(await readErrorDetail(response)); - } - const payload = (await response.json()) as { data?: FlowPacket | null }; - return payload.data ?? null; - }) - ) + void fetchFlowPacketsByIds(missingPacketIds, abort.signal) .then((packets) => { const next = new Map(); for (const packet of packets) { - if (packet) { - next.set(packet.id, packet); - } + next.set(packet.id, packet); } if (next.size > 0) { setPinnedFlowPacketMap((prev) => upsertPinnedEntries(prev, next, Date.now())); } }) .catch((error) => { + if (abort.signal.aborted || isAbortLikeError(error)) { + return; + } incrementRetentionMetric("pinnedFetchFailures", 1); console.warn("Failed to fetch smart-money flow packets", error); }); @@ -6563,37 +6641,28 @@ const useTerminalState = () => { const missingPrintIds = selectedSmartMoneyEvent.member_print_ids.filter( (id) => !resolvedOptionPrintMap.has(id) ); - if (missingPrintIds.length === 0) { - return; - } - incrementRetentionMetric("pinnedFetchMisses", missingPrintIds.length); - const url = new URL(buildApiUrl("/option-prints/by-trace")); - for (const traceId of missingPrintIds) { - url.searchParams.append("trace_id", traceId); - } - void fetch(url.toString()) - .then(async (response) => { - if (!response.ok) { - throw new Error(await readErrorDetail(response)); - } - return response.json(); - }) - .then((payload: { data?: OptionPrint[] }) => { - const next = new Map(); - for (const item of payload.data ?? []) { - if (!item || !item.trace_id) { - continue; + if (missingPrintIds.length > 0) { + incrementRetentionMetric("pinnedFetchMisses", missingPrintIds.length); + void fetchOptionPrintsByTraceIds(missingPrintIds, abort.signal) + .then((prints) => { + const next = new Map(); + for (const item of prints) { + next.set(item.trace_id, item); } - next.set(item.trace_id, item); - } - if (next.size > 0) { - setPinnedOptionPrintMap((prev) => upsertPinnedEntries(prev, next, Date.now())); - } - }) - .catch((error) => { - incrementRetentionMetric("pinnedFetchFailures", 1); - console.warn("Failed to fetch smart-money option prints", error); - }); + if (next.size > 0) { + setPinnedOptionPrintMap((prev) => upsertPinnedEntries(prev, next, Date.now())); + } + }) + .catch((error) => { + if (abort.signal.aborted || isAbortLikeError(error)) { + return; + } + incrementRetentionMetric("pinnedFetchFailures", 1); + console.warn("Failed to fetch smart-money option prints", error); + }); + } + + return () => abort.abort(); }, [mode, resolvedFlowPacketMap, resolvedOptionPrintMap, selectedSmartMoneyEvent]); const inferAlertUnderlying = useCallback( @@ -6902,6 +6971,7 @@ const useTerminalState = () => { return; } + const abort = new AbortController(); const visiblePacketIds = visibleAlerts.flatMap((alert) => getAlertFlowPacketRefs(alert)); const missingPacketIds = Array.from(new Set(visiblePacketIds)).filter( (id) => !resolvedFlowPacketMap.has(id) @@ -6909,24 +6979,11 @@ const useTerminalState = () => { if (missingPacketIds.length > 0) { incrementRetentionMetric("pinnedFetchMisses", missingPacketIds.length); - void Promise.all( - missingPacketIds.map(async (packetId) => { - const response = await fetch( - buildApiUrl(`/flow/packets/${encodeURIComponent(packetId)}`) - ); - if (!response.ok) { - throw new Error(await readErrorDetail(response)); - } - const payload = (await response.json()) as { data?: FlowPacket | null }; - return payload.data ?? null; - }) - ) + void fetchFlowPacketsByIds(missingPacketIds, abort.signal) .then((packets) => { const next = new Map(); for (const packet of packets) { - if (packet) { - next.set(packet.id, packet); - } + next.set(packet.id, packet); } if (next.size > 0) { const now = Date.now(); @@ -6934,6 +6991,9 @@ const useTerminalState = () => { } }) .catch((error) => { + if (abort.signal.aborted || isAbortLikeError(error)) { + return; + } incrementRetentionMetric("pinnedFetchFailures", 1); console.warn("Failed to prefetch visible alert packets", error); }); @@ -6942,39 +7002,29 @@ const useTerminalState = () => { const missingPrintIds = Array.from(visibleAlertEvidenceRefs).filter( (id) => !resolvedFlowPacketMap.has(id) && !resolvedOptionPrintMap.has(id) ); - if (missingPrintIds.length === 0) { - return; + if (missingPrintIds.length > 0) { + incrementRetentionMetric("pinnedFetchMisses", missingPrintIds.length); + void fetchOptionPrintsByTraceIds(missingPrintIds, abort.signal) + .then((prints) => { + const next = new Map(); + for (const item of prints) { + next.set(item.trace_id, item); + } + if (next.size > 0) { + const now = Date.now(); + setPinnedOptionPrintMap((prev) => upsertPinnedEntries(prev, next, now)); + } + }) + .catch((error) => { + if (abort.signal.aborted || isAbortLikeError(error)) { + return; + } + incrementRetentionMetric("pinnedFetchFailures", 1); + console.warn("Failed to prefetch visible alert evidence", error); + }); } - incrementRetentionMetric("pinnedFetchMisses", missingPrintIds.length); - const url = new URL(buildApiUrl("/option-prints/by-trace")); - for (const traceId of missingPrintIds) { - url.searchParams.append("trace_id", traceId); - } - void fetch(url.toString()) - .then(async (response) => { - if (!response.ok) { - throw new Error(await readErrorDetail(response)); - } - return response.json(); - }) - .then((payload: { data?: OptionPrint[] }) => { - const next = new Map(); - for (const item of payload.data ?? []) { - if (!item || !item.trace_id) { - continue; - } - next.set(item.trace_id, item); - } - if (next.size > 0) { - const now = Date.now(); - setPinnedOptionPrintMap((prev) => upsertPinnedEntries(prev, next, now)); - } - }) - .catch((error) => { - incrementRetentionMetric("pinnedFetchFailures", 1); - console.warn("Failed to prefetch visible alert evidence", error); - }); + return () => abort.abort(); }, [ mode, visibleAlerts, @@ -7866,10 +7916,11 @@ const OptionsPane = memo(({ state, limit, title = "Options", className }: Option ); return decor ? ( - + ) : (
{cells} diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index ae6d971..a723042 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -1,5 +1,26 @@ +import { networkInterfaces } from "node:os"; import { PHASE_DEVELOPMENT_SERVER } from "next/constants.js"; +const configuredAllowedDevOrigins = () => { + return (process.env.NEXT_ALLOWED_DEV_ORIGINS ?? "") + .split(",") + .map((origin) => origin.trim()) + .filter(Boolean); +}; + +const localIpv4DevOrigins = () => { + return Object.values(networkInterfaces()) + .flat() + .filter((address) => address?.family === "IPv4") + .map((address) => address.address); +}; + +const allowedDevOrigins = () => { + return Array.from( + new Set(["localhost", "127.0.0.1", ...localIpv4DevOrigins(), ...configuredAllowedDevOrigins()]) + ); +}; + /** * Keep dev and production build artifacts separate to avoid chunk/runtime * mismatches when `next dev` and `next build` are run in overlapping sessions. @@ -11,6 +32,7 @@ export default function nextConfig(phase) { const isDev = phase === PHASE_DEVELOPMENT_SERVER; return { + allowedDevOrigins: isDev ? allowedDevOrigins() : undefined, distDir: isDev ? ".next-dev" : ".next" }; } diff --git a/docs/turns/2026-06-13-1130-fix-local-backend-connectivity.html b/docs/turns/2026-06-13-1130-fix-local-backend-connectivity.html new file mode 100644 index 0000000..becf4db --- /dev/null +++ b/docs/turns/2026-06-13-1130-fix-local-backend-connectivity.html @@ -0,0 +1,380 @@ + + + + + + Fix local backend connectivity + + + +
+
+

Islandflow turn record

+

Fix local backend connectivity

+
+ api cors deployed + dev:web verified + dev:desktop verified + native deployment path +
+
+

Summary

Local web and desktop development were failing to reach the hosted Islandflow backend because browser CORS preflight requests were blocked by the native API edge. The API now reflects allowed local origins, answers OPTIONS preflight, and the local web surface connects cleanly to https://api.flow.deltaisland.io.

The terminal UI also now avoids oversized evidence URLs and stale request floods, which were showing up as noisy browser network warnings after the CORS fix landed.

+

Changes Made

API CORS layerAdded reusable CORS helpers, configured allowed origins, wrapped API responses, and handled OPTIONS globally.
Local dev originsNext dev now allows localhost, 127.0.0.1, detected local IPv4 addresses, and optional NEXT_ALLOWED_DEV_ORIGINS.
Terminal fetch stabilityChunked option evidence lookups, bounded flow packet fetch concurrency, and abort stale hydration requests.
Chart overlay capChanged the equity overlay range request from 2500 rows to the API-supported 1000-row maximum.
+

Context

The repo is using native deployment for the hosted API, not Docker compose. I deployed the API CORS fix through the native deploy path and validated the running islandflow-api.service directly after the deploy wrapper returned a nonzero verification-tail exit.

After CORS was fixed, the local browser could connect, but terminal helper fetches still produced warnings from oversized /option-prints/by-trace query strings and fast-changing live windows. Those were separate frontend request-shaping issues, not the main websocket/backend connection.

+

Important Implementation Details

  • API_CORS_ORIGINS defaults include the hosted web origin and local dev origins for ports 3000 and 3100.
  • Preflight responses reflect requested headers and allow GET, POST, PUT, and OPTIONS.
  • Terminal evidence lookups now chunk trace-id batches to avoid edge 414 Request-URI Too Large responses.
  • High-churn live hydration effects now use AbortController cleanup so stale requests do not masquerade as backend failures.
  • Classified option rows now use a focusable row container instead of nesting instrument buttons inside another button.
+

Relevant Diff Snippets

Rendered with @pierre/diffs/ssr from a representative diff covering the API CORS helper, API wiring, Next dev-origin config, and terminal fetch handling.

+

Expected Impact for End-Users

Developers can run bun run dev:web or bun run dev:desktop and see the local terminal connect to the hosted native backend without CORS failures. The live terminal should also stay calmer under evidence-heavy alert windows because it no longer emits oversized by-trace URLs or piles up stale support requests.

+

Validation

  • Ran bun test services/api/tests: 38 tests passed.
  • Ran bun run typecheck: passed across apps, packages, and services.
  • Ran bun --cwd=apps/web run build: passed Next production build.
  • Verified hosted API CORS with curl health, OPTIONS preflight, options REST, and websocket checks from local origins.
  • Verified bun run dev:web in the in-app browser at http://127.0.0.1:3000/: page showed LIVE: CONNECTED and fresh logs stayed clear of backend network warnings.
  • Verified bun run dev:desktop: Electron launched, the runner served the local web UI, and browser verification against its 127.0.0.1:3000 endpoint showed LIVE: CONNECTED.
  • Confirmed no dev server was left listening on port 3000 after validation.
+

Issues, Limitations, and Mitigations

  • The native deploy command returned a nonzero status during its verification tail, but the native user service was active and direct live API checks passed. I did not leave Docker deployment state running.
  • The web build temporarily flipped apps/web/next-env.d.ts from the dev routes file to the production routes file. That generated change was restored and excluded from the final commit.
  • The frontend request chunking fixes are validated locally. I did not deploy the hosted web frontend in this pass because the user-facing breakage was local dev access and the hosted API CORS fix is the deployed native change.
+

Follow-up Work

  • Add a POST batch endpoint for evidence lookups so the terminal never has to encode many trace IDs into a query string.
  • Add a scripted browser smoke test for local dev against https://api.flow.deltaisland.io.
  • Improve the native deploy script verification path so a successful service restart is reported cleanly.
+
+ +