configure hosted api endpoint and refresh local mock wiring #23

Merged
dirtydishes merged 4 commits from lavender/configure-hosted-api-endpoint into main 2026-06-14 19:39:48 +00:00
6 changed files with 552 additions and 95 deletions
Showing only changes of commit f716b8556f - Show all commits

View file

@ -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}

View file

@ -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

View file

@ -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

View file

@ -542,6 +542,87 @@ const readErrorDetail = async (response: Response): Promise<string> => {
}
};
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 = <T,>(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<FlowPacket[]> => {
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<OptionPrint[]> => {
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<string, FlowPacket>();
for (const packet of packets) {
if (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,27 +6641,12 @@ const useTerminalState = () => {
const missingPrintIds = selectedSmartMoneyEvent.member_print_ids.filter(
(id) => !resolvedOptionPrintMap.has(id)
);
if (missingPrintIds.length === 0) {
return;
}
if (missingPrintIds.length > 0) {
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[] }) => {
void fetchOptionPrintsByTraceIds(missingPrintIds, abort.signal)
.then((prints) => {
const next = new Map<string, OptionPrint>();
for (const item of payload.data ?? []) {
if (!item || !item.trace_id) {
continue;
}
for (const item of prints) {
next.set(item.trace_id, item);
}
if (next.size > 0) {
@ -6591,9 +6654,15 @@ const useTerminalState = () => {
}
})
.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,31 +6979,21 @@ 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<string, FlowPacket>();
for (const packet of packets) {
if (packet) {
next.set(packet.id, packet);
}
}
if (next.size > 0) {
const now = Date.now();
setPinnedFlowPacketMap((prev) => upsertPinnedEntries(prev, next, now));
}
})
.catch((error) => {
if (abort.signal.aborted || isAbortLikeError(error)) {
return;
}
incrementRetentionMetric("pinnedFetchFailures", 1);
console.warn("Failed to prefetch visible alert packets", error);
});
@ -6942,28 +7002,12 @@ 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);
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[] }) => {
void fetchOptionPrintsByTraceIds(missingPrintIds, abort.signal)
.then((prints) => {
const next = new Map<string, OptionPrint>();
for (const item of payload.data ?? []) {
if (!item || !item.trace_id) {
continue;
}
for (const item of prints) {
next.set(item.trace_id, item);
}
if (next.size > 0) {
@ -6972,9 +7016,15 @@ const useTerminalState = () => {
}
})
.catch((error) => {
if (abort.signal.aborted || isAbortLikeError(error)) {
return;
}
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 ? (
<button
type="button"
<div
{...commonProps}
key={key}
role="button"
tabIndex={0}
onClick={() =>
decor.smartMoney
? state.openFromSmartMoneyEvent(decor.smartMoney)
@ -7889,7 +7940,7 @@ const OptionsPane = memo(({ state, limit, title = "Options", className }: Option
}}
>
{cells}
</button>
</div>
) : (
<div {...commonProps} key={key}>
{cells}

View file

@ -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"
};
}

File diff suppressed because one or more lines are too long