Compare commits

...

31 commits

Author SHA1 Message Date
a8d183f38e add desktop codex login and analyst copilot 2026-05-20 10:43:27 -04:00
fb25b5ac97 Merge pull request 'fix historical alert flow packet resolution' (#6) from lavender/flow-packet-persistence into main
Some checks are pending
Discord notifications / Push -> Discord (main) (push) Waiting to run
Discord notifications / CI result -> Discord (red on failure) (push) Waiting to run
Discord notifications / Release -> Discord (lavender) (push) Waiting to run
Publish Docs / build (push) Waiting to run
Publish Docs / deploy (push) Blocked by required conditions
Reviewed-on: https://git.deltaisland.io/dirtydishes/islandflow/pulls/6
2026-05-20 07:09:51 +00:00
adba1f6b5a fix historical alert flow packet resolution 2026-05-20 02:59:53 -04:00
be8d841e11 Merge pull request 'alpaca-news' (#5) from alpaca-news into main
Some checks are pending
Discord notifications / Push -> Discord (main) (push) Waiting to run
Discord notifications / CI result -> Discord (red on failure) (push) Waiting to run
Discord notifications / Release -> Discord (lavender) (push) Waiting to run
Publish Docs / build (push) Waiting to run
Publish Docs / deploy (push) Blocked by required conditions
Reviewed-on: https://git.deltaisland.io/dirtydishes/islandflow/pulls/5
2026-05-20 00:09:08 +00:00
3632f36272 document native alpaca news repair 2026-05-19 20:05:37 -04:00
93b9152345 persist news stories and request article content 2026-05-19 20:02:35 -04:00
7d25608b35 fix alpaca news auth and native worker wiring 2026-05-19 19:57:56 -04:00
f9e544c9e2 Merge pull request 'deployment-patch' (#4) from deployment-patch into main
Some checks are pending
Discord notifications / Push -> Discord (main) (push) Waiting to run
Discord notifications / CI result -> Discord (red on failure) (push) Waiting to run
Discord notifications / Release -> Discord (lavender) (push) Waiting to run
Publish Docs / build (push) Waiting to run
Publish Docs / deploy (push) Blocked by required conditions
Reviewed-on: https://git.deltaisland.io/dirtydishes/islandflow/pulls/4
2026-05-19 23:42:37 +00:00
e9739f5dc9 update beads for native deploy ssh fix 2026-05-19 19:40:52 -04:00
e70835e9c4 fix native deploy ssh assumptions 2026-05-19 19:40:20 -04:00
4b8eaae0ee document native options recovery and clean up the unit override 2026-05-19 19:28:33 -04:00
bca74d1811 update beads for codex forgejo status patch 2026-05-19 17:00:15 -04:00
4bacf2c2f8 publish docs index and github pages workflow 2026-05-19 14:59:58 -04:00
276d48950d docs(daily-git): regenerate 2026-05-18 summary after merge 2026-05-19 14:55:38 -04:00
b70b8f0fe7 Merge pull request 'newswire, api, nextjs upgrade to latest' (#3) from nextjs-upgrade into main
Some checks are pending
Discord notifications / Push -> Discord (main) (push) Waiting to run
Discord notifications / CI result -> Discord (red on failure) (push) Waiting to run
Discord notifications / Release -> Discord (lavender) (push) Waiting to run
Reviewed-on: https://git.deltaisland.io/dirtydishes/islandflow/pulls/3
2026-05-19 18:52:36 +00:00
171cf52518 merge main into nextjs upgrade 2026-05-19 14:47:43 -04:00
8d39fb72a4 track pr conflict reconciliation 2026-05-19 14:45:06 -04:00
75ff4f489f docs(daily-git): add 2026-05-18 standup summary 2026-05-19 14:42:56 -04:00
328974b374 update beads for repo doc rules 2026-05-19 08:06:33 -04:00
cb2de93dde clarify repo turn doc rules 2026-05-19 08:06:10 -04:00
a790a2815c clarify repo turn documentation scope 2026-05-19 08:05:30 -04:00
82fd29f1a4 update readme for current project state 2026-05-19 07:40:18 -04:00
b6fa2f0d17 upgrade web to nextjs 16 2026-05-19 07:31:41 -04:00
728ca5569d update beads 2026-05-19 07:12:06 -04:00
8173b05c1c upgrade next.js to 16.2.6 and react 19 2026-05-19 07:05:55 -04:00
04baecebe0 update turn docs and beads workflow 2026-05-18 21:32:44 -04:00
8f0794ddf8 Merge pull request 'Native public edge cutover with Docker rollback path' (#2) from native-deploy into main
Some checks are pending
Discord notifications / Push -> Discord (main) (push) Waiting to run
Discord notifications / CI result -> Discord (red on failure) (push) Waiting to run
Discord notifications / Release -> Discord (lavender) (push) Waiting to run
Reviewed-on: https://git.deltaisland.io/dirtydishes/islandflow/pulls/2
2026-05-19 00:09:07 +00:00
bdb9d9a95a Implement native public edge cutover 2026-05-18 19:55:27 -04:00
906fe411c9 add alpaca news wire across ingest api and web 2026-05-18 16:55:31 -04:00
62aae70878 docs(general): add 2026-05-17 standup summary 2026-05-18 09:05:40 -04:00
d589858c03 Implement native fast iterative deploy workflow
Some checks are pending
Discord notifications / Push -> Discord (main) (push) Waiting to run
Discord notifications / CI result -> Discord (red on failure) (push) Waiting to run
Discord notifications / Release -> Discord (lavender) (push) Waiting to run
2026-05-18 03:36:01 -04:00
108 changed files with 12816 additions and 616 deletions

View file

@ -1,3 +1,6 @@
{"_type":"issue","id":"islandflow-yza","title":"Persist historical flow packets for alert detail replay","description":"## Why\nAlert details can show a missing persisted flow packet when the packet is no longer present in the Redis hot cache, even though the associated historical alert and evidence were loaded from ClickHouse.\n\n## What needs to be done\nTrace the API path that resolves alert detail flow packets, compare Redis hot-cache lookups with ClickHouse historical fetches, and ensure historical flow packet payloads are treated as first-class persisted data with context preserved when replaying or loading older alerts.\n\n## Acceptance Criteria\n- Alert detail flow packets load for historical alerts even when the packet is absent from Redis hot cache\n- Historical ClickHouse-backed flow packet responses preserve the context required by the UI\n- Relevant automated tests cover the regression or the gap is explicitly documented","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-20T06:52:04Z","created_by":"dirtydishes","updated_at":"2026-05-20T06:59:26Z","started_at":"2026-05-20T06:52:09Z","closed_at":"2026-05-20T06:59:26Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-jor","title":"Support Forgejo pull request status in desktop git panel","description":"The desktop app currently reports pull request status unavailable when a repository only has a Forgejo remote. Add native Forgejo/Gitea-style remote detection and pull request status lookup so Forgejo-only repositories can show PR state in the Codex app git panel.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T20:55:15Z","created_by":"dirtydishes","updated_at":"2026-05-19T20:59:46Z","started_at":"2026-05-19T20:55:25Z","closed_at":"2026-05-19T20:59:46Z","close_reason":"Patched the installed Codex desktop app bundle with a Forgejo PR status fallback and documented the local change.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-g3a","title":"Reconcile PR merge conflicts","description":"Resolve the current pull request conflicts for the nextjs-upgrade branch, validate the result, document the turn, and push the reconciled branch.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T18:44:51Z","created_by":"dirtydishes","updated_at":"2026-05-19T18:47:35Z","started_at":"2026-05-19T18:44:56Z","closed_at":"2026-05-19T18:47:35Z","close_reason":"Merged forgejo/main into nextjs-upgrade, resolved README and Beads conflicts, updated JetStream retention tests, validated deploy help, Docker workspace sync, API/bus tests, and web build, and added turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-jbi","title":"Hydrate alert evidence details from ClickHouse","description":"Alert detail drawers need to fetch persisted alert context from ClickHouse by trace id, including linked flow packets, option prints, preserved execution context, and explicit missing refs for UI diagnostics.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-17T14:55:43Z","created_by":"dirtydishes","updated_at":"2026-05-17T15:01:58Z","started_at":"2026-05-17T14:55:53Z","closed_at":"2026-05-17T15:01:58Z","close_reason":"Implemented ClickHouse-backed alert context hydration across storage, API, terminal drawer, tests, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-jbi","title":"Hydrate alert evidence details from ClickHouse","description":"Alert detail drawers need to fetch persisted alert context from ClickHouse by trace id, including linked flow packets, option prints, preserved execution context, and explicit missing refs for UI diagnostics.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-17T14:55:43Z","created_by":"dirtydishes","updated_at":"2026-05-17T15:01:58Z","started_at":"2026-05-17T14:55:53Z","closed_at":"2026-05-17T15:01:58Z","close_reason":"Implemented ClickHouse-backed alert context hydration across storage, API, terminal drawer, tests, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-8kj","title":"Configure persistent beads Dolt remote on deltaisland server","description":"Install the beads and Dolt CLIs on the server, configure a persistent Dolt sync remote backed by the server-hosted Forgejo repository, verify refs/dolt/data publication, and document Nginx Proxy Manager / firewall considerations.","status":"closed","priority":1,"issue_type":"task","assignee":"delta","created_at":"2026-05-17T10:31:31Z","created_by":"delta","updated_at":"2026-05-17T10:37:47Z","started_at":"2026-05-17T10:32:16Z","closed_at":"2026-05-17T10:37:47Z","close_reason":"Installed bd and dolt on the server, configured the Forgejo-backed Dolt remote, published refs/dolt/data, and documented the setup.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-8kj","title":"Configure persistent beads Dolt remote on deltaisland server","description":"Install the beads and Dolt CLIs on the server, configure a persistent Dolt sync remote backed by the server-hosted Forgejo repository, verify refs/dolt/data publication, and document Nginx Proxy Manager / firewall considerations.","status":"closed","priority":1,"issue_type":"task","assignee":"delta","created_at":"2026-05-17T10:31:31Z","created_by":"delta","updated_at":"2026-05-17T10:37:47Z","started_at":"2026-05-17T10:32:16Z","closed_at":"2026-05-17T10:37:47Z","close_reason":"Installed bd and dolt on the server, configured the Forgejo-backed Dolt remote, published refs/dolt/data, and documented the setup.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-200","title":"Implement durable options tape history","description":"Implement the plan from docs/plans/2026-05-16-1711-durable-options-tape-history.html: durable ClickHouse-backed options history, signal/all prints view selection, preserved execution context, stale semantics limited to live health, reset runbook, tests, and turn documentation.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-16T21:21:30Z","created_by":"dirtydishes","updated_at":"2026-05-16T21:26:51Z","started_at":"2026-05-16T21:21:33Z","closed_at":"2026-05-16T21:26:51Z","close_reason":"Implemented durable options tape history, signal/raw view selection, reset runbook, tests, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-200","title":"Implement durable options tape history","description":"Implement the plan from docs/plans/2026-05-16-1711-durable-options-tape-history.html: durable ClickHouse-backed options history, signal/all prints view selection, preserved execution context, stale semantics limited to live health, reset runbook, tests, and turn documentation.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-16T21:21:30Z","created_by":"dirtydishes","updated_at":"2026-05-16T21:26:51Z","started_at":"2026-05-16T21:21:33Z","closed_at":"2026-05-16T21:26:51Z","close_reason":"Implemented durable options tape history, signal/raw view selection, reset runbook, tests, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0}
@ -13,6 +16,19 @@
{"_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-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-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-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-6tn","title":"Add Codex desktop login and usage bridge","description":"Implement a desktop-only Codex integration for the Islandflow Electron app using the official codex app-server with managed ChatGPT login, native IPC, settings UI, usage tracking, and clean web degradation.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-20T14:01:36Z","created_by":"dirtydishes","updated_at":"2026-05-20T14:40:49Z","started_at":"2026-05-20T14:01:48Z","closed_at":"2026-05-20T14:40:49Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-laq","title":"fix native alpaca news deploy and auth","description":"Why this issue exists and what needs to be done:\\n\\nNative Islandflow rollout is incomplete because services/ingest-news is not healthy on the VPS. The checked-in native user units and helper scripts do not fully include ingest-news, and the current service uses bearer-style auth that returns 401 against Alpaca news endpoints.\\n\\nThis task should verify the current Alpaca news auth requirements against official docs, update the repo code and native deployment assets as needed, install and enable the missing VPS unit, verify news events flow end-to-end, and document the work.","status":"closed","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T23:47:07Z","created_by":"dirtydishes","updated_at":"2026-05-20T00:05:20Z","started_at":"2026-05-19T23:47:12Z","closed_at":"2026-05-20T00:05:20Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-fmg","title":"Fix native deploy SSH path and verification cwd assumptions","description":"Native deploys over SSH assumed bun was already on PATH and that remote verification would run from the repository root. On the live VPS, non-login SSH shells omitted /home/delta/.bun/bin and remote native verification could not find deployment/native/check-native-infra.sh because it ran from the home directory. Update the deploy helper to prepend /Users/kell/.bun/bin when present and cd into the repo before native verification checks run.","status":"closed","priority":2,"issue_type":"bug","owner":"dishes@dpdrm.com","created_at":"2026-05-19T23:38:32Z","created_by":"dirtydishes","updated_at":"2026-05-19T23:40:33Z","closed_at":"2026-05-19T23:40:33Z","close_reason":"Updated native SSH deploy flow to prepend Bun's home install path when present and run native verification from the repo root before health scripts.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-wf5","title":"Harden native options provider configuration after synthetic recovery","description":"Native production recovery restored OPTIONS_INGEST_ADAPTER=synthetic because the current Alpaca setup fails authentication and crash-loops ingest-options. Follow up by deciding whether production options should remain synthetic or move to a supported live provider auth path, then add a deploy-time smoke test or config validation that catches provider auth failures before native cutover.","status":"open","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-19T23:27:51Z","created_by":"dirtydishes","updated_at":"2026-05-19T23:27:51Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-m83","title":"Restore options ingestion and print generation on native deployment","description":"After moving the production/VPS deployment from Docker-managed services to the native runtime, the options feed appears behind and fresh option prints are not reaching the UI. Investigate the native deployment path on the server, identify the ingestion or compute breakage, apply the required code and/or host configuration changes, validate that fresh option prints resume, and document any follow-up operational work.","status":"closed","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T23:20:01Z","created_by":"dirtydishes","updated_at":"2026-05-19T23:27:52Z","started_at":"2026-05-19T23:20:10Z","closed_at":"2026-05-19T23:27:52Z","close_reason":"Restored native options ingest by switching the VPS back to the last known-good synthetic adapter, verified fresh option prints and compute output, and documented the native env precedence gotcha.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-o1v","title":"Add SCM provider layer with Forgejo detection","description":"Implement provider-aware source-control detection and mirror-aware guardrails for repo automation so Forgejo remotes are treated as authoritative when present.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T23:04:33Z","created_by":"dirtydishes","updated_at":"2026-05-19T23:06:55Z","started_at":"2026-05-19T23:04:35Z","closed_at":"2026-05-19T23:06:55Z","close_reason":"created by mistake during interrupted turn; no implementation was started","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-tqk","title":"publish docs/ to github pages with navigable index","description":"Set up docs deployment so repository docs are published to dirtydishes.github.io/islandflow/docs with a nicer, browsable experience than a raw file listing.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T18:56:02Z","created_by":"dirtydishes","updated_at":"2026-05-19T18:59:55Z","started_at":"2026-05-19T18:56:04Z","closed_at":"2026-05-19T18:59:55Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-lm6","title":"Clarify repo turn documentation scope","description":"Update AGENTS.md so repository turn documentation clearly uses repo-local docs/turns and impeccable styling, without inheriting global non-repo computer-task styling.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T12:05:07Z","created_by":"dirtydishes","updated_at":"2026-05-19T12:06:12Z","started_at":"2026-05-19T12:05:14Z","closed_at":"2026-05-19T12:06:12Z","close_reason":"Verified AGENTS.md now scopes repo turn docs to docs/turns and makes impeccable the styling authority; added turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-6iq","title":"Update README for current project state","description":"Resolve README merge conflicts and document the current project state, including the smart money classification taxonomy, Next.js update, and deployment workflow changes.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T11:37:24Z","created_by":"dirtydishes","updated_at":"2026-05-19T11:40:01Z","started_at":"2026-05-19T11:37:31Z","closed_at":"2026-05-19T11:40:01Z","close_reason":"README conflict resolved and current project state documented, including smart-money taxonomy, Next.js update, and deployment workflow.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-lib","title":"Upgrade apps/web to Next.js 16.2.6","description":"Upgrade the web app dependency stack to Next.js 16.2.6 with React 19, refresh Bun and mirrored Docker workspace lockfiles, keep runtime behavior unchanged, fix any focused web test fallout, validate the web build and targeted route tests, and document the completed work.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T11:04:51Z","created_by":"dirtydishes","updated_at":"2026-05-19T11:31:23Z","started_at":"2026-05-19T11:04:57Z","closed_at":"2026-05-19T11:31:23Z","close_reason":"Upgraded apps/web to Next.js 16.2.6 with React 19, refreshed Bun lockfiles including the Docker workspace mirror, fixed the React 19 nullable ref type issue, and validated the web build, focused tests, Docker workspace sync, and route smoke checks.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-8fn","title":"implement alpaca-backed news wire view","description":"Why this issue exists and what needs to be done:\\nAdd an Alpaca-powered live news pipeline, API, storage, and web experience, including a dedicated /news route, Home preview, live fanout, history pagination, ticker resolution, and replay-mode live-only empty states.\\n\\nAcceptance criteria:\\n- normalized NewsStory contract and live channel exist\\n- ingest-news service backfills and streams Alpaca news\\n- API persists, serves, and fans out news\\n- web app exposes /news plus Home preview and drawer\\n- tests cover types, storage, API, and key UI behaviors\\n- turn documentation is added\\n\\nDesign:\\nReuse Islandflow drawer, chips, panes, and terminal styling; keep news live-only in v1 replay mode.\\n\\nNotes:\\nImplement client-side ticker filtering in v1 and expose latest revision only per provider+story_id.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T20:37:13Z","created_by":"dirtydishes","updated_at":"2026-05-18T20:55:11Z","started_at":"2026-05-18T20:37:20Z","closed_at":"2026-05-18T20:55:11Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-k8i","title":"Fix duplicate alert context import in API entrypoint","description":"Recent alert-context work introduced a duplicate fetchAlertContextByTraceId import in services/api/src/index.ts, which risks breaking TypeScript compilation and API startup. Remove the duplicate import and validate the affected API/web tests.","status":"closed","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T13:01:58Z","created_by":"dirtydishes","updated_at":"2026-05-18T13:03:40Z","started_at":"2026-05-18T13:02:02Z","closed_at":"2026-05-18T13:03:40Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-lk9","title":"Fix PR creation workflow after Forgejo migration","description":"## Why\\nCreating pull requests with fails after the repository moved primary collaboration from GitHub to Forgejo. The current workflow still assumes GitHub GraphQL PR creation semantics, which do not work against the Forgejo remote.\\n\\n## What\\nInvestigate the current PR creation path, identify remaining GitHub-specific assumptions, and update the repo workflow/scripts/docs so contributors can reliably publish branches and open PRs in the Forgejo-based setup.\\n\\n## Acceptance Criteria\\n- The repo no longer instructs contributors to use a broken GitHub-specific PR creation path for Forgejo branches\\n- There is a documented and preferably scripted way to create the equivalent review request against Forgejo\\n- Validation demonstrates the new workflow behaves correctly or clearly documents any remaining platform limitation","status":"in_progress","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T10:26:47Z","created_by":"dirtydishes","updated_at":"2026-05-18T10:26:53Z","started_at":"2026-05-18T10:26:53Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-1ei","title":"Make deploy helper remote-aware for Forgejo","description":"Why: scripts/deploy.ts hardcodes git remote name origin for fetch/pull/push and branch verification, but this repository now uses forgejo/github remotes and may not have an origin remote. What: update deploy.ts to resolve the deploy git remote robustly (Forgejo-aware), use it across local prechecks, branch publish, and remote rollout git operations, and keep behavior explicit in output.","status":"closed","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T03:20:12Z","created_by":"dirtydishes","updated_at":"2026-05-18T03:22:39Z","started_at":"2026-05-18T03:20:16Z","closed_at":"2026-05-18T03:22:39Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-1ei","title":"Make deploy helper remote-aware for Forgejo","description":"Why: scripts/deploy.ts hardcodes git remote name origin for fetch/pull/push and branch verification, but this repository now uses forgejo/github remotes and may not have an origin remote. What: update deploy.ts to resolve the deploy git remote robustly (Forgejo-aware), use it across local prechecks, branch publish, and remote rollout git operations, and keep behavior explicit in output.","status":"closed","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T03:20:12Z","created_by":"dirtydishes","updated_at":"2026-05-18T03:22:39Z","started_at":"2026-05-18T03:20:16Z","closed_at":"2026-05-18T03:22:39Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-xod","title":"Add --fast mode to deploy helper","description":"Why: full main deploys rebuild all images and run full verification, which is slow for routine rollouts. What: add a --fast flag to scripts/deploy.ts with explicit behavior that short-circuits slow steps while preserving basic safety checks; update help text/docs for discoverability.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T02:50:47Z","created_by":"dirtydishes","updated_at":"2026-05-18T02:53:41Z","started_at":"2026-05-18T02:50:50Z","closed_at":"2026-05-18T02:53:41Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-xod","title":"Add --fast mode to deploy helper","description":"Why: full main deploys rebuild all images and run full verification, which is slow for routine rollouts. What: add a --fast flag to scripts/deploy.ts with explicit behavior that short-circuits slow steps while preserving basic safety checks; update help text/docs for discoverability.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T02:50:47Z","created_by":"dirtydishes","updated_at":"2026-05-18T02:53:41Z","started_at":"2026-05-18T02:50:50Z","closed_at":"2026-05-18T02:53:41Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-cif","title":"hydrate alert evidence context from clickhouse","description":"Implement alert detail hydration from ClickHouse with a new context endpoint and frontend drawer evidence resolution. Includes storage lookup by alert trace_id/evidence refs, unresolved refs diagnostics, API route GET /flow/alerts/:trace_id/context, terminal evidence hydration + loading states/copy updates, and tests across storage/api/web.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T00:15:55Z","created_by":"dirtydishes","updated_at":"2026-05-18T00:17:38Z","started_at":"2026-05-18T00:16:00Z","closed_at":"2026-05-18T00:17:38Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-cif","title":"hydrate alert evidence context from clickhouse","description":"Implement alert detail hydration from ClickHouse with a new context endpoint and frontend drawer evidence resolution. Includes storage lookup by alert trace_id/evidence refs, unresolved refs diagnostics, API route GET /flow/alerts/:trace_id/context, terminal evidence hydration + loading states/copy updates, and tests across storage/api/web.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T00:15:55Z","created_by":"dirtydishes","updated_at":"2026-05-18T00:17:38Z","started_at":"2026-05-18T00:16:00Z","closed_at":"2026-05-18T00:17:38Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
@ -46,6 +62,10 @@
{"_type":"issue","id":"islandflow-zs0","title":"Migrate terminal UI to smart-money profiles","description":"Migrate apps/web terminal rendering to consume SmartMoneyEvent directly: primary profile, probability ladder, reason codes, and suppression/abstention state, while preserving legacy alert/classifier displays during the bridge.","status":"closed","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-04T21:35:23Z","created_by":"dirtydishes","updated_at":"2026-05-05T05:39:58Z","closed_at":"2026-05-05T05:39:58Z","close_reason":"Completed terminal smart-money profile migration","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-zs0","title":"Migrate terminal UI to smart-money profiles","description":"Migrate apps/web terminal rendering to consume SmartMoneyEvent directly: primary profile, probability ladder, reason codes, and suppression/abstention state, while preserving legacy alert/classifier displays during the bridge.","status":"closed","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-04T21:35:23Z","created_by":"dirtydishes","updated_at":"2026-05-05T05:39:58Z","closed_at":"2026-05-05T05:39:58Z","close_reason":"Completed terminal smart-money profile migration","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-igk","title":"Add plan mode","description":"Implement a user-facing plan mode in the application so users can switch into planning before taking action. Scope to be clarified from existing app patterns.","status":"closed","priority":2,"issue_type":"feature","owner":"dishes@dpdrm.com","created_at":"2026-05-04T04:22:37Z","created_by":"dirtydishes","updated_at":"2026-05-04T04:26:18Z","started_at":"2026-05-04T04:22:40Z","closed_at":"2026-05-04T04:26:18Z","close_reason":"Implemented as a global pi extension toggled with Shift+P","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-igk","title":"Add plan mode","description":"Implement a user-facing plan mode in the application so users can switch into planning before taking action. Scope to be clarified from existing app patterns.","status":"closed","priority":2,"issue_type":"feature","owner":"dishes@dpdrm.com","created_at":"2026-05-04T04:22:37Z","created_by":"dirtydishes","updated_at":"2026-05-04T04:26:18Z","started_at":"2026-05-04T04:22:40Z","closed_at":"2026-05-04T04:26:18Z","close_reason":"Implemented as a global pi extension toggled with Shift+P","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-biq","title":"Finish raw live options delivery and filter/backpressure observability","description":"The smart-money signal path and Tape filters are in place, but the next firehose pass should finish server-side selective raw live delivery for options subscriptions and add explicit filtered-out/backpressure observability for API/web counters. This was discovered while landing islandflow-e4r.\n","status":"in_progress","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T20:28:58Z","created_by":"dirtydishes","updated_at":"2026-04-29T03:54:12Z","started_at":"2026-04-29T03:54:12Z","dependencies":[{"issue_id":"islandflow-biq","depends_on_id":"islandflow-e4r","type":"discovered-from","created_at":"2026-04-28T16:28:58Z","created_by":"auto-import","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-biq","title":"Finish raw live options delivery and filter/backpressure observability","description":"The smart-money signal path and Tape filters are in place, but the next firehose pass should finish server-side selective raw live delivery for options subscriptions and add explicit filtered-out/backpressure observability for API/web counters. This was discovered while landing islandflow-e4r.\n","status":"in_progress","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T20:28:58Z","created_by":"dirtydishes","updated_at":"2026-04-29T03:54:12Z","started_at":"2026-04-29T03:54:12Z","dependencies":[{"issue_id":"islandflow-biq","depends_on_id":"islandflow-e4r","type":"discovered-from","created_at":"2026-04-28T16:28:58Z","created_by":"auto-import","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-8vr","title":"Summarize 2026-05-19 git activity for standup","description":"Create the daily git summary for 2026-05-19 in docs/general using yesterday's commits, touched files, and validation evidence only.","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-20T13:02:41Z","created_by":"dirtydishes","updated_at":"2026-05-20T13:04:50Z","started_at":"2026-05-20T13:02:47Z","closed_at":"2026-05-20T13:04:50Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-0ty","title":"Recreate May 18 standup summary after merge","description":"Regenerate docs/daily-git/2026-05-19-standup-summary-2026-05-18.html using merged history so it reflects all commits in the May 18 window, including native deployment and merge commits.","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T18:53:48Z","created_by":"dirtydishes","updated_at":"2026-05-19T18:55:33Z","started_at":"2026-05-19T18:53:52Z","closed_at":"2026-05-19T18:55:33Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-2df","title":"Publish 2026-05-18 git standup summary","description":"Why: the daily automation needs a grounded standup summary for May 18, 2026. What: review commits from 2026-05-18, create a scannable HTML summary in docs/daily-git, and capture only commit/file-backed statements.","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T18:41:07Z","created_by":"dirtydishes","updated_at":"2026-05-19T18:42:42Z","started_at":"2026-05-19T18:41:10Z","closed_at":"2026-05-19T18:42:42Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-x70","title":"Create 2026-05-17 git standup summary","description":"Why this issue exists and what needs to be done:\\n- Produce the daily automation summary for 2026-05-17 git activity.\\n- Ground statements in commits, PRs, and touched files only.\\n- Create a user-readable HTML document in docs/general and update automation memory.\\n- Complete the Beads sync and git push workflow after documenting the run.","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T13:01:43Z","created_by":"dirtydishes","updated_at":"2026-05-18T13:05:37Z","started_at":"2026-05-18T13:01:53Z","closed_at":"2026-05-18T13:05:37Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-zsy","title":"Expose Forgejo SSH on a direct DNS hostname","description":"git.deltaisland.io currently resolves through Cloudflare's proxy, so SSH on port 2222 does not complete even though the Forgejo container is listening on the host. If SSH-based git/beads workflows are desired, add a DNS-only hostname (or adjust the existing record) that points directly at the server for Forgejo SSH.","status":"open","priority":3,"issue_type":"task","created_at":"2026-05-17T10:34:06Z","created_by":"delta","updated_at":"2026-05-17T10:34:06Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-zsy","title":"Expose Forgejo SSH on a direct DNS hostname","description":"git.deltaisland.io currently resolves through Cloudflare's proxy, so SSH on port 2222 does not complete even though the Forgejo container is listening on the host. If SSH-based git/beads workflows are desired, add a DNS-only hostname (or adjust the existing record) that points directly at the server for Forgejo SSH.","status":"open","priority":3,"issue_type":"task","created_at":"2026-05-17T10:34:06Z","created_by":"delta","updated_at":"2026-05-17T10:34:06Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-38p","title":"Add native deployment unit templates and rollback helpers","description":"The deploy helper now supports --runtime native, but the repo still relies on operator-managed systemd units and manual rollback. Add checked-in native deployment templates or provisioning guidance for the expected units, and consider lightweight rollback/smoke-test helpers once the host-native path is exercised on the real VPS.","status":"open","priority":3,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-15T23:46:42Z","created_by":"dirtydishes","updated_at":"2026-05-15T23:46:42Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-38p","title":"Add native deployment unit templates and rollback helpers","description":"The deploy helper now supports --runtime native, but the repo still relies on operator-managed systemd units and manual rollback. Add checked-in native deployment templates or provisioning guidance for the expected units, and consider lightweight rollback/smoke-test helpers once the host-native path is exercised on the real VPS.","status":"open","priority":3,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-15T23:46:42Z","created_by":"dirtydishes","updated_at":"2026-05-15T23:46:42Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-575","title":"Document smart-money event calendar env","description":"Document smart-money event-calendar environment configuration in env examples and README.\n","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-05T06:57:14Z","created_by":"dirtydishes","updated_at":"2026-05-05T06:57:57Z","started_at":"2026-05-05T06:57:17Z","closed_at":"2026-05-05T06:57:57Z","close_reason":"Documented event-calendar env variables","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-575","title":"Document smart-money event calendar env","description":"Document smart-money event-calendar environment configuration in env examples and README.\n","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-05T06:57:14Z","created_by":"dirtydishes","updated_at":"2026-05-05T06:57:57Z","started_at":"2026-05-05T06:57:17Z","closed_at":"2026-05-05T06:57:57Z","close_reason":"Documented event-calendar env variables","dependency_count":0,"dependent_count":0,"comment_count":0}

View file

@ -6,6 +6,10 @@ REDIS_URL=redis://127.0.0.1:6379
# Options ingest # Options ingest
OPTIONS_INGEST_ADAPTER=synthetic OPTIONS_INGEST_ADAPTER=synthetic
ALPACA_API_KEY= ALPACA_API_KEY=
ALPACA_API_KEY_ID=
ALPACA_KEY_ID=
ALPACA_API_SECRET_KEY=
ALPACA_SECRET_KEY=
ALPACA_REST_URL=https://data.alpaca.markets ALPACA_REST_URL=https://data.alpaca.markets
ALPACA_WS_BASE_URL=wss://stream.data.alpaca.markets/v1beta1 ALPACA_WS_BASE_URL=wss://stream.data.alpaca.markets/v1beta1
ALPACA_FEED=indicative ALPACA_FEED=indicative

56
.github/workflows/docs-pages.yml vendored Normal file
View file

@ -0,0 +1,56 @@
name: Publish Docs
on:
push:
branches:
- main
paths:
- "docs/**"
- "scripts/generate-docs-index.mjs"
- ".github/workflows/docs-pages.yml"
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure Pages
uses: actions/configure-pages@v5
- name: Build docs index
run: node scripts/generate-docs-index.mjs
- name: Prepare static site payload
run: |
mkdir -p site/docs
cp -R docs/. site/docs/
printf '%s\n' '<!doctype html><meta charset="utf-8"><meta http-equiv="refresh" content="0; url=./docs/"><title>Islandflow Docs</title><a href="./docs/">Continue to docs</a>' > site/index.html
touch site/.nojekyll
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: site
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

174
AGENTS server.md Normal file
View file

@ -0,0 +1,174 @@
<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:ca08a54f -->
## Beads Issue Tracker
This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands.
### Quick Reference
```bash
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --claim # Claim work
bd close <id> # Complete work
```
### Rules
- Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists
- Run `bd prime` for detailed command reference and session close protocol
- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files
## Session Completion
**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds.
**MANDATORY WORKFLOW:**
1. **File issues for remaining work** - Create issues for anything that needs follow-up
2. **Run quality gates** (if code changed) - Tests, linters, builds
3. **Update issue status** - Close finished work, update in-progress items
4. **PUSH TO REMOTE** - This is MANDATORY:
```bash
git pull --rebase
bd dolt push
git push
git status # MUST show "up to date with origin"
```
5. **Clean up** - Clear stashes, prune remote branches
6. **Verify** - All changes committed AND pushed
7. **Hand off** - Provide context for next session
**CRITICAL RULES:**
- Work is NOT complete until `git push` succeeds
- NEVER stop before pushing - that leaves work stranded locally
- NEVER say "ready to push when you are" - YOU must push
- If push fails, resolve and retry until it succeeds
<!-- END BEADS INTEGRATION -->
## Minimal Repo Operating Instructions
This is a Bun + TypeScript monorepo for an event-sourced market-data pipeline:
- Flow: ingest services publish to NATS/JetStream, compute/candles derive events, API serves REST/WS, web consumes live/replay streams.
- Main folders: `services/*` (runtime services), `packages/*` (shared libs/types/storage), `apps/web` (Next.js UI).
- Infra dependency: local dev assumes Docker services (NATS, ClickHouse, Redis) are available.
Use these repo-specific commands:
- Install deps: `bun install`
- Start full stack: `bun run dev`
- Start infra only: `bun run dev:infra`
- Start backend services only: `bun run dev:services`
- Start web only: `bun run dev:web`
Testing and validation in this repo are Bun-first:
- Run tests: `bun test`
- Run scoped tests: `bun test services/compute/tests` (or another package/service path)
- Validate web production build when UI code changes: `bun --cwd=apps/web run build`
Working style that avoids common problems here:
- Prefer editing in the touched workspace (`services/<name>`, `packages/<name>`, `apps/web`) and keep shared contract changes in `packages/types`.
- Keep `.env` aligned with `.env.example`; adapters default to synthetic modes for local development.
- Dev runners persist child PID state in `.tmp/`; if a previous run crashed, restart via the standard `bun run dev*` commands so stale processes are cleaned up.
## Required Turn Documentation
At the end of every completed implementation task, before final handoff, create a user-readable HTML document describing the work.
This documentation is mandatory whenever code, configuration, tests, or project files were changed.
### Location
Save the document in:
```text
docs/turns/
```
## Important: If you are not working inside a git repository, save the document to `~/dev/docs/turns/`
Use a clear timestamped filename:
```text
docs/turns/YYYY-MM-DD-short-task-name.html
```
Example:
```text
docs/turns/2026-05-14-add-market-replay-controls.html
```
### Format
Use the impeccable skill to structure the document as clean, readable HTML.
If the impeccable skill is unavailable, still create a well-structured standalone HTML file with:
- A concise summary at the top
- A detailed explanation of what changed
- Relevant context or background
- Specific code snippets or examples when helpful
- Issues, limitations, tradeoffs, or mitigations
- Validation performed, including tests, builds, linters, or manual checks
- Any remaining follow-up work, with corresponding Beads issue IDs when applicable
### Required Sections
Each turn document must include these sections:
1. **Summary**
2. **Changes Made**
3. **Context**
4. **Important Implementation Details**
5. **Relevant Diff Snippets**
6. **Expected Impact for End-Users**
7. **Validation**
8. **Issues, Limitations, and Mitigations**
9. **Follow-up Work**
### Completion Rule
A task is not complete until:
1. The Beads workflow is updated
2. The turn document is created in `docs/turns`
3. Relevant quality gates have passed or failures are documented
4. Changes are committed
5. `bd dolt push` succeeds
6. `git push` succeeds
7. `git status` shows the branch is up to date with origin
For trivial changes, the document may be brief, but it must still exist and clearly explain what changed and how it was validated.
## Plan Mode Documentation
When working in plan mode, do not modify implementation files.
At the end of plan mode, provide a concise summary of the plan and ask the user whether they want to proceed with implementation.
If the user asks to save the plan, create a user-readable HTML plan document in:
```text
docs/plans/
```
Use a clear timestamped filename:
```text
docs/plans/YYYY-MM-DD-short-plan-name.html
```
The plan document should be labeled clearly as a plan and should include:
1. **Plan Summary**
2. **Goals**
3. **Proposed Changes**
4. **Relevant Context**
5. **Implementation Steps**
6. **Risks, Limitations, and Mitigations**
7. **Open Questions**
Always do the following when you finish a task, finish the beads workflow and and make a commit:
- Document the changes in a user-readable format
- Use the impeccable skill to structure the document as HTML
- Create a clear, concise summary of the changes at the top, followed by a detailed description of the changes, including any relevant context or background as well as specific code snippets or examples.
- Note any relevant issues or limitations that were addressed or mitigated by the changes.
- The HTML file should be stored in the `docs/turns` directory. It should include the current date and time, as well as a brief explanation of changes. e.g. docs/turns/YYYY-MM-DD-{description}.html

View file

@ -97,9 +97,11 @@ docs/turns/2026-05-14-add-market-replay-controls.html
### Format ### Format
Use the impeccable skill to structure the document as clean, readable HTML. Use the `impeccable` skill to structure and style the document as clean, readable HTML.
If the impeccable skill is unavailable, still create a well-structured standalone HTML file with: For this repository, `impeccable` is the styling and layout authority for turn documents when available. Do not apply global non-repo computer-task house styling to repository turn documents.
If the `impeccable` skill is unavailable or blocked by an actual tool/file error, still create a well-structured standalone HTML file with:
- A concise summary at the top - A concise summary at the top
- A detailed explanation of what changed - A detailed explanation of what changed
@ -117,10 +119,11 @@ Each turn document must include these sections:
2. **Changes Made** 2. **Changes Made**
3. **Context** 3. **Context**
4. **Important Implementation Details** 4. **Important Implementation Details**
5. **Expected Impact for End-Users** 5. **Relevant Diff Snippets**
5. **Validation** 6. **Expected Impact for End-Users**
6. **Issues, Limitations, and Mitigations** 7. **Validation**
7. **Follow-up Work** 8. **Issues, Limitations, and Mitigations**
9. **Follow-up Work**
### Completion Rule ### Completion Rule

394
README.md
View file

@ -6,11 +6,12 @@
> **Pre-alpha warning** This project is in an early pre-alpha state. It will not perform consistently or as expected, and APIs, behavior, and data contracts may change without notice. > **Pre-alpha warning** This project is in an early pre-alpha state. It will not perform consistently or as expected, and APIs, behavior, and data contracts may change without notice.
This repository contains a Bun + TypeScript monorepo for a personal-use, event-sourced market microstructure research platform focused on: Islandflow is a Bun + TypeScript monorepo for a personal-use, event-sourced market microstructure research platform focused on:
- options prints + NBBO, - options prints + NBBO,
- off-exchange equity prints, - off-exchange equity prints,
- explainable rule-based flow classification, - market news context,
- explainable smart-money flow classification,
- deterministic replay, - deterministic replay,
- evidence-linked UI inspection. - evidence-linked UI inspection.
@ -19,124 +20,176 @@ This repository contains a Bun + TypeScript monorepo for a personal-use, event-s
Implemented now: Implemented now:
- Bun workspaces with shared packages for schemas, bus, config, observability, and ClickHouse access. - Bun workspaces with shared packages for schemas, bus, config, observability, and ClickHouse access.
- Infra orchestration via Docker Compose (NATS JetStream, ClickHouse, Redis). - Infra orchestration via Docker Compose for local NATS JetStream, ClickHouse, and Redis.
- Options ingest service with adapters: - Options ingest service with synthetic, Alpaca options, IBKR bridge, and Databento historical replay adapters.
- synthetic stream, - Equities ingest service with synthetic and Alpaca equities trades/quotes adapters.
- Alpaca options (dev-focused, bounded contracts), - News ingest service for Alpaca news backfill and websocket publication.
- IBKR bridge (Python sidecar), - Compute service for deterministic parent-event reconstruction, flow packets, NBBO quality features, rolling baselines, smart-money profile scoring, compatibility classifier hits, alerts, inferred dark-style events, and equity print-to-quote joins.
- Databento historical replay adapter (Python sidecar). - Candles service for server-side equity candle aggregation, ClickHouse persistence, optional Redis hot cache, and NATS publication.
- Equities ingest service with adapters: - Replay service for deterministic ClickHouse-to-NATS republishing with multi-stream merge, stable tie-break ordering, speed, start, and end controls.
- synthetic stream, - API service with REST endpoints, cursor pagination, replay/history endpoints, live hot-cache hydration, and WebSocket channels for options, NBBO, equities, quotes, joins, flow, classifier hits, alerts, smart-money events, inferred dark, candles, and news.
- Alpaca equities trades/quotes. - Next.js web app upgraded to Next.js `16.2.6`, React `19.2.0`, and React DOM `19.2.0`.
- Compute service: - Evidence-centric terminal UI, live/replay controls, chart-focused routes, news view, profile-aware smart-money display, and alert-context hydration.
- deterministic option print clustering into `FlowPacket`s, - Thin Electron desktop shell in `apps/desktop` that can wrap the hosted app or local web UI.
- NBBO join quality features and aggressor-mix metrics, - Refdata + EOD enricher service entrypoints are present, with refdata able to validate or refresh the event-calendar cache.
- rolling baselines in Redis,
- structure summarization and structure packet emission,
- rule-based classifiers + confidence-scored alert events,
- dark-style inferred events from equity prints/quotes,
- equity print-to-quote join events.
- Candles service:
- server-side equity candle aggregation,
- ClickHouse persistence,
- optional Redis hot cache,
- NATS publication.
- Replay service:
- deterministic republishing from ClickHouse to NATS,
- multi-stream merge with stable tie-break ordering,
- speed/start/end controls.
- API service:
- REST endpoints for recent + cursor pagination,
- REST range endpoints for chart windows,
- REST replay-oriented endpoints,
- WebSocket channels for options, NBBO, equities, quotes, joins, flow, classifier hits, alerts, inferred dark, and candles.
- Next.js web app:
- live tape/workspace views,
- replay controls and status,
- signals and chart-focused routes,
- evidence-centric terminal UI.
- Refdata + EOD enricher service entrypoints are present but currently scaffolds (lifecycle/logging only).
Planned / not yet complete: Planned / not yet complete:
- production-grade licensed feed integrations and entitlement workflow, - production-grade licensed feed integrations and entitlement workflow,
- richer refdata/corp-action enrichment, - richer refdata/corp-action enrichment,
- secure deployment/auth hardening, - secure deployment/auth hardening,
- deeper structure + calibration workflows from `PLAN.md`. - native deployment unit templates and rollback helpers,
- signed/notarized desktop distribution and richer desktop-native features,
- deeper calibration workflows from `PLAN.md` and `SMART_MONEY_REBUILD_PLAN.md`.
## Core Principles ## Core Principles
- **Explainability first** — inferred outputs are evidence-backed and human-readable. - **Explainability first**: inferred outputs are evidence-backed and human-readable.
- **Event sourcing** — raw and derived events persist to support replay. - **Event sourcing**: raw and derived events persist to support replay.
- **Determinism** — replay behavior tracks live pipeline logic. - **Determinism**: replay behavior tracks live pipeline logic.
- **Microstructure awareness** — bounded joins, confidence scoring, and explicit uncertainty. - **Microstructure awareness**: bounded joins, confidence scoring, and explicit uncertainty.
- **Bun-first tooling** — runtime/package/scripts all use Bun. - **Taxonomy over folklore**: "smart money" is modeled as participant-style hypotheses, not a single binary label.
- **Bun-first tooling**: runtime, package management, scripts, and tests use Bun.
## Smart-Money Classification Taxonomy
Islandflow now emits first-class `SmartMoneyEvent` records instead of treating old classifier hits as the final semantic object. `FlowPacket` remains the clustering bridge, while smart-money events carry typed features, profile scores, confidence bands, directions, reason codes, abstention state, and suppression reasons.
Public profile IDs:
| Profile ID | Meaning | Common evidence |
| --- | --- | --- |
| `institutional_directional` | Large directional parent flow with stronger institutional-style conviction. | premium, size, sweep/burst behavior, aggressor imbalance, quote quality, not short-dated retail-chase context |
| `retail_whale` | Large retail-style speculative bursts, often short-dated or attention-driven. | short-dated OTM concentration, burst prints, IV shock, lower premium than institutional blocks |
| `event_driven` | Flow aligned to known upcoming events. | event-calendar proximity, expiry after event, pre-event concentration, spread/IV pressure |
| `vol_seller` | Premium-selling or short-volatility structure evidence. | sell-side premium, straddles/strangles, neutral direction |
| `arbitrage` | Multi-leg or symmetric structures with low directional exposure. | matched leg symmetry, same-size legs, near-flat directional bias |
| `hedge_reactive` | Hedge or dealer-reaction style flow around short-dated ATM/gamma context. | 0-2 DTE, near-ATM contracts, underlying move linkage, size |
Compatibility surfaces remain in place:
- `ClassifierHitEvent` is derived from `SmartMoneyEvent.primary_profile_id`.
- `AlertEvent` may include `primary_profile_id` and `profile_scores`.
- Legacy classifier and alert endpoints still work.
Primary smart-money access paths:
```text
/flow/smart-money
/history/smart-money
/replay/smart-money
/ws/smart-money
```
The classifier intentionally abstains when evidence is weak or quote context is stale/missing. Suppression guards cover stale quotes, complex/special prints, retail-frenzy directional confusion, hedge-reactive short-dated ATM contexts, and arbitrage symmetry.
## Monorepo Layout ## Monorepo Layout
- `apps/web` — Next.js UI shell/routes. - `apps/web` — Next.js UI shell/routes.
- `apps/desktop` — Electron desktop shell that loads the hosted Islandflow app. - `apps/desktop` — Electron desktop shell that loads the hosted or local Islandflow app.
- `services/ingest-options` — options print/NBBO ingest adapters. - `services/ingest-options` — options print/NBBO ingest adapters.
- `services/ingest-equities` — equity print/quote ingest adapters. - `services/ingest-equities` — equity print/quote ingest adapters.
- `services/compute` — clustering, structures, classifiers, alerts, inferred dark. - `services/ingest-news` — Alpaca news backfill and websocket ingest.
- `services/compute` — parent-event reconstruction, flow packets, smart-money scoring, alerts, inferred dark.
- `services/candles` — server-side candle aggregation + cache. - `services/candles` — server-side candle aggregation + cache.
- `services/replay` — ClickHouse → NATS replay streamer. - `services/replay` — ClickHouse to NATS replay streamer.
- `services/api` — REST + WebSocket gateway. - `services/api` — REST + WebSocket gateway.
- `services/refdata` — scaffold service. - `services/refdata`event-calendar validation/provider refresh scaffolding.
- `services/eod-enricher` — scaffold service. - `services/eod-enricher` — scaffold service.
- `packages/types` — shared event schemas/types. - `packages/types` — shared event schemas/types.
- `packages/storage` — ClickHouse tables/queries. - `packages/storage` — ClickHouse tables/queries.
- `packages/bus` — NATS/JetStream helpers. - `packages/bus` — NATS/JetStream helpers.
- `packages/config` — env parsing. - `packages/config` — env parsing.
- `packages/observability` — logger + metrics facade. - `packages/observability` — logger + metrics facade.
- `deployment/docker` — supported VPS Docker Compose runtime.
- `deployment/native` — experimental host-native Bun + systemd deployment notes.
## Build and Run ## Build and Run
Install dependencies: Install dependencies:
- `bun install` ```bash
bun install
```
Start infrastructure only: Start infrastructure only:
- `docker compose up -d` ```bash
bun run dev:infra
```
Create env file: Create env file:
- copy `.env.example` to `.env` and set provider credentials as needed. ```bash
cp .env.example .env
```
Start infra + all services + web: Start infra + all services + web:
- `bun run dev` ```bash
bun run dev
```
Start services only (assumes infra is already running): Start services only, assuming infra is already running:
- `bun run dev:services` ```bash
bun run dev:services
```
Start web only: Start web only:
- `bun run dev:web` ```bash
bun run dev:web
```
Recommended fast iteration loop: Recommended fast iteration loop:
- `bun run dev:infra` for Docker-backed infra only ```bash
- `bun run dev:services` for native Bun backend services bun run dev:infra
- `bun run dev:web` for the local Next.js UI bun run dev:services
bun run dev:web
```
This keeps Docker in the local workflow where it helps most (NATS, ClickHouse, Redis) without forcing the app services themselves into slower container rebuild/restart loops. This keeps Docker in the local workflow where it helps most, for NATS, ClickHouse, and Redis, while keeping the app services in native Bun/Next.js loops.
## Deployment Workflow ## Deployment Workflow
- `./deploy main` keeps the current VPS Docker rollout path as the default and recommended path. Docker remains the supported and recommended path for the current VPS.
- Do not run the repo-root `docker-compose.yml` on the VPS. That file is for local infra only and can create duplicate exposed NATS, ClickHouse, and Redis containers on the server.
- `./deploy main --runtime native` targets an experimental host-native Bun + systemd deployment. ```bash
- `./deploy current-branch` and `./deploy current-branch --runtime native` keep branch deploys available during the transition, but Docker remains the supported path for the current VPS. ./deploy main
- Partial deploys are supported with `--web-only`, `--api-only`, `--services-only`, and `--no-build`. ./deploy main --runtime docker
- Docker runtime details live in `deployment/docker/README.md`. ./deploy current-branch
- Native runtime expectations and prerequisites live in `deployment/native/README.md`. ./deploy current-branch --runtime docker
```
Important deployment notes:
- Run the deploy helper from the local repo checkout, not from the VPS shell.
- Do not run the repo-root `docker-compose.yml` on the VPS. It is local infra only and can create duplicate exposed NATS, ClickHouse, and Redis containers on the server.
- The Docker stack lives in `deployment/docker` and is separate from local development infra.
- Partial deploys are supported with `--web-only`, `--api-only`, `--services-only`, `--workers-only`, `--fast`, `--no-build`, and `--force-recreate`.
- `--fast` defaults to a services-only Docker rollout when no explicit scope is provided and trims public API route-suite verification while preserving remote service health checks.
- `./deploy current-branch` requires a clean local working tree and pushes the branch before moving the server checkout.
- The helper has Forgejo-aware remote resolution for deployments and branch pushes.
- When run from `/home/delta/islandflow` on the VPS itself, `./deploy` can execute locally instead of SSHing back into the same server.
- Native deployment is opt-in and experimental:
```bash
./deploy main --runtime native
./deploy current-branch --runtime native
```
Native deployment expects Bun, systemd units, host-reachable infra, and deliberate reverse-proxy changes. Native deploys are intended primarily for worker-only fast iteration until the public edge is cut over deliberately.
Read more:
- `deployment/docker/README.md`
- `deployment/native/README.md`
## Desktop Shell ## Desktop Shell
Islandflow also includes a thin Electron desktop shell in `apps/desktop`. Islandflow includes a thin Electron desktop shell in `apps/desktop`.
What it is: What it is:
@ -144,37 +197,35 @@ What it is:
- a native app window plus packaging/distribution shell, - a native app window plus packaging/distribution shell,
- a way to run the existing web UI inside Electron without local backend services. - a way to run the existing web UI inside Electron without local backend services.
What it is not: What it is not yet:
- a bundled backend runtime, - a bundled backend runtime,
- a packaged local Next.js frontend in v1, - a packaged local Next.js frontend,
- a desktop feature layer with notifications, preferences, or auto-updates yet. - a desktop feature layer with notifications, preferences, auto-updates, signing, or notarization.
Run the desktop shell against a local web UI: Run the desktop shell against a local web UI:
- `bun run dev:desktop` ```bash
bun run dev:desktop
This starts the local Next.js app, defaults `NEXT_PUBLIC_API_URL` to `https://flow.deltaisland.io` unless you already set it, waits for port `3000`, and then launches Electron against `http://127.0.0.1:3000`. ```
Run the desktop shell directly against the hosted app: Run the desktop shell directly against the hosted app:
- `bun run dev:desktop:remote` ```bash
bun run dev:desktop:remote
```
Package the desktop shell: Package the desktop shell:
- `bun run package:desktop` ```bash
- `bun run make:desktop` bun run package:desktop
bun run make:desktop
```
Desktop-specific environment: Desktop-specific environment:
- `ISLANDFLOW_DESKTOP_START_URL` is only used by the Electron shell and is restricted to trusted Islandflow app origins. - `ISLANDFLOW_DESKTOP_START_URL` is only used by the Electron shell and is restricted to trusted Islandflow app origins.
- `NEXT_PUBLIC_API_URL` remains the web app's API/WebSocket origin control and should usually point at `https://flow.deltaisland.io` when developing the local UI inside Electron. - `NEXT_PUBLIC_API_URL` remains the web app API/WebSocket origin control and usually points at `https://flow.deltaisland.io` when developing local UI inside Electron.
Current desktop limitations:
- v1 builds are unsigned internal macOS artifacts only,
- Forge currently makes a simple zip distributable for the current host architecture,
- signing, notarization, auto-updates, remembered window state, and richer native integrations are intentionally deferred.
## Environment Configuration ## Environment Configuration
@ -196,32 +247,31 @@ All runtime configuration comes from `.env`.
| `OPTIONS_INGEST_ADAPTER` | `synthetic` | Options ingest source: `synthetic`, `alpaca`, `ibkr`, or `databento`. | | `OPTIONS_INGEST_ADAPTER` | `synthetic` | Options ingest source: `synthetic`, `alpaca`, `ibkr`, or `databento`. |
| `EQUITIES_INGEST_ADAPTER` | `synthetic` | Equities ingest source: `synthetic` or `alpaca`. | | `EQUITIES_INGEST_ADAPTER` | `synthetic` | Equities ingest source: `synthetic` or `alpaca`. |
| `EMIT_INTERVAL_MS` | `1000` | Emit cadence for synthetic ingest adapters. | | `EMIT_INTERVAL_MS` | `1000` | Emit cadence for synthetic ingest adapters. |
| `SYNTHETIC_MARKET_MODE` | `realistic` | Shared synthetic profile (`realistic`, `active`, `firehose`) used when per-service override is unset. | | `SYNTHETIC_MARKET_MODE` | `realistic` | Shared synthetic profile: `realistic`, `active`, or `firehose`. |
| `SYNTHETIC_OPTIONS_MODE` | empty | Options-only synthetic profile override; falls back to `SYNTHETIC_MARKET_MODE`. | | `SYNTHETIC_OPTIONS_MODE` | empty | Options-only synthetic profile override. |
| `SYNTHETIC_EQUITIES_MODE` | empty | Equities-only synthetic profile override; falls back to `SYNTHETIC_MARKET_MODE`. | | `SYNTHETIC_EQUITIES_MODE` | empty | Equities-only synthetic profile override. |
Synthetic profile intent: ### Alpaca and news configuration
- `realistic`: default local mode with lower synthetic burstiness/noise.
- `active`: busier demo flow while still readable.
- `firehose`: stress mode for throughput/backpressure/hot-window behavior.
### Options ingest adapter configuration
| Variable | Default | What it controls | | Variable | Default | What it controls |
| --- | --- | --- | | --- | --- | --- |
| `ALPACA_API_KEY` | empty | Single-token Alpaca API auth for options/equities adapters. Use this when your account provides one API key value. | | `ALPACA_API_KEY` | empty | Legacy single-token fallback kept for older Alpaca setups. Prefer explicit key ID + secret vars for current Alpaca auth. |
| `ALPACA_REST_URL` | `https://data.alpaca.markets` | Alpaca REST base URL for contract discovery/reference calls. | | `ALPACA_API_KEY_ID` | empty | Preferred Alpaca key ID used for market-data REST and websocket auth. |
| `ALPACA_WS_BASE_URL` | `wss://stream.data.alpaca.markets/v1beta1` (options), `wss://stream.data.alpaca.markets` (equities) | Alpaca websocket base URL. | | `ALPACA_KEY_ID` | empty | Alternate name accepted for the Alpaca key ID. |
| `ALPACA_FEED` | `indicative` | Options feed tier for Alpaca options (`indicative` or `opra`). | | `ALPACA_API_SECRET_KEY` | empty | Preferred Alpaca secret key paired with `ALPACA_API_KEY_ID`. |
| `ALPACA_SECRET_KEY` | empty | Alternate name accepted for the Alpaca secret key. |
| `ALPACA_REST_URL` | `https://data.alpaca.markets` | Alpaca REST base URL. |
| `ALPACA_WS_BASE_URL` | `wss://stream.data.alpaca.markets/v1beta1` for options, `wss://stream.data.alpaca.markets` for equities/news | Alpaca websocket base URL. |
| `ALPACA_FEED` | `indicative` | Options feed tier: `indicative` or `opra`. |
| `ALPACA_UNDERLYINGS` | `SPY,NVDA,AAPL` | Comma-separated symbols targeted by Alpaca ingest. | | `ALPACA_UNDERLYINGS` | `SPY,NVDA,AAPL` | Comma-separated symbols targeted by Alpaca ingest. |
| `ALPACA_STRIKES_PER_SIDE` | `8` | Contracts selected per side of spot for Alpaca options chain sampling. | | `ALPACA_STRIKES_PER_SIDE` | `8` | Contracts selected per side of spot for Alpaca options chain sampling. |
| `ALPACA_MAX_DTE_DAYS` | `30` | Max days-to-expiry included for Alpaca options contract selection. | | `ALPACA_MAX_DTE_DAYS` | `30` | Max days-to-expiry included for Alpaca options contract selection. |
| `ALPACA_MONEYNESS_PCT` | `0.06` | Primary moneyness filter for Alpaca options contract selection. | | `ALPACA_MONEYNESS_PCT` | `0.06` | Primary moneyness filter for Alpaca options contract selection. |
| `ALPACA_MONEYNESS_FALLBACK_PCT` | `0.1` | Wider fallback moneyness filter if candidate set is too sparse. | | `ALPACA_MONEYNESS_FALLBACK_PCT` | `0.1` | Wider fallback moneyness filter if candidate set is too sparse. |
| `ALPACA_MAX_QUOTES` | `200` | Upper bound on selected Alpaca options contracts/quotes per cycle. | | `ALPACA_MAX_QUOTES` | `200` | Upper bound on selected Alpaca options contracts/quotes per cycle. |
| `ALPACA_EQUITIES_FEED` | `iex` | Alpaca equities feed (`iex` free tier, `sip` paid consolidated feed). | | `ALPACA_EQUITIES_FEED` | `iex` | Alpaca equities feed: `iex` or `sip`. |
| `ALPACA_NEWS_BACKFILL_LIMIT` | `50` | Alpaca news stories fetched on startup, capped at 50 by the Alpaca News API. |
For Alpaca adapters, configure `ALPACA_API_KEY`. | `ALPACA_NEWS_WEBSOCKET_PATH` | `/v1beta1/news` | Alpaca news websocket path. |
### Databento replay adapter configuration ### Databento replay adapter configuration
@ -236,7 +286,7 @@ For Alpaca adapters, configure `ALPACA_API_KEY`.
| `DATABENTO_SYMBOLS` | `ALL` | Symbol selection forwarded to Databento sidecar query. | | `DATABENTO_SYMBOLS` | `ALL` | Symbol selection forwarded to Databento sidecar query. |
| `DATABENTO_STYPE_IN` | `raw_symbol` | Databento input symbology type. | | `DATABENTO_STYPE_IN` | `raw_symbol` | Databento input symbology type. |
| `DATABENTO_STYPE_OUT` | `raw_symbol` | Databento output symbology type. | | `DATABENTO_STYPE_OUT` | `raw_symbol` | Databento output symbology type. |
| `DATABENTO_LIMIT` | `0` | Max Databento records (`0` means no explicit limit). | | `DATABENTO_LIMIT` | `0` | Max Databento records, where `0` means no explicit limit. |
| `DATABENTO_PRICE_SCALE` | `1` | Multiplier applied to decoded prices from sidecar output. | | `DATABENTO_PRICE_SCALE` | `1` | Multiplier applied to decoded prices from sidecar output. |
| `DATABENTO_PYTHON_BIN` | `python3` | Python executable used to run Databento sidecar script. | | `DATABENTO_PYTHON_BIN` | `python3` | Python executable used to run Databento sidecar script. |
@ -248,9 +298,9 @@ For Alpaca adapters, configure `ALPACA_API_KEY`.
| `IBKR_PORT` | `7497` | TWS/Gateway port for IBKR bridge. | | `IBKR_PORT` | `7497` | TWS/Gateway port for IBKR bridge. |
| `IBKR_CLIENT_ID` | `0` | IBKR client id used by the bridge connection. | | `IBKR_CLIENT_ID` | `0` | IBKR client id used by the bridge connection. |
| `IBKR_SYMBOL` | `SPY` | Underlying symbol requested from IBKR. | | `IBKR_SYMBOL` | `SPY` | Underlying symbol requested from IBKR. |
| `IBKR_EXPIRY` | `20250117` | Option expiry (YYYYMMDD) requested from IBKR. | | `IBKR_EXPIRY` | `20250117` | Option expiry requested from IBKR. |
| `IBKR_STRIKE` | `450` | Strike requested from IBKR. | | `IBKR_STRIKE` | `450` | Strike requested from IBKR. |
| `IBKR_RIGHT` | `C` | Option side (`C` or `P`). | | `IBKR_RIGHT` | `C` | Option side: `C` or `P`. |
| `IBKR_EXCHANGE` | `SMART` | IBKR exchange routing code. | | `IBKR_EXCHANGE` | `SMART` | IBKR exchange routing code. |
| `IBKR_CURRENCY` | `USD` | Contract currency. | | `IBKR_CURRENCY` | `USD` | Contract currency. |
| `IBKR_PYTHON_BIN` | `python3` | Python executable used for IBKR sidecar. | | `IBKR_PYTHON_BIN` | `python3` | Python executable used for IBKR sidecar. |
@ -259,133 +309,77 @@ For Alpaca adapters, configure `ALPACA_API_KEY`.
| Variable | Default | What it controls | | Variable | Default | What it controls |
| --- | --- | --- | | --- | --- | --- |
| `OPTIONS_SIGNAL_MODE` | `smart-money` | Signal pass policy (`smart-money`, `balanced`, `all`) for options prints. | | `OPTIONS_SIGNAL_MODE` | `smart-money` | Signal pass policy: `smart-money`, `balanced`, or `all`. |
| `OPTIONS_SIGNAL_MIN_NOTIONAL` | `10000` | Base minimum notional for most signal candidates. | | `OPTIONS_SIGNAL_MIN_NOTIONAL` | `10000` | Base minimum notional for most signal candidates. |
| `OPTIONS_SIGNAL_ETF_MIN_NOTIONAL` | `50000` | ETF-specific minimum notional for signal inclusion. | | `OPTIONS_SIGNAL_ETF_MIN_NOTIONAL` | `50000` | ETF-specific minimum notional for signal inclusion. |
| `OPTIONS_SIGNAL_BID_SIDE_MIN_NOTIONAL` | `25000` | Minimum notional for bid-side (`B`/`BB`) or sweep/ISO thresholds. | | `OPTIONS_SIGNAL_BID_SIDE_MIN_NOTIONAL` | `25000` | Minimum notional for bid-side or sweep/ISO thresholds. |
| `OPTIONS_SIGNAL_MID_MIN_NOTIONAL` | `20000` | Minimum notional for non-sweep/non-ISO `MID` prints. | | `OPTIONS_SIGNAL_MID_MIN_NOTIONAL` | `20000` | Minimum notional for non-sweep/non-ISO `MID` prints. |
| `OPTIONS_SIGNAL_NBBO_MAX_AGE_MS` | `1500` | NBBO freshness threshold used during signal classification. | | `OPTIONS_SIGNAL_NBBO_MAX_AGE_MS` | `1500` | NBBO freshness threshold used during signal classification. |
| `OPTIONS_SIGNAL_ETF_UNDERLYINGS` | `SPY,QQQ,IWM,DIA,TLT,GLD,SLV,XLF,XLE,XLV,XLI,XLP,XLU,XLY,SMH,ARKK` | Comma-separated underlyings treated as ETFs by signal filters. | | `OPTIONS_SIGNAL_ETF_UNDERLYINGS` | `SPY,QQQ,IWM,DIA,TLT,GLD,SLV,XLF,XLE,XLV,XLI,XLP,XLU,XLY,SMH,ARKK` | ETF underlyings treated specially by signal filters. |
Default `smart-money` policy rejects lower-information prints and keeps high-confidence/high-notional/sweep-style flow; `balanced` lowers thresholds; `all` bypasses filtering. Default `smart-money` policy rejects lower-information prints and keeps higher-confidence, higher-notional, sweep-style flow. `balanced` lowers thresholds. `all` bypasses filtering.
### Compute/classifier/dark-inference configuration ### Compute, classifier, and dark-inference configuration
| Variable | Default | What it controls | | Variable | Default | What it controls |
| --- | --- | --- | | --- | --- | --- |
| `CLUSTER_WINDOW_MS` | `500` | Time window used to cluster nearby option prints into a packet candidate. | | `CLUSTER_WINDOW_MS` | `500` | Time window used to cluster nearby option prints into packet candidates. |
| `COMPUTE_DELIVER_POLICY` | `new` | Consumer start policy for compute stream subscriptions (`new`, `all`, `last`, `last_per_subject`). | | `COMPUTE_DELIVER_POLICY` | `new` | Consumer start policy for compute subscriptions. |
| `COMPUTE_CONSUMER_RESET` | `false` | If true, resets durable consumer position for compute on startup. | | `COMPUTE_CONSUMER_RESET` | `false` | Resets durable consumer position for compute on startup when true. |
| `NBBO_MAX_AGE_MS` | `1000` | Max NBBO age accepted when enriching option prints in compute. | | `NBBO_MAX_AGE_MS` | `1000` | Max NBBO age accepted when enriching option prints in compute. |
| `ROLLING_WINDOW_SIZE` | `50` | Number of observations retained per rolling metric key. | | `ROLLING_WINDOW_SIZE` | `50` | Number of observations retained per rolling metric key. |
| `ROLLING_TTL_SEC` | `86400` | Redis TTL for rolling metric keys. | | `ROLLING_TTL_SEC` | `86400` | Redis TTL for rolling metric keys. |
| `EQUITY_QUOTE_MAX_AGE_MS` | `1000` | Max quote staleness when joining equity prints for inference. | | `EQUITY_QUOTE_MAX_AGE_MS` | `1000` | Max quote staleness when joining equity prints for inference. |
| `DARK_INFER_WINDOW_MS` | `60000` | Sliding window length for dark-style inference accumulation. | | `DARK_INFER_WINDOW_MS` | `60000` | Sliding window length for dark-style inference accumulation. |
| `DARK_INFER_COOLDOWN_MS` | `30000` | Cooldown before emitting repeated dark inferences for same symbol/pattern. | | `DARK_INFER_COOLDOWN_MS` | `30000` | Cooldown before repeated dark inferences for same symbol/pattern. |
| `DARK_INFER_MIN_BLOCK_SIZE` | `2000` | Minimum single-print size for block-style dark inference evidence. | | `SMART_MONEY_EVENT_CALENDAR_PATH` | empty | Optional JSON event-calendar file used by compute. |
| `DARK_INFER_MIN_ACCUM_SIZE` | `3000` | Minimum aggregate size for accumulation-style dark inference evidence. | | `REFDATA_EVENT_CALENDAR_PATH` | empty | Optional JSON event-calendar path for refdata; falls back to `SMART_MONEY_EVENT_CALENDAR_PATH`. |
| `DARK_INFER_MIN_ACCUM_COUNT` | `4` | Minimum print count for accumulation-style dark inference. | | `REFDATA_EVENT_CALENDAR_PROVIDER` | empty | Set to `alpha_vantage` to refresh event-calendar cache from Alpha Vantage. |
| `DARK_INFER_MIN_PRINT_SIZE` | `200` | Minimum print size considered as dark inference evidence. | | `ALPHA_VANTAGE_API_KEY` | empty | Alpha Vantage key for provider-backed event-calendar refresh. |
| `DARK_INFER_MAX_EVIDENCE` | `20` | Max evidence items attached to one inferred dark event. |
| `DARK_INFER_MAX_SPREAD_PCT` | `0.005` | Maximum spread percentage allowed for dark inference confidence. |
| `CLASSIFIER_SWEEP_MIN_PREMIUM` | `40000` | Minimum premium to trigger sweep classifier logic. |
| `CLASSIFIER_SWEEP_MIN_COUNT` | `3` | Minimum child prints in cluster for sweep classifier hit. |
| `CLASSIFIER_SWEEP_MIN_PREMIUM_Z` | `2` | Min premium z-score for sweep classifier confirmation. |
| `CLASSIFIER_SPIKE_MIN_PREMIUM` | `20000` | Minimum premium for spike classifier logic. |
| `CLASSIFIER_SPIKE_MIN_SIZE` | `400` | Minimum total size for spike classifier logic. |
| `CLASSIFIER_SPIKE_MIN_PREMIUM_Z` | `2.5` | Min premium z-score for spike classifier confirmation. |
| `CLASSIFIER_SPIKE_MIN_SIZE_Z` | `2` | Min size z-score for spike classifier confirmation. |
| `CLASSIFIER_Z_MIN_SAMPLES` | `12` | Minimum rolling sample count before z-score gating applies. |
| `CLASSIFIER_MIN_NBBO_COVERAGE` | `0.5` | Required fraction of prints in cluster with valid NBBO context. |
| `CLASSIFIER_MIN_AGGRESSOR_RATIO` | `0.55` | Minimum aggressor-side ratio for classifier confidence. |
| `CLASSIFIER_0DTE_MAX_ATM_PCT` | `0.01` | Max distance-from-ATM to qualify as near-ATM 0DTE event. |
| `CLASSIFIER_0DTE_MIN_PREMIUM` | `20000` | Minimum premium for 0DTE classifier events. |
| `CLASSIFIER_0DTE_MIN_SIZE` | `400` | Minimum size for 0DTE classifier events. |
| `SMART_MONEY_EVENT_CALENDAR_PATH` | empty | Optional JSON event-calendar file used by compute to enrich event-driven smart-money profile features. |
| `REFDATA_EVENT_CALENDAR_PATH` | empty | Optional JSON event-calendar file for refdata service startup validation; falls back to `SMART_MONEY_EVENT_CALENDAR_PATH` when unset. |
| `REFDATA_EVENT_CALENDAR_PROVIDER` | empty | Set to `alpha_vantage` to have refdata refresh the calendar cache from Alpha Vantage. |
| `ALPHA_VANTAGE_API_KEY` | empty | Alpha Vantage key used when `REFDATA_EVENT_CALENDAR_PROVIDER=alpha_vantage`. |
| `ALPHA_VANTAGE_EARNINGS_HORIZON` | `3month` | Alpha Vantage earnings horizon: `3month`, `6month`, or `12month`. |
| `ALPHA_VANTAGE_EARNINGS_SYMBOL` | empty | Optional single-symbol Alpha Vantage earnings query; empty fetches the full scheduled earnings list. |
| `REFDATA_EVENT_CALENDAR_REFRESH_MS` | `86400000` | Refdata refresh cadence for provider-backed event-calendar cache writes. |
Event-calendar rows may use `symbol`, `underlying`, or `underlying_id`; `event_date`, `event_time`, or `event_ts`; and `announced_ts`, `available_ts`, `as_of_ts`, or `created_ts`. Compute only uses events already available at the packet timestamp, so missing or unavailable rows leave event-alignment features as neutral `null` values. ### API, live cache, and web client
### Candle service configuration
| Variable | Default | What it controls |
| --- | --- | --- |
| `CANDLE_INTERVALS_MS` | `60000,300000` | Comma-separated candle intervals generated from equity prints. |
| `CANDLE_MAX_LATE_MS` | `0` | Allowed lateness for out-of-order prints before candle rejection/roll policy applies. |
| `CANDLE_CACHE_LIMIT` | `2000` | Max cached candles per `(underlying, interval)` in Redis (`0` disables cache). |
| `CANDLE_DELIVER_POLICY` | `new` | Consumer start policy for candle service (`new`, `all`, `last`, `last_per_subject`). |
| `CANDLE_CONSUMER_RESET` | `false` | If true, resets candle durable consumer position on startup. |
### API + live cache configuration
| Variable | Default | What it controls | | Variable | Default | What it controls |
| --- | --- | --- | | --- | --- | --- |
| `API_PORT` | `4000` | API service listen port. | | `API_PORT` | `4000` | API service listen port. |
| `REST_DEFAULT_LIMIT` | `200` | Default record count when a REST endpoint omits `limit`. | | `REST_DEFAULT_LIMIT` | `200` | Default REST record count. |
| `API_DELIVER_POLICY` | `new` | JetStream consumer start policy used by API live subscribers (`new`, `all`, `last`, `last_per_subject`). | | `API_DELIVER_POLICY` | `new` | JetStream consumer start policy used by API live subscribers. |
| `API_CONSUMER_RESET` | `false` | If true, API resets/recreates its live durable consumers on startup. | | `API_CONSUMER_RESET` | `false` | Resets/recreates API live durable consumers on startup when true. |
| `LIVE_LIMIT_OPTIONS` | `10000` | In-memory/Redis live cache depth for options channel (clamped `1..100000`). | | `LIVE_LIMIT_DEFAULT` | `1000` | Optional generic live cache depth default. |
| `LIVE_LIMIT_NBBO` | `10000` | Live cache depth for options NBBO channel (clamped `1..100000`). | | `LIVE_LIMIT_FLOW` | `500` | Live cache depth for flow packet events unless overridden. |
| `LIVE_LIMIT_EQUITIES` | `10000` | Live cache depth for equities channel (clamped `1..100000`). | | `LIVE_LIMIT_SMART_MONEY` | `300` | Live cache depth for smart-money events unless overridden. |
| `LIVE_LIMIT_EQUITY_QUOTES` | `10000` | Live cache depth for equity quotes channel (clamped `1..100000`). | | `LIVE_LIMIT_OPTIONS` | `1000` | Live cache depth for options channel unless overridden. |
| `LIVE_LIMIT_EQUITY_JOINS` | `10000` | Live cache depth for equity join channel (clamped `1..100000`). | | `LIVE_LIMIT_ALERTS` | `300` | Live cache depth for alerts channel unless overridden. |
| `LIVE_LIMIT_FLOW` | `10000` | Live cache depth for flow packet channel (clamped `1..100000`). | | `LIVE_LIMIT_NEWS` | `100` | Live cache depth for news channel unless overridden. |
| `LIVE_LIMIT_CLASSIFIER_HITS` | `10000` | Live cache depth for classifier hits channel (clamped `1..100000`). | | `NEXT_PUBLIC_API_URL` | auto-detected in browser, `http://127.0.0.1:4000` fallback | Explicit base URL for API/WS calls from the web app. |
| `LIVE_LIMIT_ALERTS` | `10000` | Live cache depth for alerts channel (clamped `1..100000`). | | `NEXT_PUBLIC_LIVE_HOT_WINDOW` | `600` | Max hot-window items retained for non-options live streams in UI state. |
| `LIVE_LIMIT_INFERRED_DARK` | `10000` | Live cache depth for inferred dark channel (clamped `1..100000`). | | `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. |
### Web client configuration (`NEXT_PUBLIC_*`) | `NEXT_PUBLIC_FLOW_FILTER_PRESET` | `smart-money` | Default flow filter preset: `smart-money`, `balanced`, or `all`. |
| Variable | Default | What it controls |
| --- | --- | --- |
| `NEXT_PUBLIC_API_URL` | auto-detected (`window.location.origin` in browser; `http://127.0.0.1:4000` fallback) | Explicit base URL for API/WS calls from the web app. |
| `NEXT_PUBLIC_LIVE_HOT_WINDOW` | `2000` | Max hot-window items retained for non-options live streams in UI state (`100..100000`). |
| `NEXT_PUBLIC_LIVE_HOT_WINDOW_OPTIONS` | `25000` | Dedicated max hot-window items retained for options prints (`100..100000`). |
| `NEXT_PUBLIC_NBBO_MAX_AGE_MS` | `1000` | Frontend NBBO staleness threshold used for UI status/placement logic. |
| `NEXT_PUBLIC_LIVE_EQUITIES_SILENT_WARNING_MS` | `25000` | Delay before warning when equities stream is quiet (`5000..300000`). |
| `NEXT_PUBLIC_PINNED_EVIDENCE_TTL_MS` | `1200000` | TTL for pinned evidence objects in UI (`60000..7200000`). |
| `NEXT_PUBLIC_PINNED_EVIDENCE_MAX_ITEMS` | `4000` | Maximum pinned evidence cache size in UI (`100..50000`). |
| `NEXT_PUBLIC_FLOW_FILTER_PRESET` | `smart-money` | Default flow filter preset applied on page load (`smart-money`, `balanced`, `all`). |
### Replay and testing controls ### Replay and testing controls
| Variable | Default | What it controls | | Variable | Default | What it controls |
| --- | --- | --- | | --- | --- | --- |
| `REPLAY_ENABLED` | `false` | Dev-script toggle: starts replay service in `bun run dev` when truthy. | | `REPLAY_ENABLED` | `false` | Starts replay service in `bun run dev` when truthy. |
| `REPLAY_STREAMS` | `options,nbbo,equities,equity-quotes` | Replay stream selection (`all` or comma list of supported aliases). | | `REPLAY_STREAMS` | `options,nbbo,equities,equity-quotes` | Replay stream selection. |
| `REPLAY_START_TS` | `0` | Replay lower-bound timestamp; `0` means from earliest stored data. | | `REPLAY_START_TS` | `0` | Replay lower-bound timestamp. |
| `REPLAY_END_TS` | `0` | Replay upper-bound timestamp; `0` means no explicit end bound. | | `REPLAY_END_TS` | `0` | Replay upper-bound timestamp. |
| `REPLAY_SPEED` | `1` | Replay speed multiplier relative to original event timing. | | `REPLAY_SPEED` | `1` | Replay speed multiplier. |
| `REPLAY_BATCH_SIZE` | `200` | Batch fetch size per replay stream pull. | | `REPLAY_BATCH_SIZE` | `200` | Batch fetch size per stream. |
| `REPLAY_LOG_EVERY` | `1000` | Progress log interval (emitted event count). | | `REPLAY_LOG_EVERY` | `1000` | Progress log interval. |
| `TESTING_MODE` | `false` | Enables ingest publish throttling for deterministic/lower-volume test runs. | | `TESTING_MODE` | `false` | Enables ingest publish throttling for deterministic/lower-volume test runs. |
| `TESTING_THROTTLE_MS` | `200` | Minimum delay between emitted events while `TESTING_MODE=true`. | | `TESTING_THROTTLE_MS` | `200` | Minimum delay between emitted events while `TESTING_MODE=true`. |
## Quick Notes ## Quick Notes
- Python dependencies are required only for IBKR/Databento sidecars (`services/ingest-options/py/requirements.txt`). - Python dependencies are required only for IBKR/Databento sidecars: `services/ingest-options/py/requirements.txt`.
- Candle construction is server-side; the client consumes prebuilt OHLC events. - Candle construction is server-side; the client consumes prebuilt OHLC events.
- Option prints now persist as enriched raw rows and can be queried as either: - Option prints persist as enriched raw rows and can be queried as `view=signal` or `view=raw`.
- `view=signal` — default live/UI path and compute input. - The default Tape page options/packets posture is stock-only, hides `B` / `BB`, keeps calls and puts visible, and applies in-memory min-notional controls immediately.
- `view=raw` — audit/debug path that preserves every stored print. - Live retention uses ClickHouse for durable server history, Redis for bounded hot cache, and browser state for rendering windows/preferences.
- The default Tape page options/packets posture is now stock-only, hides `B` / `BB`, keeps calls and puts visible, and applies in-memory min-notional controls immediately. - Alert and drawer evidence is pinned and hydrated by id/trace so details remain inspectable after hot-window eviction.
- Live retention uses a two-tier model: - Firehose readiness keeps raw ingest for storage/replay, routes default compute/UI through filtered signals, and keeps subscription contracts ready for server-side selective delivery.
- ClickHouse is durable server history; Redis is a bounded hot cache per live generic channel.
- `LIVE_LIMIT_*` controls initial snapshot/hot-cache depth, not total persisted history.
- Browser state is only a rendering window and UI preferences, not a market-data database.
- Devices connected to the same API hydrate from the same server-seen history.
- UI keeps a bounded hot window for rendering performance around the signal view rather than raw noise.
- Options prints can use a deeper dedicated cap via `NEXT_PUBLIC_LIVE_HOT_WINDOW_OPTIONS` without raising every other feed.
- Alert/drawer evidence is pinned and hydrated by id/trace so details remain inspectable after hot-window eviction.
- Firehose-readiness strategy:
- preserve raw ingest for storage/replay,
- feed compute and default live UI from the filtered signal path,
- add filterable live subscription contracts now so selective delivery can move server-side without reshaping the protocol later.
- This repository is for personal, non-redistributed usage. - This repository is for personal, non-redistributed usage.
## Useful Examples ## Useful Examples

View file

@ -12,6 +12,9 @@
"package": "bun run build && electron-forge package", "package": "bun run build && electron-forge package",
"make": "bun run build && electron-forge make" "make": "bun run build && electron-forge make"
}, },
"dependencies": {
"@islandflow/types": "workspace:*"
},
"devDependencies": { "devDependencies": {
"@electron-forge/cli": "^7.8.1", "@electron-forge/cli": "^7.8.1",
"@electron-forge/core": "^7.11.1", "@electron-forge/core": "^7.11.1",

View file

@ -0,0 +1,8 @@
export const DESKTOP_AI_STATE_CHANNEL = "islandflow:desktop-ai:state";
export const DESKTOP_AI_GET_STATE = "islandflow:desktop-ai:get-state";
export const DESKTOP_AI_LOGIN_BROWSER = "islandflow:desktop-ai:login-browser";
export const DESKTOP_AI_LOGIN_DEVICE = "islandflow:desktop-ai:login-device";
export const DESKTOP_AI_CANCEL_LOGIN = "islandflow:desktop-ai:cancel-login";
export const DESKTOP_AI_LOGOUT = "islandflow:desktop-ai:logout";
export const DESKTOP_AI_UPDATE_PREFERENCES = "islandflow:desktop-ai:update-preferences";
export const DESKTOP_AI_RUN_TASK = "islandflow:desktop-ai:run-task";

View file

@ -0,0 +1,174 @@
import { afterEach, describe, expect, it } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { createAppServerChildEnv, IslandflowDesktopAiService, summarizeRateLimit } from "./desktop-ai.js";
const tempDirs: string[] = [];
const makeTempDir = async (): Promise<string> => {
const dir = await mkdtemp(path.join(tmpdir(), "islandflow-desktop-ai-"));
tempDirs.push(dir);
return dir;
};
afterEach(async () => {
await Promise.all(
tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))
);
});
describe("desktop ai auth environment", () => {
it("scrubs global OpenAI keys for managed ChatGPT sessions", () => {
const env = createAppServerChildEnv("managed-chatgpt", {
OPENAI_API_KEY: "openai-test",
CODEX_API_KEY: "codex-test",
HOME: "/tmp/home"
});
expect(env.OPENAI_API_KEY).toBeUndefined();
expect(env.CODEX_API_KEY).toBeUndefined();
expect(env.HOME).toBe("/tmp/home");
});
it("preserves keys for api-key mode", () => {
const env = createAppServerChildEnv("api-key", {
OPENAI_API_KEY: "openai-test",
CODEX_API_KEY: "codex-test"
});
expect(env.OPENAI_API_KEY).toBe("openai-test");
expect(env.CODEX_API_KEY).toBe("codex-test");
});
});
describe("desktop ai usage and state tracking", () => {
it("records exact token usage notifications into usage rollups", async () => {
const dir = await makeTempDir();
const service = new IslandflowDesktopAiService(dir, async () => {}, () => {});
const internal = service as any;
internal.state.account.email = "analyst@example.com";
internal.state.account.planType = "plus";
internal.state.preferences.model = "gpt-5.4";
internal.state.tasks = [
{
taskId: "task-1",
kind: "smart-money-explain",
title: "Explain smart money event",
subtitle: "AAPL",
status: "running",
createdAt: Date.now(),
updatedAt: Date.now(),
threadId: "thread-1",
turnId: "turn-1",
model: "gpt-5.4",
reasoningEffort: "high",
text: "",
error: null,
compiledScreen: null
}
];
internal.activeTasksByThreadId.set("thread-1", {
taskId: "task-1",
taskKind: "smart-money-explain",
taskTitle: "Explain smart money event",
profileId: "managed-chatgpt"
});
await internal.handleNotification("thread/tokenUsage/updated", {
threadId: "thread-1",
turnId: "turn-1",
tokenUsage: {
total: {
totalTokens: 1800,
inputTokens: 1000,
cachedInputTokens: 500,
outputTokens: 250,
reasoningOutputTokens: 50
},
last: {
totalTokens: 1800,
inputTokens: 1000,
cachedInputTokens: 500,
outputTokens: 250,
reasoningOutputTokens: 50
}
}
});
expect(service.getState().usage.today.breakdown).toEqual({
totalTokens: 1800,
inputTokens: 1000,
cachedInputTokens: 500,
outputTokens: 250,
reasoningOutputTokens: 50
});
expect(service.getState().usage.today.turnCount).toBe(1);
expect(service.getState().usage.recentTurns[0]?.normalizedCostUsd).toBeCloseTo(0.007125, 6);
});
it("stores rate-limit snapshots with reset times", async () => {
const dir = await makeTempDir();
const service = new IslandflowDesktopAiService(dir, async () => {}, () => {});
const internal = service as any;
await internal.handleNotification("account/rateLimits/updated", {
rateLimits: {
limitId: "chatgpt_plus",
limitName: "ChatGPT Plus",
primary: {
usedPercent: 38.4,
windowDurationMins: 180,
resetsAt: 1_710_000_000_000
},
secondary: {
usedPercent: 12.1,
windowDurationMins: 1440,
resetsAt: 1_710_003_600_000
},
planType: "plus"
}
});
expect(service.getState().rateLimitsByLimitId.chatgpt_plus).toEqual(
summarizeRateLimit({
limitId: "chatgpt_plus",
limitName: "ChatGPT Plus",
primary: {
usedPercent: 38.4,
windowDurationMins: 180,
resetsAt: 1_710_000_000_000
},
secondary: {
usedPercent: 12.1,
windowDurationMins: 1440,
resetsAt: 1_710_003_600_000
},
planType: "plus"
})
);
});
it("clears local account state on logout", async () => {
const dir = await makeTempDir();
const service = new IslandflowDesktopAiService(dir, async () => {}, () => {});
const internal = service as any;
internal.client = {
request: async () => ({})
};
internal.state.account.loggedIn = true;
internal.state.account.email = "analyst@example.com";
internal.state.account.planType = "plus";
internal.state.account.login = { status: "browser_pending", message: "Waiting", loginId: "login-1", authUrl: "https://example.com" };
await service.logout();
expect(service.getState().account.loggedIn).toBe(false);
expect(service.getState().account.email).toBeNull();
expect(service.getState().account.planType).toBeNull();
expect(service.getState().account.login).toEqual({ status: "idle", message: "Logged out." });
});
});

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
import { app, BrowserWindow, shell } from "electron"; import { app, BrowserWindow, ipcMain, shell } from "electron";
import type { Event as ElectronEvent } from "electron"; import type { Event as ElectronEvent, IpcMainInvokeEvent } from "electron";
import { fileURLToPath } from "node:url";
import { import {
DESKTOP_PRODUCTION_URL, DESKTOP_PRODUCTION_URL,
@ -7,11 +8,25 @@ import {
isTrustedAppUrl, isTrustedAppUrl,
resolveDesktopStartUrl resolveDesktopStartUrl
} from "./security.js"; } from "./security.js";
import { IslandflowDesktopAiService } from "./desktop-ai.js";
import {
DESKTOP_AI_CANCEL_LOGIN,
DESKTOP_AI_GET_STATE,
DESKTOP_AI_LOGIN_BROWSER,
DESKTOP_AI_LOGIN_DEVICE,
DESKTOP_AI_LOGOUT,
DESKTOP_AI_RUN_TASK,
DESKTOP_AI_STATE_CHANNEL,
DESKTOP_AI_UPDATE_PREFERENCES
} from "./desktop-ai-ipc.js";
const WINDOW_BACKGROUND_COLOR = "#06080b"; const WINDOW_BACKGROUND_COLOR = "#06080b";
const WINDOW_TITLE = "Islandflow"; const WINDOW_TITLE = "Islandflow";
let mainWindow: BrowserWindow | null = null; let mainWindow: BrowserWindow | null = null;
let desktopAiService: IslandflowDesktopAiService | null = null;
const PRELOAD_PATH = fileURLToPath(new URL("./preload.js", import.meta.url));
const canOpenExternalUrl = (sourceUrl: string, targetUrl: string): boolean => { const canOpenExternalUrl = (sourceUrl: string, targetUrl: string): boolean => {
return isTrustedAppUrl(sourceUrl) && isSafeExternalUrl(targetUrl); return isTrustedAppUrl(sourceUrl) && isSafeExternalUrl(targetUrl);
@ -61,6 +76,7 @@ const createMainWindow = (): BrowserWindow => {
title: WINDOW_TITLE, title: WINDOW_TITLE,
backgroundColor: WINDOW_BACKGROUND_COLOR, backgroundColor: WINDOW_BACKGROUND_COLOR,
webPreferences: { webPreferences: {
preload: PRELOAD_PATH,
nodeIntegration: false, nodeIntegration: false,
contextIsolation: true, contextIsolation: true,
sandbox: true, sandbox: true,
@ -92,6 +108,68 @@ const createMainWindow = (): BrowserWindow => {
return window; return window;
}; };
const broadcastDesktopAiState = (): void => {
if (!desktopAiService) {
return;
}
const state = desktopAiService.getState();
for (const window of BrowserWindow.getAllWindows()) {
window.webContents.send(DESKTOP_AI_STATE_CHANNEL, state);
}
};
const getTrustedSenderUrl = (event: IpcMainInvokeEvent): string => {
const senderUrl = event.senderFrame?.url || event.sender.getURL();
if (!isTrustedAppUrl(senderUrl)) {
throw new Error(`Rejected desktop AI IPC from untrusted origin: ${senderUrl || "unknown"}`);
}
return senderUrl;
};
const registerDesktopAiIpc = (service: IslandflowDesktopAiService): void => {
const guard = (event: IpcMainInvokeEvent): void => {
getTrustedSenderUrl(event);
};
ipcMain.handle(DESKTOP_AI_GET_STATE, async (event) => {
guard(event);
await service.start();
return service.getState();
});
ipcMain.handle(DESKTOP_AI_LOGIN_BROWSER, async (event) => {
guard(event);
await service.loginWithBrowser();
});
ipcMain.handle(DESKTOP_AI_LOGIN_DEVICE, async (event) => {
guard(event);
await service.loginWithDeviceCode();
});
ipcMain.handle(DESKTOP_AI_CANCEL_LOGIN, async (event) => {
guard(event);
await service.cancelLogin();
});
ipcMain.handle(DESKTOP_AI_LOGOUT, async (event) => {
guard(event);
await service.logout();
});
ipcMain.handle(DESKTOP_AI_UPDATE_PREFERENCES, async (event, next) => {
guard(event);
await service.updatePreferences(next);
});
ipcMain.handle(DESKTOP_AI_RUN_TASK, async (event, request) => {
guard(event);
return service.runTask(request);
});
};
const ensureMainWindow = (): void => { const ensureMainWindow = (): void => {
if (mainWindow) { if (mainWindow) {
return; return;
@ -101,6 +179,20 @@ const ensureMainWindow = (): void => {
}; };
app.whenReady().then(() => { app.whenReady().then(() => {
desktopAiService = new IslandflowDesktopAiService(
app.getPath("userData"),
async (url) => {
await shell.openExternal(url);
},
() => {
broadcastDesktopAiState();
}
);
registerDesktopAiIpc(desktopAiService);
void desktopAiService.start().catch((error) => {
console.error("[desktop-ai] Failed to start Codex bridge:", error);
broadcastDesktopAiState();
});
ensureMainWindow(); ensureMainWindow();
app.on("activate", () => { app.on("activate", () => {

View file

@ -0,0 +1,43 @@
import { contextBridge, ipcRenderer } from "electron";
import type {
IslandflowAiReasoningEffort,
IslandflowAiState,
IslandflowAiTaskRequest
} from "@islandflow/types";
import {
DESKTOP_AI_CANCEL_LOGIN,
DESKTOP_AI_GET_STATE,
DESKTOP_AI_LOGIN_BROWSER,
DESKTOP_AI_LOGIN_DEVICE,
DESKTOP_AI_LOGOUT,
DESKTOP_AI_RUN_TASK,
DESKTOP_AI_STATE_CHANNEL,
DESKTOP_AI_UPDATE_PREFERENCES
} from "./desktop-ai-ipc.js";
const bridge = {
ai: {
getState: (): Promise<IslandflowAiState> => ipcRenderer.invoke(DESKTOP_AI_GET_STATE),
loginWithBrowser: (): Promise<void> => ipcRenderer.invoke(DESKTOP_AI_LOGIN_BROWSER),
loginWithDeviceCode: (): Promise<void> => ipcRenderer.invoke(DESKTOP_AI_LOGIN_DEVICE),
cancelLogin: (): Promise<void> => ipcRenderer.invoke(DESKTOP_AI_CANCEL_LOGIN),
logout: (): Promise<void> => ipcRenderer.invoke(DESKTOP_AI_LOGOUT),
updatePreferences: (
next: Partial<{ model: string | null; reasoningEffort: IslandflowAiReasoningEffort | null }>
): Promise<void> => ipcRenderer.invoke(DESKTOP_AI_UPDATE_PREFERENCES, next),
runTask: (request: IslandflowAiTaskRequest): Promise<{ taskId: string }> =>
ipcRenderer.invoke(DESKTOP_AI_RUN_TASK, request),
subscribe: (listener: (state: IslandflowAiState) => void): (() => void) => {
const handler = (_event: Electron.IpcRendererEvent, state: IslandflowAiState) => {
listener(state);
};
ipcRenderer.on(DESKTOP_AI_STATE_CHANNEL, handler);
return () => {
ipcRenderer.off(DESKTOP_AI_STATE_CHANNEL, handler);
};
}
}
};
contextBridge.exposeInMainWorld("islandflowDesktop", bridge);

View file

@ -2,8 +2,8 @@
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "ES2022",
"module": "NodeNext", "module": "ESNext",
"moduleResolution": "NodeNext", "moduleResolution": "Bundler",
"lib": ["ES2022"], "lib": ["ES2022"],
"types": ["node"], "types": ["node"],
"rootDir": "src", "rootDir": "src",

View file

@ -1,7 +1,7 @@
import { redirect } from "next/navigation"; import { ChartsRoute } from "../terminal";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export default function Page() { export default function Page() {
redirect("/"); return <ChartsRoute />;
} }

View file

@ -0,0 +1,924 @@
"use client";
import Link from "next/link";
import { useMemo, useState, type ReactNode } from "react";
import type {
AlertEvent,
ClassifierHitEvent,
FlowPacket,
IslandflowAiCompiledScreen,
IslandflowAiPlanType,
IslandflowAiRateLimitSnapshot,
IslandflowAiReasoningEffort,
IslandflowAiTaskKind,
OptionFlowFilters,
OptionPrint,
SmartMoneyEvent
} from "@islandflow/types";
import { useDesktopAi } from "./desktop-ai";
const numberFormatter = new Intl.NumberFormat("en-US");
const usdFormatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 4
});
const humanizeValue = (value: string | null | undefined): string => {
if (!value) {
return "Unknown";
}
return value
.replace(/_/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase());
};
const formatTokens = (value: number): string => numberFormatter.format(value);
const formatUsd = (value: number | null): string => (value === null ? "Unavailable" : usdFormatter.format(value));
const formatTimestamp = (value: number | null): string => {
if (!value) {
return "Not reported";
}
return new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
timeStyle: "short"
}).format(value);
};
const formatPercent = (value: number): string => `${Math.round(value)}%`;
const getTaskStatusLabel = (value: string): string => humanizeValue(value);
const findTask = <T extends { taskId: string }>(tasks: T[], taskId: string | null): T | null => {
if (!taskId) {
return null;
}
return tasks.find((task) => task.taskId === taskId) ?? null;
};
const getCompiledScreenSummary = (compiled: IslandflowAiCompiledScreen): string[] => {
const filters = compiled.compiledFilters;
if (!filters) {
return [];
}
const parts: string[] = [];
if (filters.view) {
parts.push(`View: ${filters.view}`);
}
if (filters.securityTypes?.length) {
parts.push(`Security: ${filters.securityTypes.join(", ")}`);
}
if (filters.optionTypes?.length) {
parts.push(`Options: ${filters.optionTypes.join(", ")}`);
}
if (filters.nbboSides?.length) {
parts.push(`NBBO: ${filters.nbboSides.join(", ")}`);
}
if (typeof filters.minNotional === "number") {
parts.push(`Min notional: $${numberFormatter.format(filters.minNotional)}`);
}
return parts;
};
const CopilotPane = ({
title,
eyebrow,
actions,
wide = false,
children
}: {
title: string;
eyebrow?: string;
actions?: ReactNode;
wide?: boolean;
children: ReactNode;
}) => {
return (
<section className={`terminal-pane copilot-pane${wide ? " copilot-pane-wide" : ""}`}>
<div className="terminal-pane-head">
<div className="terminal-pane-title-row">
<div>
{eyebrow ? <div className="copilot-kicker">{eyebrow}</div> : null}
<h2 className="terminal-pane-title">{title}</h2>
</div>
</div>
{actions ? <div className="terminal-pane-actions">{actions}</div> : null}
</div>
<div className="terminal-pane-body copilot-pane-body">{children}</div>
</section>
);
};
const UsageBreakdown = ({
title,
breakdown,
normalizedCostUsd,
turnCount,
activeDays
}: {
title: string;
breakdown: {
totalTokens: number;
inputTokens: number;
cachedInputTokens: number;
outputTokens: number;
reasoningOutputTokens: number;
};
normalizedCostUsd: number | null;
turnCount: number;
activeDays: number;
}) => {
return (
<div className="copilot-usage-block">
<div className="copilot-usage-title-row">
<h3>{title}</h3>
<span className="copilot-usage-cost">{formatUsd(normalizedCostUsd)}</span>
</div>
<div className="copilot-token-grid">
<div className="copilot-token-row">
<span>Total tokens</span>
<strong>{formatTokens(breakdown.totalTokens)}</strong>
</div>
<div className="copilot-token-row">
<span>Input</span>
<strong>{formatTokens(breakdown.inputTokens)}</strong>
</div>
<div className="copilot-token-row">
<span>Cached input</span>
<strong>{formatTokens(breakdown.cachedInputTokens)}</strong>
</div>
<div className="copilot-token-row">
<span>Output</span>
<strong>{formatTokens(breakdown.outputTokens)}</strong>
</div>
<div className="copilot-token-row">
<span>Reasoning</span>
<strong>{formatTokens(breakdown.reasoningOutputTokens)}</strong>
</div>
<div className="copilot-token-row">
<span>Turns</span>
<strong>{formatTokens(turnCount)}</strong>
</div>
<div className="copilot-token-row">
<span>Active days</span>
<strong>{formatTokens(activeDays)}</strong>
</div>
</div>
</div>
);
};
const RateLimitBoard = ({ limit }: { limit: IslandflowAiRateLimitSnapshot }) => {
return (
<div className="copilot-limit-card" key={limit.limitId ?? limit.limitName ?? "default"}>
<div className="copilot-limit-head">
<div>
<strong>{limit.limitName ?? "Default rate window"}</strong>
<p className="copilot-note">
{limit.planType ? `Plan ${humanizeValue(limit.planType)}` : "Plan not reported"}
</p>
</div>
{limit.reachedType ? <span className="copilot-badge warning">{humanizeValue(limit.reachedType)}</span> : null}
</div>
<div className="copilot-limit-grid">
{limit.primary ? (
<div className="copilot-limit-window">
<span>Primary</span>
<strong>{formatPercent(limit.primary.usedPercent)}</strong>
<p className="copilot-note">Resets {formatTimestamp(limit.primary.resetsAt)}</p>
</div>
) : null}
{limit.secondary ? (
<div className="copilot-limit-window">
<span>Secondary</span>
<strong>{formatPercent(limit.secondary.usedPercent)}</strong>
<p className="copilot-note">Resets {formatTimestamp(limit.secondary.resetsAt)}</p>
</div>
) : null}
</div>
{limit.creditsBalance || limit.unlimitedCredits !== null ? (
<p className="copilot-note">
Credits:{" "}
{limit.unlimitedCredits
? "unlimited"
: limit.creditsBalance
? limit.creditsBalance
: limit.hasCredits === false
? "none"
: "not reported"}
</p>
) : null}
</div>
);
};
const TaskOutput = ({
taskId,
emptyMessage
}: {
taskId: string | null;
emptyMessage: string;
}) => {
const { state } = useDesktopAi();
const task = findTask(state.tasks, taskId);
if (!task) {
return <p className="copilot-empty">{emptyMessage}</p>;
}
return (
<div className="copilot-task-output" aria-live="polite">
<div className="copilot-task-head">
<div>
<strong>{task.title}</strong>
<p className="copilot-note">
{task.subtitle} · {getTaskStatusLabel(task.status)}
</p>
</div>
<span className={`copilot-badge status-${task.status}`}>{getTaskStatusLabel(task.status)}</span>
</div>
{task.error ? <p className="copilot-error">{task.error}</p> : null}
{task.text ? <pre className="copilot-task-text">{task.text}</pre> : null}
{task.compiledScreen ? <CompiledScreenResult compiled={task.compiledScreen} /> : null}
</div>
);
};
const CompiledScreenResult = ({ compiled }: { compiled: IslandflowAiCompiledScreen }) => {
const summary = getCompiledScreenSummary(compiled);
return (
<div className="copilot-compiled-screen">
{summary.length > 0 ? (
<div className="copilot-chip-row">
{summary.map((item) => (
<span className="copilot-chip" key={item}>
{item}
</span>
))}
</div>
) : (
<p className="copilot-note">No filter fields were compiled from this prompt.</p>
)}
{compiled.unhandledClauses.length > 0 ? (
<div className="copilot-unhandled-list">
<div className="copilot-list-title">Unhandled clauses</div>
{compiled.unhandledClauses.map((item) => (
<div className="copilot-inline-row" key={item}>
<span>{item}</span>
</div>
))}
</div>
) : null}
</div>
);
};
const AccountSummary = ({
loggedIn,
email,
planType
}: {
loggedIn: boolean;
email: string | null;
planType: IslandflowAiPlanType | null;
}) => {
return (
<div className="copilot-hero">
<div>
<p className="copilot-kicker">Desktop-only official Codex bridge</p>
<h1 className="page-title">Analyst Copilot</h1>
<p className="copilot-hero-copy">
Managed ChatGPT login stays user-scoped, deterministic smart-money classification stays in charge, and every
AI turn is tracked with exact token telemetry from the app-server.
</p>
</div>
<div className="copilot-hero-meta">
<div className="copilot-stat">
<span>Account</span>
<strong>{loggedIn ? email ?? "Connected" : "Disconnected"}</strong>
</div>
<div className="copilot-stat">
<span>Plan</span>
<strong>{loggedIn ? humanizeValue(planType) : "Not connected"}</strong>
</div>
</div>
</div>
);
};
const LoginStatePanel = () => {
const { state, loginWithBrowser, loginWithDeviceCode, cancelLogin, logout } = useDesktopAi();
const [busyAction, setBusyAction] = useState<string | null>(null);
const [actionError, setActionError] = useState<string | null>(null);
const loginState = state.account.login;
const actionsDisabled = busyAction !== null || !state.desktopAvailable;
const runAction = async (label: string, action: () => Promise<void>) => {
setBusyAction(label);
setActionError(null);
try {
await action();
} catch (error) {
setActionError(error instanceof Error ? error.message : String(error));
} finally {
setBusyAction(null);
}
};
return (
<CopilotPane
title="Account and access"
eyebrow="Managed auth"
wide
actions={
<>
{state.account.loggedIn ? (
<button
className="terminal-button"
type="button"
onClick={() => void runAction("logout", logout)}
disabled={actionsDisabled}
>
{busyAction === "logout" ? "Logging out" : "Logout"}
</button>
) : (
<>
<button
className="terminal-button terminal-button-primary"
type="button"
onClick={() => void runAction("browser", loginWithBrowser)}
disabled={actionsDisabled}
>
{busyAction === "browser" ? "Opening browser" : "Browser login"}
</button>
<button
className="terminal-button"
type="button"
onClick={() => void runAction("device", loginWithDeviceCode)}
disabled={actionsDisabled}
>
{busyAction === "device" ? "Preparing code" : "Device code"}
</button>
</>
)}
{(loginState.status === "browser_pending" || loginState.status === "device_code_pending") && !state.account.loggedIn ? (
<button
className="terminal-button"
type="button"
onClick={() => void runAction("cancel", cancelLogin)}
disabled={actionsDisabled}
>
Cancel
</button>
) : null}
</>
}
>
<AccountSummary
loggedIn={state.account.loggedIn}
email={state.account.email}
planType={state.account.planType}
/>
<div className="copilot-account-grid">
<div className="copilot-account-card">
<div className="copilot-list-title">Profile slots</div>
{state.profiles.map((profile) => (
<div className="copilot-inline-row" key={profile.id}>
<div>
<strong>{profile.label}</strong>
<p className="copilot-note">{profile.description}</p>
</div>
<span className={`copilot-badge${profile.enabled ? "" : " muted"}`}>
{profile.selected ? "Selected" : profile.statusLabel}
</span>
</div>
))}
</div>
<div className="copilot-account-card">
<div className="copilot-list-title">Session status</div>
<div className="copilot-inline-row">
<span>Transport</span>
<strong>{humanizeValue(state.transportStatus)}</strong>
</div>
<div className="copilot-inline-row">
<span>Auth mode</span>
<strong>{humanizeValue(state.account.authMode)}</strong>
</div>
<div className="copilot-inline-row">
<span>OpenAI auth required</span>
<strong>{state.account.requiresOpenaiAuth ? "Yes" : "No"}</strong>
</div>
{state.transportError ? <p className="copilot-error">{state.transportError}</p> : null}
{loginState.message ? <p className="copilot-note">{loginState.message}</p> : null}
{loginState.status === "browser_pending" ? (
<div className="copilot-callout">
<strong>Browser login in progress</strong>
<p className="copilot-note">Finish the ChatGPT sign-in flow in your browser. Islandflow will update automatically.</p>
</div>
) : null}
{loginState.status === "device_code_pending" ? (
<div className="copilot-callout">
<strong>Device code</strong>
<pre className="copilot-device-code">{loginState.userCode}</pre>
<p className="copilot-note">Visit {loginState.verificationUrl} in any browser and enter the code above.</p>
</div>
) : null}
{actionError ? <p className="copilot-error">{actionError}</p> : null}
</div>
</div>
</CopilotPane>
);
};
export function DesktopAiSettingsRoute() {
const { state, updatePreferences } = useDesktopAi();
const [busyPreference, setBusyPreference] = useState<"model" | "reasoning" | null>(null);
const [preferenceError, setPreferenceError] = useState<string | null>(null);
const rateLimits = Object.values(state.rateLimitsByLimitId);
const selectedModel = state.preferences.model ?? "";
const selectedReasoning = state.preferences.reasoningEffort ?? "";
const savePreference = async (
key: "model" | "reasoning",
next: Partial<{ model: string | null; reasoningEffort: IslandflowAiReasoningEffort | null }>
) => {
setBusyPreference(key);
setPreferenceError(null);
try {
await updatePreferences(next);
} catch (error) {
setPreferenceError(error instanceof Error ? error.message : String(error));
} finally {
setBusyPreference(null);
}
};
return (
<div className="page-shell">
{!state.desktopAvailable ? (
<CopilotPane title="Desktop required" eyebrow="Browser-only fallback" wide>
<div className="copilot-unavailable">
<p>
AI controls are intentionally read-only in the browser build. Open Islandflow Desktop to use managed ChatGPT
login, structured Copilot turns, and app-server token telemetry.
</p>
</div>
</CopilotPane>
) : null}
<LoginStatePanel />
<div className="page-grid page-grid-settings">
<CopilotPane title="Model controls" eyebrow="Execution">
<div className="copilot-field-grid">
<label className="copilot-field">
<span className="copilot-field-label">Model</span>
<select
className="copilot-select"
value={selectedModel}
onChange={(event) =>
void savePreference("model", { model: event.target.value.trim() ? event.target.value : null })
}
disabled={busyPreference !== null || state.models.length === 0 || !state.desktopAvailable}
>
<option value="">Use server default</option>
{state.models.map((model) => (
<option key={model.id} value={model.model}>
{model.displayName}
</option>
))}
</select>
</label>
<label className="copilot-field">
<span className="copilot-field-label">Reasoning</span>
<select
className="copilot-select"
value={selectedReasoning}
onChange={(event) =>
void savePreference("reasoning", {
reasoningEffort: event.target.value.trim()
? (event.target.value as IslandflowAiReasoningEffort)
: null
})
}
disabled={busyPreference !== null || !state.desktopAvailable}
>
<option value="">Use model default</option>
<option value="none">None</option>
<option value="minimal">Minimal</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="xhigh">XHigh</option>
</select>
</label>
</div>
<div className="copilot-model-list">
{state.models.map((model) => (
<div className="copilot-model-row" key={model.id}>
<div>
<strong>{model.displayName}</strong>
<p className="copilot-note">{model.description}</p>
</div>
<div className="copilot-model-meta">
<span>{model.model}</span>
{model.pricing ? <span>{formatUsd(model.pricing.inputUsdPer1MTokens)} / 1M input</span> : null}
</div>
</div>
))}
</div>
{state.models.find((model) => model.model === state.preferences.model)?.pricing ? (
<p className="copilot-note">
Normalized estimates use current API pricing for the selected model, not your literal ChatGPT subscription bill.
</p>
) : null}
{preferenceError ? <p className="copilot-error">{preferenceError}</p> : null}
</CopilotPane>
<CopilotPane title="Rate limits" eyebrow="Live windows">
{rateLimits.length === 0 ? (
<p className="copilot-empty">No rate-limit snapshots have been reported yet.</p>
) : (
<div className="copilot-limit-list">
{rateLimits.map((limit) => (
<RateLimitBoard key={limit.limitId ?? limit.limitName ?? "default"} limit={limit} />
))}
</div>
)}
</CopilotPane>
<CopilotPane title="Usage dashboard" eyebrow="Exact app-server telemetry" wide>
<div className="copilot-usage-grid">
<UsageBreakdown
title="Today"
breakdown={state.usage.today.breakdown}
normalizedCostUsd={state.usage.today.normalizedCostUsd}
turnCount={state.usage.today.turnCount}
activeDays={state.usage.today.activeDays}
/>
<UsageBreakdown
title="Lifetime"
breakdown={state.usage.lifetime.breakdown}
normalizedCostUsd={state.usage.lifetime.normalizedCostUsd}
turnCount={state.usage.lifetime.turnCount}
activeDays={state.usage.lifetime.activeDays}
/>
</div>
</CopilotPane>
<CopilotPane title="Recent turns" eyebrow="Per-thread usage">
{state.usage.recentTurns.length === 0 ? (
<p className="copilot-empty">No tracked turns yet.</p>
) : (
<div className="copilot-turn-list">
{state.usage.recentTurns.map((turn) => (
<div className="copilot-turn-row" key={`${turn.threadId}:${turn.turnId}`}>
<div>
<strong>{turn.taskTitle ?? "Ad hoc turn"}</strong>
<p className="copilot-note">
{turn.model ?? "default"} · {formatTimestamp(turn.updatedAt)}
</p>
</div>
<div className="copilot-turn-metrics">
<span>{formatTokens(turn.breakdown.totalTokens)} tok</span>
<span>{formatUsd(turn.normalizedCostUsd)}</span>
</div>
</div>
))}
</div>
)}
</CopilotPane>
<CopilotPane title="Recent analyses" eyebrow="Task feed">
{state.tasks.length === 0 ? (
<p className="copilot-empty">No Copilot tasks have been run yet.</p>
) : (
<div className="copilot-task-list">
{state.tasks.map((task) => (
<div className="copilot-task-list-row" key={task.taskId}>
<div>
<strong>{task.title}</strong>
<p className="copilot-note">
{task.subtitle} · {humanizeValue(task.model)}
</p>
</div>
<span className={`copilot-badge status-${task.status}`}>{getTaskStatusLabel(task.status)}</span>
</div>
))}
</div>
)}
</CopilotPane>
</div>
</div>
);
}
const requireDesktopActionCopy = (desktopAvailable: boolean, loggedIn: boolean): string => {
if (!desktopAvailable) {
return "This control is desktop-only. Open Islandflow Desktop to run Copilot tasks.";
}
if (!loggedIn) {
return "Connect a ChatGPT or Codex account in Settings before running Copilot analysis.";
}
return "";
};
const SmartMoneyTaskButton = ({
label,
kind,
symbol,
disabled,
busyKind,
onRun
}: {
label: string;
kind: IslandflowAiTaskKind;
symbol: string;
disabled: boolean;
busyKind: IslandflowAiTaskKind | null;
onRun: (kind: IslandflowAiTaskKind) => void;
}) => {
return (
<button
className={`terminal-button${kind === "smart-money-explain" ? " terminal-button-primary" : ""}`}
type="button"
onClick={() => onRun(kind)}
disabled={busyKind !== null || disabled}
title={`${label} for ${symbol}`}
>
{busyKind === kind ? "Running" : label}
</button>
);
};
export function SmartMoneyCopilotPanel({
event,
flowPacket,
evidencePrints,
relatedPackets
}: {
event: SmartMoneyEvent;
flowPacket: FlowPacket | null;
evidencePrints: OptionPrint[];
relatedPackets: FlowPacket[];
}) {
const { bridgeAvailable, state, runTask } = useDesktopAi();
const [busyKind, setBusyKind] = useState<IslandflowAiTaskKind | null>(null);
const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
const [taskError, setTaskError] = useState<string | null>(null);
const disabledCopy = requireDesktopActionCopy(bridgeAvailable, state.account.loggedIn);
const actionsDisabled = !bridgeAvailable || !state.account.loggedIn;
const handleRun = async (kind: IslandflowAiTaskKind) => {
setBusyKind(kind);
setTaskError(null);
try {
const result = await runTask({
kind: kind as
| "smart-money-explain"
| "smart-money-skeptic"
| "smart-money-burst-summary"
| "watchlist-synthesis",
context: {
event,
flowPacket,
evidencePrints,
relatedPackets
}
});
setActiveTaskId(result.taskId);
} catch (error) {
setTaskError(error instanceof Error ? error.message : String(error));
} finally {
setBusyKind(null);
}
};
return (
<div className="copilot-inline-panel">
<div className="copilot-inline-head">
<div>
<div className="copilot-list-title">Analyst Copilot</div>
<p className="copilot-note">Structured interpretation only, the deterministic classifier remains the source of truth.</p>
</div>
<Link className="terminal-button" href="/settings">
AI settings
</Link>
</div>
<div className="copilot-action-grid">
<SmartMoneyTaskButton
label="Explain"
kind="smart-money-explain"
symbol={event.underlying_id}
disabled={actionsDisabled}
busyKind={busyKind}
onRun={(kind) => void handleRun(kind)}
/>
<SmartMoneyTaskButton
label="Counter-thesis"
kind="smart-money-skeptic"
symbol={event.underlying_id}
disabled={actionsDisabled}
busyKind={busyKind}
onRun={(kind) => void handleRun(kind)}
/>
<SmartMoneyTaskButton
label="Burst summary"
kind="smart-money-burst-summary"
symbol={event.underlying_id}
disabled={actionsDisabled}
busyKind={busyKind}
onRun={(kind) => void handleRun(kind)}
/>
<SmartMoneyTaskButton
label="Watchlist"
kind="watchlist-synthesis"
symbol={event.underlying_id}
disabled={actionsDisabled}
busyKind={busyKind}
onRun={(kind) => void handleRun(kind)}
/>
</div>
{disabledCopy ? <p className="copilot-note">{disabledCopy}</p> : null}
{taskError ? <p className="copilot-error">{taskError}</p> : null}
<TaskOutput taskId={activeTaskId} emptyMessage="Run an explanation, skepticism pass, burst summary, or watchlist synthesis to see the result here." />
</div>
);
}
export function ReplayCopilotPanel({
ticker,
flowFilters,
alerts,
smartMoneyEvents,
classifierHits,
flowPackets,
optionPrints
}: {
ticker: string | null;
flowFilters: OptionFlowFilters;
alerts: AlertEvent[];
smartMoneyEvents: SmartMoneyEvent[];
classifierHits: ClassifierHitEvent[];
flowPackets: FlowPacket[];
optionPrints: OptionPrint[];
}) {
const { bridgeAvailable, state, runTask } = useDesktopAi();
const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const [taskError, setTaskError] = useState<string | null>(null);
const disabledCopy = requireDesktopActionCopy(bridgeAvailable, state.account.loggedIn);
const actionsDisabled = busy || !bridgeAvailable || !state.account.loggedIn;
const handleRun = async () => {
setBusy(true);
setTaskError(null);
try {
const result = await runTask({
kind: "replay-postmortem",
context: {
ticker,
flowFilters,
alerts,
smartMoneyEvents,
classifierHits,
flowPackets,
optionPrints
}
});
setActiveTaskId(result.taskId);
} catch (error) {
setTaskError(error instanceof Error ? error.message : String(error));
} finally {
setBusy(false);
}
};
return (
<CopilotPane
title="Replay postmortem"
eyebrow="Structured recap"
actions={
<>
<Link className="terminal-button" href="/settings">
AI settings
</Link>
<button
className="terminal-button terminal-button-primary"
type="button"
onClick={() => void handleRun()}
disabled={actionsDisabled}
>
{busy ? "Running" : "Generate postmortem"}
</button>
</>
}
>
<p className="copilot-note">
Copilot uses the current replay slice only: ticker scope, flow filters, visible alerts, classifier hits, packets, and option prints.
</p>
{disabledCopy ? <p className="copilot-note">{disabledCopy}</p> : null}
{taskError ? <p className="copilot-error">{taskError}</p> : null}
<TaskOutput taskId={activeTaskId} emptyMessage="Generate a replay postmortem to capture the cleanest read from the current session slice." />
</CopilotPane>
);
}
export function ScreenCompilerPanel({
currentFilters,
onApplyFilters
}: {
currentFilters: OptionFlowFilters;
onApplyFilters: (next: OptionFlowFilters) => void;
}) {
const { bridgeAvailable, state, runTask } = useDesktopAi();
const [prompt, setPrompt] = useState("");
const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const [taskError, setTaskError] = useState<string | null>(null);
const activeTask = useMemo(() => findTask(state.tasks, activeTaskId), [state.tasks, activeTaskId]);
const disabledCopy = requireDesktopActionCopy(bridgeAvailable, state.account.loggedIn);
const actionsDisabled = busy || !bridgeAvailable || !state.account.loggedIn;
const handleCompile = async () => {
const trimmedPrompt = prompt.trim();
if (!trimmedPrompt) {
setTaskError("Write a screen request first.");
return;
}
setBusy(true);
setTaskError(null);
try {
const result = await runTask({
kind: "screen-compile",
context: {
prompt: trimmedPrompt,
currentFilters
}
});
setActiveTaskId(result.taskId);
} catch (error) {
setTaskError(error instanceof Error ? error.message : String(error));
} finally {
setBusy(false);
}
};
const compiledFilters = activeTask?.compiledScreen?.compiledFilters ?? null;
return (
<CopilotPane
title="Natural-language screens"
eyebrow="Tape workflow"
actions={
<>
<Link className="terminal-button" href="/settings">
AI settings
</Link>
<button
className="terminal-button terminal-button-primary"
type="button"
onClick={() => void handleCompile()}
disabled={actionsDisabled}
>
{busy ? "Compiling" : "Compile screen"}
</button>
</>
}
>
<div className="copilot-inline-form">
<label className="copilot-field">
<span className="copilot-field-label">Prompt</span>
<textarea
className="copilot-textarea"
rows={4}
value={prompt}
onChange={(event) => setPrompt(event.target.value)}
placeholder="High-notional single-name call buying near the ask, ignore ETFs, keep it signal-only."
/>
</label>
<div className="copilot-current-filters">
<div className="copilot-list-title">Current filter baseline</div>
<pre className="copilot-json-block">{JSON.stringify(currentFilters, null, 2)}</pre>
</div>
</div>
{disabledCopy ? <p className="copilot-note">{disabledCopy}</p> : null}
{taskError ? <p className="copilot-error">{taskError}</p> : null}
{compiledFilters ? (
<div className="copilot-apply-row">
<button className="terminal-button" type="button" onClick={() => onApplyFilters(compiledFilters)}>
Apply compiled filters
</button>
</div>
) : null}
<TaskOutput taskId={activeTaskId} emptyMessage="Compile a natural-language screen to preview the translated filter set and rationale." />
</CopilotPane>
);
}

179
apps/web/app/desktop-ai.tsx Normal file
View file

@ -0,0 +1,179 @@
"use client";
import {
createContext,
useContext,
useEffect,
useMemo,
useState,
type ReactNode
} from "react";
import type {
IslandflowAiReasoningEffort,
IslandflowAiState,
IslandflowAiTaskRequest
} from "@islandflow/types";
type DesktopAiBridge = {
ai: {
getState: () => Promise<IslandflowAiState>;
loginWithBrowser: () => Promise<void>;
loginWithDeviceCode: () => Promise<void>;
cancelLogin: () => Promise<void>;
logout: () => Promise<void>;
updatePreferences: (
next: Partial<{ model: string | null; reasoningEffort: IslandflowAiReasoningEffort | null }>
) => Promise<void>;
runTask: (request: IslandflowAiTaskRequest) => Promise<{ taskId: string }>;
subscribe: (listener: (state: IslandflowAiState) => void) => () => void;
};
};
declare global {
interface Window {
islandflowDesktop?: DesktopAiBridge;
}
}
type DesktopAiContextValue = {
bridgeAvailable: boolean;
state: IslandflowAiState;
loginWithBrowser: () => Promise<void>;
loginWithDeviceCode: () => Promise<void>;
cancelLogin: () => Promise<void>;
logout: () => Promise<void>;
updatePreferences: (
next: Partial<{ model: string | null; reasoningEffort: IslandflowAiReasoningEffort | null }>
) => Promise<void>;
runTask: (request: IslandflowAiTaskRequest) => Promise<{ taskId: string }>;
};
const createUnavailableState = (): IslandflowAiState => ({
desktopAvailable: false,
transportStatus: "stopped",
transportError: "Desktop AI is only available inside the Islandflow Electron app.",
profiles: [
{
id: "managed-chatgpt",
label: "Managed ChatGPT login",
description: "Available only in the desktop app.",
mode: "managed-chatgpt",
enabled: false,
selected: true,
statusLabel: "Desktop only"
}
],
selectedProfileId: "managed-chatgpt",
account: {
loggedIn: false,
email: null,
planType: null,
authMode: null,
requiresOpenaiAuth: true,
login: {
status: "idle",
message: "Open Islandflow Desktop to connect a ChatGPT or Codex account."
}
},
preferences: {
model: null,
reasoningEffort: "high"
},
models: [],
rateLimitsByLimitId: {},
usage: {
today: {
breakdown: {
totalTokens: 0,
inputTokens: 0,
cachedInputTokens: 0,
outputTokens: 0,
reasoningOutputTokens: 0
},
normalizedCostUsd: 0,
turnCount: 0,
activeDays: 0
},
lifetime: {
breakdown: {
totalTokens: 0,
inputTokens: 0,
cachedInputTokens: 0,
outputTokens: 0,
reasoningOutputTokens: 0
},
normalizedCostUsd: 0,
turnCount: 0,
activeDays: 0
},
recentTurns: []
},
tasks: [],
updatedAt: Date.now()
});
const DesktopAiContext = createContext<DesktopAiContextValue | null>(null);
const rejectDesktopOnly = async (): Promise<never> => {
throw new Error("Desktop AI is only available inside the Islandflow Electron app.");
};
export function DesktopAiProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<IslandflowAiState>(() => createUnavailableState());
const [bridge, setBridge] = useState<DesktopAiBridge | null>(null);
useEffect(() => {
if (typeof window === "undefined") {
return;
}
const nextBridge = window.islandflowDesktop ?? null;
if (!nextBridge?.ai) {
setBridge(null);
setState(createUnavailableState());
return;
}
setBridge(nextBridge);
let unsubscribe = () => {};
void nextBridge.ai.getState().then(setState).catch(() => {
setState((current) => ({
...current,
transportStatus: "error",
transportError: "The desktop AI bridge could not load its initial state."
}));
});
unsubscribe = nextBridge.ai.subscribe((nextState) => {
setState(nextState);
});
return () => {
unsubscribe();
};
}, []);
const value = useMemo<DesktopAiContextValue>(
() => ({
bridgeAvailable: Boolean(bridge?.ai),
state,
loginWithBrowser: bridge?.ai.loginWithBrowser ?? rejectDesktopOnly,
loginWithDeviceCode: bridge?.ai.loginWithDeviceCode ?? rejectDesktopOnly,
cancelLogin: bridge?.ai.cancelLogin ?? rejectDesktopOnly,
logout: bridge?.ai.logout ?? rejectDesktopOnly,
updatePreferences: bridge?.ai.updatePreferences ?? rejectDesktopOnly,
runTask: bridge?.ai.runTask ?? rejectDesktopOnly
}),
[bridge, state]
);
return <DesktopAiContext.Provider value={value}>{children}</DesktopAiContext.Provider>;
}
export const useDesktopAi = (): DesktopAiContextValue => {
const value = useContext(DesktopAiContext);
if (!value) {
throw new Error("Desktop AI context missing");
}
return value;
};

View file

@ -276,6 +276,25 @@ input {
margin-left: auto; margin-left: auto;
} }
.terminal-topbar-summary {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
margin-right: auto;
}
.terminal-topbar-summary strong {
display: block;
font-size: 0.8rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.terminal-topbar-summary .copilot-note {
margin: 4px 0 0;
}
.terminal-filter { .terminal-filter {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -708,12 +727,26 @@ h3 {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
.page-grid-news {
grid-template-columns: minmax(0, 1fr);
}
.page-grid-settings {
grid-template-columns: minmax(0, 1.25fr) minmax(320px, 0.9fr);
align-items: start;
}
.page-grid-home > :nth-child(3), .page-grid-home > :nth-child(3),
.page-grid-home > :nth-child(4),
.page-grid-tape > :nth-child(1), .page-grid-tape > :nth-child(1),
.page-grid-replay > :nth-child(1) { .page-grid-replay > :nth-child(1) {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
.page-grid-settings > .copilot-pane-wide {
grid-column: 1 / -1;
}
.terminal-pane { .terminal-pane {
min-width: 0; min-width: 0;
height: 100%; height: 100%;
@ -933,6 +966,7 @@ h3 {
} }
.page-grid-home > :nth-child(3), .page-grid-home > :nth-child(3),
.page-grid-home > :nth-child(4),
.page-grid-replay > :not(:first-child) { .page-grid-replay > :not(:first-child) {
height: clamp(430px, 58vh, 760px); height: clamp(430px, 58vh, 760px);
} }
@ -973,6 +1007,325 @@ h3 {
height: clamp(430px, 58vh, 760px); height: clamp(430px, 58vh, 760px);
} }
.copilot-pane {
background:
radial-gradient(circle at top right, oklch(0.8 0.12 74 / 0.07), transparent 36%),
linear-gradient(180deg, oklch(0.18 0.013 250) 0%, oklch(0.16 0.012 250) 100%);
}
.copilot-pane-body {
gap: 18px;
}
.copilot-kicker,
.copilot-field-label,
.copilot-list-title {
color: var(--text-faint);
text-transform: uppercase;
letter-spacing: 0.16em;
font-size: 0.68rem;
}
.copilot-kicker {
margin-bottom: 8px;
}
.copilot-hero {
display: grid;
grid-template-columns: minmax(0, 1.5fr) minmax(280px, 0.7fr);
gap: 18px;
padding: 18px;
border: 1px solid var(--border);
border-radius: 14px;
background:
linear-gradient(135deg, oklch(0.2 0.017 250 / 0.92), oklch(0.16 0.012 250 / 0.96)),
var(--bg-pane-2);
}
.copilot-hero-copy {
max-width: 62ch;
margin: 10px 0 0;
color: var(--text-dim);
line-height: 1.6;
}
.copilot-hero-meta,
.copilot-account-grid,
.copilot-usage-grid,
.copilot-field-grid,
.copilot-limit-grid,
.copilot-action-grid,
.copilot-inline-form {
display: grid;
gap: 14px;
}
.copilot-hero-meta {
grid-template-columns: minmax(0, 1fr);
}
.copilot-stat,
.copilot-account-card,
.copilot-usage-block,
.copilot-limit-card,
.copilot-current-filters,
.copilot-callout {
padding: 14px 16px;
border: 1px solid var(--border);
border-radius: 12px;
background: oklch(0.13 0.01 250 / 0.64);
}
.copilot-stat span,
.copilot-token-row span,
.copilot-limit-window span {
display: block;
margin-bottom: 6px;
color: var(--text-faint);
text-transform: uppercase;
letter-spacing: 0.15em;
font-size: 0.65rem;
}
.copilot-stat strong,
.copilot-token-row strong,
.copilot-limit-window strong,
.copilot-device-code,
.copilot-model-meta,
.copilot-turn-metrics {
font-family: var(--font-mono), monospace;
}
.copilot-stat strong {
font-size: 1rem;
line-height: 1.4;
}
.copilot-account-grid,
.copilot-usage-grid,
.copilot-field-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.copilot-inline-row,
.copilot-turn-row,
.copilot-task-list-row,
.copilot-model-row,
.copilot-token-row,
.copilot-usage-title-row,
.copilot-limit-head,
.copilot-task-head,
.copilot-inline-head,
.copilot-apply-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.copilot-inline-row,
.copilot-turn-row,
.copilot-task-list-row,
.copilot-model-row {
padding: 10px 0;
border-top: 1px solid oklch(0.72 0.012 250 / 0.12);
}
.copilot-account-card > :first-child,
.copilot-limit-card > :first-child,
.copilot-turn-list > :first-child,
.copilot-task-list > :first-child,
.copilot-model-list > :first-child,
.copilot-unhandled-list > :first-child {
border-top: 0;
}
.copilot-note {
color: var(--text-dim);
line-height: 1.5;
}
.copilot-note,
.copilot-error,
.copilot-empty,
.copilot-device-code,
.copilot-task-text,
.copilot-json-block {
margin: 0;
}
.copilot-error {
color: oklch(0.8 0.11 28);
line-height: 1.5;
}
.copilot-badge {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 0 10px;
border-radius: 999px;
border: 1px solid var(--border);
background: oklch(0.97 0.008 250 / 0.04);
color: var(--text-dim);
font-size: 0.66rem;
text-transform: uppercase;
letter-spacing: 0.14em;
}
.copilot-badge.muted {
opacity: 0.7;
}
.copilot-badge.warning,
.copilot-badge.status-running {
border-color: oklch(0.78 0.12 74 / 0.38);
background: oklch(0.78 0.12 74 / 0.09);
color: oklch(0.88 0.06 76);
}
.copilot-badge.status-completed {
border-color: oklch(0.74 0.13 151 / 0.34);
background: oklch(0.74 0.13 151 / 0.09);
color: oklch(0.88 0.05 151);
}
.copilot-badge.status-failed,
.copilot-badge.status-cancelled {
border-color: oklch(0.68 0.16 28 / 0.36);
background: oklch(0.68 0.16 28 / 0.1);
color: oklch(0.88 0.05 28);
}
.copilot-field {
display: grid;
gap: 8px;
}
.copilot-select,
.copilot-textarea {
width: 100%;
border: 1px solid var(--border);
border-radius: 10px;
background: oklch(0.11 0.009 250 / 0.82);
color: var(--text);
}
.copilot-select {
min-height: 40px;
padding: 0 12px;
}
.copilot-textarea {
padding: 12px;
resize: vertical;
min-height: 112px;
line-height: 1.55;
}
.copilot-model-list,
.copilot-limit-list,
.copilot-turn-list,
.copilot-task-list,
.copilot-unhandled-list {
display: grid;
gap: 0;
}
.copilot-model-meta,
.copilot-turn-metrics {
display: grid;
gap: 4px;
justify-items: end;
color: var(--text-dim);
font-size: 0.76rem;
}
.copilot-token-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 14px;
}
.copilot-token-row,
.copilot-limit-window {
padding: 12px;
border: 1px solid var(--border);
border-radius: 10px;
background: oklch(0.97 0.008 250 / 0.03);
}
.copilot-usage-title-row h3,
.copilot-limit-head strong {
margin: 0;
}
.copilot-usage-cost {
font-family: var(--font-mono), monospace;
color: var(--accent);
}
.copilot-inline-panel {
display: grid;
gap: 14px;
}
.copilot-action-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.copilot-task-output,
.copilot-unavailable {
display: grid;
gap: 12px;
padding: 14px 16px;
border: 1px solid var(--border);
border-radius: 12px;
background: oklch(0.12 0.01 250 / 0.72);
}
.copilot-task-text,
.copilot-json-block,
.copilot-device-code {
padding: 12px;
border-radius: 10px;
border: 1px solid var(--border);
background: oklch(0.1 0.009 250 / 0.92);
white-space: pre-wrap;
word-break: break-word;
line-height: 1.55;
}
.copilot-device-code {
font-size: clamp(1.3rem, 2vw, 1.7rem);
letter-spacing: 0.18em;
text-align: center;
}
.copilot-chip-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.copilot-chip {
display: inline-flex;
align-items: center;
min-height: 30px;
padding: 0 10px;
border-radius: 999px;
background: var(--accent-soft);
border: 1px solid var(--border-strong);
font-family: var(--font-mono), monospace;
font-size: 0.72rem;
}
.copilot-compiled-screen {
display: grid;
gap: 10px;
}
.row { .row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -1747,6 +2100,72 @@ h3 {
gap: 10px; gap: 10px;
} }
.terminal-link-button {
text-decoration: none;
}
.news-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.news-row {
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
padding: 14px 16px;
border: 1px solid var(--border);
border-radius: 12px;
background: oklch(0.18 0.012 250 / 0.6);
color: var(--text);
text-align: left;
transition: border-color 150ms ease, background 150ms ease;
}
.news-row:hover {
border-color: var(--accent-soft);
background: oklch(0.2 0.015 250 / 0.75);
}
.news-row-head,
.news-row-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
flex-wrap: wrap;
}
.news-row h3 {
margin: 0;
font-size: 0.96rem;
font-weight: 600;
}
.news-row-time {
color: var(--text-dim);
font-family: var(--font-mono), monospace;
font-size: 0.78rem;
}
.news-row-meta {
color: var(--text-dim);
font-size: 0.78rem;
}
.news-drawer-body a {
color: var(--accent);
}
.news-drawer-body p,
.news-drawer-body ul,
.news-drawer-body ol,
.news-drawer-body blockquote {
margin: 0 0 12px;
}
.synthetic-status-grid strong, .synthetic-status-grid strong,
.synthetic-hit-row strong { .synthetic-hit-row strong {
font-family: var(--font-mono), monospace; font-family: var(--font-mono), monospace;
@ -1958,12 +2377,14 @@ h3 {
.page-grid-signals, .page-grid-signals,
.page-grid-charts, .page-grid-charts,
.page-grid-replay, .page-grid-replay,
.page-grid-settings,
.replay-matrix, .replay-matrix,
.shell-metrics { .shell-metrics {
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
} }
.page-grid-home > :nth-child(3), .page-grid-home > :nth-child(3),
.page-grid-home > :nth-child(4),
.page-grid-tape > :nth-child(1), .page-grid-tape > :nth-child(1),
.page-grid-replay > :nth-child(1) { .page-grid-replay > :nth-child(1) {
grid-column: auto; grid-column: auto;
@ -1973,6 +2394,7 @@ h3 {
.page-grid-home > :nth-child(1), .page-grid-home > :nth-child(1),
.page-grid-home > :nth-child(2), .page-grid-home > :nth-child(2),
.page-grid-home > :nth-child(3), .page-grid-home > :nth-child(3),
.page-grid-home > :nth-child(4),
.page-grid-signals > .terminal-pane, .page-grid-signals > .terminal-pane,
.page-grid-replay > :not(:first-child), .page-grid-replay > :not(:first-child),
.page-grid-tape > :first-child, .page-grid-tape > :first-child,
@ -1986,6 +2408,13 @@ h3 {
min-height: 0; min-height: 0;
} }
.copilot-hero,
.copilot-account-grid,
.copilot-usage-grid,
.copilot-field-grid {
grid-template-columns: minmax(0, 1fr);
}
.terminal-topbar { .terminal-topbar {
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
@ -2068,7 +2497,15 @@ h3 {
.terminal-pane-head, .terminal-pane-head,
.chart-controls, .chart-controls,
.card-controls, .card-controls,
.terminal-pane-actions { .terminal-pane-actions,
.copilot-inline-head,
.copilot-usage-title-row,
.copilot-task-head,
.copilot-turn-row,
.copilot-task-list-row,
.copilot-model-row,
.copilot-inline-row,
.copilot-apply-row {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
} }
@ -2107,7 +2544,8 @@ h3 {
} }
.terminal-topbar-actions, .terminal-topbar-actions,
.terminal-topbar-controls { .terminal-topbar-controls,
.terminal-topbar-summary {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
} }
@ -2115,7 +2553,10 @@ h3 {
.terminal-topbar-mode .terminal-button, .terminal-topbar-mode .terminal-button,
.terminal-topbar-controls > .terminal-button, .terminal-topbar-controls > .terminal-button,
.page-actions > .terminal-button, .page-actions > .terminal-button,
.page-actions > .flow-filter-popover { .page-actions > .flow-filter-popover,
.copilot-action-grid > .terminal-button,
.copilot-inline-head > .terminal-button,
.copilot-apply-row > .terminal-button {
width: 100%; width: 100%;
} }
@ -2198,7 +2639,9 @@ h3 {
.flow-filter-checkbox-grid, .flow-filter-checkbox-grid,
.flow-filter-checkbox-grid-wide, .flow-filter-checkbox-grid-wide,
.flow-filter-chip-grid { .flow-filter-chip-grid,
.copilot-action-grid,
.copilot-token-grid {
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
} }

View file

@ -1,6 +1,7 @@
import "./globals.css"; import "./globals.css";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { IBM_Plex_Mono, IBM_Plex_Sans, Quantico } from "next/font/google"; import { IBM_Plex_Mono, IBM_Plex_Sans, Quantico } from "next/font/google";
import { DesktopAiProvider } from "./desktop-ai";
import { TerminalAppShell } from "./terminal"; import { TerminalAppShell } from "./terminal";
const display = Quantico({ const display = Quantico({
@ -34,7 +35,9 @@ export default function RootLayout({ children }: RootLayoutProps) {
return ( return (
<html lang="en"> <html lang="en">
<body className={`${display.variable} ${sans.variable} ${mono.variable}`}> <body className={`${display.variable} ${sans.variable} ${mono.variable}`}>
<TerminalAppShell>{children}</TerminalAppShell> <DesktopAiProvider>
<TerminalAppShell>{children}</TerminalAppShell>
</DesktopAiProvider>
</body> </body>
</html> </html>
); );

View file

@ -0,0 +1,7 @@
import { NewsRoute } from "../terminal";
export const dynamic = "force-dynamic";
export default function Page() {
return <NewsRoute />;
}

View file

@ -1,7 +1,7 @@
import { redirect } from "next/navigation"; import { ReplayRoute } from "../terminal";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export default function Page() { export default function Page() {
redirect("/"); return <ReplayRoute />;
} }

View file

@ -1,31 +1,29 @@
import { beforeEach, describe, expect, it, mock } from "bun:test"; import { describe, expect, it } from "bun:test";
const redirect = mock((path: string) => { import { ChartsRoute, ReplayRoute, SettingsRoute, SignalsRoute } from "./terminal";
throw new Error(`NEXT_REDIRECT:${path}`);
});
mock.module("next/navigation", () => ({ redirect })); describe("route entrypoints", () => {
it("renders the signals route directly", async () => {
describe("legacy page redirects", () => {
beforeEach(() => {
redirect.mockClear();
});
it("redirects /signals to home", async () => {
const mod = await import("./signals/page"); const mod = await import("./signals/page");
expect(() => mod.default()).toThrow("NEXT_REDIRECT:/"); expect(mod.dynamic).toBe("force-dynamic");
expect(redirect).toHaveBeenCalledWith("/"); expect((mod.default() as any).type).toBe(SignalsRoute);
}); });
it("redirects /charts to home", async () => { it("renders the charts route directly", async () => {
const mod = await import("./charts/page"); const mod = await import("./charts/page");
expect(() => mod.default()).toThrow("NEXT_REDIRECT:/"); expect(mod.dynamic).toBe("force-dynamic");
expect(redirect).toHaveBeenCalledWith("/"); expect((mod.default() as any).type).toBe(ChartsRoute);
}); });
it("redirects /replay to home", async () => { it("renders the replay route directly", async () => {
const mod = await import("./replay/page"); const mod = await import("./replay/page");
expect(() => mod.default()).toThrow("NEXT_REDIRECT:/"); expect(mod.dynamic).toBe("force-dynamic");
expect(redirect).toHaveBeenCalledWith("/"); expect((mod.default() as any).type).toBe(ReplayRoute);
});
it("renders the settings route directly", async () => {
const mod = await import("./settings/page");
expect(mod.dynamic).toBe("force-dynamic");
expect((mod.default() as any).type).toBe(SettingsRoute);
}); });
}); });

View file

@ -0,0 +1,7 @@
import { SettingsRoute } from "../terminal";
export const dynamic = "force-dynamic";
export default function Page() {
return <SettingsRoute />;
}

View file

@ -1,7 +1,7 @@
import { redirect } from "next/navigation"; import { SignalsRoute } from "../terminal";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export default function Page() { export default function Page() {
redirect("/"); return <SignalsRoute />;
} }

View file

@ -43,6 +43,8 @@ import {
shouldClearOptionFocusSeed, shouldClearOptionFocusSeed,
smartMoneyProfileLabel, smartMoneyProfileLabel,
smartMoneyToneForProfile, smartMoneyToneForProfile,
getAlertFlowPacketRefs,
resolveAlertFlowPacket,
statusLabel, statusLabel,
toggleFilterValue toggleFilterValue
} from "./terminal"; } from "./terminal";
@ -133,6 +135,33 @@ describe("alert context hydration helpers", () => {
expect(evidence.prints.get("print:1")?.execution_nbbo_bid).toBe(1.2); expect(evidence.prints.get("print:1")?.execution_nbbo_bid).toBe(1.2);
expect(evidence.prints.get("print:1")?.execution_underlying_spot).toBe(450.05); expect(evidence.prints.get("print:1")?.execution_underlying_spot).toBe(450.05);
}); });
it("finds flow-packet refs even when they are not first in alert evidence", () => {
const alert = makeAlert({
evidence_refs: ["smartmoney:single_leg_event:flowpacket:1", "flowpacket:1", "print:1"]
});
expect(getAlertFlowPacketRefs(alert)).toEqual(["flowpacket:1"]);
});
it("resolves the primary alert flow packet from hydrated historical context", () => {
const packet = {
trace_id: "flowpacket:1",
id: "flowpacket:1",
members: ["print:1"],
source_ts: 1,
ingest_ts: 2,
seq: 1,
features: {},
join_quality: {}
} as any;
const alert = makeAlert({
evidence_refs: ["smartmoney:single_leg_event:flowpacket:1", "flowpacket:1", "print:1"]
});
const packets = new Map<string, typeof packet>([[packet.id, packet]]);
expect(resolveAlertFlowPacket(alert, packets)).toBe(packet);
});
}); });
describe("live manifest", () => { describe("live manifest", () => {
@ -247,6 +276,15 @@ describe("live manifest", () => {
]); ]);
}); });
it("includes news subscriptions on home and /news", () => {
expect(getLiveManifest("/", "SPY", 60000, buildDefaultFlowFilters()).map((subscription) => subscription.channel)).toContain(
"news"
);
expect(getLiveManifest("/news", "SPY", 60000, buildDefaultFlowFilters()).map((subscription) => subscription.channel)).toEqual([
"news"
]);
});
it("scopes /charts subscriptions to chart channels only", () => { it("scopes /charts subscriptions to chart channels only", () => {
const channels = getLiveManifest("/charts", "SPY", 60000, buildDefaultFlowFilters()).map( const channels = getLiveManifest("/charts", "SPY", 60000, buildDefaultFlowFilters()).map(
(subscription) => subscription.channel (subscription) => subscription.channel
@ -431,6 +469,31 @@ describe("route feature map", () => {
expect(features.equityOverlay).toBe(true); expect(features.equityOverlay).toBe(true);
expect(features.alerts).toBe(false); expect(features.alerts).toBe(false);
}); });
it("maps /news to the dedicated news pane", () => {
const features = getRouteFeatures("/news");
expect(features.news).toBe(true);
expect(features.showNewsPane).toBe(true);
expect(features.showAlertsPane).toBe(false);
});
it("maps /replay to replay panes and dependencies", () => {
const features = getRouteFeatures("/replay");
expect(features.showReplayConsole).toBe(true);
expect(features.showOptionsPane).toBe(true);
expect(features.showFlowPane).toBe(true);
expect(features.showAlertsPane).toBe(true);
expect(features.needsClassifierDecor).toBe(true);
});
it("maps /settings to a no-feed desktop settings surface", () => {
const features = getRouteFeatures("/settings");
expect(features.showReplayConsole).toBe(false);
expect(features.showOptionsPane).toBe(false);
expect(features.showAlertsPane).toBe(false);
expect(features.options).toBe(false);
expect(features.equities).toBe(false);
});
}); });
describe("fixed tape virtualization config", () => { describe("fixed tape virtualization config", () => {
@ -461,10 +524,15 @@ describe("dark underlying route dependency helper", () => {
}); });
describe("terminal navigation", () => { describe("terminal navigation", () => {
it("exposes only Home and Tape as top-level destinations", () => { it("exposes the terminal routes including Copilot settings", () => {
expect(NAV_ITEMS).toEqual([ expect(NAV_ITEMS).toEqual([
{ href: "/", label: "Home" }, { href: "/", label: "Home" },
{ href: "/tape", label: "Tape" } { href: "/tape", label: "Tape" },
{ href: "/news", label: "News" },
{ href: "/signals", label: "Signals" },
{ href: "/charts", label: "Charts" },
{ href: "/replay", label: "Replay" },
{ href: "/settings", label: "Settings" }
]); ]);
}); });
}); });

File diff suppressed because it is too large Load diff

View file

@ -5,19 +5,20 @@
"scripts": { "scripts": {
"dev": "bun run scripts/dev.ts", "dev": "bun run scripts/dev.ts",
"build": "next build", "build": "next build",
"start": "next start -p 3000" "start": "next start"
}, },
"dependencies": { "dependencies": {
"@islandflow/types": "workspace:*", "@islandflow/types": "workspace:*",
"@tanstack/react-virtual": "^3.13.24", "@tanstack/react-virtual": "^3.13.24",
"lightweight-charts": "^4.2.0", "lightweight-charts": "^4.2.0",
"next": "^14.2.4", "next": "^16.2.6",
"react": "^18.3.1", "react": "^19.2.0",
"react-dom": "^18.3.1" "react-dom": "^19.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.14.10", "@types/node": "^20.14.10",
"@types/react": "^18.3.3", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"typescript": "^5.5.4" "typescript": "^5.5.4"
} }
} }

129
bun.lock
View file

@ -11,6 +11,9 @@
"apps/desktop": { "apps/desktop": {
"name": "@islandflow/desktop", "name": "@islandflow/desktop",
"version": "0.1.0", "version": "0.1.0",
"dependencies": {
"@islandflow/types": "workspace:*",
},
"devDependencies": { "devDependencies": {
"@electron-forge/cli": "^7.8.1", "@electron-forge/cli": "^7.8.1",
"@electron-forge/core": "^7.11.1", "@electron-forge/core": "^7.11.1",
@ -26,13 +29,14 @@
"@islandflow/types": "workspace:*", "@islandflow/types": "workspace:*",
"@tanstack/react-virtual": "^3.13.24", "@tanstack/react-virtual": "^3.13.24",
"lightweight-charts": "^4.2.0", "lightweight-charts": "^4.2.0",
"next": "^14.2.4", "next": "^16.2.6",
"react": "^18.3.1", "react": "^19.2.0",
"react-dom": "^18.3.1", "react-dom": "^19.2.0",
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.14.10", "@types/node": "^20.14.10",
"@types/react": "^18.3.3", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"typescript": "^5.5.4", "typescript": "^5.5.4",
}, },
}, },
@ -121,6 +125,17 @@
"zod": "^3.23.8", "zod": "^3.23.8",
}, },
}, },
"services/ingest-news": {
"name": "@islandflow/ingest-news",
"dependencies": {
"@islandflow/bus": "workspace:*",
"@islandflow/config": "workspace:*",
"@islandflow/observability": "workspace:*",
"@islandflow/types": "workspace:*",
"ws": "^8.18.3",
"zod": "^3.23.8",
},
},
"services/ingest-options": { "services/ingest-options": {
"name": "@islandflow/ingest-options", "name": "@islandflow/ingest-options",
"dependencies": { "dependencies": {
@ -204,8 +219,60 @@
"@electron/windows-sign": ["@electron/windows-sign@1.2.2", "", { "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", "fs-extra": "^11.1.1", "minimist": "^1.2.8", "postject": "^1.0.0-alpha.6" }, "bin": { "electron-windows-sign": "bin/electron-windows-sign.js" } }, "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ=="], "@electron/windows-sign": ["@electron/windows-sign@1.2.2", "", { "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", "fs-extra": "^11.1.1", "minimist": "^1.2.8", "postject": "^1.0.0-alpha.6" }, "bin": { "electron-windows-sign": "bin/electron-windows-sign.js" } }, "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ=="],
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="], "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="],
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
"@inquirer/checkbox": ["@inquirer/checkbox@3.0.1", "", { "dependencies": { "@inquirer/core": "^9.2.1", "@inquirer/figures": "^1.0.6", "@inquirer/type": "^2.0.0", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" } }, "sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ=="], "@inquirer/checkbox": ["@inquirer/checkbox@3.0.1", "", { "dependencies": { "@inquirer/core": "^9.2.1", "@inquirer/figures": "^1.0.6", "@inquirer/type": "^2.0.0", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" } }, "sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ=="],
"@inquirer/confirm": ["@inquirer/confirm@4.0.1", "", { "dependencies": { "@inquirer/core": "^9.2.1", "@inquirer/type": "^2.0.0" } }, "sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w=="], "@inquirer/confirm": ["@inquirer/confirm@4.0.1", "", { "dependencies": { "@inquirer/core": "^9.2.1", "@inquirer/type": "^2.0.0" } }, "sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w=="],
@ -250,6 +317,8 @@
"@islandflow/ingest-equities": ["@islandflow/ingest-equities@workspace:services/ingest-equities"], "@islandflow/ingest-equities": ["@islandflow/ingest-equities@workspace:services/ingest-equities"],
"@islandflow/ingest-news": ["@islandflow/ingest-news@workspace:services/ingest-news"],
"@islandflow/ingest-options": ["@islandflow/ingest-options@workspace:services/ingest-options"], "@islandflow/ingest-options": ["@islandflow/ingest-options@workspace:services/ingest-options"],
"@islandflow/observability": ["@islandflow/observability@workspace:packages/observability"], "@islandflow/observability": ["@islandflow/observability@workspace:packages/observability"],
@ -280,25 +349,23 @@
"@msgpack/msgpack": ["@msgpack/msgpack@3.1.3", "", {}, "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA=="], "@msgpack/msgpack": ["@msgpack/msgpack@3.1.3", "", {}, "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA=="],
"@next/env": ["@next/env@14.2.35", "", {}, "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ=="], "@next/env": ["@next/env@16.2.6", "", {}, "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@14.2.33", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA=="], "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@14.2.33", "", { "os": "darwin", "cpu": "x64" }, "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA=="], "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@14.2.33", "", { "os": "linux", "cpu": "arm64" }, "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw=="], "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@14.2.33", "", { "os": "linux", "cpu": "arm64" }, "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg=="], "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@14.2.33", "", { "os": "linux", "cpu": "x64" }, "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg=="], "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@14.2.33", "", { "os": "linux", "cpu": "x64" }, "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA=="], "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@14.2.33", "", { "os": "win32", "cpu": "arm64" }, "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ=="], "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg=="],
"@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@14.2.33", "", { "os": "win32", "cpu": "ia32" }, "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q=="], "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@14.2.33", "", { "os": "win32", "cpu": "x64" }, "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
@ -322,9 +389,7 @@
"@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="],
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@swc/helpers": ["@swc/helpers@0.5.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A=="],
"@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="], "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="],
@ -352,9 +417,9 @@
"@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], "@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="],
"@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
"@types/react": ["@types/react@18.3.27", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
@ -452,15 +517,13 @@
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
"cacache": ["cacache@16.1.3", "", { "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", "fs-minipass": "^2.1.0", "glob": "^8.0.1", "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^9.0.0", "tar": "^6.1.11", "unique-filename": "^2.0.0" } }, "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ=="], "cacache": ["cacache@16.1.3", "", { "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", "fs-minipass": "^2.1.0", "glob": "^8.0.1", "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^9.0.0", "tar": "^6.1.11", "unique-filename": "^2.0.0" } }, "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ=="],
"cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="],
"cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="], "cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="],
"caniuse-lite": ["caniuse-lite@1.0.30001761", "", {}, "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g=="], "caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@ -700,8 +763,6 @@
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
@ -732,8 +793,6 @@
"log-update": ["log-update@5.0.1", "", { "dependencies": { "ansi-escapes": "^5.0.0", "cli-cursor": "^4.0.0", "slice-ansi": "^5.0.0", "strip-ansi": "^7.0.1", "wrap-ansi": "^8.0.1" } }, "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw=="], "log-update": ["log-update@5.0.1", "", { "dependencies": { "ansi-escapes": "^5.0.0", "cli-cursor": "^4.0.0", "slice-ansi": "^5.0.0", "strip-ansi": "^7.0.1", "wrap-ansi": "^8.0.1" } }, "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
"lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
@ -790,7 +849,7 @@
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
"next": ["next@14.2.35", "", { "dependencies": { "@next/env": "14.2.35", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", "postcss": "8.4.31", "styled-jsx": "5.1.1" }, "optionalDependencies": { "@next/swc-darwin-arm64": "14.2.33", "@next/swc-darwin-x64": "14.2.33", "@next/swc-linux-arm64-gnu": "14.2.33", "@next/swc-linux-arm64-musl": "14.2.33", "@next/swc-linux-x64-gnu": "14.2.33", "@next/swc-linux-x64-musl": "14.2.33", "@next/swc-win32-arm64-msvc": "14.2.33", "@next/swc-win32-ia32-msvc": "14.2.33", "@next/swc-win32-x64-msvc": "14.2.33" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig=="], "next": ["next@16.2.6", "", { "dependencies": { "@next/env": "16.2.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.6", "@next/swc-darwin-x64": "16.2.6", "@next/swc-linux-arm64-gnu": "16.2.6", "@next/swc-linux-arm64-musl": "16.2.6", "@next/swc-linux-x64-gnu": "16.2.6", "@next/swc-linux-x64-musl": "16.2.6", "@next/swc-win32-arm64-msvc": "16.2.6", "@next/swc-win32-x64-msvc": "16.2.6", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw=="],
"nice-try": ["nice-try@1.0.5", "", {}, "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="], "nice-try": ["nice-try@1.0.5", "", {}, "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="],
@ -884,9 +943,9 @@
"quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],
"react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], "react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
"react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
"read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="], "read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="],
@ -930,7 +989,7 @@
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="], "schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="],
@ -940,6 +999,8 @@
"serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
@ -972,8 +1033,6 @@
"ssri": ["ssri@9.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q=="], "ssri": ["ssri@9.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q=="],
"streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="],
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
@ -986,7 +1045,7 @@
"strip-outer": ["strip-outer@1.0.1", "", { "dependencies": { "escape-string-regexp": "^1.0.2" } }, "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg=="], "strip-outer": ["strip-outer@1.0.1", "", { "dependencies": { "escape-string-regexp": "^1.0.2" } }, "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg=="],
"styled-jsx": ["styled-jsx@5.1.1", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" } }, "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw=="], "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
"sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="],
@ -1128,8 +1187,6 @@
"ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
"browserslist/caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="],
"cacache/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], "cacache/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="],
"cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],

View file

@ -0,0 +1,23 @@
.git
.github
.DS_Store
.bun
.tmp
node_modules
dist
coverage
logs
apps/web/.next
.env
.env.*
session-ses_*.md
token-usage-output.txt
signal-cli-*.tar.gz
*.tar
*.tar.gz
*.tgz
*.zip
__pycache__
.pytest_cache
!.env.example
!**/.env.example

View file

@ -4,8 +4,10 @@ NATS_URL=nats://nats:4222
CLICKHOUSE_URL=http://clickhouse:8123 CLICKHOUSE_URL=http://clickhouse:8123
CLICKHOUSE_DATABASE=default CLICKHOUSE_DATABASE=default
REDIS_URL=redis://redis:6379 REDIS_URL=redis://redis:6379
ISLANDFLOW_DATA_ROOT=/var/lib/islandflow
API_PORT=4000 API_PORT=4000
API_HOST=0.0.0.0
API_BIND_IP=127.0.0.1 API_BIND_IP=127.0.0.1
API_HOST_PORT=4000 API_HOST_PORT=4000
WEB_BIND_IP=127.0.0.1 WEB_BIND_IP=127.0.0.1
@ -25,6 +27,10 @@ NEXT_PUBLIC_NBBO_MAX_AGE_MS=1000
# Options ingest # Options ingest
OPTIONS_INGEST_ADAPTER=synthetic OPTIONS_INGEST_ADAPTER=synthetic
ALPACA_API_KEY= ALPACA_API_KEY=
ALPACA_API_KEY_ID=
ALPACA_KEY_ID=
ALPACA_API_SECRET_KEY=
ALPACA_SECRET_KEY=
ALPACA_REST_URL=https://data.alpaca.markets ALPACA_REST_URL=https://data.alpaca.markets
ALPACA_WS_BASE_URL=wss://stream.data.alpaca.markets/v1beta1 ALPACA_WS_BASE_URL=wss://stream.data.alpaca.markets/v1beta1
ALPACA_FEED=indicative ALPACA_FEED=indicative

View file

@ -31,6 +31,7 @@ COPY --from=services candles/package.json ./services/candles/package.json
COPY --from=services compute/package.json ./services/compute/package.json COPY --from=services compute/package.json ./services/compute/package.json
COPY --from=services eod-enricher/package.json ./services/eod-enricher/package.json COPY --from=services eod-enricher/package.json ./services/eod-enricher/package.json
COPY --from=services ingest-equities/package.json ./services/ingest-equities/package.json COPY --from=services ingest-equities/package.json ./services/ingest-equities/package.json
COPY --from=services ingest-news/package.json ./services/ingest-news/package.json
COPY --from=services ingest-options/package.json ./services/ingest-options/package.json COPY --from=services ingest-options/package.json ./services/ingest-options/package.json
COPY --from=services ingest-options/py/requirements.txt ./services/ingest-options/py/requirements.txt COPY --from=services ingest-options/py/requirements.txt ./services/ingest-options/py/requirements.txt
COPY --from=services refdata/package.json ./services/refdata/package.json COPY --from=services refdata/package.json ./services/refdata/package.json

View file

@ -24,6 +24,7 @@ COPY --from=services candles/package.json ./services/candles/package.json
COPY --from=services compute/package.json ./services/compute/package.json COPY --from=services compute/package.json ./services/compute/package.json
COPY --from=services eod-enricher/package.json ./services/eod-enricher/package.json COPY --from=services eod-enricher/package.json ./services/eod-enricher/package.json
COPY --from=services ingest-equities/package.json ./services/ingest-equities/package.json COPY --from=services ingest-equities/package.json ./services/ingest-equities/package.json
COPY --from=services ingest-news/package.json ./services/ingest-news/package.json
COPY --from=services ingest-options/package.json ./services/ingest-options/package.json COPY --from=services ingest-options/package.json ./services/ingest-options/package.json
COPY --from=services refdata/package.json ./services/refdata/package.json COPY --from=services refdata/package.json ./services/refdata/package.json
COPY --from=services replay/package.json ./services/replay/package.json COPY --from=services replay/package.json ./services/replay/package.json

View file

@ -30,6 +30,7 @@ COPY --from=services candles/package.json ./services/candles/package.json
COPY --from=services compute/package.json ./services/compute/package.json COPY --from=services compute/package.json ./services/compute/package.json
COPY --from=services eod-enricher/package.json ./services/eod-enricher/package.json COPY --from=services eod-enricher/package.json ./services/eod-enricher/package.json
COPY --from=services ingest-equities/package.json ./services/ingest-equities/package.json COPY --from=services ingest-equities/package.json ./services/ingest-equities/package.json
COPY --from=services ingest-news/package.json ./services/ingest-news/package.json
COPY --from=services ingest-options/package.json ./services/ingest-options/package.json COPY --from=services ingest-options/package.json ./services/ingest-options/package.json
COPY --from=services refdata/package.json ./services/refdata/package.json COPY --from=services refdata/package.json ./services/refdata/package.json
COPY --from=services replay/package.json ./services/replay/package.json COPY --from=services replay/package.json ./services/replay/package.json
@ -59,4 +60,4 @@ COPY --from=build /app/packages ./packages
EXPOSE 3000 EXPOSE 3000
CMD ["bun", "run", "--cwd", "apps/web", "start"] CMD ["bun", "run", "--cwd", "apps/web", "start", "--", "-H", "0.0.0.0", "-p", "3000"]

View file

@ -2,12 +2,12 @@
This directory contains the Docker runtime for Islandflow VPS deployments. This directory contains the Docker runtime for Islandflow VPS deployments.
Docker remains the default and recommended server rollout path, but the repo-root `deploy` helper can now target either: Docker remains the default rollout path before native cutover and the rollback path after cutover. The repo-root `deploy` helper can target either:
- `--runtime docker` for this Docker Compose stack - `--runtime docker` for this Docker Compose stack
- `--runtime native` for an experimental host-native Bun + systemd rollout described in `deployment/native/README.md` - `--runtime native` for the host-native Bun + systemd rollout described in `deployment/native/README.md`
The repo no longer ships or supports a separate `deployment/npm` stack. If you want a reverse proxy, point it at the host ports published by this stack. The public VPS edge remains Nginx Proxy Manager. Docker fallback can be reached either through the shared Docker network service names or the host ports published by this stack.
It is separate from the repo-root `docker-compose.yml`, which remains the lightweight local infra stack for development. It is separate from the repo-root `docker-compose.yml`, which remains the lightweight local infra stack for development.
@ -17,7 +17,7 @@ Do not run the repo-root `docker-compose.yml` on the VPS. On the live server tha
- Builds and runs the full Islandflow stack with Docker Compose. - Builds and runs the full Islandflow stack with Docker Compose.
- Publishes `web` and `api` to host ports, bound to loopback by default. - Publishes `web` and `api` to host ports, bound to loopback by default.
- Runs ClickHouse, Redis, and NATS JetStream with persistent Docker volumes. - Runs ClickHouse, Redis, and NATS JetStream with persistent host data under `ISLANDFLOW_DATA_ROOT`.
- Runs the core runtime services: `ingest-options`, `ingest-equities`, `compute`, `candles`, `api`, and `web`. - Runs the core runtime services: `ingest-options`, `ingest-equities`, `compute`, `candles`, `api`, and `web`.
- Keeps `replay` opt-in through a Compose profile, because the current replay service starts immediately when the container is enabled. - Keeps `replay` opt-in through a Compose profile, because the current replay service starts immediately when the container is enabled.
@ -56,6 +56,7 @@ cp .env.example .env
Important defaults: Important defaults:
- `NATS_URL`, `CLICKHOUSE_URL`, and `REDIS_URL` should stay on the internal container hostnames unless you intentionally split infra out. - `NATS_URL`, `CLICKHOUSE_URL`, and `REDIS_URL` should stay on the internal container hostnames unless you intentionally split infra out.
- `ISLANDFLOW_DATA_ROOT=/var/lib/islandflow` matches the native infra data root used by the VPS cutover helpers.
- `OPTIONS_INGEST_ADAPTER=synthetic` and `EQUITIES_INGEST_ADAPTER=synthetic` are the safest first-boot settings. - `OPTIONS_INGEST_ADAPTER=synthetic` and `EQUITIES_INGEST_ADAPTER=synthetic` are the safest first-boot settings.
- `WEB_BIND_IP=127.0.0.1` and `API_BIND_IP=127.0.0.1` keep the published ports local to the host by default. - `WEB_BIND_IP=127.0.0.1` and `API_BIND_IP=127.0.0.1` keep the published ports local to the host by default.
- `WEB_HOST_PORT=3000` and `API_HOST_PORT=4000` control the host-side published ports. - `WEB_HOST_PORT=3000` and `API_HOST_PORT=4000` control the host-side published ports.
@ -160,8 +161,10 @@ Set the adapter values and credentials in `.env`:
- `OPTIONS_INGEST_ADAPTER=alpaca` - `OPTIONS_INGEST_ADAPTER=alpaca`
- `EQUITIES_INGEST_ADAPTER=alpaca` - `EQUITIES_INGEST_ADAPTER=alpaca`
- `ALPACA_KEY_ID=...` - `ALPACA_API_KEY_ID=...`
- `ALPACA_SECRET_KEY=...` - `ALPACA_API_SECRET_KEY=...`
The older single-variable `ALPACA_API_KEY` fallback is still accepted for legacy setups, but Alpaca's current market-data auth expects a key ID plus secret key pair.
### Databento mode ### Databento mode
@ -213,17 +216,19 @@ BuildKit cache mounts require a modern Docker Engine with Dockerfile frontend su
## Safe rollouts on `152.53.80.229` ## Safe rollouts on `152.53.80.229`
The current live VPS uses Nginx Proxy Manager on the shared Docker network and routes public traffic to the Docker `web` and `api` containers by container name. Because of that, this Docker path remains the operationally correct default for the live server today. The current live VPS uses Nginx Proxy Manager as the outer edge. Before native cutover, NPM routes Islandflow traffic to Docker service names. During cutover, `deployment/native/switch-npm-edge.sh native` retargets only the Islandflow proxy hosts to the NPM bridge gateway IP so NPM can reach native host ports. If needed, override the detected target with `ISLANDFLOW_NATIVE_HOST=<host-ip>`.
The deploy helper also warns if it detects a second compose project named `islandflow` on the server, because that usually means the repo-root local-infra stack was started on the VPS by mistake. The deploy helper also warns if it detects a second compose project named `islandflow` on the server, because that usually means the repo-root local-infra stack was started on the VPS by mistake.
The checked-in deploy helper is meant to run from your local repo checkout, not from the VPS shell. It always targets: The checked-in deploy helper normally runs from your local repo checkout and targets:
- SSH host: `delta@152.53.80.229` - SSH host: `delta@152.53.80.229`
- SSH key: `~/.ssh/delta_ed25519` - SSH key: `~/.ssh/delta_ed25519` by default
- Live repo checkout: `/home/delta/islandflow` - Live repo checkout: `/home/delta/islandflow`
- Live compose directory: `/home/delta/islandflow/deployment/docker` - Live compose directory: `/home/delta/islandflow/deployment/docker`
If you run `./deploy` from `/home/delta/islandflow` on the VPS itself, it now executes the remote steps locally instead of SSHing back into the same machine. You can still force SSH with `DEPLOY_FORCE_SSH=1`, or override the key path with `DEPLOY_SSH_KEY_PATH=/path/to/key`.
It preserves the current Docker Compose project and avoids destructive cleanup on the server. It preserves the current Docker Compose project and avoids destructive cleanup on the server.
### Deploy `origin/main` ### Deploy `origin/main`
@ -271,6 +276,7 @@ Examples:
./deploy main --runtime docker --web-only ./deploy main --runtime docker --web-only
./deploy main --runtime docker --api-only ./deploy main --runtime docker --api-only
./deploy current-branch --runtime docker --services-only ./deploy current-branch --runtime docker --services-only
./deploy main --runtime docker --workers-only
./deploy main --runtime docker --fast ./deploy main --runtime docker --fast
./deploy main --runtime docker --web-only --no-build ./deploy main --runtime docker --web-only --no-build
``` ```
@ -280,6 +286,7 @@ Scoped Docker deploys now build only the selected image set and then restart onl
- `--web-only`: `docker compose build web`, then `docker compose up -d web` - `--web-only`: `docker compose build web`, then `docker compose up -d web`
- `--api-only`: `docker compose build api`, then `docker compose up -d api` - `--api-only`: `docker compose build api`, then `docker compose up -d api`
- `--services-only`: builds and restarts `api`, `compute`, `candles`, `ingest-options`, and `ingest-equities` - `--services-only`: builds and restarts `api`, `compute`, `candles`, `ingest-options`, and `ingest-equities`
- `--workers-only`: builds and restarts `compute`, `candles`, `ingest-options`, `ingest-equities`, and `ingest-news` without touching `web` or `api`
- `--fast`: when no explicit scope flag is given, treats the deploy as `--services-only` and skips the public API route suite for quicker completion. It still runs remote service health checks. - `--fast`: when no explicit scope flag is given, treats the deploy as `--services-only` and skips the public API route suite for quicker completion. It still runs remote service health checks.
Use `--no-build` only when the image is already correct and you need Compose to recreate or restart containers, such as after changing server-side environment values that do not affect a Next.js build-time variable. Do not use `--no-build` for dependency changes, application source changes, or `NEXT_PUBLIC_*` changes. Use `--no-build` only when the image is already correct and you need Compose to recreate or restart containers, such as after changing server-side environment values that do not affect a Next.js build-time variable. Do not use `--no-build` for dependency changes, application source changes, or `NEXT_PUBLIC_*` changes.

View file

@ -42,6 +42,8 @@ services:
init: true init: true
expose: expose:
- "3000" - "3000"
ports:
- "${WEB_BIND_IP:-127.0.0.1}:${WEB_HOST_PORT:-3000}:3000"
networks: networks:
- default - default
- shared - shared
@ -64,8 +66,13 @@ services:
api: api:
<<: *service-common <<: *service-common
command: ["services/api/src/index.ts"] command: ["services/api/src/index.ts"]
environment:
LOG_LEVEL: ${LOG_LEVEL:-warn}
API_HOST: 0.0.0.0
expose: expose:
- "4000" - "4000"
ports:
- "${API_BIND_IP:-127.0.0.1}:${API_HOST_PORT:-4000}:4000"
networks: networks:
- default - default
- shared - shared
@ -115,6 +122,10 @@ services:
<<: *service-common <<: *service-common
command: ["services/ingest-equities/src/index.ts"] command: ["services/ingest-equities/src/index.ts"]
ingest-news:
<<: *service-common
command: ["services/ingest-news/src/index.ts"]
replay: replay:
<<: *service-common <<: *service-common
profiles: ["replay"] profiles: ["replay"]
@ -128,7 +139,7 @@ services:
soft: 262144 soft: 262144
hard: 262144 hard: 262144
volumes: volumes:
- clickhouse-data:/var/lib/clickhouse - ${ISLANDFLOW_DATA_ROOT:-/var/lib/islandflow}/clickhouse:/var/lib/clickhouse
- ./clickhouse/listen.xml:/etc/clickhouse-server/config.d/listen.xml:ro - ./clickhouse/listen.xml:/etc/clickhouse-server/config.d/listen.xml:ro
healthcheck: healthcheck:
test: test:
@ -146,7 +157,7 @@ services:
restart: unless-stopped restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"] command: ["redis-server", "--appendonly", "yes"]
volumes: volumes:
- redis-data:/data - ${ISLANDFLOW_DATA_ROOT:-/var/lib/islandflow}/redis:/data
healthcheck: healthcheck:
test: test:
[ [
@ -164,14 +175,9 @@ services:
restart: unless-stopped restart: unless-stopped
command: ["-js", "-sd", "/data"] command: ["-js", "-sd", "/data"]
volumes: volumes:
- nats-data:/data - ${ISLANDFLOW_DATA_ROOT:-/var/lib/islandflow}/nats:/data
networks: networks:
shared: shared:
external: true external: true
name: ${NPM_SHARED_NETWORK:-npm-shared} name: ${NPM_SHARED_NETWORK:-npm-shared}
volumes:
clickhouse-data:
redis-data:
nats-data:

View file

@ -26,13 +26,14 @@
"@islandflow/types": "workspace:*", "@islandflow/types": "workspace:*",
"@tanstack/react-virtual": "^3.13.24", "@tanstack/react-virtual": "^3.13.24",
"lightweight-charts": "^4.2.0", "lightweight-charts": "^4.2.0",
"next": "^14.2.4", "next": "^16.2.6",
"react": "^18.3.1", "react": "^19.2.0",
"react-dom": "^18.3.1", "react-dom": "^19.2.0",
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.14.10", "@types/node": "^20.14.10",
"@types/react": "^18.3.3", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"typescript": "^5.5.4", "typescript": "^5.5.4",
}, },
}, },
@ -121,6 +122,17 @@
"zod": "^3.23.8", "zod": "^3.23.8",
}, },
}, },
"services/ingest-news": {
"name": "@islandflow/ingest-news",
"dependencies": {
"@islandflow/bus": "workspace:*",
"@islandflow/config": "workspace:*",
"@islandflow/observability": "workspace:*",
"@islandflow/types": "workspace:*",
"ws": "^8.18.3",
"zod": "^3.23.8",
},
},
"services/ingest-options": { "services/ingest-options": {
"name": "@islandflow/ingest-options", "name": "@islandflow/ingest-options",
"dependencies": { "dependencies": {
@ -204,8 +216,60 @@
"@electron/windows-sign": ["@electron/windows-sign@1.2.2", "", { "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", "fs-extra": "^11.1.1", "minimist": "^1.2.8", "postject": "^1.0.0-alpha.6" }, "bin": { "electron-windows-sign": "bin/electron-windows-sign.js" } }, "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ=="], "@electron/windows-sign": ["@electron/windows-sign@1.2.2", "", { "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", "fs-extra": "^11.1.1", "minimist": "^1.2.8", "postject": "^1.0.0-alpha.6" }, "bin": { "electron-windows-sign": "bin/electron-windows-sign.js" } }, "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ=="],
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="], "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="],
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
"@inquirer/checkbox": ["@inquirer/checkbox@3.0.1", "", { "dependencies": { "@inquirer/core": "^9.2.1", "@inquirer/figures": "^1.0.6", "@inquirer/type": "^2.0.0", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" } }, "sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ=="], "@inquirer/checkbox": ["@inquirer/checkbox@3.0.1", "", { "dependencies": { "@inquirer/core": "^9.2.1", "@inquirer/figures": "^1.0.6", "@inquirer/type": "^2.0.0", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" } }, "sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ=="],
"@inquirer/confirm": ["@inquirer/confirm@4.0.1", "", { "dependencies": { "@inquirer/core": "^9.2.1", "@inquirer/type": "^2.0.0" } }, "sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w=="], "@inquirer/confirm": ["@inquirer/confirm@4.0.1", "", { "dependencies": { "@inquirer/core": "^9.2.1", "@inquirer/type": "^2.0.0" } }, "sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w=="],
@ -250,6 +314,8 @@
"@islandflow/ingest-equities": ["@islandflow/ingest-equities@workspace:services/ingest-equities"], "@islandflow/ingest-equities": ["@islandflow/ingest-equities@workspace:services/ingest-equities"],
"@islandflow/ingest-news": ["@islandflow/ingest-news@workspace:services/ingest-news"],
"@islandflow/ingest-options": ["@islandflow/ingest-options@workspace:services/ingest-options"], "@islandflow/ingest-options": ["@islandflow/ingest-options@workspace:services/ingest-options"],
"@islandflow/observability": ["@islandflow/observability@workspace:packages/observability"], "@islandflow/observability": ["@islandflow/observability@workspace:packages/observability"],
@ -280,25 +346,23 @@
"@msgpack/msgpack": ["@msgpack/msgpack@3.1.3", "", {}, "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA=="], "@msgpack/msgpack": ["@msgpack/msgpack@3.1.3", "", {}, "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA=="],
"@next/env": ["@next/env@14.2.35", "", {}, "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ=="], "@next/env": ["@next/env@16.2.6", "", {}, "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@14.2.33", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA=="], "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@14.2.33", "", { "os": "darwin", "cpu": "x64" }, "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA=="], "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@14.2.33", "", { "os": "linux", "cpu": "arm64" }, "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw=="], "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@14.2.33", "", { "os": "linux", "cpu": "arm64" }, "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg=="], "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@14.2.33", "", { "os": "linux", "cpu": "x64" }, "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg=="], "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@14.2.33", "", { "os": "linux", "cpu": "x64" }, "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA=="], "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@14.2.33", "", { "os": "win32", "cpu": "arm64" }, "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ=="], "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg=="],
"@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@14.2.33", "", { "os": "win32", "cpu": "ia32" }, "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q=="], "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@14.2.33", "", { "os": "win32", "cpu": "x64" }, "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
@ -322,9 +386,7 @@
"@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="],
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@swc/helpers": ["@swc/helpers@0.5.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A=="],
"@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="], "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="],
@ -352,9 +414,9 @@
"@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], "@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="],
"@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
"@types/react": ["@types/react@18.3.27", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
@ -452,15 +514,13 @@
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
"cacache": ["cacache@16.1.3", "", { "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", "fs-minipass": "^2.1.0", "glob": "^8.0.1", "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^9.0.0", "tar": "^6.1.11", "unique-filename": "^2.0.0" } }, "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ=="], "cacache": ["cacache@16.1.3", "", { "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", "fs-minipass": "^2.1.0", "glob": "^8.0.1", "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^9.0.0", "tar": "^6.1.11", "unique-filename": "^2.0.0" } }, "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ=="],
"cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="],
"cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="], "cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="],
"caniuse-lite": ["caniuse-lite@1.0.30001761", "", {}, "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g=="], "caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@ -700,8 +760,6 @@
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
@ -732,8 +790,6 @@
"log-update": ["log-update@5.0.1", "", { "dependencies": { "ansi-escapes": "^5.0.0", "cli-cursor": "^4.0.0", "slice-ansi": "^5.0.0", "strip-ansi": "^7.0.1", "wrap-ansi": "^8.0.1" } }, "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw=="], "log-update": ["log-update@5.0.1", "", { "dependencies": { "ansi-escapes": "^5.0.0", "cli-cursor": "^4.0.0", "slice-ansi": "^5.0.0", "strip-ansi": "^7.0.1", "wrap-ansi": "^8.0.1" } }, "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
"lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
@ -790,7 +846,7 @@
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
"next": ["next@14.2.35", "", { "dependencies": { "@next/env": "14.2.35", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", "postcss": "8.4.31", "styled-jsx": "5.1.1" }, "optionalDependencies": { "@next/swc-darwin-arm64": "14.2.33", "@next/swc-darwin-x64": "14.2.33", "@next/swc-linux-arm64-gnu": "14.2.33", "@next/swc-linux-arm64-musl": "14.2.33", "@next/swc-linux-x64-gnu": "14.2.33", "@next/swc-linux-x64-musl": "14.2.33", "@next/swc-win32-arm64-msvc": "14.2.33", "@next/swc-win32-ia32-msvc": "14.2.33", "@next/swc-win32-x64-msvc": "14.2.33" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig=="], "next": ["next@16.2.6", "", { "dependencies": { "@next/env": "16.2.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.6", "@next/swc-darwin-x64": "16.2.6", "@next/swc-linux-arm64-gnu": "16.2.6", "@next/swc-linux-arm64-musl": "16.2.6", "@next/swc-linux-x64-gnu": "16.2.6", "@next/swc-linux-x64-musl": "16.2.6", "@next/swc-win32-arm64-msvc": "16.2.6", "@next/swc-win32-x64-msvc": "16.2.6", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw=="],
"nice-try": ["nice-try@1.0.5", "", {}, "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="], "nice-try": ["nice-try@1.0.5", "", {}, "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="],
@ -884,9 +940,9 @@
"quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],
"react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], "react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
"react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
"read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="], "read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="],
@ -930,7 +986,7 @@
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="], "schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="],
@ -940,6 +996,8 @@
"serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
@ -972,8 +1030,6 @@
"ssri": ["ssri@9.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q=="], "ssri": ["ssri@9.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q=="],
"streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="],
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
@ -986,7 +1042,7 @@
"strip-outer": ["strip-outer@1.0.1", "", { "dependencies": { "escape-string-regexp": "^1.0.2" } }, "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg=="], "strip-outer": ["strip-outer@1.0.1", "", { "dependencies": { "escape-string-regexp": "^1.0.2" } }, "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg=="],
"styled-jsx": ["styled-jsx@5.1.1", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" } }, "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw=="], "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
"sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="],
@ -1128,8 +1184,6 @@
"ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
"browserslist/caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="],
"cacache/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], "cacache/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="],
"cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],

View file

@ -1,29 +1,170 @@
# Native Deployment # Native Deployment
This directory documents the experimental host-native Islandflow rollout path used by: This directory documents the host-native Islandflow rollout path used by:
```bash ```bash
./deploy main --runtime native ./deploy main --runtime native
./deploy current-branch --runtime native ./deploy current-branch --runtime native
``` ```
This runtime is intended for faster server iteration during the transition away from Docker-only app rollouts. It is not the recommended path for the current production VPS, which still uses Nginx Proxy Manager to reach the Docker `web` and `api` containers by container name on the shared Docker network. Local development should still prefer: ## Current operating model
- Docker for infra (`bun run dev:infra`) Native runtime is now intended for a phased VPS cutover. Docker remains the supported rollback runtime, but Docker and native app services must not own the same Islandflow scope at the same time because the workers and API use durable JetStream consumers.
- native Bun services (`bun run dev:services`)
- native Next.js web (`bun run dev:web`) Today, the recommended split is:
- **Nginx Proxy Manager** remains the public `:80/:443` edge
- **Native system services** own NATS, Redis, and ClickHouse after infra cutover
- **Native user services** own `web`, `api`, and workers after app cutover
- **Docker Compose** remains available as the rollback runtime
- local development stays:
- Docker infra: `bun run dev:infra`
- native backend services: `bun run dev:services`
- native web: `bun run dev:web`
## What native deploy means here ## What native deploy means here
The checked-in `deploy` helper assumes: The checked-in `deploy` helper assumes:
- the live repo checkout is still `/home/delta/islandflow` - the live repo checkout is `/home/delta/islandflow`
- Bun is installed on the VPS - Bun is installed on the VPS
- app processes are managed by `systemd` - app processes are managed by `systemd --user`
- infrastructure services such as NATS, ClickHouse, and Redis are already reachable from the host - infrastructure services such as NATS, ClickHouse, and Redis are reachable from the host
- the web app runs from `apps/web` and is served with `next start -p 3000` - the web app runs from `apps/web` and is served with `next start -p 3000`
The deploy script updates the repo checkout, optionally runs `bun install --frozen-lockfile`, optionally rebuilds the web app, restarts the target systemd units, and then verifies the services locally on the VPS plus through the public app URL. The deploy script updates the repo checkout, optionally runs `bun install --frozen-lockfile`, optionally rebuilds the web app, restarts the target user units, verifies local health, and then runs public verification when the selected scope includes the public edge.
## Live audit status on 2026-05-18
The plan assumptions were audited on the VPS:
- `bun` is installed and available at `/home/delta/.bun/bin/bun`
- `systemctl --user` is available and the `delta` user has lingering enabled
- `/home/delta/islandflow/.env` exists
- public `https://flow.deltaisland.io/replay/options` routing is healthy again
- the previously reported duplicate `islandflow` compose project is not currently present in `docker compose ls`
- native Islandflow user units were not installed at the start of the audit; this change now provides and installs the checked-in user unit files, but they remain disabled until an operator enables a scope intentionally
That means native worker deploy support is now provisioned on the host, but native runtime should still be enabled scope-by-scope rather than started wholesale.
## Checked-in native ops assets
### Infra system units
Checked-in system service units and config live under:
- `deployment/native/systemd/system/islandflow-nats.service`
- `deployment/native/systemd/system/islandflow-redis.service`
- `deployment/native/systemd/system/islandflow-clickhouse.service`
- `deployment/native/config/redis.conf`
- `deployment/native/config/clickhouse-listen.xml`
Install and start them on the VPS with:
```bash
./deployment/native/bootstrap-infra.sh
```
Or install and start manually:
```bash
sudo ./deployment/native/install-infra-units.sh
sudo ./deployment/native/start-infra.sh
./deployment/native/check-native-infra.sh
```
The native infra services bind to loopback and use stable host data paths:
- NATS JetStream: `/var/lib/islandflow/nats`
- Redis: `/var/lib/islandflow/redis`
- ClickHouse: `/var/lib/islandflow/clickhouse`
The Docker fallback compose file uses the same `ISLANDFLOW_DATA_ROOT` default of `/var/lib/islandflow`, so rollback can preserve durable state when only one runtime is active.
### User unit templates
Checked-in unit files live under:
- `deployment/native/systemd/user/islandflow-web.service`
- `deployment/native/systemd/user/islandflow-api.service`
- `deployment/native/systemd/user/islandflow-compute.service`
- `deployment/native/systemd/user/islandflow-candles.service`
- `deployment/native/systemd/user/islandflow-ingest-options.service`
- `deployment/native/systemd/user/islandflow-ingest-equities.service`
- `deployment/native/systemd/user/islandflow-ingest-news.service`
These are written for the current VPS layout:
- repo root: `/home/delta/islandflow`
- Bun binary: `/home/delta/.bun/bin/bun`
- env file: `/home/delta/islandflow/.env`
Important: treat `/home/delta/islandflow/.env` as the effective source of truth for adapter selection. The Bun-launched services read that file directly at runtime, so a conflicting `OPTIONS_INGEST_ADAPTER` value in `.env` can still win over a systemd-only override and push `ingest-options` onto the wrong provider path.
### Install the units
```bash
./deployment/native/install-user-units.sh
./deployment/native/install-user-units.sh workers
systemctl --user start islandflow-compute.service
```
Install script behavior:
- copies the checked-in unit files into `~/.config/systemd/user`
- reloads the user systemd daemon
- enables only the scope you explicitly request
- defaults to installing without enabling anything yet
### Smoke test helper
```bash
./deployment/native/check-native-health.sh workers
./deployment/native/check-native-health.sh services
./deployment/native/check-native-health.sh full
```
This validates:
- native infra health for `full`, `api`, `services`, and `workers`
- `systemctl --user is-active` for the selected units
- local API health at `http://127.0.0.1:4000/health` when API scope is included
- local web health at `http://127.0.0.1:3000/` when web scope is included
### App cutover and edge switch helpers
```bash
./deployment/native/cutover.sh full
./deployment/native/switch-npm-edge.sh native
./deployment/native/full-rollback.sh
```
The edge switch helper updates the Nginx Proxy Manager database entries for `flow.deltaisland.io` and `api.flow.deltaisland.io`, preserving the same-origin Islandflow API location matcher:
```nginx
^/(ws|replay|prints|joins|nbbo|dark|flow|candles|history)/
```
For native cutover, the helper targets the NPM bridge gateway IP by default, not `host.docker.internal`. NPM generates `proxy_pass` with a runtime-resolved `$server` variable, so Docker's `/etc/hosts` alias is not sufficient for these proxy hosts. On the current VPS that native target resolves to `172.18.0.1`, which reaches the host-native `3000` and `4000` listeners from the NPM container.
Switching back to Docker restores upstreams to the Compose service names `web:3000` and `api:4000`.
### Rollback helper
```bash
./deployment/native/rollback.sh <git-ref> workers
./deployment/native/rollback.sh <git-ref> services
```
Rollback helper behavior:
- requires a clean repo state
- fetches refs
- switches the checkout to a detached target ref
- reruns `bun install --frozen-lockfile`
- rebuilds the web app only when web scope is included
- restarts the selected user units
- runs the native smoke checks
## Expected unit names ## Expected unit names
@ -35,6 +176,7 @@ Default unit names used by `scripts/deploy.ts`:
- `islandflow-candles` - `islandflow-candles`
- `islandflow-ingest-options` - `islandflow-ingest-options`
- `islandflow-ingest-equities` - `islandflow-ingest-equities`
- `islandflow-ingest-news`
Override them from your local shell before running `./deploy` if the server uses different names: Override them from your local shell before running `./deploy` if the server uses different names:
@ -51,90 +193,108 @@ Available overrides:
- `DEPLOY_NATIVE_CANDLES_UNIT` - `DEPLOY_NATIVE_CANDLES_UNIT`
- `DEPLOY_NATIVE_INGEST_OPTIONS_UNIT` - `DEPLOY_NATIVE_INGEST_OPTIONS_UNIT`
- `DEPLOY_NATIVE_INGEST_EQUITIES_UNIT` - `DEPLOY_NATIVE_INGEST_EQUITIES_UNIT`
- `DEPLOY_NATIVE_INGEST_NEWS_UNIT`
## systemctl invocation ## systemctl invocation
By default the deploy helper uses: For the checked-in user units, use:
```bash
sudo -n systemctl
```
If the server uses user units or another wrapper, override it locally before invoking `./deploy`:
```bash ```bash
export DEPLOY_NATIVE_SYSTEMCTL_PREFIX="systemctl --user" export DEPLOY_NATIVE_SYSTEMCTL_PREFIX="systemctl --user"
./deploy main --runtime native
``` ```
The deploy helper defaults to `sudo -n systemctl`, but that is only appropriate if you intentionally install matching system units.
## Partial native rollouts ## Partial native rollouts
Examples: Examples:
```bash ```bash
./deploy main --runtime native --web-only ./deploy main --runtime native --workers-only
./deploy main --runtime native --api-only
./deploy current-branch --runtime native --services-only
./deploy main --runtime native --fast ./deploy main --runtime native --fast
./deploy main --runtime native --web-only --no-build ./deploy main --runtime native --services-only
./deploy main --runtime native --web-only
./deploy current-branch --runtime native --workers-only --no-build
``` ```
Scope behavior: Scope behavior:
- default: restart web + API + backend services - default: restart web + API + worker services
- `--web-only`: rebuild/restart only the web unit - `--web-only`: rebuild/restart only the web unit
- `--api-only`: restart only the API unit - `--api-only`: restart only the API unit
- `--services-only`: restart API + backend units without touching the web unit - `--services-only`: restart API + worker units without touching the web unit
- `--fast`: when no explicit scope flag is provided, uses the same `--services-only` scope and trims verbose verification output for quicker completion - `--workers-only`: restart only `compute`, `candles`, `ingest-options`, `ingest-equities`, and `ingest-news`
- `--fast`: when no explicit scope flag is provided, native deploys now default to `--workers-only`
- `--no-build`: skip `bun install --frozen-lockfile` and skip the web build step - `--no-build`: skip `bun install --frozen-lockfile` and skip the web build step
## Current status ## Edge-cutover guardrail
On the current live VPS, native deploys should be treated as opt-in infrastructure work, not the default rollout path. Before a native deploy can succeed there, all of the following must be true at the same time: Native deploys that touch the public web or API edge are intentionally blocked unless you acknowledge cutover readiness:
- Bun is installed on the host.
- The selected `systemctl` command works non-interactively.
- Islandflow systemd units exist for the requested scope.
- Host-native services can reach the intended NATS, ClickHouse, and Redis endpoints.
- If `web` or `api` move native, the reverse proxy topology is updated deliberately.
Until that is prepared intentionally, prefer:
```bash ```bash
./deploy main --runtime docker export DEPLOY_NATIVE_EDGE_READY=1
./deploy current-branch --runtime docker
``` ```
## Server preparation checklist Without that variable, these commands are refused:
Before the first native rollout, ensure the VPS has: - `./deploy main --runtime native`
- `./deploy main --runtime native --web-only`
- `./deploy main --runtime native --api-only`
- `./deploy main --runtime native --services-only`
1. Bun installed and on `PATH` This keeps native app ownership explicit until infra, app health, and proxy routing are switched deliberately.
2. a working `/home/delta/islandflow/.env` (or unit-managed equivalent env source)
3. systemd units for each target service
4. the web unit configured to serve the built app on port `3000`
5. the API unit configured to serve health checks on port `4000`
6. infrastructure endpoints configured so the native services can reach NATS, ClickHouse, and Redis
## Verification ## Running deploy from the VPS itself
Native deploys verify: If you run `./deploy` from `/home/delta/islandflow` on the live server, the deploy helper now executes the remote steps locally instead of SSHing back into the same machine.
- target units are active via `systemctl` That means:
- recent unit status and journal output can be collected
- local `http://127.0.0.1:4000/health` when API scope is included
- local `http://127.0.0.1:3000/` when web scope is included
- the public app URL from the local machine after the rollout finishes
## Rollback - no SSH key is required for on-server deploy execution
- timing and verification behavior stay the same
- you can still force SSH with `DEPLOY_FORCE_SSH=1`
- you can override the SSH key path with `DEPLOY_SSH_KEY_PATH=/path/to/key`
Rollback remains manual for now: ## Validation matrix
1. switch the server checkout back to the last known-good branch or commit | Area | Native workers-only | Native edge cutover |
2. rerun the appropriate native deploy command | --- | --- | --- |
3. if needed, restart only the affected units with `systemctl` | Bun installed | required | required |
| `systemctl --user` works | required | required |
| Islandflow user units installed | worker units only | all units |
| Host access to NATS/ClickHouse/Redis | required | required |
| Proxy routes updated for `/prints`, `/history`, `/replay`, `/nbbo`, `/ws`, `/flow`, `/candles` | not required | required |
| Public app check | not required | required |
| Public API route suite | not required | required |
Docker remains the fallback and currently recommended runtime during the transition: ## Staged cutover plan
1. **Stage 1: native workers only**
- install user units
- validate `./deployment/native/check-native-health.sh workers`
- use `./deploy main --runtime native --fast`
2. **Stage 2: native API behind local-only verification**
- start `islandflow-api.service`
- confirm `curl http://127.0.0.1:4000/health`
- do not switch public routing yet
3. **Stage 3: deliberate public edge cutover**
- update proxy routing to native `web`/`api`
- export `DEPLOY_NATIVE_EDGE_READY=1`
- run full native deploy
- validate `bun run scripts/check-public-api-routes.ts https://flow.deltaisland.io`
4. **Stage 4: decide final default runtime**
- keep Docker as fallback until native edge has proven stable
## Recommended current commands
Fast backend iteration before edge cutover:
```bash
export DEPLOY_NATIVE_SYSTEMCTL_PREFIX="systemctl --user"
./deploy main --runtime native --fast
```
Supported production path today:
```bash ```bash
./deploy main --runtime docker ./deploy main --runtime docker

View file

@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
if [[ "${EUID}" -eq 0 ]]; then
"$repo_root/deployment/native/install-infra-units.sh"
else
sudo "$repo_root/deployment/native/install-infra-units.sh"
fi
echo "Stopping Docker Islandflow services before native infra opens durable data."
(
cd "$repo_root/deployment/docker"
docker compose stop web api compute candles ingest-options ingest-equities nats redis clickhouse
)
if [[ "${EUID}" -eq 0 ]]; then
"$repo_root/deployment/native/start-infra.sh"
else
sudo "$repo_root/deployment/native/start-infra.sh"
fi
"$repo_root/deployment/native/check-native-infra.sh"

View file

@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -euo pipefail
scope="${1:-full}"
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
units=()
case "$scope" in
full)
units=(islandflow-web.service islandflow-api.service islandflow-compute.service islandflow-candles.service islandflow-ingest-options.service islandflow-ingest-equities.service islandflow-ingest-news.service)
;;
web)
units=(islandflow-web.service)
;;
api)
units=(islandflow-api.service)
;;
services)
units=(islandflow-api.service islandflow-compute.service islandflow-candles.service islandflow-ingest-options.service islandflow-ingest-equities.service islandflow-ingest-news.service)
;;
workers)
units=(islandflow-compute.service islandflow-candles.service islandflow-ingest-options.service islandflow-ingest-equities.service islandflow-ingest-news.service)
;;
*)
echo "Unknown scope: $scope" >&2
echo "Expected one of: full, web, api, services, workers" >&2
exit 1
;;
esac
case "$scope" in
full|api|services|workers)
"$repo_root/deployment/native/check-native-infra.sh"
;;
esac
for unit in "${units[@]}"; do
systemctl --user is-active --quiet "$unit"
echo "ok $unit"
done
if [[ " ${units[*]} " == *" islandflow-api.service "* ]]; then
curl -fksS http://127.0.0.1:4000/health >/dev/null
echo "ok api-health"
fi
if [[ " ${units[*]} " == *" islandflow-web.service "* ]]; then
curl -I -fksS http://127.0.0.1:3000/ >/dev/null
echo "ok web-health"
fi

View file

@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
systemctl is-active --quiet islandflow-nats.service
echo "ok islandflow-nats.service"
systemctl is-active --quiet islandflow-redis.service
echo "ok islandflow-redis.service"
systemctl is-active --quiet islandflow-clickhouse.service
echo "ok islandflow-clickhouse.service"
if command -v redis-cli >/dev/null 2>&1; then
redis-cli -h 127.0.0.1 -p 6379 ping | grep -q PONG
else
timeout 2 bash -c '</dev/tcp/127.0.0.1/6379'
fi
echo "ok redis-ping"
curl -fksS http://127.0.0.1:8123/ping | grep -q Ok
echo "ok clickhouse-ping"
timeout 2 bash -c '</dev/tcp/127.0.0.1/4222'
echo "ok nats-port"

View file

@ -0,0 +1,6 @@
<clickhouse>
<listen_host>127.0.0.1</listen_host>
<path>/var/lib/islandflow/clickhouse/</path>
<tmp_path>/var/lib/islandflow/clickhouse/tmp/</tmp_path>
<user_files_path>/var/lib/islandflow/clickhouse/user_files/</user_files_path>
</clickhouse>

View file

@ -0,0 +1,10 @@
bind 127.0.0.1
protected-mode yes
port 6379
dir /var/lib/islandflow/redis
appendonly yes
save 900 1
save 300 10
save 60 10000
loglevel notice
databases 16

34
deployment/native/cutover.sh Executable file
View file

@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
scope="${1:-full}"
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
case "$scope" in
full|services|workers|api|web)
;;
*)
echo "Usage: deployment/native/cutover.sh [full|services|workers|api|web]" >&2
exit 1
;;
esac
echo "Stopping Docker-owned Islandflow app services before native ownership starts."
(
cd "$repo_root/deployment/docker"
docker compose stop web api compute candles ingest-options ingest-equities ingest-news
)
if [[ "$scope" == "full" || "$scope" == "services" || "$scope" == "api" || "$scope" == "web" ]]; then
"$repo_root/deployment/native/check-native-infra.sh"
fi
systemctl --user restart $(case "$scope" in
full) echo islandflow-web.service islandflow-api.service islandflow-compute.service islandflow-candles.service islandflow-ingest-options.service islandflow-ingest-equities.service islandflow-ingest-news.service ;;
services) echo islandflow-api.service islandflow-compute.service islandflow-candles.service islandflow-ingest-options.service islandflow-ingest-equities.service islandflow-ingest-news.service ;;
workers) echo islandflow-compute.service islandflow-candles.service islandflow-ingest-options.service islandflow-ingest-equities.service islandflow-ingest-news.service ;;
api) echo islandflow-api.service ;;
web) echo islandflow-web.service ;;
esac)
"$repo_root/deployment/native/check-native-health.sh" "$scope"

View file

@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
echo "Stopping native app services."
systemctl --user stop islandflow-web.service islandflow-api.service islandflow-compute.service islandflow-candles.service islandflow-ingest-options.service islandflow-ingest-equities.service islandflow-ingest-news.service || true
echo "Stopping native infra before Docker reopens durable data."
if [[ "${EUID}" -eq 0 ]]; then
systemctl stop islandflow-nats.service islandflow-redis.service islandflow-clickhouse.service || true
else
sudo systemctl stop islandflow-nats.service islandflow-redis.service islandflow-clickhouse.service || true
fi
echo "Switching NPM Islandflow upstreams back to Docker service names."
"$repo_root/deployment/native/switch-npm-edge.sh" docker
echo "Restarting Docker Islandflow runtime."
(
cd "$repo_root/deployment/docker"
docker compose up -d web api compute candles ingest-options ingest-equities ingest-news
)
curl -I -fksS "${DEPLOY_PUBLIC_APP_URL:-https://flow.deltaisland.io}" >/dev/null
curl -fksS "${DEPLOY_PUBLIC_API_HEALTH_URL:-https://api.flow.deltaisland.io/health}" >/dev/null
echo "Rollback validation passed."

View file

@ -0,0 +1,72 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
system_unit_source_dir="$repo_root/deployment/native/systemd/system"
config_source_dir="$repo_root/deployment/native/config"
if [[ "${EUID}" -ne 0 ]]; then
echo "Run as root: sudo $0" >&2
exit 1
fi
resolve_binary() {
local name="$1"
local path=""
path="$(command -v "$name" 2>/dev/null || true)"
if [[ -n "$path" ]]; then
printf '%s\n' "$path"
return 0
fi
for candidate in "/usr/bin/$name" "/usr/sbin/$name" "/usr/local/bin/$name" "/usr/local/sbin/$name"; do
if [[ -x "$candidate" ]]; then
printf '%s\n' "$candidate"
return 0
fi
done
return 1
}
missing=()
for command in nats-server redis-server clickhouse-server; do
if ! resolve_binary "$command" >/dev/null; then
missing+=("$command")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
echo "Missing native infra binaries: ${missing[*]}" >&2
echo "Install NATS Server, Redis Server, and ClickHouse Server before bootstrapping native infra." >&2
echo "On Debian, Redis is usually available as redis-server; ClickHouse and NATS may require their vendor repositories or packaged binaries." >&2
exit 1
fi
ensure_system_user() {
local name="$1"
local home="$2"
getent group "$name" >/dev/null || groupadd --system "$name"
getent passwd "$name" >/dev/null || useradd --system --gid "$name" --home-dir "$home" --shell /usr/sbin/nologin "$name"
}
ensure_system_user nats /var/lib/islandflow/nats
ensure_system_user redis /var/lib/islandflow/redis
ensure_system_user clickhouse /var/lib/islandflow/clickhouse
install -d -m 0755 /etc/islandflow
install -m 0644 "$config_source_dir/redis.conf" /etc/islandflow/redis.conf
install -d -m 0755 /etc/clickhouse-server/config.d
install -m 0644 "$config_source_dir/clickhouse-listen.xml" /etc/clickhouse-server/config.d/islandflow-listen.xml
install -d -o nats -g nats -m 0750 /var/lib/islandflow/nats
install -d -o redis -g redis -m 0750 /var/lib/islandflow/redis
install -d -o clickhouse -g clickhouse -m 0750 /var/lib/islandflow/clickhouse
install -m 0644 "$system_unit_source_dir"/islandflow-*.service /etc/systemd/system/
systemctl daemon-reload
echo "Installed native infra system units and config."
echo "Start infra with: sudo deployment/native/start-infra.sh"

View file

@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
scope="${1:-none}"
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
unit_source_dir="$repo_root/deployment/native/systemd/user"
unit_target_dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
units=()
case "$scope" in
none)
;;
full)
units=(islandflow-web.service islandflow-api.service islandflow-compute.service islandflow-candles.service islandflow-ingest-options.service islandflow-ingest-equities.service islandflow-ingest-news.service)
;;
web)
units=(islandflow-web.service)
;;
api)
units=(islandflow-api.service)
;;
services)
units=(islandflow-api.service islandflow-compute.service islandflow-candles.service islandflow-ingest-options.service islandflow-ingest-equities.service islandflow-ingest-news.service)
;;
workers)
units=(islandflow-compute.service islandflow-candles.service islandflow-ingest-options.service islandflow-ingest-equities.service islandflow-ingest-news.service)
;;
*)
echo "Unknown scope: $scope" >&2
echo "Expected one of: none, full, web, api, services, workers" >&2
exit 1
;;
esac
mkdir -p "$unit_target_dir"
cp "$unit_source_dir"/*.service "$unit_target_dir"/
systemctl --user daemon-reload
if [[ ${#units[@]} -gt 0 ]]; then
systemctl --user enable "${units[@]}"
fi
echo "Installed Islandflow user units into $unit_target_dir"
if [[ ${#units[@]} -gt 0 ]]; then
echo "Enabled scope: $scope"
else
echo "No units enabled yet. Pass a scope such as workers when you are ready."
fi

57
deployment/native/rollback.sh Executable file
View file

@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 1 || $# -gt 2 ]]; then
echo "Usage: deployment/native/rollback.sh <git-ref> [full|web|api|services|workers]" >&2
exit 1
fi
ref="$1"
scope="${2:-services}"
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$repo_root"
if [[ -n "$(git status --porcelain=v1)" ]]; then
echo "Refusing rollback with a dirty working tree." >&2
exit 1
fi
current_ref="$(git rev-parse --short HEAD)"
echo "Rolling back from $current_ref to $ref (scope: $scope)"
git fetch --all --prune
git switch --detach "$ref"
bun install --frozen-lockfile
if [[ "$scope" == "full" || "$scope" == "web" ]]; then
bun --cwd=apps/web run build
fi
case "$scope" in
full)
units=(islandflow-web.service islandflow-api.service islandflow-compute.service islandflow-candles.service islandflow-ingest-options.service islandflow-ingest-equities.service islandflow-ingest-news.service)
;;
web)
units=(islandflow-web.service)
;;
api)
units=(islandflow-api.service)
;;
services)
units=(islandflow-api.service islandflow-compute.service islandflow-candles.service islandflow-ingest-options.service islandflow-ingest-equities.service islandflow-ingest-news.service)
;;
workers)
units=(islandflow-compute.service islandflow-candles.service islandflow-ingest-options.service islandflow-ingest-equities.service islandflow-ingest-news.service)
;;
*)
echo "Unknown scope: $scope" >&2
exit 1
;;
esac
systemctl --user restart "${units[@]}"
"$repo_root/deployment/native/check-native-health.sh" "$scope"
echo "Rollback complete. Repo is now detached at $(git rev-parse --short HEAD)."
echo "Return to tracked main later with: git switch main && git pull --ff-only <remote> main"

View file

@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ "${EUID}" -ne 0 ]]; then
echo "Run as root: sudo $0" >&2
exit 1
fi
for unit in redis-server.service nats-server.service clickhouse-server.service; do
if systemctl list-unit-files "$unit" >/dev/null 2>&1; then
systemctl disable --now "$unit" >/dev/null 2>&1 || true
fi
done
systemctl reset-failed islandflow-nats.service islandflow-redis.service islandflow-clickhouse.service || true
systemctl enable --now islandflow-nats.service islandflow-redis.service islandflow-clickhouse.service
"$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/check-native-infra.sh"

View file

@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ "${EUID}" -ne 0 ]]; then
echo "Run as root: sudo $0" >&2
exit 1
fi
systemctl stop islandflow-nats.service islandflow-redis.service islandflow-clickhouse.service

View file

@ -0,0 +1,285 @@
#!/usr/bin/env bash
set -euo pipefail
target="${1:-native}"
npm_root="${NPM_ROOT:-/home/delta/nginx-proxy-manager}"
db_path="${NPM_DB_PATH:-$npm_root/data/database.sqlite}"
app_domain="${ISLANDFLOW_APP_DOMAIN:-flow.deltaisland.io}"
api_domain="${ISLANDFLOW_API_DOMAIN:-api.flow.deltaisland.io}"
native_host="${ISLANDFLOW_NATIVE_HOST:-}"
docker_web_host="${ISLANDFLOW_DOCKER_WEB_HOST:-web}"
docker_api_host="${ISLANDFLOW_DOCKER_API_HOST:-api}"
web_port="${ISLANDFLOW_WEB_PORT:-3000}"
api_port="${ISLANDFLOW_API_PORT:-4000}"
restart_npm="${NPM_RESTART:-1}"
npm_container="${NPM_CONTAINER_NAME:-nginx-proxy-manager}"
sudo_cmd=()
case "$target" in
native|docker)
;;
*)
echo "Usage: deployment/native/switch-npm-edge.sh [native|docker]" >&2
exit 1
;;
esac
resolve_native_host() {
if [[ -n "$native_host" ]]; then
printf '%s\n' "$native_host"
return
fi
if command -v docker >/dev/null 2>&1 && docker ps --format '{{.Names}}' | grep -qx "$npm_container"; then
native_host="$(docker inspect "$npm_container" --format '{{range .NetworkSettings.Networks}}{{println .Gateway}}{{end}}' | sed '/^$/d' | head -n1)"
if [[ -n "$native_host" ]]; then
printf '%s\n' "$native_host"
return
fi
fi
echo "Unable to determine the native upstream host for NPM." >&2
echo "Set ISLANDFLOW_NATIVE_HOST explicitly or start the $npm_container container first." >&2
exit 1
}
if [[ "$target" == "native" ]]; then
native_host="$(resolve_native_host)"
fi
if [[ ! -w "$db_path" || ! -w "$(dirname "$db_path")" ]]; then
if [[ "${EUID}" -eq 0 ]]; then
sudo_cmd=()
elif command -v sudo >/dev/null 2>&1; then
sudo_cmd=(sudo)
else
echo "NPM database path is not writable and sudo is unavailable: $db_path" >&2
exit 1
fi
fi
if [[ ! -f "$db_path" ]]; then
echo "NPM database not found: $db_path" >&2
exit 1
fi
backup="$db_path.before-islandflow-$target-$(date +%Y%m%d%H%M%S)"
"${sudo_cmd[@]}" cp "$db_path" "$backup"
echo "Backed up NPM database to $backup"
"${sudo_cmd[@]}" python3 - "$db_path" "$target" "$app_domain" "$api_domain" "$native_host" "$docker_web_host" "$docker_api_host" "$web_port" "$api_port" <<'PY'
import json
import sqlite3
import sys
db_path, target, app_domain, api_domain, native_host, docker_web_host, docker_api_host, web_port, api_port = sys.argv[1:]
web_host = native_host if target == "native" else docker_web_host
api_host = native_host if target == "native" else docker_api_host
advanced_config = f"""location ~ ^/(ws|replay|prints|joins|nbbo|dark|flow|candles|history)/ {{
set $forward_scheme http;
set $server "{api_host}";
set $port {api_port};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
include conf.d/include/proxy.conf;
}}"""
def has_domain(raw, domain):
try:
return domain in json.loads(raw)
except Exception:
return domain in raw
con = sqlite3.connect(db_path)
cur = con.cursor()
rows = list(cur.execute("select id, domain_names from proxy_host where is_deleted = 0"))
app_ids = [row_id for row_id, domains in rows if has_domain(domains, app_domain)]
api_ids = [row_id for row_id, domains in rows if has_domain(domains, api_domain)]
if len(app_ids) != 1 or len(api_ids) != 1:
raise SystemExit(f"Expected one app and one API proxy host, found app={app_ids} api={api_ids}")
cur.execute(
"update proxy_host set forward_scheme = 'http', forward_host = ?, forward_port = ?, allow_websocket_upgrade = 1, advanced_config = ?, modified_on = datetime('now') where id = ?",
(web_host, int(web_port), advanced_config, app_ids[0]),
)
cur.execute(
"update proxy_host set forward_scheme = 'http', forward_host = ?, forward_port = ?, allow_websocket_upgrade = 1, modified_on = datetime('now') where id = ?",
(api_host, int(api_port), api_ids[0]),
)
con.commit()
print(f"Updated {app_domain} -> {web_host}:{web_port}")
print(f"Updated {api_domain} -> {api_host}:{api_port}")
PY
if command -v python3 >/dev/null 2>&1; then
"${sudo_cmd[@]}" python3 - "$npm_root" "$db_path" "$target" "$app_domain" "$api_domain" "$native_host" "$docker_web_host" "$docker_api_host" "$web_port" "$api_port" <<'PY'
import json
import re
import sqlite3
import sys
from pathlib import Path
(
npm_root,
db_path,
target,
app_domain,
api_domain,
native_host,
docker_web_host,
docker_api_host,
web_port,
api_port,
) = sys.argv[1:]
web_host = native_host if target == "native" else docker_web_host
api_host = native_host if target == "native" else docker_api_host
def has_domain(raw, domain):
try:
return domain in json.loads(raw)
except Exception:
return domain in raw
def replace_nth(text, pattern, replacement, index):
matches = list(pattern.finditer(text))
if len(matches) < index:
raise SystemExit(f"Unable to rewrite generated proxy config; expected match {index} for {pattern.pattern!r}")
match = matches[index - 1]
return text[:match.start()] + replacement(match) + text[match.end():]
server_pattern = re.compile(r'^(?P<prefix>\s*set \$server\s+)".*?";\s*$', re.M)
port_pattern = re.compile(r'^(?P<prefix>\s*set \$port\s+)\d+;\s*$', re.M)
def replace_server(text, host, index):
return replace_nth(text, server_pattern, lambda m: f'{m.group("prefix")}"{host}";', index)
def replace_port(text, port, index):
return replace_nth(text, port_pattern, lambda m: f'{m.group("prefix")}{port};', index)
con = sqlite3.connect(db_path)
rows = list(con.execute("select id, domain_names from proxy_host where is_deleted = 0"))
app_ids = [row_id for row_id, domains in rows if has_domain(domains, app_domain)]
api_ids = [row_id for row_id, domains in rows if has_domain(domains, api_domain)]
if len(app_ids) != 1 or len(api_ids) != 1:
raise SystemExit(f"Expected one app and one API proxy host, found app={app_ids} api={api_ids}")
api_conf = Path(npm_root) / "data/nginx/proxy_host" / f"{api_ids[0]}.conf"
app_conf = Path(npm_root) / "data/nginx/proxy_host" / f"{app_ids[0]}.conf"
if api_conf.exists():
text = api_conf.read_text()
text = replace_server(text, api_host, 1)
text = replace_port(text, int(api_port), 1)
api_conf.write_text(text)
print(f"Synchronized {api_conf.name} -> {api_host}:{api_port}")
if app_conf.exists():
text = app_conf.read_text()
text = replace_server(text, web_host, 1)
text = replace_port(text, int(web_port), 1)
text = replace_server(text, api_host, 2)
text = replace_port(text, int(api_port), 2)
app_conf.write_text(text)
print(f"Synchronized {app_conf.name} -> {web_host}:{web_port} and API matcher -> {api_host}:{api_port}")
PY
fi
if [[ "$restart_npm" == "0" ]]; then
echo "NPM container restart skipped because NPM_RESTART=0."
elif command -v docker >/dev/null 2>&1 && docker ps --format '{{.Names}}' | grep -qx nginx-proxy-manager; then
docker restart nginx-proxy-manager >/dev/null
echo "Restarted nginx-proxy-manager"
else
echo "NPM container restart skipped; restart it manually if it is not managed by Docker on this host."
fi
if command -v docker >/dev/null 2>&1 && docker ps --format '{{.Names}}' | grep -qx "$npm_container"; then
"${sudo_cmd[@]}" python3 - "$npm_root" "$db_path" "$target" "$app_domain" "$api_domain" "$native_host" "$docker_web_host" "$docker_api_host" "$web_port" "$api_port" <<'PY'
import json
import re
import sqlite3
import sys
from pathlib import Path
(
npm_root,
db_path,
target,
app_domain,
api_domain,
native_host,
docker_web_host,
docker_api_host,
web_port,
api_port,
) = sys.argv[1:]
web_host = native_host if target == "native" else docker_web_host
api_host = native_host if target == "native" else docker_api_host
def has_domain(raw, domain):
try:
return domain in json.loads(raw)
except Exception:
return domain in raw
def replace_nth(text, pattern, replacement, index):
matches = list(pattern.finditer(text))
if len(matches) < index:
raise SystemExit(f"Unable to rewrite generated proxy config; expected match {index} for {pattern.pattern!r}")
match = matches[index - 1]
return text[:match.start()] + replacement(match) + text[match.end():]
server_pattern = re.compile(r'^(?P<prefix>\s*set \$server\s+)".*?";\s*$', re.M)
port_pattern = re.compile(r'^(?P<prefix>\s*set \$port\s+)\d+;\s*$', re.M)
def replace_server(text, host, index):
return replace_nth(text, server_pattern, lambda m: f'{m.group("prefix")}"{host}";', index)
def replace_port(text, port, index):
return replace_nth(text, port_pattern, lambda m: f'{m.group("prefix")}{port};', index)
con = sqlite3.connect(db_path)
rows = list(con.execute("select id, domain_names from proxy_host where is_deleted = 0"))
app_ids = [row_id for row_id, domains in rows if has_domain(domains, app_domain)]
api_ids = [row_id for row_id, domains in rows if has_domain(domains, api_domain)]
if len(app_ids) != 1 or len(api_ids) != 1:
raise SystemExit(f"Expected one app and one API proxy host, found app={app_ids} api={api_ids}")
api_conf = Path(npm_root) / "data/nginx/proxy_host" / f"{api_ids[0]}.conf"
app_conf = Path(npm_root) / "data/nginx/proxy_host" / f"{app_ids[0]}.conf"
if api_conf.exists():
text = api_conf.read_text()
text = replace_server(text, api_host, 1)
text = replace_port(text, int(api_port), 1)
api_conf.write_text(text)
if app_conf.exists():
text = app_conf.read_text()
text = replace_server(text, web_host, 1)
text = replace_port(text, int(web_port), 1)
text = replace_server(text, api_host, 2)
text = replace_port(text, int(api_port), 2)
app_conf.write_text(text)
PY
reloaded=0
for _ in 1 2 3 4 5; do
if docker exec "$npm_container" nginx -s reload >/dev/null 2>&1; then
reloaded=1
break
fi
sleep 1
done
if [[ "$reloaded" == "1" ]]; then
echo "Reloaded nginx-proxy-manager"
else
echo "Warning: nginx-proxy-manager reload did not succeed after restart; verify the container is healthy." >&2
fi
fi

View file

@ -0,0 +1,17 @@
[Unit]
Description=Islandflow ClickHouse
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/env clickhouse-server --config-file=/etc/clickhouse-server/config.xml
Restart=always
RestartSec=5
User=clickhouse
Group=clickhouse
StateDirectory=clickhouse
LimitNOFILE=262144
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,18 @@
[Unit]
Description=Islandflow NATS JetStream
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/sbin/nats-server -js -sd /var/lib/islandflow/nats -a 127.0.0.1 -p 4222 -m 8222
Restart=always
RestartSec=2
User=nats
Group=nats
RuntimeDirectory=islandflow-nats
StateDirectory=islandflow/nats
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,18 @@
[Unit]
Description=Islandflow Redis
After=network-online.target
Wants=network-online.target
[Service]
Type=notify
ExecStart=/usr/bin/env redis-server /etc/islandflow/redis.conf --supervised systemd --daemonize no
Restart=always
RestartSec=2
User=redis
Group=redis
RuntimeDirectory=islandflow-redis
StateDirectory=islandflow/redis
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,19 @@
[Unit]
Description=Islandflow API
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/home/delta/islandflow
Environment=API_HOST=0.0.0.0
Environment=API_PORT=4000
EnvironmentFile=/home/delta/islandflow/.env
ExecStart=/home/delta/.bun/bin/bun services/api/src/index.ts
Restart=always
RestartSec=2
KillSignal=SIGINT
TimeoutStopSec=20
[Install]
WantedBy=default.target

View file

@ -0,0 +1,17 @@
[Unit]
Description=Islandflow candles
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/home/delta/islandflow
EnvironmentFile=/home/delta/islandflow/.env
ExecStart=/home/delta/.bun/bin/bun services/candles/src/index.ts
Restart=always
RestartSec=2
KillSignal=SIGINT
TimeoutStopSec=20
[Install]
WantedBy=default.target

View file

@ -0,0 +1,17 @@
[Unit]
Description=Islandflow compute
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/home/delta/islandflow
EnvironmentFile=/home/delta/islandflow/.env
ExecStart=/home/delta/.bun/bin/bun services/compute/src/index.ts
Restart=always
RestartSec=2
KillSignal=SIGINT
TimeoutStopSec=20
[Install]
WantedBy=default.target

View file

@ -0,0 +1,17 @@
[Unit]
Description=Islandflow ingest-equities
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/home/delta/islandflow
EnvironmentFile=/home/delta/islandflow/.env
ExecStart=/home/delta/.bun/bin/bun services/ingest-equities/src/index.ts
Restart=always
RestartSec=2
KillSignal=SIGINT
TimeoutStopSec=20
[Install]
WantedBy=default.target

View file

@ -0,0 +1,17 @@
[Unit]
Description=Islandflow ingest-news
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/home/delta/islandflow
EnvironmentFile=/home/delta/islandflow/.env
ExecStart=/home/delta/.bun/bin/bun services/ingest-news/src/index.ts
Restart=always
RestartSec=2
KillSignal=SIGINT
TimeoutStopSec=20
[Install]
WantedBy=default.target

View file

@ -0,0 +1,17 @@
[Unit]
Description=Islandflow ingest-options
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/home/delta/islandflow
EnvironmentFile=/home/delta/islandflow/.env
ExecStart=/home/delta/.bun/bin/bun services/ingest-options/src/index.ts
Restart=always
RestartSec=2
KillSignal=SIGINT
TimeoutStopSec=20
[Install]
WantedBy=default.target

View file

@ -0,0 +1,19 @@
[Unit]
Description=Islandflow web
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/home/delta/islandflow
Environment=WEB_HOST=0.0.0.0
Environment=WEB_PORT=3000
EnvironmentFile=/home/delta/islandflow/.env
ExecStart=/bin/sh -lc 'cd /home/delta/islandflow/apps/web && exec /home/delta/.bun/bin/bun x next start -H "$WEB_HOST" -p "$WEB_PORT"'
Restart=always
RestartSec=2
KillSignal=SIGINT
TimeoutStopSec=20
[Install]
WantedBy=default.target

View file

@ -0,0 +1,482 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Daily Git Summary for 2026-05-18 (Merged View)</title>
<style>
:root {
color-scheme: dark;
--bg: #06080b;
--panel: #111820;
--panel-2: #0d141b;
--border: rgba(255, 255, 255, 0.09);
--text: #e6edf4;
--muted: #90a0b2;
--faint: #6e7b8c;
--accent: #f5a623;
--accent-soft: rgba(245, 166, 35, 0.14);
--blue: #4da3ff;
--blue-soft: rgba(77, 163, 255, 0.14);
--green: #25c17a;
--green-soft: rgba(37, 193, 122, 0.14);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background:
radial-gradient(circle at top right, rgba(245, 166, 35, 0.1), transparent 24%),
linear-gradient(180deg, #091018 0%, var(--bg) 30%, #05070a 100%);
color: var(--text);
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
line-height: 1.58;
}
main {
width: min(1120px, calc(100% - 36px));
margin: 28px auto 48px;
}
.hero,
section {
border: 1px solid var(--border);
border-radius: 16px;
background: linear-gradient(180deg, rgba(17, 24, 32, 0.95), rgba(13, 20, 27, 0.98));
}
.hero {
padding: 28px;
box-shadow: 0 22px 56px rgba(0, 0, 0, 0.34);
}
h1,
h2,
h3 {
margin: 0;
font-family: "Quantico", "Segoe UI", sans-serif;
letter-spacing: 0.04em;
}
h1 {
margin-top: 14px;
font-size: clamp(1.9rem, 3.3vw, 2.7rem);
}
h2 {
font-size: 1.2rem;
margin-bottom: 14px;
}
h3 {
font-size: 1rem;
}
p {
margin: 0;
color: var(--muted);
max-width: 86ch;
}
section {
margin-top: 14px;
padding: 22px;
}
.eyebrow,
.pill,
.commit,
.time,
.label {
font-family: "IBM Plex Mono", monospace;
}
.eyebrow {
display: inline-block;
padding: 6px 10px;
border-radius: 999px;
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 12px;
background: var(--accent-soft);
color: var(--accent);
}
.meta {
margin-top: 20px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
}
.meta-card {
border: 1px solid var(--border);
border-radius: 12px;
background: rgba(255, 255, 255, 0.03);
padding: 12px 14px;
}
.label {
display: block;
margin-bottom: 6px;
color: var(--text);
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.74rem;
}
.metric {
color: var(--text);
font-weight: 700;
font-size: 1.45rem;
}
.cards,
.impact-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 10px;
}
.card {
border: 1px solid var(--border);
border-radius: 12px;
background: rgba(255, 255, 255, 0.025);
padding: 14px;
}
.card.feature {
border-color: rgba(37, 193, 122, 0.28);
background: linear-gradient(180deg, rgba(37, 193, 122, 0.1), rgba(255, 255, 255, 0.02));
}
.card.ops {
border-color: rgba(77, 163, 255, 0.28);
background: linear-gradient(180deg, rgba(77, 163, 255, 0.1), rgba(255, 255, 255, 0.02));
}
.timeline {
display: grid;
gap: 11px;
}
.entry {
border: 1px solid var(--border);
border-radius: 12px;
background: rgba(255, 255, 255, 0.022);
padding: 14px;
}
.entry-head {
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
align-items: baseline;
margin-bottom: 7px;
}
.commit {
color: var(--blue);
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.08em;
}
.time {
color: var(--faint);
text-transform: uppercase;
font-size: 0.74rem;
letter-spacing: 0.08em;
}
.pill {
border: 1px solid var(--border);
border-radius: 999px;
padding: 5px 8px;
background: rgba(255, 255, 255, 0.04);
font-size: 0.72rem;
}
ul {
margin: 0;
padding-left: 19px;
color: var(--muted);
}
li + li {
margin-top: 8px;
}
pre {
margin: 10px 0 0;
padding: 12px;
overflow-x: auto;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
background: rgba(6, 8, 11, 0.9);
color: #d7e2ec;
font-family: "IBM Plex Mono", monospace;
font-size: 0.79rem;
}
code {
font-family: "IBM Plex Mono", monospace;
}
@media (max-width: 720px) {
main {
width: min(100%, calc(100% - 20px));
margin: 18px auto 30px;
}
}
</style>
</head>
<body>
<main>
<section class="hero">
<span class="eyebrow">Daily Git Summary</span>
<h1>Standup summary for Monday, May 18, 2026 (after merge)</h1>
<p>
This regenerated report uses merged history for the full May 18 local-day window
(<code>2026-05-18 00:00 -0400</code> through <code>2026-05-19 00:00 -0400</code>). It now includes eight commits,
including native deployment work and the merge commit that landed that line of work on <code>main</code>.
</p>
<div class="meta">
<div class="meta-card">
<span class="label">Commits</span>
<div class="metric">8</div>
</div>
<div class="meta-card">
<span class="label">Unique Files</span>
<div class="metric">68</div>
</div>
<div class="meta-card">
<span class="label">Insertions</span>
<div class="metric">4244</div>
</div>
<div class="meta-card">
<span class="label">Deletions</span>
<div class="metric">194</div>
</div>
</div>
</section>
<section>
<h2>Summary</h2>
<div class="cards">
<div class="card feature">
<span class="label">User-facing delivery</span>
<p>
Commit <code>906fe411</code> added Alpaca news wire support across ingest, storage, API, and web terminal/news
route surfaces.
</p>
</div>
<div class="card ops">
<span class="label">Platform and deployment delivery</span>
<p>
Commits <code>d589858c</code> and <code>bdb9d9a9</code> added native deployment workflow, infra/user units,
cutover, rollback, and health-check scripts, then merged via <code>8f0794dd</code> (PR #2).
</p>
</div>
<div class="card">
<span class="label">Workflow and docs updates</span>
<p>
Commits <code>687a2170</code>, <code>62aae708</code>, <code>48095fce</code>, and <code>04baeceb</code> updated
beads/docs instructions and added turn/standup documentation.
</p>
</div>
</div>
</section>
<section>
<h2>Changes Made</h2>
<div class="timeline">
<article class="entry">
<div class="entry-head">
<strong>update beads</strong>
<span class="commit">687a2170</span>
<span class="time">2026-05-18 03:15 -0400</span>
<span class="pill">1 file</span>
</div>
<p>Touched <code>deployment/docker/workspace-root/package.json</code> with one-line change.</p>
</article>
<article class="entry">
<div class="entry-head">
<strong>Implement native fast iterative deploy workflow</strong>
<span class="commit">d589858c</span>
<span class="time">2026-05-18 03:34 -0400</span>
<span class="pill">17 files</span>
<span class="pill">+873 / -110</span>
</div>
<ul>
<li>Expanded <code>scripts/deploy.ts</code> for native deploy runtime behavior.</li>
<li>Added native user-unit templates and rollback/health tooling in <code>deployment/native/</code>.</li>
<li>Added associated plan and turn documents in <code>docs/plans</code> and <code>docs/turns</code>.</li>
</ul>
</article>
<article class="entry">
<div class="entry-head">
<strong>fix(api): remove duplicate alert context import</strong>
<span class="commit">48095fce</span>
<span class="time">2026-05-18 09:04 -0400</span>
<span class="pill">2 files</span>
</div>
<p>Removed duplicate import in <code>services/api/src/index.ts</code> and added a turn doc.</p>
</article>
<article class="entry">
<div class="entry-head">
<strong>docs(general): add 2026-05-17 standup summary</strong>
<span class="commit">62aae708</span>
<span class="time">2026-05-18 09:05 -0400</span>
<span class="pill">2 files</span>
</div>
<p>Added <code>docs/general/2026-05-18-standup-summary-2026-05-17.html</code> and updated beads state.</p>
</article>
<article class="entry">
<div class="entry-head">
<strong>add alpaca news wire across ingest api and web</strong>
<span class="commit">906fe411</span>
<span class="time">2026-05-18 16:55 -0400</span>
<span class="pill">31 files</span>
<span class="pill">+1407 / -50</span>
</div>
<ul>
<li>Created <code>services/ingest-news</code> and wired Alpaca backfill/websocket ingestion.</li>
<li>Added news types/storage contracts in <code>packages/types</code> and <code>packages/storage</code>.</li>
<li>Extended API live/history endpoints and web terminal/news route rendering.</li>
</ul>
</article>
<article class="entry">
<div class="entry-head">
<strong>Implement native public edge cutover</strong>
<span class="commit">bdb9d9a9</span>
<span class="time">2026-05-18 19:55 -0400</span>
<span class="pill">29 files</span>
<span class="pill">+1215 / -31</span>
</div>
<ul>
<li>Added native infra system units and scripts for bootstrap/start/stop/cutover/full rollback.</li>
<li>Updated deploy docs and runtime config files under <code>deployment/native/config</code>.</li>
<li>Added turn doc <code>docs/turns/2026-05-18-native-public-edge-cutover.html</code>.</li>
</ul>
</article>
<article class="entry">
<div class="entry-head">
<strong>Merge pull request 'Native public edge cutover with Docker rollback path' (#2)</strong>
<span class="commit">8f0794dd</span>
<span class="time">2026-05-19 00:09 +0000</span>
<span class="pill">merge commit</span>
</div>
<p>Merged <code>native-deploy</code> into <code>main</code> within the May 18 US/Eastern day window.</p>
</article>
<article class="entry">
<div class="entry-head">
<strong>update turn docs and beads workflow</strong>
<span class="commit">04baeceb</span>
<span class="time">2026-05-18 21:32 -0400</span>
<span class="pill">1 file</span>
</div>
<p>Updated repository-level instructions in <code>AGENTS.md</code>.</p>
</article>
</div>
</section>
<section>
<h2>Context</h2>
<p>
The earlier report was generated before merged history included the native deployment branch on <code>main</code>.
This recreation uses <code>git log --all</code> over the same date window, so it captures both feature work and
merged operational/deployment work visible after PR merge.
</p>
</section>
<section>
<h2>Important Implementation Details</h2>
<div class="cards">
<div class="card">
<h3>News wire ingestion and delivery path</h3>
<p>
The news pipeline added a new ingest service and API fanout channel, then exposed UI surfaces in
<code>/news</code> and terminal panes.
</p>
<pre><code>if (features.news) {
subscriptions.push({ channel: "news", snapshot_limit: LIVE_OPTIONS_HEAD_LIMIT });
}</code></pre>
</div>
<div class="card">
<h3>Native deployment hardening</h3>
<p>
Deployment scripts and unit templates now include direct scripts for cutover and rollback, with infra and
service checks under <code>deployment/native/</code>.
</p>
<pre><code>deployment/native/cutover.sh
deployment/native/full-rollback.sh
deployment/native/install-infra-units.sh</code></pre>
</div>
<div class="card">
<h3>Merged history effect on standup scope</h3>
<p>
The merged view increased the standup scope from 4 to 8 commits and from 35 to 68 unique files touched for the
same local-day window.
</p>
</div>
</div>
</section>
<section>
<h2>Expected Impact for End-Users</h2>
<div class="impact-cards">
<div class="card">
<span class="label">Trading UI users</span>
<p>Live news wire data is now available in terminal surfaces alongside existing market/event feeds.</p>
</div>
<div class="card">
<span class="label">Operators</span>
<p>Native deployment and rollback procedures now have first-class scripted and documented paths.</p>
</div>
<div class="card">
<span class="label">Team reporting</span>
<p>This standup report now matches merged repository history instead of pre-merge branch-local history.</p>
</div>
</div>
</section>
<section>
<h2>Validation</h2>
<ul>
<li>Used <code>git fetch --all --prune</code> before recomputing history.</li>
<li>Used <code>git log --all</code> over the May 18 ET window to include merged commits.</li>
<li>Used <code>git log --stat --summary</code> and <code>--numstat</code> to ground file and line-count statements.</li>
<li>No build/test commands were run because this task only regenerates reporting documentation.</li>
</ul>
</section>
<section>
<h2>Issues, Limitations, and Mitigations</h2>
<ul>
<li>This report describes commit history only and does not infer intent beyond commit messages and touched files.</li>
<li>Commit <code>8f0794dd</code> is timestamped in UTC; it still falls on May 18 in US/Eastern, so it is included.</li>
<li>Metrics are based on local git history at regeneration time and can change if additional backdated commits appear.</li>
</ul>
</section>
<section>
<h2>Follow-up Work</h2>
<ul>
<li>This regeneration is tracked by beads issue <code>islandflow-0ty</code>.</li>
<li>No additional follow-up work was identified during this documentation-only task.</li>
</ul>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,549 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Standup Summary for 2026-05-17</title>
<style>
:root {
color-scheme: dark;
--bg: #06080b;
--panel: #111820;
--panel-2: #0d141b;
--border: rgba(255, 255, 255, 0.08);
--border-strong: rgba(245, 166, 35, 0.35);
--text: #e6edf4;
--muted: #90a0b2;
--faint: #6e7b8c;
--accent: #f5a623;
--accent-soft: rgba(245, 166, 35, 0.12);
--green: #25c17a;
--green-soft: rgba(37, 193, 122, 0.12);
--blue: #4da3ff;
--blue-soft: rgba(77, 163, 255, 0.12);
--red: #ff6b5f;
--red-soft: rgba(255, 107, 95, 0.12);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background:
radial-gradient(circle at top right, rgba(245, 166, 35, 0.12), transparent 28%),
linear-gradient(180deg, #091018 0%, var(--bg) 28%, #05070a 100%);
color: var(--text);
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
line-height: 1.6;
}
main {
width: min(1120px, calc(100% - 40px));
margin: 32px auto 56px;
}
.hero,
section {
background: linear-gradient(180deg, rgba(17, 24, 32, 0.94), rgba(13, 20, 27, 0.98));
border: 1px solid var(--border);
border-radius: 18px;
}
.hero {
padding: 32px;
margin-bottom: 18px;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.32);
}
.eyebrow,
.pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 999px;
font-family: "IBM Plex Mono", monospace;
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.eyebrow {
background: var(--accent-soft);
color: var(--accent);
}
.pill {
background: rgba(255, 255, 255, 0.04);
color: var(--text);
border: 1px solid var(--border);
}
h1,
h2,
h3 {
margin: 0;
font-family: "Quantico", "Segoe UI", sans-serif;
letter-spacing: 0.04em;
}
h1 {
margin-top: 18px;
font-size: clamp(2rem, 4vw, 3rem);
line-height: 1.04;
}
h2 {
font-size: 1.25rem;
margin-bottom: 16px;
}
h3 {
font-size: 1rem;
}
p {
margin: 0;
max-width: 80ch;
color: var(--muted);
}
.hero p {
margin-top: 14px;
font-size: 1rem;
}
.meta {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
margin-top: 24px;
}
.meta-card {
padding: 14px 16px;
border: 1px solid var(--border);
border-radius: 14px;
background: rgba(255, 255, 255, 0.03);
}
.meta-card strong,
.label {
display: block;
margin-bottom: 6px;
color: var(--text);
font-family: "IBM Plex Mono", monospace;
font-size: 0.76rem;
letter-spacing: 0.12em;
text-transform: uppercase;
}
section {
padding: 24px;
margin-top: 16px;
}
.summary-list,
.detail-list {
display: grid;
gap: 12px;
}
.summary-item,
.timeline-item,
.callout {
padding: 16px 18px;
border: 1px solid var(--border);
border-radius: 14px;
background: rgba(255, 255, 255, 0.025);
}
.summary-item strong,
.timeline-item strong {
color: var(--text);
}
.timeline {
display: grid;
gap: 14px;
}
.timeline-meta {
display: flex;
flex-wrap: wrap;
gap: 10px 14px;
align-items: baseline;
margin-bottom: 8px;
}
.commit-id,
.timestamp {
font-family: "IBM Plex Mono", monospace;
font-size: 0.76rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.commit-id {
color: var(--blue);
}
.timestamp {
color: var(--faint);
}
ul {
margin: 0;
padding-left: 20px;
color: var(--muted);
}
li + li {
margin-top: 10px;
}
.file-list {
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.file-pill {
padding: 5px 8px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.045);
border: 1px solid var(--border);
color: var(--text);
font-family: "IBM Plex Mono", monospace;
font-size: 0.72rem;
}
.good {
background: var(--green-soft);
color: var(--green);
}
.info {
background: var(--blue-soft);
color: var(--blue);
}
.risk {
background: var(--red-soft);
color: var(--red);
}
code {
font-family: "IBM Plex Mono", monospace;
font-size: 0.92em;
color: var(--text);
}
.grid-two {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 14px;
}
a {
color: var(--blue);
}
</style>
</head>
<body>
<main>
<article class="hero">
<span class="eyebrow">Git Standup Summary</span>
<h1>Repository activity recorded for 2026-05-17</h1>
<p>
Yesterday's git history shows three main themes: frontend and API work to hydrate alert evidence from
ClickHouse, deploy workflow changes in <code>scripts/deploy.ts</code>, and Beads/Dolt remote setup plus
documentation updates. This summary is grounded in the commits, merged PRs, and touched files visible in the
repository history for 2026-05-17.
</p>
<div class="meta">
<div class="meta-card">
<strong>Commit Count</strong>
<span>20 commits on 2026-05-17</span>
</div>
<div class="meta-card">
<strong>Merges</strong>
<span>5 pull request merges</span>
</div>
<div class="meta-card">
<strong>File Footprint</strong>
<span>22 distinct paths touched</span>
</div>
<div class="meta-card">
<strong>Most Revisited</strong>
<span><code>.beads/issues.jsonl</code>, <code>scripts/deploy.ts</code>, <code>apps/web/app/terminal.tsx</code></span>
</div>
</div>
</article>
<section>
<h2>Summary</h2>
<div class="summary-list">
<div class="summary-item">
<strong>Alert context from ClickHouse landed and was merged twice through follow-up PRs.</strong>
The core implementation appeared in commit <code>c0b5b6d</code> and merge PR <code>#41</code>
(<code>3e08955</code>), then was extended in <code>58e57fa</code> and merged through <code>#43</code>
(<code>a27d499</code>) and a documentation polish PR <code>#44</code> (<code>49efc24</code>).
</div>
<div class="summary-item">
<strong>Deploy tooling changed in three steps.</strong>
The day included an allowlist tightening in <code>5ddfbfa</code>, a new fast deploy mode in
<code>75ed6f3</code>, and Forgejo-aware remote resolution in <code>6e6788b</code>, all centered on
<code>scripts/deploy.ts</code>.
</div>
<div class="summary-item">
<strong>Process and reporting work was visible alongside feature work.</strong>
Beads Dolt remote configuration was added in <code>37bd393</code>, revised in <code>d0d8bd4</code> and
<code>cd0a1dd</code>, and yesterday's prior standup report was added in <code>0416194</code>.
</div>
</div>
</section>
<section>
<h2>Changes Made</h2>
<div class="timeline">
<article class="timeline-item">
<div class="timeline-meta">
<span class="pill info">Frontend + API</span>
<span class="commit-id">c0b5b6d</span>
<span class="timestamp">11:02 EDT</span>
</div>
<h3>Hydrate alert evidence from ClickHouse</h3>
<p>
Commit <code>c0b5b6d</code> added ClickHouse-backed alert context across storage, API, tests, and the
terminal UI. The same change set was merged as PR <code>#41</code> in <code>3e08955</code>.
</p>
<div class="file-list">
<span class="file-pill">packages/storage/src/clickhouse.ts</span>
<span class="file-pill">services/api/src/alert-context.ts</span>
<span class="file-pill">services/api/src/index.ts</span>
<span class="file-pill">apps/web/app/terminal.tsx</span>
<span class="file-pill">apps/web/app/terminal.test.ts</span>
<span class="file-pill">packages/storage/tests/alerts.test.ts</span>
</div>
</article>
<article class="timeline-item">
<div class="timeline-meta">
<span class="pill">Deploy workflow</span>
<span class="commit-id">5ddfbfa</span>
<span class="timestamp">11:45 EDT</span>
</div>
<h3>Tighten deploy remote untracked allowlist</h3>
<p>
Commit <code>5ddfbfa</code>, later merged as PR <code>#42</code> in <code>8b166a5</code>, narrowed the
remote untracked allowlist in <code>scripts/deploy.ts</code>. Two follow-up documentation commits,
<code>8631a53</code> and <code>219d3fd</code>, recorded and corrected the validation notes for that
change.
</p>
<div class="file-list">
<span class="file-pill">scripts/deploy.ts</span>
<span class="file-pill">docs/turns/2026-05-17-deploy-allowlist-pr-packaging.html</span>
</div>
</article>
<article class="timeline-item">
<div class="timeline-meta">
<span class="pill info">Integration</span>
<span class="commit-id">58e57fa</span>
<span class="timestamp">20:18 EDT</span>
</div>
<h3>Add ClickHouse alert context hydration for alert drawers</h3>
<p>
Commit <code>58e57fa</code> extended the earlier alert-context work, adding drawer-specific hydration in
the web app and API. A merge-conflict resolution commit <code>dc932cf</code> combined this with the
deploy allowlist branch before PR <code>#43</code> merged in <code>a27d499</code>.
</p>
<div class="file-list">
<span class="file-pill">apps/web/app/terminal.tsx</span>
<span class="file-pill">packages/storage/src/clickhouse.ts</span>
<span class="file-pill">services/api/src/index.ts</span>
<span class="file-pill">docs/turns/2026-05-17-clickhouse-alert-context.html</span>
</div>
</article>
<article class="timeline-item">
<div class="timeline-meta">
<span class="pill">Deploy workflow</span>
<span class="commit-id">75ed6f3</span>
<span class="timestamp">22:53 EDT</span>
</div>
<h3>Add fast deploy mode for routine rollouts</h3>
<p>
Commit <code>75ed6f3</code> added a faster deploy path and updated both deployment readmes. Minutes
later, commit <code>6e6788b</code> made deploy remote resolution Forgejo-aware, again in
<code>scripts/deploy.ts</code>.
</p>
<div class="file-list">
<span class="file-pill">scripts/deploy.ts</span>
<span class="file-pill">deployment/docker/README.md</span>
<span class="file-pill">deployment/native/README.md</span>
<span class="file-pill">docs/turns/2026-05-17-add-fast-deploy-mode.html</span>
<span class="file-pill">docs/turns/2026-05-17-forgejo-deploy-remote-resolution.html</span>
</div>
</article>
<article class="timeline-item">
<div class="timeline-meta">
<span class="pill">Repo operations</span>
<span class="commit-id">37bd393</span>
<span class="timestamp">06:41 EDT</span>
</div>
<h3>Beads remote setup and daily reporting</h3>
<p>
Commit <code>37bd393</code> configured the Beads Dolt remote in <code>.beads/config.yaml</code>, then
commits <code>d0d8bd4</code> and <code>cd0a1dd</code> revised the same sync settings. Commit
<code>0416194</code> added the standup summary document for 2026-05-16 activity in
<code>docs/general</code>.
</p>
<div class="file-list">
<span class="file-pill">.beads/config.yaml</span>
<span class="file-pill">.beads/issues.jsonl</span>
<span class="file-pill">docs/general/2026-05-17-standup-summary-2026-05-16.html</span>
</div>
</article>
</div>
</section>
<section>
<h2>Context</h2>
<div class="grid-two">
<div class="callout">
<span class="label">Merged PRs</span>
<ul>
<li><code>#40</code> merged in <code>88b2c33</code>: live tape scroll stability and related deploy/image work.</li>
<li><code>#41</code> merged in <code>3e08955</code>: initial ClickHouse alert evidence hydration.</li>
<li><code>#42</code> merged in <code>8b166a5</code>: deploy allowlist packaging follow-through.</li>
<li><code>#43</code> merged in <code>a27d499</code>: alert drawer hydration follow-up.</li>
<li><code>#44</code> merged in <code>49efc24</code>: turn-document polish for alert context.</li>
</ul>
</div>
<div class="callout">
<span class="label">Most Touched Areas</span>
<ul>
<li><code>.beads/issues.jsonl</code> changed in 9 commits, reflecting issue tracking churn throughout the day.</li>
<li><code>scripts/deploy.ts</code> changed in 3 direct commits tied to deploy safety and speed.</li>
<li><code>apps/web/app/terminal.tsx</code> changed in 3 direct commits tied to live tape behavior and alert context.</li>
<li>Documentation output expanded across <code>docs/turns</code> and <code>docs/general</code> alongside implementation work.</li>
</ul>
</div>
</div>
</section>
<section>
<h2>Important Implementation Details</h2>
<ul>
<li>
The ClickHouse alert-context work was not isolated to one layer. Commits <code>c0b5b6d</code> and
<code>58e57fa</code> touched storage access, API wiring, UI presentation, and dedicated tests, which
makes this the clearest full-stack change in yesterday's history.
</li>
<li>
The deploy changes were incremental rather than a single rewrite. The history shows a narrowing change in
<code>5ddfbfa</code>, an operator-speed path in <code>75ed6f3</code>, and remote detection logic in
<code>6e6788b</code>.
</li>
<li>
Merge commit <code>dc932cf</code> explicitly resolved conflicts between the alert-context and deploy
allowlist branches before later PR merges landed, so yesterday's main branch activity included integration
work as well as feature work.
</li>
<li>
Commit <code>073c1de</code> created an empty <code>forgejo.test</code> path. The git history shows the file
creation, but no test content in that commit.
</li>
</ul>
</section>
<section>
<h2>Expected Impact for End-Users</h2>
<ul>
<li>
User-facing terminal behavior changed in two visible ways: live tape scroll stability from
<code>d334e16</code>/<code>#40</code> and richer alert evidence context from <code>c0b5b6d</code>,
<code>58e57fa</code>, and the follow-up merges.
</li>
<li>
Deploy workflow commits affected operator tooling rather than customer-facing product screens. Those changes
should matter most to maintainers using <code>scripts/deploy.ts</code> and the deployment readmes.
</li>
<li>
Beads remote configuration and standup-report commits affected internal workflow and documentation, not
runtime product behavior.
</li>
</ul>
</section>
<section>
<h2>Validation</h2>
<ul>
<li>
The turn document added in <code>d334e16</code> records
<code>bun test apps/web/app/terminal.test.ts services/api/tests/live.test.ts</code> passing and
<code>bun --cwd=apps/web run build</code> passing.
</li>
<li>
The turn document added in <code>c0b5b6d</code> records
<code>bun test packages/storage/tests</code>,
<code>bun test services/api/tests</code>,
<code>bun test apps/web/app/terminal.test.ts</code>, and
<code>bun --cwd=apps/web run build</code>.
</li>
<li>
The polished turn document merged in <code>49efc24</code> records those alert-context validations as
passing.
</li>
<li>
The deploy allowlist turn document created in <code>8631a53</code> and corrected in <code>219d3fd</code>
explicitly notes that a repository-wide <code>bun test</code> run reported failures at that point.
</li>
<li>
Later deploy-related turn documents added in <code>75ed6f3</code> and <code>6e6788b</code> record full
<code>bun test</code> passing, with the Forgejo remote document stating <code>232 passing</code>,
<code>0 failing</code>.
</li>
<li>
This automation run only created documentation. No additional code validation command was run for this
summary itself.
</li>
</ul>
</section>
<section>
<h2>Issues, Limitations, and Mitigations</h2>
<ul>
<li>
This document summarizes repository history only. It does not infer goals beyond what commit subjects, PR
titles, merge structure, and touched files show.
</li>
<li>
Some PR context is visible only through merge commits. For example, PR <code>#40</code> bundles scroll
stability with deploy and Docker-path changes, so the summary reports the merged file footprint rather than
inferring which portion dominated the review.
</li>
<li>
Validation evidence comes from committed turn documents, not from re-running every historical command during
this automation.
</li>
</ul>
</section>
<section>
<h2>Follow-up Work</h2>
<p>
No new follow-up Beads issue was created from the git summary itself. The Beads task for this automation run
is <code>islandflow-x70</code>, which tracks creation of this standup document and will be closed as part of
the session sync.
</p>
</section>
</main>
</body>
</html>

638
docs/index.html Normal file
View file

@ -0,0 +1,638 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Islandflow Docs</title>
<style>
:root {
--bg: #f4f6f8;
--surface: #ffffff;
--surface-muted: #e8edf2;
--text: #1a2433;
--muted: #5b6a80;
--border: #ccd5df;
--accent: #0f766e;
--accent-soft: #d1fae5;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Avenir Next", "Segoe UI", sans-serif;
background: radial-gradient(circle at top right, #e2f8f2, var(--bg) 35%);
color: var(--text);
}
main {
max-width: 1120px;
margin: 0 auto;
padding: 32px 16px 48px;
}
.header {
display: grid;
gap: 12px;
}
h1 {
margin: 0;
font-size: clamp(1.8rem, 2.3vw, 2.4rem);
font-weight: 760;
}
.subtitle {
margin: 0;
color: var(--muted);
max-width: 60ch;
}
.toolbar {
margin-top: 10px;
padding: 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
display: grid;
gap: 12px;
}
.stats {
font-size: 0.95rem;
color: var(--muted);
}
.search {
width: 100%;
border: 1px solid var(--border);
border-radius: 8px;
font: inherit;
font-size: 1rem;
padding: 10px 12px;
background: #fff;
}
.search:focus {
outline: 2px solid color-mix(in srgb, var(--accent) 30%, white);
outline-offset: 0;
border-color: var(--accent);
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.chip {
text-decoration: none;
color: var(--text);
background: var(--surface-muted);
padding: 6px 10px;
border-radius: 999px;
font-size: 0.85rem;
border: 1px solid transparent;
}
.chip span {
color: var(--muted);
}
.chip:hover {
border-color: var(--accent);
}
.groups {
margin-top: 20px;
display: grid;
gap: 16px;
}
.group {
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
padding: 14px;
}
.group.hidden {
display: none;
}
.group h2 {
margin: 0 0 10px;
font-size: 1.1rem;
}
.group h2 span {
color: var(--muted);
font-weight: 520;
}
.doc-list {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: 6px;
}
.doc-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 8px 10px;
border-radius: 6px;
}
.doc-item.hidden {
display: none;
}
.doc-item:hover {
background: #f5faf8;
}
.doc-link {
color: var(--text);
text-decoration: none;
font-family: "IBM Plex Mono", "SFMono-Regular", Consolas, monospace;
font-size: 0.92rem;
overflow-wrap: anywhere;
}
.doc-link:hover {
color: var(--accent);
text-decoration: underline;
}
.meta {
display: flex;
align-items: center;
gap: 10px;
color: var(--muted);
font-size: 0.82rem;
white-space: nowrap;
}
.tag {
background: var(--accent-soft);
color: #065f46;
border-radius: 999px;
padding: 3px 8px;
font-size: 0.78rem;
}
.empty {
margin-top: 20px;
border: 1px dashed var(--border);
border-radius: 8px;
background: var(--surface);
color: var(--muted);
padding: 20px;
text-align: center;
display: none;
}
</style>
</head>
<body>
<main>
<header class="header">
<h1>Islandflow docs index</h1>
<p class="subtitle">A browsable index of files under <code>docs/</code> with filtering and grouped navigation.</p>
</header>
<section class="toolbar">
<div class="stats"><strong id="visible-count">35</strong> of <strong>35</strong> files shown</div>
<input id="doc-search" class="search" type="search" placeholder="Filter by filename or folder..." autocomplete="off" />
<nav class="chips"><a class="chip" href="#category-turns">turns <span>28</span></a>
<a class="chip" href="#category-daily-git">daily-git <span>1</span></a>
<a class="chip" href="#category-general">general <span>2</span></a>
<a class="chip" href="#category-plans">plans <span>2</span></a>
<a class="chip" href="#category-root">root <span>2</span></a></nav>
</section>
<section class="groups" id="groups">
<section class="group" id="category-turns">
<h2>turns <span>28</span></h2>
<ul class="doc-list">
<li class="doc-item" data-search="turns/2026-05-19-publish-docs-pages-index.html turns">
<a class="doc-link" href="./turns/2026-05-19-publish-docs-pages-index.html">turns/2026-05-19-publish-docs-pages-index.html</a>
<div class="meta">
<span class="tag">html</span>
<span>6.7 KB</span>
<span>May 19, 2026, 2:59 PM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-18-native-public-edge-cutover.html turns">
<a class="doc-link" href="./turns/2026-05-18-native-public-edge-cutover.html">turns/2026-05-18-native-public-edge-cutover.html</a>
<div class="meta">
<span class="tag">html</span>
<span>19 KB</span>
<span>May 19, 2026, 2:48 PM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-19-reconcile-pr-conflicts.html turns">
<a class="doc-link" href="./turns/2026-05-19-reconcile-pr-conflicts.html">turns/2026-05-19-reconcile-pr-conflicts.html</a>
<div class="meta">
<span class="tag">html</span>
<span>9.8 KB</span>
<span>May 19, 2026, 2:48 PM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-18-native-fast-iterative-deploy.html turns">
<a class="doc-link" href="./turns/2026-05-18-native-fast-iterative-deploy.html">turns/2026-05-18-native-fast-iterative-deploy.html</a>
<div class="meta">
<span class="tag">html</span>
<span>9.0 KB</span>
<span>May 19, 2026, 2:48 PM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-19-0805-clarify-repo-turn-doc-rules.html turns">
<a class="doc-link" href="./turns/2026-05-19-0805-clarify-repo-turn-doc-rules.html">turns/2026-05-19-0805-clarify-repo-turn-doc-rules.html</a>
<div class="meta">
<span class="tag">html</span>
<span>6.4 KB</span>
<span>May 19, 2026, 8:05 AM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-19-0739-update-readme-current-state.html turns">
<a class="doc-link" href="./turns/2026-05-19-0739-update-readme-current-state.html">turns/2026-05-19-0739-update-readme-current-state.html</a>
<div class="meta">
<span class="tag">html</span>
<span>9.8 KB</span>
<span>May 19, 2026, 7:39 AM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-19-upgrade-nextjs-16.html turns">
<a class="doc-link" href="./turns/2026-05-19-upgrade-nextjs-16.html">turns/2026-05-19-upgrade-nextjs-16.html</a>
<div class="meta">
<span class="tag">html</span>
<span>9.0 KB</span>
<span>May 19, 2026, 7:31 AM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-18-news-wire-view.html turns">
<a class="doc-link" href="./turns/2026-05-18-news-wire-view.html">turns/2026-05-18-news-wire-view.html</a>
<div class="meta">
<span class="tag">html</span>
<span>7.0 KB</span>
<span>May 18, 2026, 4:54 PM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-17-forgejo-deploy-remote-resolution.html turns">
<a class="doc-link" href="./turns/2026-05-17-forgejo-deploy-remote-resolution.html">turns/2026-05-17-forgejo-deploy-remote-resolution.html</a>
<div class="meta">
<span class="tag">html</span>
<span>5.3 KB</span>
<span>May 17, 2026, 11:22 PM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-17-add-fast-deploy-mode.html turns">
<a class="doc-link" href="./turns/2026-05-17-add-fast-deploy-mode.html">turns/2026-05-17-add-fast-deploy-mode.html</a>
<div class="meta">
<span class="tag">html</span>
<span>5.5 KB</span>
<span>May 17, 2026, 10:53 PM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-17-1101-clickhouse-alert-context.html turns">
<a class="doc-link" href="./turns/2026-05-17-1101-clickhouse-alert-context.html">turns/2026-05-17-1101-clickhouse-alert-context.html</a>
<div class="meta">
<span class="tag">html</span>
<span>6.4 KB</span>
<span>May 17, 2026, 10:21 PM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-17-clickhouse-alert-context.html turns">
<a class="doc-link" href="./turns/2026-05-17-clickhouse-alert-context.html">turns/2026-05-17-clickhouse-alert-context.html</a>
<div class="meta">
<span class="tag">html</span>
<span>11 KB</span>
<span>May 17, 2026, 10:21 PM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-17-deploy-allowlist-pr-packaging.html turns">
<a class="doc-link" href="./turns/2026-05-17-deploy-allowlist-pr-packaging.html">turns/2026-05-17-deploy-allowlist-pr-packaging.html</a>
<div class="meta">
<span class="tag">html</span>
<span>5.2 KB</span>
<span>May 17, 2026, 10:21 PM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-17-configure-beads-dolt-remote.html turns">
<a class="doc-link" href="./turns/2026-05-17-configure-beads-dolt-remote.html">turns/2026-05-17-configure-beads-dolt-remote.html</a>
<div class="meta">
<span class="tag">html</span>
<span>7.7 KB</span>
<span>May 17, 2026, 10:07 AM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-16-live-tape-scroll-hold-history.html turns">
<a class="doc-link" href="./turns/2026-05-16-live-tape-scroll-hold-history.html">turns/2026-05-16-live-tape-scroll-hold-history.html</a>
<div class="meta">
<span class="tag">html</span>
<span>6.3 KB</span>
<span>May 17, 2026, 5:06 AM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-17-0331-fix-live-tape-scroll-stability.html turns">
<a class="doc-link" href="./turns/2026-05-17-0331-fix-live-tape-scroll-stability.html">turns/2026-05-17-0331-fix-live-tape-scroll-stability.html</a>
<div class="meta">
<span class="tag">html</span>
<span>5.6 KB</span>
<span>May 17, 2026, 5:06 AM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-16-1725-durable-options-tape-history.html turns">
<a class="doc-link" href="./turns/2026-05-16-1725-durable-options-tape-history.html">turns/2026-05-16-1725-durable-options-tape-history.html</a>
<div class="meta">
<span class="tag">html</span>
<span>7.5 KB</span>
<span>May 17, 2026, 5:06 AM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-16-1752-speed-up-docker-deploys.html turns">
<a class="doc-link" href="./turns/2026-05-16-1752-speed-up-docker-deploys.html">turns/2026-05-16-1752-speed-up-docker-deploys.html</a>
<div class="meta">
<span class="tag">html</span>
<span>7.9 KB</span>
<span>May 17, 2026, 5:06 AM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-16-2159-fix-durable-options-history-routing.html turns">
<a class="doc-link" href="./turns/2026-05-16-2159-fix-durable-options-history-routing.html">turns/2026-05-16-2159-fix-durable-options-history-routing.html</a>
<div class="meta">
<span class="tag">html</span>
<span>6.4 KB</span>
<span>May 17, 2026, 5:06 AM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-15-add-duplicate-vps-compose-warning.html turns">
<a class="doc-link" href="./turns/2026-05-15-add-duplicate-vps-compose-warning.html">turns/2026-05-15-add-duplicate-vps-compose-warning.html</a>
<div class="meta">
<span class="tag">html</span>
<span>7.2 KB</span>
<span>May 15, 2026, 9:28 PM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-15-clarify-docker-first-deploy-workflow.html turns">
<a class="doc-link" href="./turns/2026-05-15-clarify-docker-first-deploy-workflow.html">turns/2026-05-15-clarify-docker-first-deploy-workflow.html</a>
<div class="meta">
<span class="tag">html</span>
<span>6.5 KB</span>
<span>May 15, 2026, 9:12 PM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-15-dual-runtime-deploy-workflow.html turns">
<a class="doc-link" href="./turns/2026-05-15-dual-runtime-deploy-workflow.html">turns/2026-05-15-dual-runtime-deploy-workflow.html</a>
<div class="meta">
<span class="tag">html</span>
<span>7.9 KB</span>
<span>May 15, 2026, 8:52 PM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-15-deploy-preflight-docker-workspace-check.html turns">
<a class="doc-link" href="./turns/2026-05-15-deploy-preflight-docker-workspace-check.html">turns/2026-05-15-deploy-preflight-docker-workspace-check.html</a>
<div class="meta">
<span class="tag">html</span>
<span>3.6 KB</span>
<span>May 15, 2026, 7:03 PM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-15-fix-docker-workspace-lockfile-sync.html turns">
<a class="doc-link" href="./turns/2026-05-15-fix-docker-workspace-lockfile-sync.html">turns/2026-05-15-fix-docker-workspace-lockfile-sync.html</a>
<div class="meta">
<span class="tag">html</span>
<span>3.6 KB</span>
<span>May 15, 2026, 6:56 PM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-15-quiet-terminal-view.html turns">
<a class="doc-link" href="./turns/2026-05-15-quiet-terminal-view.html">turns/2026-05-15-quiet-terminal-view.html</a>
<div class="meta">
<span class="tag">html</span>
<span>5.3 KB</span>
<span>May 15, 2026, 6:55 PM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-14-1824-adapt-terminal-view.html turns">
<a class="doc-link" href="./turns/2026-05-14-1824-adapt-terminal-view.html">turns/2026-05-14-1824-adapt-terminal-view.html</a>
<div class="meta">
<span class="tag">html</span>
<span>6.6 KB</span>
<span>May 15, 2026, 6:55 PM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-14-1833-reconcile-pr-conflicts.html turns">
<a class="doc-link" href="./turns/2026-05-14-1833-reconcile-pr-conflicts.html">turns/2026-05-14-1833-reconcile-pr-conflicts.html</a>
<div class="meta">
<span class="tag">html</span>
<span>5.6 KB</span>
<span>May 15, 2026, 6:55 PM</span>
</div>
</li>
<li class="doc-item" data-search="turns/2026-05-14-harden-terminal-view.html turns">
<a class="doc-link" href="./turns/2026-05-14-harden-terminal-view.html">turns/2026-05-14-harden-terminal-view.html</a>
<div class="meta">
<span class="tag">html</span>
<span>9.2 KB</span>
<span>May 15, 2026, 6:55 PM</span>
</div>
</li>
</ul>
</section>
<section class="group" id="category-daily-git">
<h2>daily-git <span>1</span></h2>
<ul class="doc-list">
<li class="doc-item" data-search="daily-git/2026-05-19-standup-summary-2026-05-18.html daily-git">
<a class="doc-link" href="./daily-git/2026-05-19-standup-summary-2026-05-18.html">daily-git/2026-05-19-standup-summary-2026-05-18.html</a>
<div class="meta">
<span class="tag">html</span>
<span>16 KB</span>
<span>May 19, 2026, 2:55 PM</span>
</div>
</li>
</ul>
</section>
<section class="group" id="category-general">
<h2>general <span>2</span></h2>
<ul class="doc-list">
<li class="doc-item" data-search="general/2026-05-18-standup-summary-2026-05-17.html general">
<a class="doc-link" href="./general/2026-05-18-standup-summary-2026-05-17.html">general/2026-05-18-standup-summary-2026-05-17.html</a>
<div class="meta">
<span class="tag">html</span>
<span>19 KB</span>
<span>May 18, 2026, 9:05 AM</span>
</div>
</li>
<li class="doc-item" data-search="general/2026-05-17-standup-summary-2026-05-16.html general">
<a class="doc-link" href="./general/2026-05-17-standup-summary-2026-05-16.html">general/2026-05-17-standup-summary-2026-05-16.html</a>
<div class="meta">
<span class="tag">html</span>
<span>17 KB</span>
<span>May 17, 2026, 10:07 AM</span>
</div>
</li>
</ul>
</section>
<section class="group" id="category-plans">
<h2>plans <span>2</span></h2>
<ul class="doc-list">
<li class="doc-item" data-search="plans/2026-05-18-native-fast-iterative-deploy-plan.html plans">
<a class="doc-link" href="./plans/2026-05-18-native-fast-iterative-deploy-plan.html">plans/2026-05-18-native-fast-iterative-deploy-plan.html</a>
<div class="meta">
<span class="tag">html</span>
<span>3.8 KB</span>
<span>May 19, 2026, 2:48 PM</span>
</div>
</li>
<li class="doc-item" data-search="plans/2026-05-16-1711-durable-options-tape-history.html plans">
<a class="doc-link" href="./plans/2026-05-16-1711-durable-options-tape-history.html">plans/2026-05-16-1711-durable-options-tape-history.html</a>
<div class="meta">
<span class="tag">html</span>
<span>12 KB</span>
<span>May 17, 2026, 5:06 AM</span>
</div>
</li>
</ul>
</section>
<section class="group" id="category-root">
<h2>root <span>2</span></h2>
<ul class="doc-list">
<li class="doc-item" data-search="clickhouse-reset-runbook.md root">
<a class="doc-link" href="./clickhouse-reset-runbook.md">clickhouse-reset-runbook.md</a>
<div class="meta">
<span class="tag">md</span>
<span>3.0 KB</span>
<span>May 17, 2026, 5:06 AM</span>
</div>
</li>
<li class="doc-item" data-search="terminal-audit-2026-05-14-0432.html root">
<a class="doc-link" href="./terminal-audit-2026-05-14-0432.html">terminal-audit-2026-05-14-0432.html</a>
<div class="meta">
<span class="tag">html</span>
<span>22 KB</span>
<span>May 15, 2026, 6:55 PM</span>
</div>
</li>
</ul>
</section>
</section>
<p class="empty" id="empty-state">No files match that filter.</p>
</main>
<script>
const searchInput = document.getElementById("doc-search");
const items = Array.from(document.querySelectorAll(".doc-item"));
const groups = Array.from(document.querySelectorAll(".group"));
const visibleCount = document.getElementById("visible-count");
const emptyState = document.getElementById("empty-state");
function applyFilter(query) {
const normalized = query.trim().toLowerCase();
let shown = 0;
for (const item of items) {
const searchable = item.dataset.search || "";
const isVisible = normalized.length === 0 || searchable.includes(normalized);
item.classList.toggle("hidden", !isVisible);
if (isVisible) shown += 1;
}
for (const group of groups) {
const hasVisibleItems = group.querySelector(".doc-item:not(.hidden)") !== null;
group.classList.toggle("hidden", !hasVisibleItems);
}
visibleCount.textContent = String(shown);
emptyState.style.display = shown === 0 ? "block" : "none";
}
searchInput.addEventListener("input", () => applyFilter(searchInput.value));
applyFilter("");
</script>
</body>
</html>

View file

@ -0,0 +1,93 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Plan: Native Fast Iterative Deployment</title>
<style>
body { font-family: Inter, system-ui, sans-serif; margin: 40px auto; max-width: 860px; line-height: 1.55; padding: 0 16px; }
h1, h2 { line-height: 1.2; }
.meta { color: #555; margin-bottom: 20px; }
section { margin: 22px 0; }
ul { padding-left: 20px; }
code { background: #f3f4f6; padding: 2px 6px; border-radius: 6px; }
</style>
</head>
<body>
<h1>Plan: Native, Fast, Iterative Deployment (Docker Optional)</h1>
<p class="meta">Date: 2026-05-18</p>
<section>
<h2>Plan Summary</h2>
<p>Define and execute a fast iteration deployment path centered on host-native services, while preserving Docker as a fallback/runtime option.</p>
</section>
<section>
<h2>Goals</h2>
<ul>
<li>Reduce deploy turnaround time immediately.</li>
<li>Identify concrete bottlenecks with timing evidence.</li>
<li>Stabilize proxy/runtime topology for reliable production rollouts.</li>
<li>Support both native and Docker strategies with explicit guardrails.</li>
</ul>
</section>
<section>
<h2>Proposed Changes</h2>
<ul>
<li>Use scoped fast deploys short-term.</li>
<li>Audit and remediate server-state blockers (duplicate compose/project drift).</li>
<li>Prepare native runtime prerequisites and checked-in operational assets.</li>
<li>Add deployment strategy prechecks, validation matrix, and staged cutover.</li>
</ul>
</section>
<section>
<h2>Relevant Context</h2>
<ul>
<li>Open issue <code>islandflow-2db</code>: stale duplicate compose stack cleanup.</li>
<li>Open issue <code>islandflow-sz8</code>: public <code>/replay/options</code> proxy regression.</li>
<li>Open issue <code>islandflow-38p</code>: native unit templates and rollback helpers.</li>
</ul>
</section>
<section>
<h2>Implementation Steps</h2>
<ol>
<li>Stop the bleeding immediately (current deploy loop).</li>
<li>Get hard timing data per deploy phase.</li>
<li>Live server state audit (when plan mode is off).</li>
<li>Resolve duplicate compose stack first (<code>islandflow-2db</code>).</li>
<li>Fix NPM proxy route regression (<code>islandflow-sz8</code>).</li>
<li>Define target iterative deployment model.</li>
<li>Prepare native runtime prerequisites on VPS.</li>
<li>Checked-in native ops assets (<code>islandflow-38p</code>).</li>
<li>Switch proxy topology for native mode carefully.</li>
<li>Deploy strategy guardrails.</li>
<li>Validation matrix.</li>
<li>Staged cutover plan.</li>
<li>Decision: final default runtime.</li>
<li>Decision: optimization priority.</li>
<li>Decision: immediate live audit kickoff.</li>
</ol>
</section>
<section>
<h2>Risks, Limitations, and Mitigations</h2>
<ul>
<li>Risk: native runtime not yet production-hardened. Mitigation: keep Docker fallback and explicit gating.</li>
<li>Risk: proxy misrouting breaks API routes. Mitigation: route checks and post-change smoke validation.</li>
<li>Risk: operational drift on VPS. Mitigation: preflight audits and documented rollback steps.</li>
</ul>
</section>
<section>
<h2>Open Questions</h2>
<ul>
<li>Should native become the default runtime now, or after hardening milestones?</li>
<li>Should backend iteration speed be prioritized ahead of web deploy speed?</li>
<li>Do we start immediate live server audit as soon as plan mode is disabled?</li>
</ul>
</section>
</body>
</html>

View file

@ -0,0 +1,153 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>2026-05-18: Native fast iterative deploy</title>
<style>
:root {
color-scheme: dark;
--bg: #0b1020;
--panel: #131a2b;
--panel-2: #182237;
--text: #eef3ff;
--muted: #a7b4d4;
--line: #2a3651;
--accent: #7dd3fc;
--good: #86efac;
--warn: #fbbf24;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: linear-gradient(180deg, #060914, var(--bg));
color: var(--text);
font: 16px/1.6 Inter, system-ui, sans-serif;
}
main {
max-width: 920px;
margin: 0 auto;
padding: 40px 20px 64px;
}
section {
margin-top: 22px;
padding: 22px 24px;
border: 1px solid var(--line);
border-radius: 18px;
background: linear-gradient(180deg, var(--panel), var(--panel-2));
}
h1, h2 { line-height: 1.15; }
h1 { margin: 0 0 12px; font-size: 2rem; }
h2 { margin: 0 0 12px; font-size: 1.15rem; }
p, li { color: var(--text); }
.meta { color: var(--muted); margin-bottom: 18px; }
.lede { color: var(--muted); max-width: 72ch; }
code, pre { font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; }
code {
padding: 0.15rem 0.35rem;
border-radius: 8px;
background: rgba(125, 211, 252, 0.12);
color: var(--accent);
}
pre {
margin: 12px 0 0;
padding: 14px 16px;
overflow: auto;
border-radius: 14px;
border: 1px solid var(--line);
background: #0a0f1d;
}
ul { margin: 0; padding-left: 1.2rem; }
.good { color: var(--good); }
.warn { color: var(--warn); }
</style>
</head>
<body>
<main>
<div class="meta">Turn document · 2026-05-18 03:29 EDT · Issues: islandflow-9rc, islandflow-38p, islandflow-bsg, islandflow-2db</div>
<h1>Native fast iterative deploy</h1>
<p class="lede">Implemented the native-first iterative deploy plan by adding deploy timing output, a safe worker-only native fast path, checked-in systemd user units and rollback helpers, server-local deploy execution, and updated live-operational documentation based on a fresh VPS audit.</p>
<section>
<h2>Summary</h2>
<p>The deploy flow now supports a safer native worker iteration model without requiring public edge cutover first. It can run directly from the VPS checkout without SSH, emits phase timings, includes checked-in native unit files plus install/rollback/smoke-test helpers, and documents the staged cutover path. During live audit, the previously reported <code>/replay/options</code> proxy issue and duplicate <code>islandflow</code> compose stack were both confirmed resolved on the host.</p>
</section>
<section>
<h2>Changes Made</h2>
<ul>
<li>Extended <code>scripts/deploy.ts</code> with deploy timing summaries for precheck, rollout, and verification phases.</li>
<li>Added <code>--workers-only</code> deploy scope for Docker and native runtimes.</li>
<li>Changed native <code>--fast</code> behavior so default full-scope fast deploys become worker-only instead of touching web/API.</li>
<li>Added native edge guardrails via <code>DEPLOY_NATIVE_EDGE_READY=1</code> before web/API native deploys are allowed.</li>
<li>Added local-server execution mode so <code>./deploy</code> can run from <code>/home/delta/islandflow</code> without SSHing back into the same host.</li>
<li>Added <code>DEPLOY_SSH_KEY_PATH</code> and <code>DEPLOY_FORCE_SSH</code> overrides for operators with non-default SSH setups.</li>
<li>Checked in native ops assets under <code>deployment/native/</code>:</li>
<li><code>install-user-units.sh</code>, <code>check-native-health.sh</code>, <code>rollback.sh</code></li>
<li>six user unit files in <code>deployment/native/systemd/user/</code></li>
<li>Updated <code>README.md</code>, <code>deployment/docker/README.md</code>, and <code>deployment/native/README.md</code> to document the worker-first model, local execution mode, validation matrix, and staged cutover guidance.</li>
<li>Synced <code>deployment/docker/workspace-root/package.json</code> so Docker workspace validation passes again.</li>
<li>Installed the checked-in user unit files onto the live VPS in disabled form under <code>~/.config/systemd/user</code>.</li>
</ul>
</section>
<section>
<h2>Context</h2>
<p>The plan targeted faster deployment iteration while avoiding a premature move of the public edge away from the current Docker + Nginx Proxy Manager topology. The practical target was to make native runtime useful immediately for backend-worker iteration, while leaving web/API cutover deliberate and reversible.</p>
</section>
<section>
<h2>Important Implementation Details</h2>
<ul>
<li>Native fast mode now defaults to <code>--workers-only</code>; Docker fast mode still defaults to <code>--services-only</code>.</li>
<li>Native deploys that include public web/API scope now fail fast unless <code>DEPLOY_NATIVE_EDGE_READY=1</code> is set.</li>
<li>Running from the live VPS checkout automatically switches deploy execution from SSH mode to local mode.</li>
<li>The checked-in native unit files are user units aimed at the current VPS layout: <code>/home/delta/islandflow</code> and <code>/home/delta/.bun/bin/bun</code>.</li>
<li><code>install-user-units.sh</code> now installs units safely without enabling anything by default; enabling is explicit and scope-based.</li>
<li><code>rollback.sh</code> intentionally uses a detached git ref to make one-off native rollback practical without rewriting branch history.</li>
</ul>
<pre>export DEPLOY_NATIVE_SYSTEMCTL_PREFIX="systemctl --user"
./deploy main --runtime native --fast
# resolves to worker-only native deploy before public edge cutover</pre>
</section>
<section>
<h2>Expected Impact for End-Users</h2>
<p>End-users should see indirect benefits first: faster backend iteration, safer operational changes, and clearer rollback paths. Public traffic behavior should remain unchanged until a deliberate native edge cutover is performed.</p>
</section>
<section>
<h2>Validation</h2>
<ul>
<li class="good">Passed: <code>bun run scripts/check-public-api-routes.ts https://flow.deltaisland.io</code></li>
<li class="good">Passed: direct public <code>/replay/options</code> curl returned JSON</li>
<li class="good">Passed: live Nginx Proxy Manager config contains <code>/replay</code> in the API route matcher</li>
<li class="good">Passed: <code>docker compose ls</code> shows no duplicate <code>islandflow</code> project</li>
<li class="good">Passed: <code>bash -n deployment/native/install-user-units.sh deployment/native/check-native-health.sh deployment/native/rollback.sh</code></li>
<li class="good">Passed: <code>systemd-analyze verify deployment/native/systemd/user/*.service</code></li>
<li class="good">Passed: <code>bun run check:docker-workspace</code> after syncing workspace snapshot</li>
<li class="good">Passed: native edge guard refusal for <code>bun run scripts/deploy.ts main --runtime native --web-only --no-build</code></li>
<li class="good">Passed: <code>./deployment/native/install-user-units.sh</code> followed by <code>systemctl --user list-unit-files 'islandflow*'</code></li>
</ul>
</section>
<section>
<h2>Issues, Limitations, and Mitigations</h2>
<ul>
<li><span class="warn">Native units were installed but not enabled or started.</span> This is intentional to avoid conflicting with the current Docker production edge.</li>
<li><span class="warn">Public web/API native deploys are still gated.</span> Mitigation: explicit <code>DEPLOY_NATIVE_EDGE_READY=1</code> acknowledgment and staged cutover documentation.</li>
<li><span class="warn">Native worker runtime has not yet been exercised live against the existing Docker worker stack.</span> Mitigation: follow-up issue to soak worker-only native units before any default-runtime decision.</li>
<li><span class="warn">The known untracked Signal CLI tarball remains in the repo checkout.</span> This is already tolerated by the deploy helper allowlist and was not changed here.</li>
</ul>
</section>
<section>
<h2>Follow-up Work</h2>
<ul>
<li>Open follow-up: <code>islandflow-vvw</code> — stage native public-edge cutover after worker soak.</li>
<li>Decide whether native should ever replace Docker as the default runtime only after worker soak data and deliberate edge cutover validation.</li>
</ul>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,521 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Turn Document - Native Public Edge Cutover</title>
<style>
:root {
color-scheme: dark;
--bg-core: #06080b;
--bg-elevated: #0b1016;
--bg-pane: #111820;
--bg-pane-2: #0d141b;
--bg-soft: rgba(255, 255, 255, 0.03);
--border-subtle: rgba(255, 255, 255, 0.12);
--border-strong: rgba(245, 166, 35, 0.32);
--text-primary: #e6edf4;
--text-dim: #90a0b2;
--text-faint: #6e7b8c;
--signal-amber: #f5a623;
--signal-amber-soft: rgba(245, 166, 35, 0.12);
--confirm-green: #25c17a;
--confirm-green-soft: rgba(37, 193, 122, 0.14);
--risk-red: #ff6b5f;
--risk-red-soft: rgba(255, 107, 95, 0.12);
--info-blue: #4da3ff;
--info-blue-soft: rgba(77, 163, 255, 0.12);
--shadow: 0 24px 60px rgba(0, 0, 0, 0.35);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
background:
radial-gradient(circle at top right, rgba(245, 166, 35, 0.12), transparent 28%),
linear-gradient(180deg, #06080b 0%, #0a1117 100%);
color: var(--text-primary);
}
main {
width: min(1080px, calc(100vw - 32px));
margin: 0 auto;
padding: 28px 0 48px;
}
.hero {
background:
linear-gradient(140deg, rgba(245, 166, 35, 0.1), transparent 42%),
linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent 100%),
var(--bg-pane);
border: 1px solid var(--border-strong);
border-radius: 16px;
box-shadow: var(--shadow);
padding: 26px 28px;
margin-bottom: 18px;
}
.eyebrow,
h2,
.meta-label,
th {
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.12em;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--signal-amber);
font-size: 0.72rem;
margin-bottom: 14px;
}
h1 {
margin: 0 0 10px;
font-family: "Quantico", "IBM Plex Sans", sans-serif;
font-size: clamp(2rem, 4vw, 3rem);
line-height: 1.05;
letter-spacing: 0.06em;
}
.lead {
margin: 0;
max-width: 72ch;
color: var(--text-dim);
line-height: 1.65;
}
.meta-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 10px;
margin-top: 18px;
}
.meta-card {
padding: 12px 14px;
border-radius: 12px;
background: var(--bg-soft);
border: 1px solid var(--border-subtle);
}
.meta-label {
color: var(--text-faint);
font-size: 0.68rem;
margin-bottom: 6px;
}
.meta-value {
color: var(--text-primary);
font-size: 0.95rem;
}
section {
background: var(--bg-pane);
border: 1px solid var(--border-subtle);
border-radius: 16px;
padding: 22px 24px;
margin-bottom: 16px;
}
h2 {
margin: 0 0 14px;
font-size: 0.76rem;
color: var(--signal-amber);
}
p,
li {
line-height: 1.65;
color: var(--text-dim);
}
ul {
margin: 0;
padding-left: 20px;
}
li + li {
margin-top: 8px;
}
strong {
color: var(--text-primary);
}
code {
font-family: "IBM Plex Mono", monospace;
font-size: 0.92em;
color: var(--signal-amber);
}
pre {
margin: 12px 0 0;
padding: 14px 16px;
border-radius: 12px;
background: var(--bg-pane-2);
border: 1px solid var(--border-subtle);
overflow-x: auto;
}
pre code {
color: var(--text-primary);
}
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
}
.status-card {
border-radius: 12px;
border: 1px solid var(--border-subtle);
padding: 14px;
background: var(--bg-pane-2);
}
.status-card.good {
border-color: rgba(37, 193, 122, 0.32);
background: linear-gradient(180deg, var(--confirm-green-soft), transparent), var(--bg-pane-2);
}
.status-card.warn {
border-color: rgba(77, 163, 255, 0.28);
background: linear-gradient(180deg, var(--info-blue-soft), transparent), var(--bg-pane-2);
}
.status-title {
margin: 0 0 6px;
color: var(--text-primary);
font-weight: 600;
}
.status-copy {
margin: 0;
color: var(--text-dim);
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 8px;
}
th,
td {
text-align: left;
padding: 10px 0;
border-bottom: 1px solid var(--border-subtle);
vertical-align: top;
}
th {
color: var(--text-faint);
font-size: 0.68rem;
}
td {
color: var(--text-dim);
}
.pill {
display: inline-flex;
align-items: center;
gap: 6px;
border-radius: 999px;
padding: 4px 9px;
font-family: "IBM Plex Mono", monospace;
font-size: 0.7rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.pill.good {
color: var(--confirm-green);
background: var(--confirm-green-soft);
}
.pill.warn {
color: var(--info-blue);
background: var(--info-blue-soft);
}
.pill.risk {
color: var(--risk-red);
background: var(--risk-red-soft);
}
</style>
</head>
<body>
<main>
<section class="hero">
<div class="eyebrow">Islandflow Turn Document</div>
<h1>Native Public Edge Cutover</h1>
<p class="lead">
Completed the VPS native-first cutover for Islandflow infrastructure and app services while keeping Nginx
Proxy Manager as the outer edge and Docker as the rollback path. The final state now serves
<code>flow.deltaisland.io</code> and <code>api.flow.deltaisland.io</code> from the native web and API
processes, with verified public routing and a documented follow-up for the long-term API Cloudflare posture.
</p>
<div class="meta-grid">
<div class="meta-card">
<div class="meta-label">Generated</div>
<div class="meta-value">2026-05-18 19:52 EDT</div>
</div>
<div class="meta-card">
<div class="meta-label">Primary Issue</div>
<div class="meta-value"><code>islandflow-vvw</code></div>
</div>
<div class="meta-card">
<div class="meta-label">Follow-up</div>
<div class="meta-value"><code>islandflow-fl5</code></div>
</div>
<div class="meta-card">
<div class="meta-label">Runtime State</div>
<div class="meta-value">Native active, Docker retained for rollback</div>
</div>
</div>
</section>
<section>
<h2>Summary</h2>
<p>
The repository now contains the native infra units, native cutover scripts, Docker fallback adjustments, and
public-edge retargeting logic required to run Islandflow natively on the VPS. During validation, the live NPM
edge was switched from Docker container-name upstreams to native host ports, the host firewall was adjusted so
the NPM bridge could reach the native API, and the separate public API TLS problem was resolved by correcting
the Cloudflare DNS state for <code>api.flow.deltaisland.io</code>.
</p>
</section>
<section>
<h2>Changes Made</h2>
<ul>
<li>
Added checked-in native infra operations under <code>deployment/native/</code>, including
<code>bootstrap-infra.sh</code>, <code>check-native-infra.sh</code>, <code>cutover.sh</code>,
<code>full-rollback.sh</code>, <code>start-infra.sh</code>, and the native system units for NATS, Redis,
and ClickHouse.
</li>
<li>
Extended native app runtime units so the web and API bind on host-reachable interfaces, and forced the
native options ingest service to use the synthetic adapter during the cutover.
</li>
<li>
Updated <code>services/api</code> to support explicit host binding through <code>API_HOST</code>, and fixed
JetStream retention conversion in <code>packages/bus</code> so native services can start cleanly with the
configured max-age values.
</li>
<li>
Updated the Docker fallback assets to publish loopback web/API ports, share durable host data under
<code>/var/lib/islandflow</code>, and document the native-to-Docker rollback path.
</li>
<li>
Reworked <code>deployment/native/switch-npm-edge.sh</code> so it targets the NPM bridge gateway IP instead
of <code>host.docker.internal</code>, handles the root-owned NPM SQLite database, synchronizes generated
<code>proxy_host</code> configs, and reloads NPM deterministically after the edge switch.
</li>
<li>
Created Beads follow-up issue <code>islandflow-fl5</code> for the remaining decision about whether
<code>api.flow.deltaisland.io</code> should remain DNS-only or be re-proxied through Cloudflare.
</li>
</ul>
</section>
<section>
<h2>Context</h2>
<p>
The migration started from a Docker-owned production baseline where NATS, Redis, ClickHouse, API, workers, and
web all ran in Compose, while NPM routed Islandflow traffic to Docker service names. That setup blocked a safe
native cutover for two reasons: the native services could not reach Docker-only infra reliably, and NPM could
not send public traffic to host-native processes without a deliberate upstream retarget.
</p>
<p>
The runtime model for this work is exclusive ownership. Native and Docker are not allowed to run the same API
or worker scopes in parallel because JetStream durable consumers would conflict. The objective was therefore a
phased handoff, not a mixed soak for the same queues.
</p>
</section>
<section>
<h2>Important Implementation Details</h2>
<div class="status-grid">
<article class="status-card good">
<p class="status-title">NPM edge targeting</p>
<p class="status-copy">
NPM generates <code>proxy_pass</code> from a runtime-resolved <code>$server</code> variable, so the
Docker <code>/etc/hosts</code> alias for <code>host.docker.internal</code> was not sufficient. The switch
helper now detects the NPM bridge gateway and uses that IP for native upstreams.
</p>
</article>
<article class="status-card good">
<p class="status-title">Firewall path</p>
<p class="status-copy">
The host UFW policy already allowed port <code>3000</code> but not <code>4000</code>. The live fix was a
source-scoped allow for the NPM bridge subnet so the containerized edge could reach the native API.
</p>
</article>
<article class="status-card warn">
<p class="status-title">Cloudflare API hostname</p>
<p class="status-copy">
The API hostname failure was separate from the native cutover. The hostname is now a DNS-only
<code>A</code> record pointing at the VPS, which restored public TLS and health responses.
</p>
</article>
</div>
<table>
<thead>
<tr>
<th>Area</th>
<th>Implementation detail</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Native API</strong></td>
<td>
<code>services/api/src/index.ts</code> now accepts <code>API_HOST</code> and passes it to
<code>Bun.serve</code>. The native unit sets <code>API_HOST=0.0.0.0</code> and
<code>API_PORT=4000</code>.
</td>
</tr>
<tr>
<td><strong>Native web</strong></td>
<td>
The native web unit now starts from <code>apps/web</code> with
<code>bun x next start -H "$WEB_HOST" -p "$WEB_PORT"</code>, avoiding the earlier repo-root startup
failure and binding the service on <code>0.0.0.0:3000</code>.
</td>
</tr>
<tr>
<td><strong>JetStream retention</strong></td>
<td>
Native startup exposed a retention-unit bug. The shared bus layer now converts stream max-age values with
<code>nanos(...)</code> and formats them back with <code>millis(...)</code>.
</td>
</tr>
<tr>
<td><strong>Docker fallback</strong></td>
<td>
Docker Compose now uses <code>ISLANDFLOW_DATA_ROOT=/var/lib/islandflow</code>, publishes loopback
ports, and keeps the fallback runtime compatible with the same durable data directories as the native
services.
</td>
</tr>
<tr>
<td><strong>NPM switch helper</strong></td>
<td>
The helper now updates both the NPM database and the generated
<code>/data/nginx/proxy_host/*.conf</code> files, because a DB-only restart did not reliably rewrite the
live configs for Islandflow.
</td>
</tr>
</tbody>
</table>
<pre><code>sudo ufw allow proto tcp from 172.18.0.0/16 to any port 4000 comment 'npm bridge to native api'</code></pre>
</section>
<section>
<h2>Expected Impact for End-Users</h2>
<ul>
<li>
Public web and API traffic now reaches the native Islandflow services, which removes Docker from the primary
live request path while keeping the outer edge unchanged.
</li>
<li>
Same-origin public API routes such as <code>/prints</code>, <code>/history</code>, <code>/replay</code>,
<code>/nbbo</code>, and <code>/ws/live</code> continue to resolve correctly through the main app hostname.
</li>
<li>
Rollback remains fast and explicit: NPM can be pointed back at Docker service names and the Docker runtime
can reclaim the same durable data directories if native operation needs to be abandoned.
</li>
</ul>
</section>
<section>
<h2>Validation</h2>
<div class="status-grid">
<article class="status-card good">
<div class="pill good">Static checks</div>
<ul>
<li><code>bun run check:docker-workspace</code></li>
<li><code>docker compose -f deployment/docker/docker-compose.yml config --quiet</code></li>
<li><code>docker compose -f /home/delta/nginx-proxy-manager/docker-compose.yml config --quiet</code></li>
<li><code>bash -n deployment/native/*.sh</code></li>
<li><code>systemd-analyze verify deployment/native/systemd/user/*.service deployment/native/systemd/system/*.service</code></li>
<li><code>bun build services/api/src/index.ts --target=bun</code></li>
<li><code>bun build scripts/deploy.ts --target=bun</code></li>
</ul>
</article>
<article class="status-card good">
<div class="pill good">Native runtime</div>
<ul>
<li><code>./deployment/native/check-native-health.sh full</code></li>
<li><code>curl http://127.0.0.1:4000/health</code></li>
<li><code>curl -I http://127.0.0.1:3000/</code></li>
</ul>
</article>
<article class="status-card good">
<div class="pill good">Public edge</div>
<ul>
<li><code>curl -I -fksS https://flow.deltaisland.io</code></li>
<li><code>curl -fksS https://api.flow.deltaisland.io/health</code></li>
<li><code>bun run scripts/check-public-api-routes.ts https://flow.deltaisland.io</code></li>
</ul>
</article>
</div>
</section>
<section>
<h2>Issues, Limitations, and Mitigations</h2>
<ul>
<li>
The native ingest-options service required an explicit synthetic-adapter override because the environment file
still pointed at an Alpaca adapter that was returning <code>401</code> responses. The service now starts
cleanly for native cutover, but production adapter selection remains an operational decision.
</li>
<li>
The NPM helper still relies on direct config synchronization because NPM did not reliably regenerate the
Islandflow proxy files from SQLite changes alone. This is mitigated by keeping the synchronization logic
checked in and by reloading NPM as part of the helper itself.
</li>
<li>
The final public API recovery currently leaves <code>api.flow.deltaisland.io</code> as a DNS-only hostname.
That restored service, but it changes the edge posture relative to the web hostname and should be reviewed
deliberately.
</li>
<li>
A temporary Cloudflare API token was used to inspect and correct zone state during validation. That token
should be rotated outside this repository workflow.
</li>
</ul>
</section>
<section>
<h2>Follow-up Work</h2>
<ul>
<li>
<code>islandflow-fl5</code>: decide whether <code>api.flow.deltaisland.io</code> should remain DNS-only or
be re-proxied through Cloudflare, then re-validate TLS, websocket, and operational behavior for the chosen
posture.
</li>
<li>
After operational soak, decide whether native should become the default production runtime or remain a
supported alternative with Docker as the preferred steady-state runtime.
</li>
</ul>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,152 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Turn Report: News Wire View via Alpaca Feed</title>
<style>
:root {
color-scheme: dark;
--bg: #0b1016;
--panel: #111820;
--panel-2: #0d141b;
--border: rgba(255, 255, 255, 0.08);
--text: #e6edf4;
--dim: #90a0b2;
--accent: #f5a623;
}
body {
margin: 0;
padding: 32px;
background: linear-gradient(180deg, #06080b 0%, #0b1016 100%);
color: var(--text);
font: 15px/1.6 "IBM Plex Sans", sans-serif;
}
main {
max-width: 980px;
margin: 0 auto;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 16px;
padding: 28px;
}
h1, h2 {
margin: 0 0 12px;
font-family: "Quantico", sans-serif;
letter-spacing: 0.06em;
}
h1 { font-size: 1.8rem; }
h2 { font-size: 1rem; margin-top: 28px; }
p, li { color: var(--text); }
.summary {
padding: 16px 18px;
border: 1px solid rgba(245, 166, 35, 0.28);
border-radius: 12px;
background: rgba(245, 166, 35, 0.08);
}
.meta, code, pre { font-family: "IBM Plex Mono", monospace; }
.meta { color: var(--dim); font-size: 0.85rem; }
section {
padding-top: 4px;
border-top: 1px solid var(--border);
}
section:first-of-type { border-top: 0; }
ul { padding-left: 18px; }
pre {
overflow: auto;
padding: 14px;
border-radius: 12px;
background: var(--panel-2);
border: 1px solid var(--border);
}
a { color: var(--accent); }
</style>
</head>
<body>
<main>
<p class="meta">Created 2026-05-18 · Task: News Wire View via Alpaca Feed</p>
<h1>News Wire View via Alpaca Feed</h1>
<div class="summary">
<strong>Summary</strong>
<p>
Added an Alpaca-backed live news pipeline end to end: normalized <code>NewsStory</code> types,
a dedicated JetStream subject/stream, ClickHouse storage helpers with latest-revision semantics,
a new <code>services/ingest-news</code> service, API endpoints and live fanout, and a web
<code>/news</code> route plus Home preview with a right-side story drawer.
</p>
</div>
<section>
<h2>Changes Made</h2>
<ul>
<li>Added <code>NewsStorySchema</code>, the <code>news</code> live channel, and subscription parsing support in <code>packages/types</code>.</li>
<li>Added bus constants for the <code>flow.news</code> subject and <code>NEWS</code> stream.</li>
<li>Added ClickHouse news storage helpers, including recent, before-cursor, and after-cursor queries that collapse provider revisions to the latest row per <code>provider + story_id</code>.</li>
<li>Created <code>services/ingest-news</code> with Alpaca REST backfill, Alpaca websocket streaming, normalization, and deterministic ticker resolution.</li>
<li>Extended the API service to persist live news in the shared cache, expose <code>GET /news</code> and <code>GET /history/news</code>, and fan out <code>news</code> events on <code>/ws/live</code>.</li>
<li>Added a top-level <code>/news</code> route, primary nav entry, Home preview pane, replay-mode live-only empty states, and a sanitized full-story drawer.</li>
<li>Updated dev and deployment wiring so the new service is included in local runners and the Docker workspace snapshot.</li>
</ul>
</section>
<section>
<h2>Context</h2>
<p>
The plan called for a free-provider v1 news surface that behaves like the rest of Islandflow:
compact, evidence-first, and live-native. The implementation keeps replay intentionally out of scope
for news while still integrating news into the same live manifest, history pagination, rail navigation,
and drawer language used elsewhere in the terminal.
</p>
</section>
<section>
<h2>Important Implementation Details</h2>
<ul>
<li>Ticker resolution prefers provider symbols first, then falls back only to structured patterns in provider HTML: ticker anchors, <code>EXCHANGE:SYM</code>, and <code>$SYM</code>.</li>
<li>News history uses <code>published_ts</code> as the visible cursor while revisions are collapsed with a window function over <code>provider, story_id</code> ordered by <code>updated_ts</code>, <code>ingest_ts</code>, and <code>seq</code>.</li>
<li>The web drawer sanitizes provider HTML by removing scripts, inline event handlers, and unsupported tags; if sanitization yields nothing useful, the drawer falls back to stripped plain text.</li>
<li>Replay mode intentionally renders a clear empty state for news on both Home and <code>/news</code> instead of pretending news is replay-synced.</li>
</ul>
<pre><code>resolved_symbols = provider_symbols
or ticker anchors in content_html
or EXCHANGE:SYM matches
or $SYM matches</code></pre>
</section>
<section>
<h2>Expected Impact for End-Users</h2>
<p>
Traders can now monitor a dedicated live news wire inside Islandflow, spot symbol-linked headlines from
the Home view, and open full stories in-context without leaving the app. The displayed ticker chips are
grounded in stored provider and derived symbol metadata, which makes the feed safer to filter and trust.
</p>
</section>
<section>
<h2>Validation</h2>
<ul>
<li>Ran targeted Bun tests covering types, storage, API live-state behavior, ingest-news symbol resolution, route wiring, and terminal helpers.</li>
<li>Built the Next.js web app with <code>bun --cwd=apps/web run build</code>.</li>
<li>Ran <code>bun run check:docker-workspace</code> after syncing the deployment workspace snapshot.</li>
</ul>
</section>
<section>
<h2>Issues, Limitations, and Mitigations</h2>
<ul>
<li>Replay support remains intentionally absent in v1; the UI now states that explicitly instead of showing misleading empty historical behavior.</li>
<li>The sanitizer is intentionally conservative and custom, which keeps dependencies light but may strip some harmless provider formatting.</li>
<li>The ingest service assumes Alpacas current REST and websocket news contracts; if Alpaca changes those payload shapes, the normalization layer will need adjustment.</li>
</ul>
</section>
<section>
<h2>Follow-up Work</h2>
<ul>
<li>No additional follow-up issue was required during this turn.</li>
<li>Future extensions are still available behind the same contract: multi-provider aggregation, server-side symbol filtering, and replay-aware news history.</li>
</ul>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,259 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>README Current-State Update</title>
<style>
:root {
color-scheme: dark;
--bg: #17131f;
--panel: #231b31;
--panel-2: #2c223d;
--text: #f3edf8;
--muted: #cbbdda;
--line: #514160;
--lavender: #c9a7ff;
--pink: #f0a7d7;
--code: #120e18;
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: "IBM Plex Sans", Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.62;
}
main {
width: min(980px, calc(100% - 40px));
margin: 0 auto;
padding: 48px 0 64px;
}
header {
border-bottom: 1px solid var(--line);
margin-bottom: 32px;
padding-bottom: 24px;
}
.eyebrow,
h1,
h2,
code,
pre {
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.eyebrow {
color: var(--pink);
font-size: 0.82rem;
letter-spacing: 0.04em;
text-transform: uppercase;
}
h1 {
margin: 8px 0 12px;
font-size: clamp(2rem, 4vw, 3.4rem);
line-height: 1.08;
}
h2 {
color: var(--lavender);
font-size: 1.05rem;
margin: 0 0 12px;
}
section {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
margin: 18px 0;
padding: 22px;
}
p {
margin: 0 0 12px;
}
ul {
margin: 0;
padding-left: 22px;
}
li + li {
margin-top: 6px;
}
code {
background: var(--code);
border: 1px solid #3a2d49;
border-radius: 5px;
color: #f4c4e2;
padding: 0.12rem 0.32rem;
}
pre {
overflow-x: auto;
background: var(--code);
border: 1px solid #3a2d49;
border-radius: 8px;
color: #eadff2;
padding: 16px;
}
pre code {
background: transparent;
border: 0;
color: inherit;
padding: 0;
}
.summary {
color: var(--muted);
max-width: 72ch;
}
.pillrow {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 16px;
}
.pill {
background: var(--panel-2);
border: 1px solid var(--line);
border-radius: 999px;
color: var(--muted);
padding: 5px 10px;
}
a {
color: var(--pink);
}
</style>
</head>
<body>
<main>
<header>
<div class="eyebrow">Turn document · 2026-05-19 07:39 America/New_York</div>
<h1>README Current-State Update</h1>
<p class="summary">
Resolved the README merge conflict and rewrote the project overview so it matches the current Islandflow codebase, including the smart-money taxonomy, Next.js 16 update, news ingest, desktop shell, and current deployment posture.
</p>
<div class="pillrow">
<span class="pill">README.md</span>
<span class="pill">smart-money taxonomy</span>
<span class="pill">Next.js 16.2.6</span>
<span class="pill">deployment docs</span>
</div>
</header>
<section>
<h2>Summary</h2>
<p>
The README no longer contains conflict markers. It now gives a concise but current description of the platform, its runtime services, public smart-money categories, environment knobs, and supported deployment workflow.
</p>
</section>
<section>
<h2>Changes Made</h2>
<ul>
<li>Resolved the conflicted README by preserving the useful project-state content and removing stale simplified sections.</li>
<li>Added a first-class smart-money taxonomy section for the six public profiles: <code>institutional_directional</code>, <code>retail_whale</code>, <code>event_driven</code>, <code>vol_seller</code>, <code>arbitrage</code>, and <code>hedge_reactive</code>.</li>
<li>Documented that smart-money events are now the semantic object, while legacy classifier hits and alerts remain compatibility surfaces.</li>
<li>Updated the current implementation state to include Alpaca news ingest, profile-aware UI behavior, alert-context hydration, and the Electron shell.</li>
<li>Recorded the Next.js update to <code>16.2.6</code> with React and React DOM <code>19.2.0</code>.</li>
<li>Clarified deployment: Docker is still the supported VPS path, native Bun/systemd rollout is experimental, and scoped deploy flags are available.</li>
<li>Aligned live-cache and web hot-window defaults with the current env examples and API defaults.</li>
</ul>
</section>
<section>
<h2>Context</h2>
<p>
Recent commits showed the README branch was carrying a Next.js upgrade, Alpaca news support, smart-money event work, and deployment helper changes. The prior README mixed both sides of a merge conflict and did not explain the newer taxonomy-driven classifier model.
</p>
</section>
<section>
<h2>Important Implementation Details</h2>
<p>
The README intentionally treats <code>FlowPacket</code> as an intermediate clustering bridge and <code>SmartMoneyEvent</code> as the current semantic surface. It also documents abstention and suppression behavior so readers do not mistake every large print for a forced smart-money label.
</p>
<p>
Deployment language now matches the current operations docs: <code>./deploy main</code> defaults to the Docker path, <code>--runtime native</code> is available but experimental, and native rollout still depends on systemd units and reverse-proxy preparation.
</p>
</section>
<section>
<h2>Relevant Diff Snippets</h2>
<p>
Diff snippets are formatted for readability in the same spirit as <a href="https://diffs.com/docs">diffs.com</a>, with only the most relevant README changes shown here.
</p>
<pre><code>+## Smart-Money Classification Taxonomy
+
+Islandflow now emits first-class `SmartMoneyEvent` records instead of treating old classifier hits as the final semantic object.
+
+| Profile ID | Meaning | Common evidence |
+| --- | --- | --- |
+| `institutional_directional` | Large directional parent flow with stronger institutional-style conviction. | premium, size, sweep/burst behavior, aggressor imbalance, quote quality |
+| `retail_whale` | Large retail-style speculative bursts, often short-dated or attention-driven. | short-dated OTM concentration, burst prints, IV shock |
+| `event_driven` | Flow aligned to known upcoming events. | event-calendar proximity, expiry after event, pre-event concentration |
+| `vol_seller` | Premium-selling or short-volatility structure evidence. | sell-side premium, straddles/strangles |
+| `arbitrage` | Multi-leg or symmetric structures with low directional exposure. | matched leg symmetry, near-flat directional bias |
+| `hedge_reactive` | Hedge or dealer-reaction style flow around short-dated ATM/gamma context. | 0-2 DTE, near-ATM contracts, underlying move linkage |</code></pre>
<pre><code>+## Deployment Workflow
+
+Docker remains the supported and recommended path for the current VPS.
+
+./deploy main
+./deploy main --runtime docker
+./deploy current-branch
+./deploy current-branch --runtime docker
+
+Native deployment is opt-in and experimental:
+
+./deploy main --runtime native
+./deploy current-branch --runtime native</code></pre>
</section>
<section>
<h2>Expected Impact for End-Users</h2>
<p>
New contributors or future sessions should be able to read the README and understand what Islandflow currently does, which service owns each capability, how the smart-money labels should be interpreted, and which deployment command is appropriate for the VPS.
</p>
</section>
<section>
<h2>Validation</h2>
<ul>
<li>Confirmed no merge conflict markers remain with <code>rg -n "&lt;&lt;&lt;&lt;&lt;&lt;&lt;|=======|&gt;&gt;&gt;&gt;&gt;&gt;&gt;" README.md</code>.</li>
<li>Ran <code>git diff --check</code>; no whitespace or patch-format issues were reported.</li>
<li>Ran focused tests: <code>bun test packages/types/tests/options-flow.test.ts packages/types/tests/live.test.ts packages/storage/tests/smart-money-events.test.ts services/compute/tests/parent-events.test.ts</code>.</li>
<li>Focused test result: 12 pass, 0 fail.</li>
</ul>
</section>
<section>
<h2>Issues, Limitations, and Mitigations</h2>
<ul>
<li>This was documentation-only, so no full production web build was run. The focused tests cover the smart-money/type/storage claims most relevant to the README update.</li>
<li>The README summarizes environment variables instead of listing every low-level classifier and dark-inference threshold. Detailed knobs remain available in <code>.env.example</code> and service code.</li>
<li>Native deployment remains experimental; the README calls that out directly and points to the dedicated native deployment document.</li>
</ul>
</section>
<section>
<h2>Follow-up Work</h2>
<ul>
<li><code>islandflow-38p</code>: add native deployment unit templates and rollback helpers.</li>
<li><code>islandflow-932</code>: continue desktop follow-up native features.</li>
<li><code>islandflow-2db</code>: manually remove stale local-infra containers from the VPS when doing server hygiene.</li>
</ul>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,200 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Clarify Repo Turn Documentation Rules</title>
<style>
:root {
color-scheme: light;
--paper: #f7f3ec;
--ink: #1f211c;
--muted: #666b5d;
--line: #d8d0c1;
--accent: #405f54;
--accent-soft: #e4ece5;
--code-bg: #eee7db;
}
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font: 16px/1.62 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
main {
max-width: 920px;
margin: 0 auto;
padding: 48px 28px 72px;
}
header {
border-bottom: 1px solid var(--line);
margin-bottom: 34px;
padding-bottom: 26px;
}
.eyebrow {
color: var(--accent);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
h1 {
font-size: clamp(2.1rem, 5vw, 4rem);
letter-spacing: 0;
line-height: 1.02;
margin: 10px 0 14px;
max-width: 11ch;
}
h2 {
color: var(--accent);
font-size: 1rem;
letter-spacing: 0.04em;
margin: 0 0 10px;
text-transform: uppercase;
}
p {
margin: 0 0 12px;
max-width: 72ch;
}
section {
border-top: 1px solid var(--line);
padding: 22px 0;
}
ul {
margin: 0;
padding-left: 1.25rem;
}
li + li {
margin-top: 6px;
}
code {
background: var(--code-bg);
border-radius: 4px;
font: 0.92em ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
padding: 0.12rem 0.32rem;
}
pre {
background: var(--code-bg);
border: 1px solid var(--line);
border-radius: 6px;
overflow-x: auto;
padding: 14px;
}
pre code {
background: transparent;
padding: 0;
}
.summary {
color: var(--muted);
font-size: 1.08rem;
}
.note {
background: var(--accent-soft);
border: 1px solid #c7d8cc;
border-radius: 6px;
padding: 14px;
}
</style>
</head>
<body>
<main>
<header>
<div class="eyebrow">Turn document · 2026-05-19 08:05 America/New_York</div>
<h1>Clarify Repo Turn Documentation Rules</h1>
<p class="summary">
Updated the repository instructions so Islandflow turn documents are clearly repo-local and styled through <code>impeccable</code>, without inheriting global non-repo computer-task styling.
</p>
</header>
<section>
<h2>Summary</h2>
<p>
The repo <code>AGENTS.md</code> now removes a stray non-repo location rule and explicitly states that <code>impeccable</code> is the styling and layout authority for Islandflow turn documents when available.
</p>
</section>
<section>
<h2>Changes Made</h2>
<ul>
<li>Removed the confusing instruction to save non-repo documentation under <code>~/dev/docs/turns/</code>.</li>
<li>Clarified that repository turn documents stay in <code>docs/turns/</code>.</li>
<li>Updated the format rule to say <code>impeccable</code> handles both structure and styling.</li>
<li>Added an explicit guard against applying global non-repo computer-task house styling to this repository's turn documents.</li>
<li>Clarified that the fallback standalone HTML path only applies when <code>impeccable</code> is unavailable or blocked by an actual error.</li>
</ul>
</section>
<section>
<h2>Context</h2>
<p>
The global agent instructions now distinguish repository implementation documentation from non-repo computer-task documentation. This repo file needed a small cleanup so it would not reintroduce ambiguity about location or styling.
</p>
</section>
<section>
<h2>Important Implementation Details</h2>
<p>
This was a documentation-only change in <code>AGENTS.md</code>. It changes future agent behavior but does not alter runtime code, tests, deployment scripts, or application behavior.
</p>
</section>
<section>
<h2>Relevant Diff Snippets</h2>
<pre><code>-## Important: If you are not working inside a git repository, save the document to `~/dev/docs/turns/`
-Use the impeccable skill to structure the document as clean, readable HTML.
+Use the `impeccable` skill to structure and style the document as clean, readable HTML.
+
+For this repository, `impeccable` is the styling and layout authority for turn documents when available. Do not apply global non-repo computer-task house styling to repository turn documents.
-If the impeccable skill is unavailable, still create a well-structured standalone HTML file with:
+If the `impeccable` skill is unavailable or blocked by an actual tool/file error, still create a well-structured standalone HTML file with:</code></pre>
</section>
<section>
<h2>Expected Impact for End-Users</h2>
<p>
Future Islandflow turns should produce documentation in the repo's <code>docs/turns/</code> folder and let <code>impeccable</code> drive the visual treatment, making repo documentation less likely to inherit global computer-task styling.
</p>
</section>
<section>
<h2>Validation</h2>
<ul>
<li>Reviewed the <code>AGENTS.md</code> diff after patching.</li>
<li>Ran <code>git diff --check</code> with no whitespace errors.</li>
<li>No application test suite was run because this change only updates repository instructions.</li>
</ul>
</section>
<section>
<h2>Issues, Limitations, and Mitigations</h2>
<p class="note">
This clarification depends on future agents reading both global and repo instructions. The new wording is intentionally direct about repo scope, location, and styling to reduce that risk.
</p>
</section>
<section>
<h2>Follow-up Work</h2>
<p>
No follow-up issue is required for this patch. The related Beads task for this documentation cleanup is <code>islandflow-lm6</code>.
</p>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,233 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Turn Report: Fix Native Alpaca News</title>
<style>
:root {
color-scheme: dark;
--bg: #0b0f14;
--panel: #121821;
--panel-2: #0f141b;
--border: rgba(255, 255, 255, 0.08);
--text: #e8eef5;
--muted: #93a3b5;
--accent: #7dd3fc;
--accent-2: #a78bfa;
--good: #86efac;
--warn: #fbbf24;
}
* { box-sizing: border-box; }
body {
margin: 0;
background:
radial-gradient(circle at top left, rgba(125, 211, 252, 0.12), transparent 28%),
radial-gradient(circle at top right, rgba(167, 139, 250, 0.12), transparent 32%),
linear-gradient(180deg, #080b10 0%, var(--bg) 100%);
color: var(--text);
font: 15px/1.65 "IBM Plex Sans", "Segoe UI", sans-serif;
padding: 32px;
}
main {
max-width: 1040px;
margin: 0 auto;
background: rgba(18, 24, 33, 0.92);
border: 1px solid var(--border);
border-radius: 20px;
padding: 32px;
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
}
h1, h2 {
margin: 0 0 12px;
font-family: "IBM Plex Mono", monospace;
letter-spacing: 0.04em;
}
h1 { font-size: 1.85rem; }
h2 { font-size: 1rem; margin-top: 28px; }
p, li { margin: 0 0 12px; }
.meta {
color: var(--muted);
font-size: 0.9rem;
margin-bottom: 18px;
}
.summary {
padding: 18px 20px;
border-radius: 16px;
border: 1px solid rgba(125, 211, 252, 0.24);
background: linear-gradient(135deg, rgba(125, 211, 252, 0.10), rgba(167, 139, 250, 0.10));
}
section {
margin-top: 28px;
padding-top: 22px;
border-top: 1px solid var(--border);
}
ul {
margin: 0;
padding-left: 18px;
}
code, pre {
font-family: "IBM Plex Mono", monospace;
}
pre {
margin: 0 0 14px;
padding: 16px;
overflow: auto;
border-radius: 14px;
background: var(--panel-2);
border: 1px solid var(--border);
}
.pill-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 14px 0 0;
}
.pill {
padding: 7px 10px;
border-radius: 999px;
border: 1px solid var(--border);
background: rgba(255, 255, 255, 0.03);
color: var(--muted);
font-size: 0.84rem;
}
.good { color: var(--good); }
.warn { color: var(--warn); }
a { color: var(--accent); }
</style>
</head>
<body>
<main>
<p class="meta">Created 2026-05-19 20:05 EDT · Branch: <code>alpaca-news</code> · Issue: <code>islandflow-laq</code></p>
<h1>Fix Native Alpaca News</h1>
<div class="summary">
<p>
Restored the native Alpaca news pipeline on the VPS by correcting Alpaca auth to use key ID + secret,
adding the missing native <code>islandflow-ingest-news</code> unit and worker-scope wiring, fixing the
Alpaca news backfill defaults to match the current API contract, requesting article content explicitly,
and repairing API-side news persistence so the feed is both live and queryable.
</p>
<div class="pill-row">
<span class="pill">VPS unit installed and enabled</span>
<span class="pill">Alpaca auth aligned to current docs</span>
<span class="pill">Live news confirmed</span>
<span class="pill">ClickHouse news history confirmed</span>
</div>
</div>
<section>
<h2>Summary</h2>
<p>
The original native news rollout failed for two separate reasons: the repo never fully wired
<code>ingest-news</code> into the native worker templates, and the service was still using bearer-style
Alpaca auth plus an oversized backfill limit that Alpaca's current News API rejects. After the service
started flowing again, one more pipeline gap appeared: the API fanned news out live but never persisted it
to ClickHouse, so <code>/news</code> stayed empty even when headlines showed up in the UI.
</p>
</section>
<section>
<h2>Changes Made</h2>
<ul>
<li>Added shared Alpaca credential helpers in <code>packages/config</code> with support for official key ID + secret auth and a legacy bearer fallback.</li>
<li>Rewired the Alpaca news, options, and equities adapters to use the shared auth model instead of hardcoded bearer headers and empty websocket secrets.</li>
<li>Added the checked-in native user unit <code>deployment/native/systemd/user/islandflow-ingest-news.service</code>.</li>
<li>Updated native install, health, cutover, rollback, and deploy-scope scripts so worker/native rollouts include <code>ingest-news</code>.</li>
<li>Corrected the native and Docker env/docs story to advertise current Alpaca credential names.</li>
<li>Lowered the default Alpaca news backfill limit from <code>100</code> to <code>50</code> to match the current endpoint contract.</li>
<li>Requested <code>include_content=true</code> for Alpaca news backfill and added a safe summary fallback when article content is missing.</li>
<li>Fixed API-side persistence by inserting each consumed news story into ClickHouse before live fanout.</li>
<li>On the VPS, created a fresh <code>.env</code> backup, added <code>ALPACA_API_KEY_ID</code> and <code>ALPACA_API_SECRET_KEY</code>, set <code>ALPACA_NEWS_BACKFILL_LIMIT=50</code>, switched the server checkout to <code>alpaca-news</code>, installed the new user unit, and restarted <code>api</code> plus <code>ingest-news</code>.</li>
</ul>
</section>
<section>
<h2>Context</h2>
<p>
Alpaca's current official auth docs require the <code>APCA-API-KEY-ID</code> and
<code>APCA-API-SECRET-KEY</code> header pair for market-data requests, and the current News endpoint
documents a <code>limit</code> range of <code>1..50</code> plus optional
<code>include_content</code>. This turn aligned Islandflow's native news path with those present-day
contracts instead of relying on the older single-token assumption that had drifted into the repo.
</p>
</section>
<section>
<h2>Important Implementation Details</h2>
<ul>
<li>The shared helper prefers <code>ALPACA_API_KEY_ID</code> + <code>ALPACA_API_SECRET_KEY</code>, also accepts <code>ALPACA_KEY_ID</code> + <code>ALPACA_SECRET_KEY</code>, and only falls back to legacy bearer auth when no secret is present.</li>
<li>The news backfill now requests article bodies explicitly. When Alpaca still omits full content, the service emits an escaped summary paragraph instead of a blank story body.</li>
<li>The native worker scope now treats <code>ingest-news</code> as a first-class worker everywhere the repo previously only handled options and equities.</li>
<li>The API now persists each consumed news story into ClickHouse before live fanout, which restores <code>/news</code> and history behavior without removing the live websocket path.</li>
</ul>
</section>
<section>
<h2>Relevant Diff Snippets</h2>
<pre><code class="language-diff">diff --git a/packages/config/src/alpaca.ts b/packages/config/src/alpaca.ts
+export const buildAlpacaAuthHeaders = (credentials) =&gt; ({
+ "APCA-API-KEY-ID": credentials.keyId,
+ "APCA-API-SECRET-KEY": credentials.secret
+})
+export const buildAlpacaWebSocketAuthMessage = (credentials) =&gt; ({
+ action: "auth",
+ key: credentials.keyId,
+ secret: credentials.secret
+})</code></pre>
<pre><code class="language-diff">diff --git a/services/ingest-news/src/index.ts b/services/ingest-news/src/index.ts
- ALPACA_NEWS_BACKFILL_LIMIT: z.coerce.number().int().positive().max(200).default(100),
+ ALPACA_NEWS_BACKFILL_LIMIT: z.coerce.number().int().positive().max(50).default(50),
+ url.searchParams.set("include_content", "true");
+ const contentHtml = item.content?.trim() || (summary ? `&lt;p&gt;${escapeHtml(summary)}&lt;/p&gt;` : "");</code></pre>
<pre><code class="language-diff">diff --git a/services/api/src/index.ts b/services/api/src/index.ts
const payload = NewsStorySchema.parse(newsSubscription.decode(msg));
+ await insertNewsStory(clickhouse, payload);
await fanoutLive({ channel: "news" }, payload, "news");
msg.ack();</code></pre>
<p class="meta">These snippets are included in a diff-style rendering format for fast review.</p>
</section>
<section>
<h2>Expected Impact for End-Users</h2>
<p>
Native Islandflow deployments on the VPS now have a real Alpaca-backed news worker instead of a missing unit
and a crash loop. News stories populate with actual article body content in the feed more reliably, and the
API's <code>/news</code> path can serve persisted recent stories instead of only depending on live websocket
state.
</p>
</section>
<section>
<h2>Validation</h2>
<ul>
<li>Ran local targeted tests: <code>bun test packages/config/tests packages/storage/tests/news.test.ts services/ingest-news/tests services/ingest-equities/tests</code> and all passed.</li>
<li>Ran <code>bun run check:docker-workspace</code> and confirmed the Docker workspace snapshot stayed in sync.</li>
<li>Verified against current Alpaca docs that market-data auth uses key ID + secret and that the news endpoint limit is capped at 50.</li>
<li>On the VPS, confirmed the new <code>islandflow-ingest-news.service</code> unit is installed, enabled, and active under <code>systemd --user</code>.</li>
<li>Queried Alpaca directly from the VPS with the configured credentials and confirmed <code>GET https://data.alpaca.markets/v1beta1/news?limit=1&amp;sort=desc</code> returned <span class="good">HTTP 200</span>.</li>
<li>Restarted the VPS <code>api</code> and <code>ingest-news</code> services after the persistence fix so the API would store newly republished backfill stories.</li>
<li>Verified VPS API output: <code>GET http://127.0.0.1:4000/news?limit=3</code> returned 3 recent real Alpaca stories with non-empty <code>content_html</code> payloads.</li>
<li>Verified ClickHouse persistence: <code>SELECT count(), max(story_id), max(published_ts) FROM news</code> returned <code>50</code> rows after the republished backfill.</li>
</ul>
</section>
<section>
<h2>Issues, Limitations, and Mitigations</h2>
<ul>
<li>The server checkout still carries an unrelated untracked file, <code>deployment/docker/signal-cli-0.14.3-Linux-native.tar.gz</code>. It does not block the news fix, but it is repo hygiene debt on the VPS checkout.</li>
<li>The shared Alpaca helper keeps a legacy bearer fallback so older setups do not fail immediately, but the repo documentation now treats key ID + secret as the supported path.</li>
<li>Some Alpaca/Benzinga stories may still omit full content. The summary fallback prevents a blank drawer in those cases, but it cannot synthesize text Alpaca does not send.</li>
</ul>
</section>
<section>
<h2>Follow-up Work</h2>
<ul>
<li>No new follow-up Beads issue was required to ship this repair.</li>
<li>If native Alpaca options or equities are re-enabled later, the shared credential changes in this turn already cover the same key ID + secret auth model.</li>
<li>If the team wants historical news beyond the startup backfill, the next logical extension is a scheduled catch-up cursor instead of only restart-time republishing.</li>
</ul>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,191 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>2026-05-19 Harden Native SSH Deploy Checks</title>
<style>
:root {
color-scheme: light;
--bg: #f6f4f8;
--surface: #ffffff;
--ink: #1f1726;
--muted: #5f536d;
--line: #ddd4e6;
--accent: #7c4dff;
--accent-soft: #efe7ff;
--code-bg: #17131d;
--code-ink: #f7f2ff;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.55;
}
main {
max-width: 980px;
margin: 0 auto;
padding: 40px 24px 72px;
}
header, section {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 14px;
padding: 24px;
margin-bottom: 18px;
}
h1, h2 {
margin: 0 0 12px;
line-height: 1.15;
}
h1 { font-size: 2rem; }
h2 { font-size: 1.15rem; }
.lede {
font-size: 1.05rem;
color: var(--muted);
}
.meta {
display: inline-block;
margin-top: 10px;
padding: 6px 10px;
border-radius: 999px;
background: var(--accent-soft);
color: var(--accent);
font-size: 0.9rem;
font-weight: 600;
}
ul {
margin: 0;
padding-left: 18px;
}
pre {
margin: 0;
padding: 16px;
overflow: auto;
border-radius: 10px;
background: var(--code-bg);
color: var(--code-ink);
}
code {
font-family: "SFMono-Regular", ui-monospace, monospace;
font-size: 0.94rem;
}
.note {
margin-top: 10px;
color: var(--muted);
font-size: 0.92rem;
}
</style>
</head>
<body>
<main>
<header>
<h1>Harden Native SSH Deploy Checks</h1>
<p class="lede">
Native deploys over SSH were failing for avoidable operator reasons: the remote shell did not inherit Bun's install path, and native verification assumed it was already running from the repository root before it called checked-in health scripts. This patch makes the SSH path more forgiving and fixes the verification working directory.
</p>
<div class="meta">Generated 2026-05-19 19:38 EDT</div>
</header>
<section>
<h2>Summary</h2>
<p>
Updated <code>scripts/deploy.ts</code> so native SSH deploys prepend <code>$HOME/.bun/bin</code> when it exists, and native verification now explicitly <code>cd</code>s into the remote repo before running the checked-in health helpers.
</p>
</section>
<section>
<h2>Changes Made</h2>
<ul>
<li>Prepended <code>$HOME/.bun/bin</code> during native remote precheck when available.</li>
<li>Prepended <code>$HOME/.bun/bin</code> during native remote rollout when available.</li>
<li>Changed native remote verification to run from <code>/home/delta/islandflow</code> before calling <code>deployment/native/check-native-infra.sh</code>.</li>
</ul>
</section>
<section>
<h2>Context</h2>
<p>
During a live native rollout, the deploy helper failed first because the non-login SSH shell could not find <code>bun</code> even though it was installed under the deploy user's home directory. After that was corrected on the host, worker rollout still reported failure because remote verification executed from the home directory and could not resolve the relative path to the checked-in infra check script.
</p>
</section>
<section>
<h2>Important Implementation Details</h2>
<ul>
<li>The fallback only adjusts <code>PATH</code> when <code>$HOME/.bun/bin/bun</code> exists, so it stays harmless on hosts that already expose Bun globally.</li>
<li>The repo-root <code>cd</code> keeps the existing relative helper calls intact instead of hardcoding every individual script path in multiple places.</li>
<li>This change improves SSH-based deploys without changing local-server deploy behavior.</li>
</ul>
</section>
<section>
<h2>Relevant Diff Snippets</h2>
<p class="note">Unified diff blocks below are formatted for diffs-compatible rendering.</p>
<pre><code class="language-diff">diff --git a/scripts/deploy.ts b/scripts/deploy.ts
@@ -754,6 +754,10 @@ set -euo pipefail
cd ${shellEscape(REMOTE_REPO)}
+if [[ -x "$HOME/.bun/bin/bun" ]]; then
+ export PATH="$HOME/.bun/bin:$PATH"
+fi
+
if ! command -v bun >/dev/null 2>&1; then
@@ -855,6 +859,10 @@ set -euo pipefail
+if [[ -x "$HOME/.bun/bin/bun" ]]; then
+ export PATH="$HOME/.bun/bin:$PATH"
+fi
+
${remoteGitUpdateScript(mode, remote, branch)}
@@ -943,6 +951,12 @@ set -euo pipefail
+cd ${shellEscape(REMOTE_REPO)}
+
+if [[ -x "$HOME/.bun/bin/bun" ]]; then
+ export PATH="$HOME/.bun/bin:$PATH"
+fi
+
declare -a units=(${units})</code></pre>
</section>
<section>
<h2>Expected Impact for End-Users</h2>
<p>
End users should see fewer failed native deploy attempts and fewer partial restarts caused by tooling assumptions rather than application health. This lowers the odds of avoidable downtime during native rollouts.
</p>
</section>
<section>
<h2>Validation</h2>
<ul>
<li>Observed the original failures during live rollout: missing <code>bun</code> in SSH PATH and missing <code>deployment/native/check-native-infra.sh</code> during remote verification.</li>
<li>Used the patched operational path to complete native worker, API, and web rollouts successfully on the VPS.</li>
<li>Verified API health at <code>http://127.0.0.1:4000/health</code> and web health at both <code>http://127.0.0.1:3000/</code> and <code>https://flow.deltaisland.io</code>.</li>
</ul>
</section>
<section>
<h2>Issues, Limitations, and Mitigations</h2>
<ul>
<li>This patch does not solve the separate <code>ingest-news</code> credential problem. Full native deploys still need that unit and provider path to be made healthy before they are completely clean.</li>
<li>The VPS also needed a host-level Bun symlink during this recovery. The repo patch reduces dependence on that fix for future SSH deploys but does not remove it retroactively.</li>
</ul>
</section>
<section>
<h2>Follow-up Work</h2>
<ul>
<li><code>islandflow-fmg</code>: Keep the deploy helper aligned with the actual VPS runtime assumptions and add regression checks around native verification paths.</li>
<li><code>islandflow-wf5</code>: Decide whether <code>ingest-news</code> and live options should stay provider-backed or remain intentionally synthetic until auth is hardened.</li>
</ul>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,183 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>2026-05-19 Native Options Recovery Guardrails</title>
<style>
:root {
color-scheme: light;
--bg: #f6f4f8;
--surface: #ffffff;
--ink: #1f1726;
--muted: #5f536d;
--line: #ddd4e6;
--accent: #7c4dff;
--accent-soft: #efe7ff;
--code-bg: #17131d;
--code-ink: #f7f2ff;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.55;
}
main {
max-width: 980px;
margin: 0 auto;
padding: 40px 24px 72px;
}
header, section {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 14px;
padding: 24px;
margin-bottom: 18px;
}
h1, h2 {
margin: 0 0 12px;
line-height: 1.15;
}
h1 { font-size: 2rem; }
h2 { font-size: 1.15rem; }
p, li { color: var(--ink); }
.lede {
font-size: 1.05rem;
color: var(--muted);
}
.meta {
display: inline-block;
margin-top: 10px;
padding: 6px 10px;
border-radius: 999px;
background: var(--accent-soft);
color: var(--accent);
font-size: 0.9rem;
font-weight: 600;
}
ul {
margin: 0;
padding-left: 18px;
}
pre {
margin: 0;
padding: 16px;
overflow: auto;
border-radius: 10px;
background: var(--code-bg);
color: var(--code-ink);
}
code {
font-family: "SFMono-Regular", ui-monospace, monospace;
font-size: 0.94rem;
}
.note {
margin-top: 10px;
color: var(--muted);
font-size: 0.92rem;
}
a { color: var(--accent); }
</style>
</head>
<body>
<main>
<header>
<h1>Native Options Recovery Guardrails</h1>
<p class="lede">
The production outage turned out to be a native deployment config mismatch, not a data-pipeline code failure. I restored the VPS to the last known-good synthetic options adapter, then tightened the checked-in native deployment assets so they no longer imply a systemd override will beat the repo <code>.env</code>.
</p>
<div class="meta">Generated 2026-05-19 19:24 EDT</div>
</header>
<section>
<h2>Summary</h2>
<p>
The repo-side change is small and targeted: remove the misleading <code>Environment=OPTIONS_INGEST_ADAPTER=synthetic</code> line from the checked-in native <code>ingest-options</code> unit, and document that Bun-launched services effectively take adapter selection from <code>/home/delta/islandflow/.env</code>.
</p>
</section>
<section>
<h2>Changes Made</h2>
<ul>
<li>Removed the checked-in systemd override from <code>deployment/native/systemd/user/islandflow-ingest-options.service</code>.</li>
<li>Added an explicit env-precedence warning to <code>deployment/native/README.md</code>.</li>
<li>Captured the live diagnosis that the native server had drifted to <code>OPTIONS_INGEST_ADAPTER=alpaca</code> while the prior Docker deployment was running synthetic options.</li>
</ul>
</section>
<section>
<h2>Context</h2>
<p>
On the VPS, <code>islandflow-ingest-options.service</code> was crash-looping with repeated <code>401 Unauthorized</code> responses from Alpaca while the rest of the native stack stayed healthy. The previous Docker-owned <code>islandflow-vps-ingest-options-1</code> container showed <code>OPTIONS_INGEST_ADAPTER=synthetic</code>, which explains why the UI had been healthy before the runtime transition.
</p>
</section>
<section>
<h2>Important Implementation Details</h2>
<ul>
<li>The checked-in unit already referenced <code>/home/delta/islandflow/.env</code>, and Bun's runtime env loading meant a conflicting adapter value there still won in practice.</li>
<li>The static key currently stored as <code>ALPACA_API_KEY</code> does not authenticate the failing market-data snapshot request as a Bearer token.</li>
<li>Because the real outage fix required a server-side <code>.env</code> correction, this repo patch focuses on preventing operator confusion during the next native cutover.</li>
</ul>
</section>
<section>
<h2>Relevant Diff Snippets</h2>
<p class="note">Unified diff blocks below are formatted for diffs-compatible rendering.</p>
<pre><code class="language-diff">diff --git a/deployment/native/README.md b/deployment/native/README.md
@@ -98,6 +98,8 @@ These are written for the current VPS layout:
- Bun binary: `/home/delta/.bun/bin/bun`
- env file: `/home/delta/islandflow/.env`
+Important: treat `/home/delta/islandflow/.env` as the effective source of truth for adapter selection. The Bun-launched services read that file directly at runtime, so a conflicting `OPTIONS_INGEST_ADAPTER` value in `.env` can still win over a systemd-only override and push `ingest-options` onto the wrong provider path.
+
### Install the units
diff --git a/deployment/native/systemd/user/islandflow-ingest-options.service b/deployment/native/systemd/user/islandflow-ingest-options.service
@@ -7,7 +7,6 @@ Wants=network-online.target
Type=simple
WorkingDirectory=/home/delta/islandflow
EnvironmentFile=/home/delta/islandflow/.env
-Environment=OPTIONS_INGEST_ADAPTER=synthetic
ExecStart=/home/delta/.bun/bin/bun services/ingest-options/src/index.ts</code></pre>
</section>
<section>
<h2>Expected Impact for End-Users</h2>
<p>
End users should not see the options tape stall the next time native units are installed or audited by following the checked-in assets. Operators now have a clearer paper trail that the actual runtime adapter comes from the deployment env file.
</p>
</section>
<section>
<h2>Validation</h2>
<ul>
<li>Verified the native outage mode on the VPS: <code>islandflow-ingest-options.service</code> crash-looped on Alpaca <code>401</code> responses.</li>
<li>Confirmed the previous Docker container had been running <code>OPTIONS_INGEST_ADAPTER=synthetic</code>.</li>
<li>After the server-side env fix, confirmed fresh rows in <code>default.option_prints</code> and new compute emissions in the native logs.</li>
<li>Ran <code>git diff</code> to verify the repo change stayed scoped to the deployment README and the checked-in user unit.</li>
</ul>
</section>
<section>
<h2>Issues, Limitations, and Mitigations</h2>
<ul>
<li>The repo patch does not add new credential support for Alpaca. It only documents the current env-precedence behavior and removes a misleading override.</li>
<li>The live server is restored with synthetic options, which matches the last known-good Docker behavior, but it is not a true live Alpaca ingest path.</li>
</ul>
</section>
<section>
<h2>Follow-up Work</h2>
<ul>
<li><code>islandflow-wf5</code>: Decide whether production options should remain synthetic or move to a fully supported live provider configuration.</li>
<li><code>islandflow-wf5</code>: If Alpaca live data is still desired, add a validated auth flow and a deploy-time smoke test that catches provider auth failures before cutover.</li>
</ul>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,195 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Turn Report - Publish Docs to GitHub Pages</title>
<style>
:root {
--bg: #f6f8fb;
--surface: #ffffff;
--text: #172133;
--muted: #56637a;
--border: #d5dde8;
--accent: #0b7a75;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: "Inter", "Segoe UI", sans-serif;
line-height: 1.5;
}
main {
max-width: 980px;
margin: 0 auto;
padding: 28px 16px 44px;
}
h1, h2 { line-height: 1.2; }
h1 { margin: 0 0 8px; font-size: clamp(1.6rem, 2.2vw, 2.1rem); }
h2 {
margin: 0;
font-size: 1.18rem;
}
.meta {
margin: 0 0 20px;
color: var(--muted);
}
section {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px 16px;
margin-bottom: 12px;
}
p, li { margin: 0; }
p + p { margin-top: 8px; }
ul {
margin: 10px 0 0;
padding-left: 18px;
}
li + li { margin-top: 6px; }
code {
font-family: "IBM Plex Mono", "SFMono-Regular", Consolas, monospace;
font-size: 0.92em;
}
pre {
margin: 10px 0 0;
overflow-x: auto;
padding: 12px;
border-radius: 8px;
background: #0f172a;
color: #e2e8f0;
border: 1px solid #1e293b;
}
.note {
margin-top: 10px;
color: var(--muted);
font-size: 0.95rem;
}
a { color: var(--accent); }
</style>
</head>
<body>
<main>
<h1>Publish docs/ to GitHub Pages with navigable index</h1>
<p class="meta">Completed on May 19, 2026 at 9:38 AM ET.</p>
<section>
<h2>Summary</h2>
<p>
Added an automated docs publishing flow to GitHub Pages and generated a new
<code>docs/index.html</code> browsing experience so docs are easy to navigate at
<code>/islandflow/docs/</code>.
</p>
</section>
<section>
<h2>Changes Made</h2>
<ul>
<li>Added <code>scripts/generate-docs-index.mjs</code> to build a browsable index of files under <code>docs/</code>.</li>
<li>Added <code>.github/workflows/docs-pages.yml</code> to publish docs to GitHub Pages on pushes to <code>main</code>.</li>
<li>Generated <code>docs/index.html</code> from current docs content.</li>
<li>Configured deployment artifact layout so docs are available at <code>/docs/</code> under the project Pages site.</li>
</ul>
</section>
<section>
<h2>Context</h2>
<p>
The repository already stores operational and implementation documentation under
<code>docs/</code>, but there was no dedicated GitHub Pages pipeline and no curated
index page for discovery. This task focused on syncing that folder to Pages and
making it easy to browse by category and filename.
</p>
</section>
<section>
<h2>Important Implementation Details</h2>
<ul>
<li>The index generator excludes hidden files and avoids self-including <code>docs/index.html</code>.</li>
<li>Files are grouped by first path segment (<code>turns</code>, <code>general</code>, <code>plans</code>, and others) with quick category chips.</li>
<li>The index includes client-side filtering so users can search docs by path text in-browser.</li>
<li>Pages deployment packages a <code>site/</code> payload where docs are copied into <code>site/docs</code> and root redirects to <code>./docs/</code>.</li>
</ul>
</section>
<section>
<h2>Relevant Diff Snippets</h2>
<p class="note">
Snippets are shown in a compact style aligned with <a href="https://diffs.com/docs">diffs.com</a> presentation patterns.
</p>
<pre><code class="language-diff">+++ .github/workflows/docs-pages.yml
name: Publish Docs
on:
push:
branches: [main]
paths:
- "docs/**"
- "scripts/generate-docs-index.mjs"
- ".github/workflows/docs-pages.yml"
workflow_dispatch:
jobs:
build:
steps:
- uses: actions/checkout@v4
- uses: actions/configure-pages@v5
- run: node scripts/generate-docs-index.mjs
- run: cp -R docs/. site/docs/
- uses: actions/upload-pages-artifact@v3
deploy:
needs: build
steps:
- uses: actions/deploy-pages@v4</code></pre>
<pre><code class="language-diff">+++ scripts/generate-docs-index.mjs
const files = await collectDocsFiles(docsDir);
const html = renderDocument(files);
await fs.writeFile(outputFile, html, "utf8");
// Generated index features:
// - grouped sections
// - search filter
// - file size and modified time metadata
// - links preserving docs folder structure</code></pre>
</section>
<section>
<h2>Expected Impact for End-Users</h2>
<ul>
<li>Docs are reachable via a stable Pages URL path: <code>dirtydishes.github.io/islandflow/docs/</code>.</li>
<li>Readers can quickly scan categories and search by filename instead of relying on raw directory browsing.</li>
<li>New docs added to the repository are published automatically on <code>main</code> pushes.</li>
</ul>
</section>
<section>
<h2>Validation</h2>
<ul>
<li>Ran <code>node scripts/generate-docs-index.mjs</code> successfully.</li>
<li>Ran <code>node --check scripts/generate-docs-index.mjs</code> for syntax validation.</li>
<li>Confirmed generated index contains expected navigation/search markers and category anchors.</li>
</ul>
</section>
<section>
<h2>Issues, Limitations, and Mitigations</h2>
<ul>
<li>GitHub Pages must be enabled for this repository and set to GitHub Actions deployment.</li>
<li>The index reflects files present at build time and does not include full-text search inside documents.</li>
<li>Markdown files are linked as-is; rendering behavior depends on GitHub Pages static hosting behavior.</li>
</ul>
</section>
<section>
<h2>Follow-up Work</h2>
<ul>
<li>Add a docs landing page summary for key collections (turn docs, runbooks, daily notes).</li>
<li>Optionally add link-checking in CI for docs URLs and local references.</li>
<li>Consider tagging docs with metadata for richer filtering by date, topic, and type.</li>
</ul>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,276 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Reconcile PR Conflicts</title>
<style>
:root {
color-scheme: dark;
--bg: oklch(13% 0.018 252);
--panel: oklch(18% 0.022 248);
--panel-2: oklch(22% 0.026 248);
--line: oklch(72% 0.03 248 / 0.18);
--text: oklch(93% 0.012 248);
--muted: oklch(72% 0.025 248);
--faint: oklch(58% 0.025 248);
--amber: oklch(78% 0.16 72);
--green: oklch(73% 0.16 154);
--blue: oklch(70% 0.14 245);
--red: oklch(68% 0.18 32);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif;
line-height: 1.55;
}
main {
width: min(1120px, calc(100% - 32px));
margin: 0 auto;
padding: 48px 0 72px;
}
header {
border-bottom: 1px solid var(--line);
padding-bottom: 28px;
margin-bottom: 28px;
}
.eyebrow,
h1,
h2,
code,
pre {
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}
.eyebrow {
color: var(--amber);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
h1 {
margin: 10px 0 12px;
font-size: clamp(2rem, 4vw, 3.2rem);
line-height: 1.06;
letter-spacing: 0;
}
h2 {
margin: 0 0 12px;
font-size: 1rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
p {
max-width: 76ch;
margin: 0 0 12px;
}
a {
color: var(--blue);
}
section {
padding: 24px 0;
border-bottom: 1px solid var(--line);
}
ul {
margin: 0;
padding-left: 20px;
}
li + li {
margin-top: 8px;
}
.summary {
color: var(--muted);
font-size: 1.05rem;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 12px;
margin-top: 14px;
}
.tile {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
padding: 14px;
}
.tile strong {
display: block;
margin-bottom: 6px;
}
code {
color: var(--text);
background: oklch(100% 0 0 / 0.06);
border: 1px solid oklch(100% 0 0 / 0.08);
border-radius: 6px;
padding: 0.08rem 0.28rem;
}
pre {
overflow: auto;
background: var(--panel-2);
border: 1px solid var(--line);
border-radius: 8px;
padding: 14px;
font-size: 0.86rem;
}
pre code {
background: transparent;
border: 0;
padding: 0;
}
.diff-add {
color: var(--green);
}
.diff-del {
color: var(--red);
}
.diff-meta {
color: var(--faint);
}
.callout {
border: 1px solid oklch(78% 0.16 72 / 0.35);
background: oklch(78% 0.16 72 / 0.08);
border-radius: 8px;
padding: 14px;
}
</style>
</head>
<body>
<main>
<header>
<div class="eyebrow">Turn document • 2026-05-19 18:56 ET</div>
<h1>Reconcile PR Conflicts</h1>
<p class="summary">
Merged <code>forgejo/main</code> into <code>nextjs-upgrade</code>, resolved the checked-in Beads and README conflicts, kept the native deployment work from main, and updated the JetStream tests for the merged nanosecond retention behavior.
</p>
</header>
<section>
<h2>Summary</h2>
<p>
The PR branch now incorporates the current mainline deployment changes while preserving the Next.js upgrade branch. The only hand-edited conflict resolution was in <code>.beads/issues.jsonl</code> and <code>README.md</code>; the rest of the mainline merge applied cleanly.
</p>
</section>
<section>
<h2>Changes Made</h2>
<ul>
<li>Resolved <code>.beads/issues.jsonl</code> by keeping issue records from both sides of the merge.</li>
<li>Resolved the README deployment workflow section by combining the branchs command-oriented guidance with mains newer worker-only, local-server, and native edge cutover notes.</li>
<li>Accepted mainline native deployment assets, Docker deployment refinements, API host binding support, deploy timing output, and worker-only deployment scope.</li>
<li>Adjusted <code>packages/bus/tests/jetstream.test.ts</code> so retention assertions expect NATS nanoseconds after the merged runtime change.</li>
</ul>
</section>
<section>
<h2>Context</h2>
<p>
The branch was clean before the merge, but Forgejo reported PR conflicts against <code>main</code>. Reproducing the merge locally showed conflicts in the Beads export file and the README deployment section. The automatic merge also brought in mainline native deployment work that touched deploy scripts, Docker deployment files, native systemd templates, public edge documentation, the API host setting, and JetStream retention units.
</p>
</section>
<section>
<h2>Important Implementation Details</h2>
<div class="grid">
<div class="tile">
<strong>README resolution</strong>
<p>Kept Docker as the recommended VPS path, preserved explicit deploy commands, and added <code>--workers-only</code>, local server execution, and native worker iteration guidance.</p>
</div>
<div class="tile">
<strong>Beads resolution</strong>
<p>Removed conflict markers without dropping either branchs issue records, so Beads history remains complete.</p>
</div>
<div class="tile">
<strong>Test repair</strong>
<p>Main now stores JetStream <code>max_age</code> in nanoseconds via NATS helpers. Tests now assert against <code>nanos(...)</code> instead of raw millisecond values.</p>
</div>
</div>
</section>
<section>
<h2>Relevant Diff Snippets</h2>
<p>
Diff snippets are presented in the style of <a href="https://diffs.com/docs">diffs.com</a>, using structured additions and deletions for quick review.
</p>
<pre><code><span class="diff-meta">diff --git a/README.md b/README.md</span>
<span class="diff-del">- Partial deploys are supported with `--web-only`, `--api-only`, `--services-only`, `--fast`, `--no-build`, and `--force-recreate`.</span>
<span class="diff-add">+ Partial deploys are supported with `--web-only`, `--api-only`, `--services-only`, `--workers-only`, `--fast`, `--no-build`, and `--force-recreate`.</span>
<span class="diff-add">+ When run from `/home/delta/islandflow` on the VPS itself, `./deploy` can execute locally instead of SSHing back into the same server.</span>
<span class="diff-del">- Native deployment expects Bun, systemd units, host-reachable infra, and deliberate reverse-proxy changes. The open follow-up is to add native unit templates and rollback helpers.</span>
<span class="diff-add">+ Native deployment expects Bun, systemd units, host-reachable infra, and deliberate reverse-proxy changes. Native deploys are intended primarily for worker-only fast iteration until the public edge is cut over deliberately.</span></code></pre>
<pre><code><span class="diff-meta">diff --git a/packages/bus/tests/jetstream.test.ts b/packages/bus/tests/jetstream.test.ts</span>
<span class="diff-del">- import type { JetStreamManager, StreamConfig } from "nats";</span>
<span class="diff-add">+ import { nanos, type JetStreamManager, type StreamConfig } from "nats";</span>
<span class="diff-del">- max_age: 3_600_000,</span>
<span class="diff-add">+ max_age: nanos(3_600_000),</span>
<span class="diff-del">- max_age: 43_200_000,</span>
<span class="diff-add">+ max_age: nanos(43_200_000),</span></code></pre>
</section>
<section>
<h2>Expected Impact for End-Users</h2>
<p>
The PR should no longer show merge conflicts against main. Users and operators get the Next.js upgrade branch plus the newer deployment safety work from main, including worker-only native deploy guidance and current Docker deployment notes.
</p>
</section>
<section>
<h2>Validation</h2>
<ul>
<li><code>git diff --check</code> passed.</li>
<li><code>bun run scripts/deploy.ts --help</code> passed.</li>
<li><code>bun run check:docker-workspace</code> passed.</li>
<li><code>bun test services/api/tests packages/bus/tests</code> passed with 45 tests.</li>
<li><code>bun --cwd=apps/web run build</code> passed on Next.js 16.2.6.</li>
</ul>
</section>
<section>
<h2>Issues, Limitations, and Mitigations</h2>
<div class="callout">
<p>
The first focused test run failed because the merged JetStream implementation correctly returned nanosecond retention values while the existing tests still expected milliseconds. The tests were updated to use the same NATS <code>nanos</code> helper as the runtime behavior, then the suite passed.
</p>
</div>
</section>
<section>
<h2>Follow-up Work</h2>
<ul>
<li>No new follow-up was created from this reconciliation.</li>
<li>Existing deployment follow-ups remain in Beads, including native public edge posture and cutover decisions.</li>
</ul>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,229 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Upgrade apps/web to Next.js 16.2.6</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500;600&family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
color-scheme: dark;
--bg: #17111d;
--panel: #24172d;
--panel-strong: #30203b;
--text: #f3eaf8;
--muted: #c8b7d4;
--accent: #d9a6ff;
--accent-2: #ff9fc7;
--line: #5d3f70;
--code: #130d18;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background:
radial-gradient(circle at 20% 0%, rgba(217, 166, 255, 0.18), transparent 32rem),
linear-gradient(145deg, #17111d 0%, #1d1424 56%, #140f19 100%);
color: var(--text);
font-family: "IBM Plex Sans", system-ui, sans-serif;
line-height: 1.62;
}
main {
width: min(980px, calc(100% - 32px));
margin: 0 auto;
padding: 56px 0 72px;
}
header {
margin-bottom: 34px;
padding-bottom: 26px;
border-bottom: 1px solid var(--line);
}
.eyebrow {
margin: 0 0 10px;
color: var(--accent-2);
font-family: "IBM Plex Mono", monospace;
font-size: 0.82rem;
letter-spacing: 0;
text-transform: uppercase;
}
h1,
h2 {
font-family: "IBM Plex Mono", monospace;
letter-spacing: 0;
line-height: 1.2;
}
h1 {
max-width: 780px;
margin: 0;
font-size: clamp(2rem, 5vw, 4.2rem);
}
h2 {
margin: 0 0 12px;
color: var(--accent);
font-size: 1.18rem;
}
section {
margin-top: 22px;
padding: 22px;
border: 1px solid rgba(217, 166, 255, 0.22);
border-radius: 8px;
background: rgba(36, 23, 45, 0.72);
}
p,
li {
max-width: 74ch;
}
ul {
padding-left: 1.2rem;
}
code,
pre {
font-family: "IBM Plex Mono", monospace;
}
code {
color: #f6c0dd;
}
pre {
overflow-x: auto;
margin: 14px 0 0;
padding: 16px;
border: 1px solid rgba(255, 159, 199, 0.22);
border-radius: 8px;
background: var(--code);
color: #f6eafd;
font-size: 0.88rem;
line-height: 1.55;
}
.summary {
font-size: 1.08rem;
color: var(--muted);
}
.checklist li::marker {
color: var(--accent-2);
}
a {
color: #f0b7ff;
}
</style>
</head>
<body>
<main>
<header>
<p class="eyebrow">Turn document · 2026-05-19</p>
<h1>Upgrade apps/web to Next.js 16.2.6</h1>
<p class="summary">The web app now builds and passes focused validation on Next.js 16.2.6 with React 19. The change keeps route behavior and synthetic admin proxy behavior intact while refreshing the root and Docker workspace Bun lockfiles.</p>
</header>
<section>
<h2>Summary</h2>
<p>Upgraded <code>apps/web</code> from the Next 14 / React 18 stack to Next 16.2.6 and React 19.2.x. The Bun lockfile was refreshed, the Docker workspace lock snapshot was synced, and a React 19 nullable ref type issue exposed by the Next 16 build was fixed.</p>
</section>
<section>
<h2>Changes Made</h2>
<ul>
<li>Updated <code>apps/web/package.json</code> to request <code>next ^16.2.6</code>, <code>react ^19.2.0</code>, and <code>react-dom ^19.2.0</code>.</li>
<li>Updated React type dependencies to <code>@types/react ^19.2.7</code> and added <code>@types/react-dom ^19.2.3</code>.</li>
<li>Ran <code>bun install</code>, which resolved Next to <code>16.2.6</code> and React/React DOM to <code>19.2.6</code> in <code>bun.lock</code>.</li>
<li>Ran <code>bun run sync:docker-workspace</code> so <code>deployment/docker/workspace-root/bun.lock</code> matches the root lock snapshot.</li>
<li>Adjusted the terminal list ref types to accept <code>HTMLDivElement | null</code>, matching React 19's stricter ref object typing.</li>
<li>Allowed Next 16 to regenerate <code>apps/web/next-env.d.ts</code> with its updated TypeScript reference comment and generated route type import.</li>
</ul>
</section>
<section>
<h2>Context</h2>
<p>The requested upgrade was intentionally dependency-focused. No routes, backend contracts, environment variable names, or shared package exports were changed. Before editing, the web build and the targeted route tests passed on the previous locked Next 14.2.35 stack.</p>
</section>
<section>
<h2>Important Implementation Details</h2>
<p>No broad codemod was run. The only source-code change was a targeted type correction in <code>apps/web/app/terminal.tsx</code>. Next 16's build now runs with Turbopack by default in this project and completed successfully after the ref typing was narrowed to the actual nullable runtime value.</p>
<p>The Docker workspace sync changed the mirrored lockfile, but did not need to rewrite the mirrored package manifest or TypeScript base config.</p>
</section>
<section>
<h2>Relevant Diff Snippets</h2>
<pre><code>"next": "^16.2.6",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3"</code></pre>
<pre><code>type ListScrollState = {
listRef: React.RefObject&lt;HTMLDivElement | null&gt;;
listNode: HTMLDivElement | null;
setListRef: (node: HTMLDivElement | null) =&gt; void;
};</code></pre>
</section>
<section>
<h2>Expected Impact for End-Users</h2>
<p>There should be no intentional user-facing behavior change. The expected visible behavior remains: <code>/</code>, <code>/tape</code>, and <code>/news</code> render the terminal app; <code>/signals</code>, <code>/charts</code>, and <code>/replay</code> redirect to <code>/</code>; synthetic admin API routes keep their gated proxy behavior.</p>
</section>
<section>
<h2>Validation</h2>
<ul class="checklist">
<li>Baseline before edits: <code>bun --cwd=apps/web run build</code> passed on Next 14.2.35.</li>
<li>Baseline before edits: <code>bun test apps/web/app/routes.test.ts</code> passed, 3 tests.</li>
<li>Baseline before edits: <code>bun test apps/web/app/terminal.test.ts</code> passed, 70 tests.</li>
<li>Baseline before edits: <code>bun test apps/web/app/api/admin/synthetic/routes.test.ts</code> passed, 4 tests.</li>
<li>After upgrade: <code>bun --cwd=apps/web run build</code> passed on Next 16.2.6.</li>
<li>After upgrade: <code>bun test apps/web/app/routes.test.ts</code> passed, 3 tests.</li>
<li>After upgrade: <code>bun test apps/web/app/terminal.test.ts</code> passed, 70 tests.</li>
<li>After upgrade: <code>bun test apps/web/app/api/admin/synthetic/routes.test.ts</code> passed, 4 tests.</li>
<li>After upgrade: <code>bun run check:docker-workspace</code> passed.</li>
<li>Manual smoke: <code>bun run dev:web</code> served Next 16.2.6 on <code>localhost:3000</code>.</li>
<li>Manual smoke: browser checks confirmed <code>/</code>, <code>/tape</code>, and <code>/news</code> render with title <code>Islandflow Terminal</code>.</li>
<li>Manual smoke: <code>/signals</code>, <code>/charts</code>, and <code>/replay</code> returned <code>307</code> redirects to <code>/</code>.</li>
<li>Manual smoke: synthetic admin status and control routes returned gated <code>404</code> responses when the internal UI flag was off.</li>
</ul>
</section>
<section>
<h2>Issues, Limitations, and Mitigations</h2>
<p>During <code>dev:web</code> smoke testing, the browser logged a live socket channel validation warning because only the web app was running, not the full backend service stack. Route rendering, redirect behavior, and gated synthetic admin proxy behavior were still verified. A full-stack live feed verification can be done separately with <code>bun run dev</code> if needed.</p>
<p>The upgrade did not include a full monorepo test run because the acceptance bar was intentionally web-focused.</p>
</section>
<section>
<h2>Follow-up Work</h2>
<ul>
<li>No required follow-up Beads issue was opened for this upgrade.</li>
<li>Optional: run a full-stack live feed smoke with infra and services running if you want runtime stream confidence beyond the web-focused acceptance checks.</li>
<li>Optional: run the full monorepo <code>bun test</code> suite before a larger release branch merge.</li>
</ul>
</section>
<section>
<h2>Helpful Links</h2>
<ul>
<li><a href="https://nextjs.org/docs/app/guides/upgrading/version-16">Next.js 16 upgrade guide</a></li>
<li><a href="https://react.dev/blog/2024/12/05/react-19">React 19 release notes</a></li>
<li><a href="https://bun.sh/docs/cli/install">Bun install documentation</a></li>
</ul>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,538 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>2026-05-20 · Codex Desktop Login And Copilot</title>
<style>
:root {
color-scheme: dark;
--bg: oklch(0.11 0.01 250);
--panel: oklch(0.16 0.013 250 / 0.92);
--panel-2: oklch(0.14 0.012 250 / 0.9);
--text: oklch(0.93 0.014 250);
--muted: oklch(0.74 0.018 250);
--faint: oklch(0.6 0.016 250);
--line: oklch(0.75 0.014 250 / 0.14);
--accent: oklch(0.79 0.12 74);
--accent-soft: oklch(0.79 0.12 74 / 0.12);
--green: oklch(0.75 0.12 151);
--green-soft: oklch(0.75 0.12 151 / 0.12);
--red: oklch(0.72 0.14 28);
--red-soft: oklch(0.72 0.14 28 / 0.12);
--blue-soft: oklch(0.7 0.1 247 / 0.12);
--shadow: 0 24px 80px oklch(0.02 0.01 250 / 0.45);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif;
background:
radial-gradient(circle at top left, oklch(0.8 0.12 74 / 0.09), transparent 28%),
linear-gradient(180deg, oklch(0.15 0.012 250) 0%, oklch(0.1 0.01 250) 100%);
color: var(--text);
}
main {
width: min(1160px, calc(100vw - 40px));
margin: 0 auto;
padding: 36px 0 64px;
}
h1,
h2,
h3,
.eyebrow,
.meta,
code,
pre {
font-family: "IBM Plex Mono", ui-monospace, monospace;
}
h1,
h2,
h3 {
margin: 0;
}
p,
li {
color: var(--muted);
line-height: 1.7;
}
a {
color: inherit;
}
.hero {
display: grid;
gap: 20px;
padding: 28px;
border: 1px solid var(--line);
border-radius: 24px;
background:
linear-gradient(135deg, oklch(0.2 0.017 250 / 0.96), oklch(0.14 0.012 250 / 0.98)),
var(--panel);
box-shadow: var(--shadow);
}
.eyebrow {
margin: 0 0 10px;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.18em;
font-size: 0.76rem;
}
.hero h1 {
font-size: clamp(2rem, 3.4vw, 3.6rem);
line-height: 0.95;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.hero-copy {
max-width: 72ch;
margin: 14px 0 0;
}
.hero-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
.stat {
padding: 16px 18px;
border: 1px solid var(--line);
border-radius: 16px;
background: oklch(0.12 0.01 250 / 0.5);
}
.stat span {
display: block;
margin-bottom: 8px;
color: var(--faint);
text-transform: uppercase;
letter-spacing: 0.16em;
font-size: 0.68rem;
}
.stat strong {
display: block;
color: var(--text);
font-size: 1rem;
}
.section-grid {
display: grid;
gap: 18px;
margin-top: 22px;
}
section {
padding: 24px;
border: 1px solid var(--line);
border-radius: 20px;
background: var(--panel-2);
box-shadow: var(--shadow);
}
section h2 {
margin-bottom: 14px;
font-size: 1rem;
letter-spacing: 0.12em;
text-transform: uppercase;
}
ul {
margin: 0;
padding-left: 20px;
}
li + li {
margin-top: 10px;
}
.two-col {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.8fr);
gap: 18px;
}
.callout {
padding: 16px 18px;
border-radius: 16px;
border: 1px solid var(--line);
background: var(--blue-soft);
}
.callout strong {
color: var(--text);
}
.meta {
color: var(--faint);
font-size: 0.82rem;
}
.validation-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.validation-card {
padding: 16px 18px;
border-radius: 16px;
border: 1px solid var(--line);
background: oklch(0.11 0.01 250 / 0.62);
}
.validation-card.good {
background: var(--green-soft);
}
.validation-card.warn {
background: var(--red-soft);
}
pre.diff {
margin: 0;
padding: 16px 18px;
overflow: auto;
border-radius: 16px;
border: 1px solid var(--line);
background: oklch(0.08 0.008 250 / 0.95);
color: var(--text);
line-height: 1.5;
font-size: 0.78rem;
}
.diff-title {
margin-bottom: 10px;
color: var(--faint);
font-size: 0.76rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.diff-note {
margin-top: 10px;
font-size: 0.82rem;
}
.chip-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.chip {
display: inline-flex;
align-items: center;
min-height: 30px;
padding: 0 10px;
border-radius: 999px;
border: 1px solid oklch(0.8 0.12 74 / 0.28);
background: var(--accent-soft);
color: var(--text);
font-size: 0.74rem;
}
@media (max-width: 860px) {
.hero-grid,
.two-col,
.validation-grid {
grid-template-columns: minmax(0, 1fr);
}
main {
width: min(100vw - 24px, 1160px);
padding-top: 18px;
}
section,
.hero {
padding: 18px;
}
}
</style>
</head>
<body>
<main>
<article class="hero">
<div>
<p class="eyebrow">Turn Document · Repository Implementation</p>
<h1>Codex Desktop Login And Analyst Copilot</h1>
<p class="hero-copy">
Islandflow Desktop now boots an official <code>codex app-server</code> bridge, lets each desktop user log
into a ChatGPT-backed Codex account, exposes a narrow Electron IPC surface to the renderer, and adds an AI
settings and copilot experience without turning AI into the live first-pass classifier.
</p>
</div>
<div class="hero-grid">
<div class="stat">
<span>Scope</span>
<strong>Electron bridge, auth, usage telemetry, renderer UI, tests</strong>
</div>
<div class="stat">
<span>Beads</span>
<strong><code>islandflow-6tn</code></strong>
</div>
<div class="stat">
<span>Validation</span>
<strong>Desktop tests, web tests, shared-type check, desktop typecheck, production web build</strong>
</div>
</div>
</article>
<div class="section-grid">
<section>
<h2>Summary</h2>
<p>
This turn added a desktop-only Codex capability that respects the repo plan: managed ChatGPT login comes
first, the existing deterministic smart-money classifier remains the source of truth, and AI is used as a
structured analyst copilot on top of existing Islandflow artifacts.
</p>
<div class="chip-row">
<span class="chip">Managed ChatGPT browser login</span>
<span class="chip">Device-code fallback</span>
<span class="chip">Electron preload + IPC bridge</span>
<span class="chip">Usage and rate-limit dashboard</span>
<span class="chip">Smart-money and replay copilot actions</span>
<span class="chip">Browser-safe degradation</span>
</div>
</section>
<section>
<h2>Changes Made</h2>
<ul>
<li>Added a native desktop AI service in <code>apps/desktop/src/desktop-ai.ts</code> that starts <code>codex app-server</code>, handles account state, usage notifications, rate limits, task execution, and preference persistence.</li>
<li>Added preload and IPC plumbing in <code>apps/desktop/src/preload.ts</code>, <code>apps/desktop/src/desktop-ai-ipc.ts</code>, and <code>apps/desktop/src/main.ts</code> with trusted-origin enforcement.</li>
<li>Introduced shared AI contracts in <code>packages/types/src/desktop-ai.ts</code> for auth state, model controls, token usage, rate limits, task payloads, and compiled screen responses.</li>
<li>Added a renderer-side desktop AI provider in <code>apps/web/app/desktop-ai.tsx</code> and richer UI surfaces in <code>apps/web/app/desktop-ai-panels.tsx</code>.</li>
<li>Enabled previously latent routes for <code>/signals</code>, <code>/charts</code>, and <code>/replay</code>, plus a new <code>/settings</code> route.</li>
<li>Extended the terminal shell and styles so users can reach AI Settings, compile natural-language screens on Tape, run replay postmortems, and investigate smart-money events inline.</li>
<li>Added desktop tests for env scrubbing, token usage accounting, rate-limit snapshots, and logout state reset, plus updated web route and navigation tests.</li>
</ul>
</section>
<section>
<h2>Context</h2>
<p>
The desktop app was previously a secure Electron wrapper around the web terminal. That meant there was no
authenticated native bridge, no preload API for AI state, and no desktop-only place to manage account
status, model controls, or token telemetry. The goal of this turn was to add those capabilities without
weakening the shell security model and without letting AI replace the deterministic classification pipeline.
</p>
<div class="callout">
<strong>Deliberate product boundary:</strong>
<p>
The Copilot works only from structured Islandflow payloads such as <code>SmartMoneyEvent</code>,
<code>ClassifierHitEvent</code>, <code>FlowPacket</code>, and current replay slices. It does not become a
freeform live classifier in v1.
</p>
</div>
</section>
<section class="two-col">
<div>
<h2>Important Implementation Details</h2>
<ul>
<li>The child <code>codex app-server</code> environment now explicitly clears <code>OPENAI_API_KEY</code> and <code>CODEX_API_KEY</code> unless the selected profile mode is API-key-based, which prevents accidental auth-mode drift during ChatGPT subscription sessions.</li>
<li>All desktop AI access goes through a narrow preload bridge instead of exposing Node or Electron primitives to the renderer.</li>
<li>IPC handlers validate the sender URL with the existing trusted-origin policy before serving account or task requests.</li>
<li>Usage is persisted by <code>threadId</code> and <code>turnId</code>, then rolled up into today and lifetime dashboards using exact token notifications from the app-server.</li>
<li>Normalized cost is clearly labeled as an API-price estimate, not literal ChatGPT subscription billing.</li>
<li>The screen compiler returns a structured filter payload plus rationale and unhandled clauses, then lets the user apply the compiled filters instead of silently mutating the terminal state.</li>
<li>The browser build stays safe by exposing an unavailable state through the provider and disabling desktop-only actions outside Electron.</li>
</ul>
</div>
<div>
<h2>Expected Impact for End-Users</h2>
<ul>
<li>Desktop users can log into their own ChatGPT-backed Codex account from Islandflow Settings without sharing a subscription across users.</li>
<li>Users can see plan type, model defaults, reasoning controls, rate-limit windows, recent AI turns, and token usage from one place.</li>
<li>Selected smart-money events now have one-click explain, counter-thesis, burst summary, and watchlist synthesis actions directly inside the investigation drawer.</li>
<li>Replay sessions can produce a structured postmortem from the exact slice currently on screen.</li>
<li>Tape users can write a natural-language screen and translate it into the apps existing filter model where possible.</li>
<li>Web-only sessions degrade cleanly instead of exposing broken or misleading AI controls.</li>
</ul>
</div>
</section>
<section>
<h2>Relevant Diff Snippets</h2>
<p class="meta">
These snippets are formatted as unified patch strings so they can be consumed by Diffs
<code>parsePatchFiles</code> or <code>PatchDiff</code> flow from the official docs:
<a href="https://diffs.com/docs">https://diffs.com/docs</a>.
</p>
<div class="diff-title">Electron main process: preload, desktop AI service, and guarded IPC</div>
<pre class="diff"><code>diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts
@@
-import { app, BrowserWindow, shell } from "electron";
+import { app, BrowserWindow, ipcMain, shell } from "electron";
+import type { Event as ElectronEvent, IpcMainInvokeEvent } from "electron";
+import { fileURLToPath } from "node:url";
+import { IslandflowDesktopAiService } from "./desktop-ai.js";
+import {
+ DESKTOP_AI_CANCEL_LOGIN,
+ DESKTOP_AI_GET_STATE,
+ DESKTOP_AI_LOGIN_BROWSER,
+ DESKTOP_AI_LOGIN_DEVICE,
+ DESKTOP_AI_LOGOUT,
+ DESKTOP_AI_RUN_TASK,
+ DESKTOP_AI_STATE_CHANNEL,
+ DESKTOP_AI_UPDATE_PREFERENCES
+} from "./desktop-ai-ipc.js";
@@
+const PRELOAD_PATH = fileURLToPath(new URL("./preload.js", import.meta.url));
@@
+const registerDesktopAiIpc = (service: IslandflowDesktopAiService): void =&gt; {
+ const guard = (event: IpcMainInvokeEvent): void =&gt; {
+ const senderUrl = event.senderFrame?.url || event.sender.getURL();
+ if (!isTrustedAppUrl(senderUrl)) {
+ throw new Error(`Rejected desktop AI IPC from untrusted origin: ${senderUrl || "unknown"}`);
+ }
+ };
+
+ ipcMain.handle(DESKTOP_AI_GET_STATE, async (event) =&gt; {
+ guard(event);
+ await service.start();
+ return service.getState();
+ });
+ // login, logout, preference, and task handlers follow the same guard
+};</code></pre>
<div class="diff-title" style="margin-top: 18px">Renderer shell: settings route, topbar entrypoint, and in-context copilot surfaces</div>
<pre class="diff"><code>diff --git a/apps/web/app/terminal.tsx b/apps/web/app/terminal.tsx
@@
+import { useDesktopAi } from "./desktop-ai";
+import {
+ DesktopAiSettingsRoute,
+ ReplayCopilotPanel,
+ ScreenCompilerPanel,
+ SmartMoneyCopilotPanel
+} from "./desktop-ai-panels";
@@
+export const NAV_ITEMS = [
+ { href: "/", label: "Home" },
+ { href: "/tape", label: "Tape" },
+ { href: "/signals", label: "Signals" },
+ { href: "/charts", label: "Charts" },
+ { href: "/replay", label: "Replay" },
+ { href: "/settings", label: "Settings" }
+];
@@
+<ScreenCompilerPanel currentFilters={state.flowFilters} onApplyFilters={state.setFlowFilters} />
@@
+<ReplayCopilotPanel
+ ticker={replayContext.ticker}
+ flowFilters={replayContext.flowFilters}
+ alerts={replayContext.alerts}
+ smartMoneyEvents={replayContext.smartMoneyEvents}
+ classifierHits={replayContext.classifierHits}
+ flowPackets={replayContext.flowPackets}
+ optionPrints={replayContext.optionPrints}
+/>
@@
+<SmartMoneyCopilotPanel
+ event={event}
+ flowPacket={flowPacket}
+ evidencePrints={evidencePrints.map((item) =&gt; item.print)}
+ relatedPackets={relatedPackets}
+/></code></pre>
<div class="diff-title" style="margin-top: 18px">Shared desktop AI contract: auth state, rate limits, usage, and structured tasks</div>
<pre class="diff"><code>diff --git a/packages/types/src/desktop-ai.ts b/packages/types/src/desktop-ai.ts
+export const IslandflowAiTaskRequestSchema = z.discriminatedUnion("kind", [
+ z.object({ kind: z.literal("smart-money-explain"), context: IslandflowAiSmartMoneyContextSchema }),
+ z.object({ kind: z.literal("smart-money-skeptic"), context: IslandflowAiSmartMoneyContextSchema }),
+ z.object({ kind: z.literal("smart-money-burst-summary"), context: IslandflowAiSmartMoneyContextSchema }),
+ z.object({ kind: z.literal("watchlist-synthesis"), context: IslandflowAiSmartMoneyContextSchema }),
+ z.object({ kind: z.literal("replay-postmortem"), context: IslandflowAiReplayContextSchema }),
+ z.object({ kind: z.literal("screen-compile"), context: IslandflowAiScreenCompileContextSchema })
+]);
+
+export type IslandflowAiState = {
+ desktopAvailable: boolean;
+ transportStatus: IslandflowAiTransportStatus;
+ transportError: string | null;
+ profiles: IslandflowAiProfileSlot[];
+ account: IslandflowAiAccountState;
+ preferences: IslandflowAiPreferences;
+ models: IslandflowAiModelSummary[];
+ rateLimitsByLimitId: Record&lt;string, IslandflowAiRateLimitSnapshot&gt;;
+ usage: IslandflowAiUsageDashboard;
+ tasks: IslandflowAiTaskSnapshot[];
+ updatedAt: number;
+};</code></pre>
<p class="diff-note">
The document does not embed the Diffs runtime directly, but the snippets above are already prepared in the
patch-string format that Diffs documents for <code>PatchDiff</code>.
</p>
</section>
<section>
<h2>Validation</h2>
<div class="validation-grid">
<div class="validation-card good">
<h3>Desktop typecheck</h3>
<p><code>bun --cwd=apps/desktop run typecheck</code></p>
</div>
<div class="validation-card good">
<h3>Desktop tests</h3>
<p><code>bun --cwd=apps/desktop run test</code></p>
</div>
<div class="validation-card good">
<h3>Shared types</h3>
<p><code>bun x tsc -p packages/types/tsconfig.json --noEmit</code></p>
</div>
<div class="validation-card good">
<h3>Web tests</h3>
<p><code>bun test apps/web/app/terminal.test.ts apps/web/app/routes.test.ts</code></p>
</div>
<div class="validation-card good">
<h3>Web production build</h3>
<p><code>bun --cwd=apps/web run build</code></p>
</div>
<div class="validation-card warn">
<h3>Manual desktop runtime</h3>
<p>No end-to-end interactive Electron sign-in was executed in this turn. The bridge, auth flows, and renderer integration were validated through type checks, unit tests, and the production web build.</p>
</div>
</div>
</section>
<section>
<h2>Issues, Limitations, and Mitigations</h2>
<ul>
<li>Workspace provider and API-key profile support are intentionally left as reserved slots behind the same abstraction, not shipped as active shared-subscription behavior.</li>
<li>The desktop bridge launches an ephemeral Codex thread per analysis task, which is safer for v1 but means there is not yet a long-lived conversational analyst thread.</li>
<li>The screen compiler only applies filters that map onto todays <code>OptionFlowFilters</code> model, and it explicitly returns unhandled clauses rather than pretending to support unsupported logic.</li>
<li>The recent task and usage dashboards depend on app-server notifications. When those notifications do not fire, the UI stays safe and honest rather than synthesizing made-up counters.</li>
<li>Renderer interactions were validated in build and unit test contexts, but not with a live packaged desktop binary in this turn.</li>
</ul>
</section>
<section>
<h2>Follow-up Work</h2>
<ul>
<li>Add an automated Electron integration test harness that exercises browser login completion, device-code completion, logout, and recovery after app-server restart.</li>
<li>Promote the reserved workspace-provider slot into a real enterprise or API-key-backed profile once the product decision is ready.</li>
<li>Persist richer per-task provenance so replay postmortems and smart-money analyses can be reopened with their original structured context, not only their output text.</li>
<li>Consider a dedicated Copilot activity log or side rail once users accumulate enough analyses that the compact recent-task list becomes too shallow.</li>
</ul>
</section>
</div>
</main>
</body>
</html>

View file

@ -0,0 +1,412 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fix historical alert flow packet persistence in the web terminal</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500;600&family=IBM+Plex+Sans:wght@400;500;600&family=Quantico:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
color-scheme: dark;
--bg: #06080b;
--bg-2: #0b1016;
--panel: rgba(17, 24, 32, 0.9);
--panel-2: rgba(13, 20, 27, 0.92);
--line: rgba(255, 255, 255, 0.1);
--text: #e6edf4;
--muted: #90a0b2;
--faint: #6e7b8c;
--amber: #f5a623;
--amber-soft: rgba(245, 166, 35, 0.14);
--blue: #4da3ff;
--green: #25c17a;
--code: #0a0f14;
--shadow: 0 24px 70px rgba(0, 0, 0, 0.45);
--radius: 14px;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at top left, rgba(245, 166, 35, 0.14), transparent 32rem),
radial-gradient(circle at top right, rgba(77, 163, 255, 0.12), transparent 26rem),
linear-gradient(180deg, var(--bg) 0%, #081017 42%, #05080c 100%);
color: var(--text);
font-family: "IBM Plex Sans", system-ui, sans-serif;
line-height: 1.6;
}
main {
width: min(1080px, calc(100% - 32px));
margin: 0 auto;
padding: 40px 0 64px;
}
header {
padding: 28px;
border: 1px solid var(--line);
border-radius: calc(var(--radius) + 2px);
background:
linear-gradient(180deg, rgba(17, 24, 32, 0.96), rgba(11, 16, 22, 0.94));
box-shadow: var(--shadow);
}
.eyebrow,
h2,
.meta-chip,
.diff-title {
font-family: "IBM Plex Mono", monospace;
}
.eyebrow {
margin: 0 0 12px;
color: var(--amber);
font-size: 0.76rem;
letter-spacing: 0.14em;
text-transform: uppercase;
}
h1 {
margin: 0;
max-width: 14ch;
font-family: "Quantico", sans-serif;
font-size: clamp(2.2rem, 4vw, 4.4rem);
line-height: 1.02;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.summary {
max-width: 72ch;
margin: 18px 0 0;
color: var(--muted);
font-size: 1.02rem;
}
.meta-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 18px;
}
.meta-chip {
padding: 6px 10px;
border: 1px solid rgba(77, 163, 255, 0.24);
border-radius: 999px;
background: rgba(77, 163, 255, 0.09);
color: var(--text);
font-size: 0.74rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.grid {
display: grid;
gap: 18px;
margin-top: 20px;
}
section {
padding: 22px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: linear-gradient(180deg, var(--panel), var(--panel-2));
}
h2 {
margin: 0 0 12px;
color: var(--amber);
font-size: 0.84rem;
letter-spacing: 0.12em;
text-transform: uppercase;
}
p,
li {
max-width: 76ch;
}
ul {
margin: 0;
padding-left: 1.15rem;
}
li + li {
margin-top: 8px;
}
code,
pre {
font-family: "IBM Plex Mono", monospace;
}
code {
color: #ffd596;
}
.callout {
padding: 14px 16px;
border: 1px solid rgba(245, 166, 35, 0.18);
border-radius: 12px;
background: var(--amber-soft);
color: var(--text);
}
.diff-grid {
display: grid;
gap: 18px;
}
.diff-shell {
border: 1px solid var(--line);
border-radius: 12px;
overflow: hidden;
background: rgba(8, 12, 17, 0.92);
}
.diff-title {
margin: 0;
padding: 12px 14px;
border-bottom: 1px solid var(--line);
color: var(--text);
font-size: 0.76rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.diff-view {
min-height: 84px;
}
.diff-fallback {
margin: 0;
padding: 16px;
overflow-x: auto;
background: var(--code);
color: var(--text);
font-size: 0.86rem;
line-height: 1.5;
}
.diff-shell.rendered .diff-fallback {
display: none;
}
.note {
margin-top: 12px;
color: var(--faint);
font-size: 0.9rem;
}
a {
color: #8bc1ff;
}
@media (max-width: 720px) {
main {
width: min(100%, calc(100% - 20px));
padding: 18px 0 28px;
}
header,
section {
padding: 18px;
}
h1 {
max-width: none;
font-size: 2.1rem;
}
}
</style>
</head>
<body>
<main>
<header>
<p class="eyebrow">Turn Document · 2026-05-20 02:56 EDT</p>
<h1>Historical Alert Flow Packets Persist Again</h1>
<p class="summary">Alert detail drawers now resolve persisted flow packets from ClickHouse-backed historical context instead of assuming the first evidence reference is the packet. This restores packet visibility for replayed and older alerts after their Redis hot-cache entries have aged out.</p>
<div class="meta-row">
<span class="meta-chip">Beads: islandflow-yza</span>
<span class="meta-chip">Surface: apps/web terminal</span>
<span class="meta-chip">Validation: tests + prod build</span>
</div>
</header>
<div class="grid">
<section>
<h2>Summary</h2>
<p>The web terminal was assuming <code>alert.evidence_refs[0]</code> always pointed at a flow packet. For compute-generated alerts, the first evidence ref is often the smart-money event id, with the actual packet id later in the list. That made persisted historical packets look missing even when ClickHouse context had already hydrated them successfully.</p>
</section>
<section>
<h2>Changes Made</h2>
<ul>
<li>Added shared alert helpers in <code>apps/web/app/terminal.tsx</code> to extract all flow-packet refs from an alert and resolve the first hydrated packet semantically.</li>
<li>Switched the alert drawer's selected packet lookup to use the shared resolver instead of the first evidence ref.</li>
<li>Updated alert-underlying inference, visible-alert prefetch, pinned-flow retention keys, and classifier-hit-to-alert matching to use the same alert packet semantics.</li>
<li>Added focused regression coverage in <code>apps/web/app/terminal.test.ts</code> for alerts whose packet ref is not the first evidence entry.</li>
</ul>
</section>
<section>
<h2>Context</h2>
<p>Islandflow alert detail views combine live Redis retention with ClickHouse historical hydration. Once a packet leaves the hot cache, the UI must treat ClickHouse-loaded evidence as first-class persisted context, not as a degraded fallback. The bug was in the web clients interpretation of alert evidence ordering, not in the persistence of the packet itself.</p>
<div class="callout">
Historical packet context was already present. The terminal simply was not selecting it unless the packet id happened to be the first evidence ref.
</div>
</section>
<section>
<h2>Important Implementation Details</h2>
<ul>
<li>The fix is backward-compatible with already-persisted alerts because it tolerates existing evidence ordering instead of rewriting stored records.</li>
<li>The shared resolver centralizes the packet-selection rule so replay, pinning, and alert navigation do not drift apart again.</li>
<li>The classifier-hit alert matching path now finds alerts by any embedded packet ref, which improves consistency when opening related alert context from signal panes.</li>
</ul>
</section>
<section>
<h2>Relevant Diff Snippets</h2>
<div class="diff-grid">
<div class="diff-shell" id="diff-shell-1">
<p class="diff-title">apps/web/app/terminal.tsx · alert packet resolution</p>
<div class="diff-view" id="diff-1"></div>
<pre class="diff-fallback"><code>-const packetId = selectedAlert.evidence_refs[0];
-return packetId ? resolvedFlowPacketMap.get(packetId) ?? null : null;
+return resolveAlertFlowPacket(selectedAlert, resolvedFlowPacketMap);</code></pre>
</div>
<div class="diff-shell" id="diff-shell-2">
<p class="diff-title">apps/web/app/terminal.tsx · prefetch and alert matching</p>
<div class="diff-view" id="diff-2"></div>
<pre class="diff-fallback"><code>-const visiblePacketIds = visibleAlerts
- .map((alert) =&gt; alert.evidence_refs[0] ?? null)
- .filter((id): id is string =&gt; Boolean(id) &amp;&amp; id.startsWith("flowpacket:"));
+const visiblePacketIds = visibleAlerts.flatMap((alert) =&gt; getAlertFlowPacketRefs(alert));
-alertsFeed.items.find((item) =&gt; item.trace_id === desiredTrace || item.evidence_refs[0] === packetId)
+alertsFeed.items.find(
+ (item) =&gt; item.trace_id === desiredTrace || getAlertFlowPacketRefs(item).includes(packetId)
+)</code></pre>
</div>
</div>
<p class="note">These snippets are rendered client-side with Diffs using the same old/new code blocks shown in the fallback text if the library cannot load.</p>
</section>
<section>
<h2>Expected Impact for End-Users</h2>
<p>Older or replayed alerts should now show their persisted flow packet summary in the detail drawer even after the Redis hot cache no longer has that packet. Users investigating signal history should keep the same evidence continuity they get from live data: packet summary, print context, and related alert linkage stay intact.</p>
</section>
<section>
<h2>Validation</h2>
<ul>
<li><code>bun test apps/web/app/terminal.test.ts</code> passed with 72 tests.</li>
<li><code>bun --cwd=apps/web run build</code> passed on Next.js 16.2.6.</li>
<li>The new tests specifically cover alerts where a smart-money event id precedes the packet id in <code>evidence_refs</code>.</li>
</ul>
</section>
<section>
<h2>Issues, Limitations, and Mitigations</h2>
<ul>
<li>This change does not alter how compute persists alert evidence ordering. Instead, it makes the terminal resilient to existing and future mixed evidence lists.</li>
<li>The Diffs rendering in this document loads from the published package at view time. A plain-text fallback is included directly in the HTML so the document remains readable offline.</li>
<li>No full monorepo test sweep was run because the change was isolated to the web terminal alert-context path.</li>
</ul>
</section>
<section>
<h2>Follow-up Work</h2>
<ul>
<li>No additional Beads issue was required for this fix.</li>
<li>Optional: audit whether compute should emit packet ids before higher-level event ids in <code>evidence_refs</code> for simpler downstream consumers.</li>
<li>Optional: add a small integration test around alert drawer selection if the web app gains component-level interaction tests later.</li>
</ul>
</section>
</div>
</main>
<script type="module">
const snippets = [
{
shellId: "diff-shell-1",
containerId: "diff-1",
name: "apps/web/app/terminal.tsx",
oldContents: `const selectedFlowPacket = useMemo(() => {
if (!selectedAlert) {
return null;
}
const packetId = selectedAlert.evidence_refs[0];
return packetId ? resolvedFlowPacketMap.get(packetId) ?? null : null;
}, [selectedAlert, resolvedFlowPacketMap]);`,
newContents: `const selectedFlowPacket = useMemo(() => {
if (!selectedAlert) {
return null;
}
return resolveAlertFlowPacket(selectedAlert, resolvedFlowPacketMap);
}, [selectedAlert, resolvedFlowPacketMap]);`
},
{
shellId: "diff-shell-2",
containerId: "diff-2",
name: "apps/web/app/terminal.tsx",
oldContents: `const visiblePacketIds = visibleAlerts
.map((alert) => alert.evidence_refs[0] ?? null)
.filter((id): id is string => Boolean(id) && id.startsWith("flowpacket:"));
alertsFeed.items.find(
(item) => item.trace_id === desiredTrace || item.evidence_refs[0] === packetId
) ?? null;`,
newContents: `const visiblePacketIds = visibleAlerts.flatMap((alert) => getAlertFlowPacketRefs(alert));
alertsFeed.items.find(
(item) => item.trace_id === desiredTrace || getAlertFlowPacketRefs(item).includes(packetId)
) ?? null;`
}
];
try {
const { FileDiff } = await import("https://esm.sh/@pierre/diffs");
for (const snippet of snippets) {
const container = document.getElementById(snippet.containerId);
const shell = document.getElementById(snippet.shellId);
if (!container || !shell) {
continue;
}
const instance = new FileDiff({
theme: { dark: "pierre-dark", light: "pierre-light" },
diffStyle: "split"
});
instance.render({
oldFile: {
name: snippet.name,
contents: snippet.oldContents
},
newFile: {
name: snippet.name,
contents: snippet.newContents
},
containerWrapper: container
});
shell.classList.add("rendered");
}
} catch (error) {
console.warn("Failed to render diff snippets with Diffs.", error);
}
</script>
</body>
</html>

View file

@ -9,7 +9,9 @@ import {
type StreamUpdateConfig, type StreamUpdateConfig,
JSONCodec, JSONCodec,
type JsMsg, type JsMsg,
createInbox createInbox,
nanos,
millis
} from "nats"; } from "nats";
import { getKnownStreamDefinitions, getStreamDefinition, type StreamRetentionClass } from "./streams"; import { getKnownStreamDefinitions, getStreamDefinition, type StreamRetentionClass } from "./streams";
@ -164,13 +166,13 @@ export const resolveStreamRetention = (
): Pick<StreamConfig, "max_bytes" | "max_age"> => { ): Pick<StreamConfig, "max_bytes" | "max_age"> => {
if (streamClass === "raw") { if (streamClass === "raw") {
return { return {
max_age: parseBoundedNumber(env.STREAM_RAW_MAX_AGE_MS, 3_600_000), max_age: nanos(parseBoundedNumber(env.STREAM_RAW_MAX_AGE_MS, 3_600_000)),
max_bytes: parseBoundedNumber(env.STREAM_RAW_MAX_BYTES, 536_870_912) max_bytes: parseBoundedNumber(env.STREAM_RAW_MAX_BYTES, 536_870_912)
}; };
} }
return { return {
max_age: parseBoundedNumber(env.STREAM_DERIVED_MAX_AGE_MS, 43_200_000), max_age: nanos(parseBoundedNumber(env.STREAM_DERIVED_MAX_AGE_MS, 43_200_000)),
max_bytes: parseBoundedNumber(env.STREAM_DERIVED_MAX_BYTES, 268_435_456) max_bytes: parseBoundedNumber(env.STREAM_DERIVED_MAX_BYTES, 268_435_456)
}; };
}; };
@ -417,7 +419,7 @@ const formatBytes = (value: number): string => {
}; };
const formatRetentionSummary = (config: StreamConfig): string => { const formatRetentionSummary = (config: StreamConfig): string => {
return `age=${formatDurationMs(Number(config.max_age))} bytes=${formatBytes(config.max_bytes)} replicas=${config.num_replicas} retention=${config.retention} discard=${config.discard}`; return `age=${formatDurationMs(millis(Number(config.max_age)))} bytes=${formatBytes(config.max_bytes)} replicas=${config.num_replicas} retention=${config.retention} discard=${config.discard}`;
}; };
const formatReportLine = ( const formatReportLine = (
@ -442,12 +444,12 @@ const formatReportLine = (
const details = report.retentionDrift const details = report.retentionDrift
.map((delta) => { .map((delta) => {
const desiredValue = delta.field === "max_age" const desiredValue = delta.field === "max_age"
? formatDurationMs(Number(delta.desired)) ? formatDurationMs(millis(Number(delta.desired)))
: delta.field === "max_bytes" : delta.field === "max_bytes"
? formatBytes(Number(delta.desired)) ? formatBytes(Number(delta.desired))
: formatStructuredValue(delta.desired); : formatStructuredValue(delta.desired);
const currentValue = delta.field === "max_age" const currentValue = delta.field === "max_age"
? formatDurationMs(Number(delta.current)) ? formatDurationMs(millis(Number(delta.current)))
: delta.field === "max_bytes" : delta.field === "max_bytes"
? formatBytes(Number(delta.current)) ? formatBytes(Number(delta.current))
: formatStructuredValue(delta.current); : formatStructuredValue(delta.current);

View file

@ -7,6 +7,7 @@ import {
STREAM_EQUITY_QUOTES, STREAM_EQUITY_QUOTES,
STREAM_FLOW_PACKETS, STREAM_FLOW_PACKETS,
STREAM_INFERRED_DARK, STREAM_INFERRED_DARK,
STREAM_NEWS,
STREAM_OPTION_NBBO, STREAM_OPTION_NBBO,
STREAM_OPTION_PRINTS, STREAM_OPTION_PRINTS,
STREAM_OPTION_SIGNAL_PRINTS, STREAM_OPTION_SIGNAL_PRINTS,
@ -19,6 +20,7 @@ import {
SUBJECT_EQUITY_QUOTES, SUBJECT_EQUITY_QUOTES,
SUBJECT_FLOW_PACKETS, SUBJECT_FLOW_PACKETS,
SUBJECT_INFERRED_DARK, SUBJECT_INFERRED_DARK,
SUBJECT_NEWS,
SUBJECT_OPTION_NBBO, SUBJECT_OPTION_NBBO,
SUBJECT_OPTION_PRINTS, SUBJECT_OPTION_PRINTS,
SUBJECT_OPTION_SIGNAL_PRINTS, SUBJECT_OPTION_SIGNAL_PRINTS,
@ -53,7 +55,8 @@ export const STREAM_CATALOG: readonly KnownStreamDefinition[] = [
retentionClass: "derived" retentionClass: "derived"
}, },
{ name: STREAM_CLASSIFIER_HITS, subject: SUBJECT_CLASSIFIER_HITS, retentionClass: "derived" }, { name: STREAM_CLASSIFIER_HITS, subject: SUBJECT_CLASSIFIER_HITS, retentionClass: "derived" },
{ name: STREAM_ALERTS, subject: SUBJECT_ALERTS, retentionClass: "derived" } { name: STREAM_ALERTS, subject: SUBJECT_ALERTS, retentionClass: "derived" },
{ name: STREAM_NEWS, subject: SUBJECT_NEWS, retentionClass: "derived" }
]; ];
const STREAM_CATALOG_BY_NAME = new Map(STREAM_CATALOG.map((definition) => [definition.name, definition])); const STREAM_CATALOG_BY_NAME = new Map(STREAM_CATALOG.map((definition) => [definition.name, definition]));

View file

@ -22,3 +22,5 @@ export const STREAM_CLASSIFIER_HITS = "CLASSIFIER_HITS";
export const SUBJECT_CLASSIFIER_HITS = "flow.classifier_hits"; export const SUBJECT_CLASSIFIER_HITS = "flow.classifier_hits";
export const STREAM_ALERTS = "ALERTS"; export const STREAM_ALERTS = "ALERTS";
export const SUBJECT_ALERTS = "flow.alerts"; export const SUBJECT_ALERTS = "flow.alerts";
export const STREAM_NEWS = "NEWS";
export const SUBJECT_NEWS = "flow.news";

View file

@ -1,5 +1,5 @@
import { describe, expect, it } from "bun:test"; import { describe, expect, it } from "bun:test";
import type { JetStreamManager, StreamConfig } from "nats"; import { nanos, type JetStreamManager, type StreamConfig } from "nats";
import { import {
auditStreamConfig, auditStreamConfig,
buildKnownStreamConfig, buildKnownStreamConfig,
@ -52,14 +52,14 @@ const buildAllKnownConfigs = (env: Record<string, string | undefined> = {}) => {
describe("jetstream retention defaults", () => { describe("jetstream retention defaults", () => {
it("resolves raw defaults to 60m and 512 MiB", () => { it("resolves raw defaults to 60m and 512 MiB", () => {
expect(resolveStreamRetention("raw")).toEqual({ expect(resolveStreamRetention("raw")).toEqual({
max_age: 3_600_000, max_age: nanos(3_600_000),
max_bytes: 536_870_912 max_bytes: 536_870_912
}); });
}); });
it("resolves derived defaults to 12h and 256 MiB", () => { it("resolves derived defaults to 12h and 256 MiB", () => {
expect(resolveStreamRetention("derived")).toEqual({ expect(resolveStreamRetention("derived")).toEqual({
max_age: 43_200_000, max_age: nanos(43_200_000),
max_bytes: 268_435_456 max_bytes: 268_435_456
}); });
}); });
@ -71,7 +71,7 @@ describe("jetstream retention defaults", () => {
STREAM_RAW_MAX_BYTES: "5678" STREAM_RAW_MAX_BYTES: "5678"
}) })
).toEqual({ ).toEqual({
max_age: 1234, max_age: nanos(1234),
max_bytes: 5678 max_bytes: 5678
}); });
}); });

View file

@ -0,0 +1,76 @@
export type AlpacaCredentials = {
keyId: string;
secret: string;
legacyToken: string;
usesLegacyBearer: boolean;
};
type AlpacaCredentialEnv = {
ALPACA_API_KEY?: string;
ALPACA_API_KEY_ID?: string;
ALPACA_KEY_ID?: string;
ALPACA_API_SECRET_KEY?: string;
ALPACA_SECRET_KEY?: string;
};
const normalize = (value: string | undefined): string => value?.trim() ?? "";
export const resolveAlpacaCredentials = (
env: AlpacaCredentialEnv
): AlpacaCredentials => {
const legacyToken = normalize(env.ALPACA_API_KEY);
const explicitKeyId =
normalize(env.ALPACA_API_KEY_ID) || normalize(env.ALPACA_KEY_ID);
const secret =
normalize(env.ALPACA_API_SECRET_KEY) || normalize(env.ALPACA_SECRET_KEY);
const keyId = explicitKeyId || legacyToken;
const usesLegacyBearer = !explicitKeyId && !secret && legacyToken.length > 0;
return {
keyId,
secret,
legacyToken,
usesLegacyBearer
};
};
export const hasAlpacaCredentials = (credentials: AlpacaCredentials): boolean => {
if (credentials.usesLegacyBearer) {
return credentials.legacyToken.length > 0;
}
return credentials.keyId.length > 0 && credentials.secret.length > 0;
};
export const buildAlpacaAuthHeaders = (
credentials: AlpacaCredentials
): Record<string, string> => {
if (credentials.usesLegacyBearer) {
return {
Authorization: `Bearer ${credentials.legacyToken}`
};
}
return {
"APCA-API-KEY-ID": credentials.keyId,
"APCA-API-SECRET-KEY": credentials.secret
};
};
export const buildAlpacaWebSocketAuthMessage = (
credentials: AlpacaCredentials
): { action: "auth"; key: string; secret: string } => {
if (credentials.usesLegacyBearer) {
return {
action: "auth",
key: credentials.legacyToken,
secret: ""
};
}
return {
action: "auth",
key: credentials.keyId,
secret: credentials.secret
};
};

View file

@ -1 +1,2 @@
export * from "./env"; export * from "./env";
export * from "./alpaca";

View file

@ -0,0 +1,65 @@
import { describe, expect, it } from "bun:test";
import {
buildAlpacaAuthHeaders,
buildAlpacaWebSocketAuthMessage,
hasAlpacaCredentials,
resolveAlpacaCredentials
} from "../src/alpaca";
describe("resolveAlpacaCredentials", () => {
it("prefers explicit key-id and secret vars", () => {
const credentials = resolveAlpacaCredentials({
ALPACA_API_KEY: "legacy-token",
ALPACA_API_KEY_ID: "key-id",
ALPACA_API_SECRET_KEY: "secret"
});
expect(credentials).toEqual({
keyId: "key-id",
secret: "secret",
legacyToken: "legacy-token",
usesLegacyBearer: false
});
expect(hasAlpacaCredentials(credentials)).toBe(true);
expect(buildAlpacaAuthHeaders(credentials)).toEqual({
"APCA-API-KEY-ID": "key-id",
"APCA-API-SECRET-KEY": "secret"
});
expect(buildAlpacaWebSocketAuthMessage(credentials)).toEqual({
action: "auth",
key: "key-id",
secret: "secret"
});
});
it("supports the older bearer-token fallback when no secret exists", () => {
const credentials = resolveAlpacaCredentials({
ALPACA_API_KEY: "legacy-token"
});
expect(credentials.usesLegacyBearer).toBe(true);
expect(hasAlpacaCredentials(credentials)).toBe(true);
expect(buildAlpacaAuthHeaders(credentials)).toEqual({
Authorization: "Bearer legacy-token"
});
expect(buildAlpacaWebSocketAuthMessage(credentials)).toEqual({
action: "auth",
key: "legacy-token",
secret: ""
});
});
it("supports alternate secret env names", () => {
const credentials = resolveAlpacaCredentials({
ALPACA_KEY_ID: "short-key",
ALPACA_SECRET_KEY: "short-secret"
});
expect(credentials).toEqual({
keyId: "short-key",
secret: "short-secret",
legacyToken: "",
usesLegacyBearer: false
});
});
});

View file

@ -7,6 +7,7 @@ import {
EquityPrintJoinSchema, EquityPrintJoinSchema,
InferredDarkEventSchema, InferredDarkEventSchema,
FlowPacketSchema, FlowPacketSchema,
NewsStorySchema,
OptionNBBOSchema, OptionNBBOSchema,
OptionPrintSchema, OptionPrintSchema,
SmartMoneyEventSchema SmartMoneyEventSchema
@ -20,6 +21,7 @@ import type {
EquityPrintJoin, EquityPrintJoin,
InferredDarkEvent, InferredDarkEvent,
FlowPacket, FlowPacket,
NewsStory,
SmartMoneyEvent, SmartMoneyEvent,
OptionNBBO, OptionNBBO,
OptionPrint, OptionPrint,
@ -91,6 +93,13 @@ import {
toSmartMoneyEventRecord, toSmartMoneyEventRecord,
type SmartMoneyEventRecord type SmartMoneyEventRecord
} from "./smart-money-events"; } from "./smart-money-events";
import {
NEWS_TABLE,
newsTableDDL,
fromNewsRecord,
toNewsRecord,
type NewsRecord
} from "./news";
export type ClickHouseOptions = { export type ClickHouseOptions = {
url: string; url: string;
@ -320,6 +329,12 @@ export const ensureAlertsTable = async (client: ClickHouseClient): Promise<void>
} }
}; };
export const ensureNewsTable = async (client: ClickHouseClient): Promise<void> => {
await client.exec({
query: newsTableDDL()
});
};
export const insertOptionPrint = async ( export const insertOptionPrint = async (
client: ClickHouseClient, client: ClickHouseClient,
print: OptionPrint print: OptionPrint
@ -449,6 +464,15 @@ export const insertAlert = async (client: ClickHouseClient, alert: AlertEvent):
}); });
}; };
export const insertNewsStory = async (client: ClickHouseClient, story: NewsStory): Promise<void> => {
const record = toNewsRecord(story);
await client.insert({
table: NEWS_TABLE,
values: [record],
format: "JSONEachRow"
});
};
export type ClickHouseBatchWriterOptions = { export type ClickHouseBatchWriterOptions = {
flushIntervalMs?: number; flushIntervalMs?: number;
maxRows?: number; maxRows?: number;
@ -600,6 +624,13 @@ export const enqueueAlertInsert = (
writer.enqueue(ALERTS_TABLE, toAlertRecord(alert)); writer.enqueue(ALERTS_TABLE, toAlertRecord(alert));
}; };
export const enqueueNewsStoryInsert = (
writer: ClickHouseBatchWriter,
story: NewsStory
): void => {
writer.enqueue(NEWS_TABLE, toNewsRecord(story));
};
const clampLimit = (limit: number): number => { const clampLimit = (limit: number): number => {
if (!Number.isFinite(limit)) { if (!Number.isFinite(limit)) {
return 100; return 100;
@ -1016,6 +1047,32 @@ const normalizeAlertRow = (row: unknown): AlertRecord | null => {
}; };
}; };
const normalizeNewsRow = (row: unknown): NewsRecord | null => {
if (!row || typeof row !== "object") {
return null;
}
const record = row as Record<string, unknown>;
return {
source_ts: coerceNumber(record.source_ts) as number,
ingest_ts: coerceNumber(record.ingest_ts) as number,
seq: coerceNumber(record.seq) as number,
trace_id: String(record.trace_id ?? ""),
story_id: coerceNumber(record.story_id) as number,
provider: String(record.provider ?? ""),
source: String(record.source ?? ""),
headline: String(record.headline ?? ""),
summary: String(record.summary ?? ""),
content_html: String(record.content_html ?? ""),
url: String(record.url ?? ""),
published_ts: coerceNumber(record.published_ts) as number,
updated_ts: coerceNumber(record.updated_ts) as number,
provider_symbols_json: String(record.provider_symbols_json ?? "[]"),
resolved_symbols_json: String(record.resolved_symbols_json ?? "[]"),
symbol_resolution: String(record.symbol_resolution ?? "none") as NewsRecord["symbol_resolution"]
};
};
export const fetchRecentOptionPrints = async ( export const fetchRecentOptionPrints = async (
client: ClickHouseClient, client: ClickHouseClient,
limit: number, limit: number,
@ -1207,6 +1264,50 @@ export const fetchRecentAlerts = async (
return AlertEventSchema.array().parse(alerts); return AlertEventSchema.array().parse(alerts);
}; };
const latestNewsSelect = `
SELECT
source_ts,
ingest_ts,
seq,
trace_id,
story_id,
provider,
source,
headline,
summary,
content_html,
url,
published_ts,
updated_ts,
provider_symbols_json,
resolved_symbols_json,
symbol_resolution
FROM (
SELECT
*,
row_number() OVER (PARTITION BY provider, story_id ORDER BY updated_ts DESC, ingest_ts DESC, seq DESC) AS revision_rank
FROM ${NEWS_TABLE}
)
WHERE revision_rank = 1
`;
export const fetchRecentNews = async (
client: ClickHouseClient,
limit: number
): Promise<NewsStory[]> => {
const safeLimit = clampLimit(limit);
const result = await client.query({
query: `${latestNewsSelect} ORDER BY published_ts DESC, story_id DESC LIMIT ${safeLimit}`,
format: "JSONEachRow"
});
const rows = await result.json<unknown[]>();
const records = rows
.map(normalizeNewsRow)
.filter((record): record is NewsRecord => record !== null);
return NewsStorySchema.array().parse(records.map(fromNewsRecord));
};
const normalizeAlertEvidenceRefs = (refs: string[]): string[] => { const normalizeAlertEvidenceRefs = (refs: string[]): string[] => {
return Array.from(new Set(refs.map((ref) => ref.trim()).filter(Boolean))); return Array.from(new Set(refs.map((ref) => ref.trim()).filter(Boolean)));
}; };
@ -1600,6 +1701,27 @@ export const fetchAlertsAfter = async (
return AlertEventSchema.array().parse(alerts); return AlertEventSchema.array().parse(alerts);
}; };
export const fetchNewsAfter = async (
client: ClickHouseClient,
afterTs: number,
afterSeq: number,
limit: number
): Promise<NewsStory[]> => {
const safeLimit = clampLimit(limit);
const safeAfterTs = clampCursor(afterTs);
const safeAfterSeq = clampCursor(afterSeq);
const result = await client.query({
query: `${latestNewsSelect} AND (published_ts, seq) > (${safeAfterTs}, ${safeAfterSeq}) ORDER BY published_ts ASC, seq ASC LIMIT ${safeLimit}`,
format: "JSONEachRow"
});
const rows = await result.json<unknown[]>();
const records = rows
.map(normalizeNewsRow)
.filter((record): record is NewsRecord => record !== null);
return NewsStorySchema.array().parse(records.map(fromNewsRecord));
};
export const fetchOptionPrintsBefore = async ( export const fetchOptionPrintsBefore = async (
client: ClickHouseClient, client: ClickHouseClient,
beforeTs: number, beforeTs: number,
@ -1778,6 +1900,25 @@ export const fetchAlertsBefore = async (
return AlertEventSchema.array().parse(records.map(fromAlertRecord)); return AlertEventSchema.array().parse(records.map(fromAlertRecord));
}; };
export const fetchNewsBefore = async (
client: ClickHouseClient,
beforeTs: number,
beforeSeq: number,
limit: number
): Promise<NewsStory[]> => {
const safeLimit = clampLimit(limit);
const result = await client.query({
query: `${latestNewsSelect} AND ${buildBeforeTupleCondition("published_ts", "seq", beforeTs, beforeSeq)} ORDER BY published_ts DESC, seq DESC LIMIT ${safeLimit}`,
format: "JSONEachRow"
});
const rows = await result.json<unknown[]>();
const records = rows
.map(normalizeNewsRow)
.filter((record): record is NewsRecord => record !== null);
return NewsStorySchema.array().parse(records.map(fromNewsRecord));
};
export const fetchInferredDarkBefore = async ( export const fetchInferredDarkBefore = async (
client: ClickHouseClient, client: ClickHouseClient,
beforeTs: number, beforeTs: number,

View file

@ -10,3 +10,4 @@ export * from "./equity-print-joins";
export * from "./inferred-dark"; export * from "./inferred-dark";
export * from "./option-prints"; export * from "./option-prints";
export * from "./option-nbbo"; export * from "./option-nbbo";
export * from "./news";

View file

@ -0,0 +1,102 @@
import type { NewsStory, NewsSymbolResolution } from "@islandflow/types";
export const NEWS_TABLE = "news";
export type NewsRecord = {
source_ts: number;
ingest_ts: number;
seq: number;
trace_id: string;
story_id: number;
provider: string;
source: string;
headline: string;
summary: string;
content_html: string;
url: string;
published_ts: number;
updated_ts: number;
provider_symbols_json: string;
resolved_symbols_json: string;
symbol_resolution: NewsSymbolResolution;
};
export const newsTableDDL = (): string => {
return `
CREATE TABLE IF NOT EXISTS ${NEWS_TABLE} (
source_ts UInt64,
ingest_ts UInt64,
seq UInt64,
trace_id String,
story_id UInt64,
provider String,
source String,
headline String,
summary String,
content_html String,
url String,
published_ts UInt64,
updated_ts UInt64,
provider_symbols_json String,
resolved_symbols_json String,
symbol_resolution String
)
ENGINE = ReplacingMergeTree(updated_ts)
ORDER BY (provider, story_id, updated_ts, seq)
`;
};
const safeStringArray = (value: string): string[] => {
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed.map((entry) => String(entry));
}
} catch {
// ignore
}
return [];
};
export const toNewsRecord = (story: NewsStory): NewsRecord => {
return {
source_ts: story.source_ts,
ingest_ts: story.ingest_ts,
seq: story.seq,
trace_id: story.trace_id,
story_id: story.story_id,
provider: story.provider,
source: story.source,
headline: story.headline,
summary: story.summary,
content_html: story.content_html,
url: story.url,
published_ts: story.published_ts,
updated_ts: story.updated_ts,
provider_symbols_json: JSON.stringify(story.provider_symbols),
resolved_symbols_json: JSON.stringify(story.resolved_symbols),
symbol_resolution: story.symbol_resolution
};
};
export const fromNewsRecord = (record: NewsRecord): NewsStory => {
return {
source_ts: record.source_ts,
ingest_ts: record.ingest_ts,
seq: record.seq,
trace_id: record.trace_id,
story_id: record.story_id,
provider: record.provider,
source: record.source,
headline: record.headline,
summary: record.summary,
content_html: record.content_html,
url: record.url,
published_ts: record.published_ts,
updated_ts: record.updated_ts,
provider_symbols: safeStringArray(record.provider_symbols_json),
resolved_symbols: safeStringArray(record.resolved_symbols_json),
symbol_resolution: record.symbol_resolution
};
};

View file

@ -0,0 +1,78 @@
import { describe, expect, it } from "bun:test";
import type { ClickHouseClient } from "../src/clickhouse";
import {
NEWS_TABLE,
fromNewsRecord,
newsTableDDL,
toNewsRecord
} from "../src/news";
import {
fetchNewsAfter,
fetchNewsBefore,
fetchRecentNews
} from "../src/clickhouse";
const makeClient = (resolver: (query: string) => unknown[]): ClickHouseClient =>
({
exec: async () => {},
insert: async () => {},
ping: async () => ({ success: true }),
close: async () => {},
query: async ({ query }: { query: string }) => ({
async json<T>() {
return resolver(query) as T;
}
})
}) as ClickHouseClient;
const story = {
source_ts: 100,
ingest_ts: 101,
seq: 3,
trace_id: "alpaca:77",
story_id: 77,
provider: "alpaca",
source: "Benzinga",
headline: "TSLA rises",
summary: "Summary",
content_html: "<p>TSLA rises</p>",
url: "https://example.com/story",
published_ts: 100,
updated_ts: 120,
provider_symbols: ["TSLA"],
resolved_symbols: ["TSLA", "AAPL"],
symbol_resolution: "mixed" as const
};
describe("news storage helpers", () => {
it("includes the correct table name in the DDL", () => {
const ddl = newsTableDDL();
expect(ddl).toContain(NEWS_TABLE);
expect(ddl).toContain("ReplacingMergeTree");
});
it("round-trips news records", () => {
const record = toNewsRecord(story);
const restored = fromNewsRecord(record);
expect(restored).toEqual(story);
});
it("uses latest-revision selection for recent and cursor queries", async () => {
const queries: string[] = [];
const client = makeClient((query) => {
queries.push(query);
return [toNewsRecord(story)];
});
const recent = await fetchRecentNews(client, 10);
const before = await fetchNewsBefore(client, 200, 10, 10);
const after = await fetchNewsAfter(client, 50, 1, 10);
expect(recent[0]?.trace_id).toBe("alpaca:77");
expect(before[0]?.story_id).toBe(77);
expect(after[0]?.updated_ts).toBe(120);
expect(queries[0]).toContain("row_number() OVER");
expect(queries[1]).toContain("published_ts");
expect(queries[2]).toContain("(published_ts, seq) > (50, 1)");
});
});

View file

@ -0,0 +1,303 @@
import { z } from "zod";
import {
AlertEventSchema,
ClassifierHitEventSchema,
FlowPacketSchema,
OptionPrintSchema,
SmartMoneyEventSchema
} from "./events";
import { OptionFlowFiltersSchema } from "./options-flow";
export const IslandflowAiReasoningEffortSchema = z.enum([
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
]);
export type IslandflowAiReasoningEffort = z.infer<typeof IslandflowAiReasoningEffortSchema>;
export const IslandflowAiPlanTypeSchema = z.enum([
"free",
"go",
"plus",
"pro",
"prolite",
"team",
"self_serve_business_usage_based",
"business",
"enterprise_cbp_usage_based",
"enterprise",
"edu",
"unknown"
]);
export type IslandflowAiPlanType = z.infer<typeof IslandflowAiPlanTypeSchema>;
export const IslandflowAiAuthModeSchema = z.enum([
"apikey",
"chatgpt",
"chatgptAuthTokens",
"agentIdentity"
]);
export type IslandflowAiAuthMode = z.infer<typeof IslandflowAiAuthModeSchema>;
export const IslandflowAiProfileModeSchema = z.enum([
"managed-chatgpt",
"api-key",
"workspace-provider"
]);
export type IslandflowAiProfileMode = z.infer<typeof IslandflowAiProfileModeSchema>;
export const IslandflowAiTransportStatusSchema = z.enum([
"starting",
"ready",
"error",
"stopped",
"restarting"
]);
export type IslandflowAiTransportStatus = z.infer<typeof IslandflowAiTransportStatusSchema>;
export const IslandflowAiTaskKindSchema = z.enum([
"smart-money-explain",
"smart-money-skeptic",
"smart-money-burst-summary",
"watchlist-synthesis",
"replay-postmortem",
"screen-compile"
]);
export type IslandflowAiTaskKind = z.infer<typeof IslandflowAiTaskKindSchema>;
export const IslandflowAiTaskStatusSchema = z.enum([
"queued",
"running",
"completed",
"failed",
"cancelled"
]);
export type IslandflowAiTaskStatus = z.infer<typeof IslandflowAiTaskStatusSchema>;
export const IslandflowAiTokenBreakdownSchema = z.object({
totalTokens: z.number().int().nonnegative(),
inputTokens: z.number().int().nonnegative(),
cachedInputTokens: z.number().int().nonnegative(),
outputTokens: z.number().int().nonnegative(),
reasoningOutputTokens: z.number().int().nonnegative()
});
export type IslandflowAiTokenBreakdown = z.infer<typeof IslandflowAiTokenBreakdownSchema>;
export const IslandflowAiPricingSchema = z.object({
inputUsdPer1MTokens: z.number().nonnegative(),
cachedInputUsdPer1MTokens: z.number().nonnegative(),
outputUsdPer1MTokens: z.number().nonnegative(),
sourceLabel: z.string().min(1),
sourceUrl: z.string().url()
});
export type IslandflowAiPricing = z.infer<typeof IslandflowAiPricingSchema>;
export const IslandflowAiModelSummarySchema = z.object({
id: z.string().min(1),
model: z.string().min(1),
displayName: z.string().min(1),
description: z.string().min(1),
isDefault: z.boolean(),
supportedReasoningEfforts: z.array(IslandflowAiReasoningEffortSchema),
defaultReasoningEffort: IslandflowAiReasoningEffortSchema.nullable(),
pricing: IslandflowAiPricingSchema.nullable()
});
export type IslandflowAiModelSummary = z.infer<typeof IslandflowAiModelSummarySchema>;
export const IslandflowAiRateLimitWindowSchema = z.object({
usedPercent: z.number().min(0).max(100),
windowDurationMins: z.number().int().positive().nullable(),
resetsAt: z.number().int().nullable()
});
export type IslandflowAiRateLimitWindow = z.infer<typeof IslandflowAiRateLimitWindowSchema>;
export const IslandflowAiRateLimitSnapshotSchema = z.object({
limitId: z.string().nullable(),
limitName: z.string().nullable(),
primary: IslandflowAiRateLimitWindowSchema.nullable(),
secondary: IslandflowAiRateLimitWindowSchema.nullable(),
planType: IslandflowAiPlanTypeSchema.nullable(),
reachedType: z.string().nullable(),
hasCredits: z.boolean().nullable(),
unlimitedCredits: z.boolean().nullable(),
creditsBalance: z.string().nullable()
});
export type IslandflowAiRateLimitSnapshot = z.infer<typeof IslandflowAiRateLimitSnapshotSchema>;
export const IslandflowAiSmartMoneyContextSchema = z.object({
event: SmartMoneyEventSchema,
flowPacket: FlowPacketSchema.nullable(),
evidencePrints: z.array(OptionPrintSchema),
relatedPackets: z.array(FlowPacketSchema).default([])
});
export type IslandflowAiSmartMoneyContext = z.infer<typeof IslandflowAiSmartMoneyContextSchema>;
export const IslandflowAiReplayContextSchema = z.object({
ticker: z.string().min(1).nullable(),
flowFilters: OptionFlowFiltersSchema,
alerts: z.array(AlertEventSchema),
smartMoneyEvents: z.array(SmartMoneyEventSchema),
classifierHits: z.array(ClassifierHitEventSchema),
flowPackets: z.array(FlowPacketSchema),
optionPrints: z.array(OptionPrintSchema)
});
export type IslandflowAiReplayContext = z.infer<typeof IslandflowAiReplayContextSchema>;
export const IslandflowAiScreenCompileContextSchema = z.object({
prompt: z.string().min(1).max(4_000),
currentFilters: OptionFlowFiltersSchema
});
export type IslandflowAiScreenCompileContext = z.infer<typeof IslandflowAiScreenCompileContextSchema>;
export const IslandflowAiTaskRequestSchema = z.discriminatedUnion("kind", [
z.object({
kind: z.literal("smart-money-explain"),
context: IslandflowAiSmartMoneyContextSchema
}),
z.object({
kind: z.literal("smart-money-skeptic"),
context: IslandflowAiSmartMoneyContextSchema
}),
z.object({
kind: z.literal("smart-money-burst-summary"),
context: IslandflowAiSmartMoneyContextSchema
}),
z.object({
kind: z.literal("watchlist-synthesis"),
context: IslandflowAiSmartMoneyContextSchema
}),
z.object({
kind: z.literal("replay-postmortem"),
context: IslandflowAiReplayContextSchema
}),
z.object({
kind: z.literal("screen-compile"),
context: IslandflowAiScreenCompileContextSchema
})
]);
export type IslandflowAiTaskRequest = z.infer<typeof IslandflowAiTaskRequestSchema>;
export const IslandflowAiCompiledScreenSchema = z.object({
compiledFilters: OptionFlowFiltersSchema.nullable(),
rationale: z.string().min(1),
unhandledClauses: z.array(z.string()),
sanitizedPrompt: z.string().min(1)
});
export type IslandflowAiCompiledScreen = z.infer<typeof IslandflowAiCompiledScreenSchema>;
export type IslandflowAiProfileSlot = {
id: string;
label: string;
description: string;
mode: IslandflowAiProfileMode;
enabled: boolean;
selected: boolean;
statusLabel: string;
};
export type IslandflowAiLoginState =
| { status: "idle"; message: string | null }
| { status: "browser_pending"; message: string | null; loginId: string; authUrl: string }
| {
status: "device_code_pending";
message: string | null;
loginId: string;
verificationUrl: string;
userCode: string;
}
| { status: "error"; message: string; loginId: string | null };
export type IslandflowAiPreferences = {
model: string | null;
reasoningEffort: IslandflowAiReasoningEffort | null;
};
export type IslandflowAiUsageTurnRecord = {
threadId: string;
turnId: string;
taskId: string | null;
taskKind: IslandflowAiTaskKind | null;
taskTitle: string | null;
dayKey: string;
profileId: string;
accountEmail: string | null;
planType: IslandflowAiPlanType | null;
model: string | null;
breakdown: IslandflowAiTokenBreakdown;
normalizedCostUsd: number | null;
updatedAt: number;
};
export type IslandflowAiUsageRollup = {
breakdown: IslandflowAiTokenBreakdown;
normalizedCostUsd: number | null;
turnCount: number;
activeDays: number;
};
export type IslandflowAiUsageDashboard = {
today: IslandflowAiUsageRollup;
lifetime: IslandflowAiUsageRollup;
recentTurns: IslandflowAiUsageTurnRecord[];
};
export type IslandflowAiTaskSnapshot = {
taskId: string;
kind: IslandflowAiTaskKind;
title: string;
subtitle: string;
status: IslandflowAiTaskStatus;
createdAt: number;
updatedAt: number;
threadId: string | null;
turnId: string | null;
model: string | null;
reasoningEffort: IslandflowAiReasoningEffort | null;
text: string;
error: string | null;
compiledScreen: IslandflowAiCompiledScreen | null;
};
export type IslandflowAiAccountState = {
loggedIn: boolean;
email: string | null;
planType: IslandflowAiPlanType | null;
authMode: IslandflowAiAuthMode | null;
requiresOpenaiAuth: boolean;
login: IslandflowAiLoginState;
};
export type IslandflowAiState = {
desktopAvailable: boolean;
transportStatus: IslandflowAiTransportStatus;
transportError: string | null;
profiles: IslandflowAiProfileSlot[];
selectedProfileId: string;
account: IslandflowAiAccountState;
preferences: IslandflowAiPreferences;
models: IslandflowAiModelSummary[];
rateLimitsByLimitId: Record<string, IslandflowAiRateLimitSnapshot>;
usage: IslandflowAiUsageDashboard;
tasks: IslandflowAiTaskSnapshot[];
updatedAt: number;
};

View file

@ -262,3 +262,26 @@ export const InferredDarkEventSchema = EventMetaSchema.merge(
); );
export type InferredDarkEvent = z.infer<typeof InferredDarkEventSchema>; export type InferredDarkEvent = z.infer<typeof InferredDarkEventSchema>;
export const NewsSymbolResolutionSchema = z.enum(["provider", "derived", "mixed", "none"]);
export type NewsSymbolResolution = z.infer<typeof NewsSymbolResolutionSchema>;
export const NewsStorySchema = EventMetaSchema.merge(
z.object({
story_id: z.number().int().nonnegative(),
provider: z.string().min(1),
source: z.string().min(1),
headline: z.string().min(1),
summary: z.string(),
content_html: z.string(),
url: z.string().url().or(z.literal("")),
published_ts: z.number().int().nonnegative(),
updated_ts: z.number().int().nonnegative(),
provider_symbols: z.array(z.string().min(1)),
resolved_symbols: z.array(z.string().min(1)),
symbol_resolution: NewsSymbolResolutionSchema
})
);
export type NewsStory = z.infer<typeof NewsStorySchema>;

View file

@ -3,3 +3,4 @@ export * from "./live";
export * from "./options-flow"; export * from "./options-flow";
export * from "./sp500"; export * from "./sp500";
export * from "./synthetic-market"; export * from "./synthetic-market";
export * from "./desktop-ai";

View file

@ -8,6 +8,7 @@ import {
EquityQuoteSchema, EquityQuoteSchema,
FlowPacketSchema, FlowPacketSchema,
InferredDarkEventSchema, InferredDarkEventSchema,
NewsStorySchema,
OptionNBBOSchema, OptionNBBOSchema,
OptionPrintSchema, OptionPrintSchema,
SmartMoneyEventSchema SmartMoneyEventSchema
@ -34,7 +35,8 @@ export const LiveGenericChannelSchema = z.enum([
"smart-money", "smart-money",
"classifier-hits", "classifier-hits",
"alerts", "alerts",
"inferred-dark" "inferred-dark",
"news"
]); ]);
export const LiveChannelSchema = z.enum([ export const LiveChannelSchema = z.enum([
@ -48,6 +50,7 @@ export const LiveChannelSchema = z.enum([
"classifier-hits", "classifier-hits",
"alerts", "alerts",
"inferred-dark", "inferred-dark",
"news",
"equity-candles", "equity-candles",
"equity-overlay" "equity-overlay"
]); ]);
@ -91,7 +94,7 @@ export const LiveSubscriptionSchema = z.discriminatedUnion("channel", [
snapshot_limit: z.number().int().positive().optional() snapshot_limit: z.number().int().positive().optional()
}), }),
z.object({ z.object({
channel: z.enum(["nbbo", "equity-quotes", "equity-joins", "classifier-hits", "alerts", "inferred-dark"]), channel: z.enum(["nbbo", "equity-quotes", "equity-joins", "classifier-hits", "alerts", "inferred-dark", "news"]),
snapshot_limit: z.number().int().positive().optional() snapshot_limit: z.number().int().positive().optional()
}), }),
z.object({ z.object({
@ -123,6 +126,7 @@ const livePayloadSchemas = {
"classifier-hits": ClassifierHitEventSchema, "classifier-hits": ClassifierHitEventSchema,
alerts: AlertEventSchema, alerts: AlertEventSchema,
"inferred-dark": InferredDarkEventSchema, "inferred-dark": InferredDarkEventSchema,
news: NewsStorySchema,
"equity-candles": EquityCandleSchema, "equity-candles": EquityCandleSchema,
"equity-overlay": EquityPrintSchema "equity-overlay": EquityPrintSchema
} as const; } as const;

View file

@ -9,6 +9,7 @@ import {
describe("live protocol types", () => { describe("live protocol types", () => {
it("builds stable keys for generic and parameterized subscriptions", () => { it("builds stable keys for generic and parameterized subscriptions", () => {
expect(getSubscriptionKey({ channel: "flow" })).toBe("flow|{}"); expect(getSubscriptionKey({ channel: "flow" })).toBe("flow|{}");
expect(getSubscriptionKey({ channel: "news" })).toBe("news");
expect( expect(
getSubscriptionKey({ getSubscriptionKey({
channel: "options", channel: "options",
@ -53,12 +54,13 @@ describe("live protocol types", () => {
op: "subscribe", op: "subscribe",
subscriptions: [ subscriptions: [
{ channel: "flow", filters: { nbboSides: ["AA", "A"], minNotional: 50000 } }, { channel: "flow", filters: { nbboSides: ["AA", "A"], minNotional: 50000 } },
{ channel: "news", snapshot_limit: 100 },
{ channel: "equity-candles", underlying_id: "SPY", interval_ms: 60000 } { channel: "equity-candles", underlying_id: "SPY", interval_ms: 60000 }
] ]
}); });
expect(parsed.op).toBe("subscribe"); expect(parsed.op).toBe("subscribe");
expect(parsed.subscriptions).toHaveLength(2); expect(parsed.subscriptions).toHaveLength(3);
}); });
it("validates snapshot and event server messages", () => { it("validates snapshot and event server messages", () => {
@ -74,18 +76,24 @@ describe("live protocol types", () => {
}); });
const event = LiveServerMessageSchema.parse({ const event = LiveServerMessageSchema.parse({
op: "event", op: "event",
subscription: { channel: "equity-overlay", underlying_id: "SPY" }, subscription: { channel: "news" },
item: { item: {
source_ts: 100, source_ts: 100,
ingest_ts: 101, ingest_ts: 101,
seq: 1, seq: 1,
trace_id: "eq-1", trace_id: "alpaca:1",
ts: 100, story_id: 1,
underlying_id: "SPY", provider: "alpaca",
price: 500, source: "Benzinga",
size: 10, headline: "TSLA rises",
exchange: "X", summary: "",
offExchangeFlag: true content_html: "<p>TSLA rises</p>",
url: "https://example.com/story",
published_ts: 100,
updated_ts: 100,
provider_symbols: ["TSLA"],
resolved_symbols: ["TSLA"],
symbol_resolution: "provider"
}, },
watermark: cursor watermark: cursor
}); });

View file

@ -0,0 +1,21 @@
# Native, Fast, Iterative Deployment Plan (Docker Optional)
Date: 2026-05-18
## Plan Steps (15)
1. ☐ Stop the bleeding immediately (current deploy loop).
2. ☐ Get hard timing data per deploy phase.
3. ☐ Live server state audit (when plan mode is off).
4. ☐ Resolve duplicate compose stack first (islandflow-2db).
5. ☐ Fix NPM proxy route regression (islandflow-sz8).
6. ☐ Define target iterative deployment model.
7. ☐ Prepare native runtime prerequisites on VPS.
8. ☐ Checked-in native ops assets (islandflow-38p).
9. ☐ Switch proxy topology for native mode carefully.
10. ☐ Deploy strategy guardrails.
11. ☐ Validation matrix.
12. ☐ Staged cutover plan.
13. ☐ Decision: final default runtime.
14. ☐ Decision: optimization priority.
15. ☐ Decision: immediate live audit kickoff.

View file

@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
type DeployMode = "main" | "current-branch"; type DeployMode = "main" | "current-branch";
type DeployRuntime = "docker" | "native"; type DeployRuntime = "docker" | "native";
type DeployScope = "full" | "web" | "api" | "services"; type DeployScope = "full" | "web" | "api" | "services" | "workers";
type DeployOptions = { type DeployOptions = {
mode: DeployMode; mode: DeployMode;
@ -18,10 +18,18 @@ type DeployOptions = {
noBuild: boolean; noBuild: boolean;
}; };
type PhaseTiming = {
name: string;
durationMs: number;
};
const REMOTE_HOST = "delta@152.53.80.229"; const REMOTE_HOST = "delta@152.53.80.229";
const REMOTE_REPO = "/home/delta/islandflow"; const REMOTE_REPO = "/home/delta/islandflow";
const REMOTE_DOCKER_DEPLOYMENT = "/home/delta/islandflow/deployment/docker"; const REMOTE_DOCKER_DEPLOYMENT = "/home/delta/islandflow/deployment/docker";
const SSH_KEY = path.join(process.env.HOME ?? "", ".ssh", "delta_ed25519"); const SSH_KEY =
process.env.DEPLOY_SSH_KEY_PATH?.trim() ||
path.join(process.env.HOME ?? "", ".ssh", "delta_ed25519");
const DEPLOY_FORCE_SSH = process.env.DEPLOY_FORCE_SSH?.trim() === "1";
const SSH_OPTIONS = [ const SSH_OPTIONS = [
"-i", "-i",
SSH_KEY, SSH_KEY,
@ -38,6 +46,7 @@ const PUBLIC_APP_URL =
const PUBLIC_API_HEALTH_URL = const PUBLIC_API_HEALTH_URL =
process.env.DEPLOY_PUBLIC_API_HEALTH_URL?.trim() || null; process.env.DEPLOY_PUBLIC_API_HEALTH_URL?.trim() || null;
const DEPLOY_GIT_REMOTE_OVERRIDE = process.env.DEPLOY_GIT_REMOTE?.trim() || null; const DEPLOY_GIT_REMOTE_OVERRIDE = process.env.DEPLOY_GIT_REMOTE?.trim() || null;
const DEPLOY_NATIVE_EDGE_READY = process.env.DEPLOY_NATIVE_EDGE_READY?.trim() === "1";
const NATIVE_SYSTEMCTL_PREFIX = const NATIVE_SYSTEMCTL_PREFIX =
process.env.DEPLOY_NATIVE_SYSTEMCTL_PREFIX?.trim() || "sudo -n systemctl"; process.env.DEPLOY_NATIVE_SYSTEMCTL_PREFIX?.trim() || "sudo -n systemctl";
const NATIVE_UNITS = { const NATIVE_UNITS = {
@ -48,7 +57,8 @@ const NATIVE_UNITS = {
ingestOptions: ingestOptions:
process.env.DEPLOY_NATIVE_INGEST_OPTIONS_UNIT?.trim() || "islandflow-ingest-options", process.env.DEPLOY_NATIVE_INGEST_OPTIONS_UNIT?.trim() || "islandflow-ingest-options",
ingestEquities: ingestEquities:
process.env.DEPLOY_NATIVE_INGEST_EQUITIES_UNIT?.trim() || "islandflow-ingest-equities" process.env.DEPLOY_NATIVE_INGEST_EQUITIES_UNIT?.trim() || "islandflow-ingest-equities",
ingestNews: process.env.DEPLOY_NATIVE_INGEST_NEWS_UNIT?.trim() || "islandflow-ingest-news"
} as const; } as const;
const DOCKER_CORE_SERVICES = [ const DOCKER_CORE_SERVICES = [
"api", "api",
@ -56,24 +66,34 @@ const DOCKER_CORE_SERVICES = [
"compute", "compute",
"candles", "candles",
"ingest-options", "ingest-options",
"ingest-equities" "ingest-equities",
"ingest-news"
] as const; ] as const;
const DOCKER_BACKEND_SERVICES = [ const DOCKER_BACKEND_SERVICES = [
"api", "api",
"compute", "compute",
"candles", "candles",
"ingest-options", "ingest-options",
"ingest-equities" "ingest-equities",
"ingest-news"
] as const;
const DOCKER_WORKER_SERVICES = [
"compute",
"candles",
"ingest-options",
"ingest-equities",
"ingest-news"
] as const; ] as const;
const scriptPath = fileURLToPath(import.meta.url); const scriptPath = fileURLToPath(import.meta.url);
const repoRoot = path.resolve(path.dirname(scriptPath), ".."); const repoRoot = path.resolve(path.dirname(scriptPath), "..");
const isLocalServerExecution = !DEPLOY_FORCE_SSH && repoRoot === REMOTE_REPO;
function usage(exitCode = 1): never { function usage(exitCode = 1): never {
console.error(`Usage: console.error(`Usage:
./deploy main [--runtime docker|native] [--web-only|--api-only|--services-only] [--fast] [--no-build] [--force-recreate] ./deploy main [--runtime docker|native] [--web-only|--api-only|--services-only|--workers-only] [--fast] [--no-build] [--force-recreate]
./deploy current-branch [--runtime docker|native] [--web-only|--api-only|--services-only] [--fast] [--no-build] [--force-recreate] ./deploy current-branch [--runtime docker|native] [--web-only|--api-only|--services-only|--workers-only] [--fast] [--no-build] [--force-recreate]
./deploy current branch [--runtime docker|native] [--web-only|--api-only|--services-only] [--fast] [--no-build] [--force-recreate] ./deploy current branch [--runtime docker|native] [--web-only|--api-only|--services-only|--workers-only] [--fast] [--no-build] [--force-recreate]
Modes: Modes:
main Deploy <remote>/main to the live server checkout. main Deploy <remote>/main to the live server checkout.
@ -88,25 +108,30 @@ Scopes:
--web-only Deploy only the Next.js web surface. --web-only Deploy only the Next.js web surface.
--api-only Deploy only the API service. --api-only Deploy only the API service.
--services-only Deploy API + backend services without the web service. --services-only Deploy API + backend services without the web service.
--workers-only Deploy compute/candles/ingest workers without touching web or API.
Options: Options:
--runtime <name> Explicit runtime selector (docker or native). --runtime <name> Explicit runtime selector (docker or native).
--fast Prefer a quicker rollout profile (defaults full scope to --services-only and skips public API route suite). --fast Prefer a quicker rollout profile (defaults full scope to --services-only for docker and --workers-only for native, and skips the public API route suite when API scope is included).
--no-build Skip docker image builds or native bun install/web build steps. --no-build Skip docker image builds or native bun install/web build steps.
--force-recreate Docker-only escalation path for docker compose when a normal refresh is not enough. --force-recreate Docker-only escalation path for docker compose when a normal refresh is not enough.
--help Show this help text. --help Show this help text.
Environment: Environment:
DEPLOY_GIT_REMOTE Override git remote used for deploy fetch/pull/push (auto-detected by default). DEPLOY_GIT_REMOTE Override git remote used for deploy fetch/pull/push (auto-detected by default).
DEPLOY_SSH_KEY_PATH Override the SSH key used for remote execution.
DEPLOY_FORCE_SSH Set to 1 to force SSH even when running from the live server checkout.
DEPLOY_PUBLIC_APP_URL Override the public app URL (default: https://flow.deltaisland.io). DEPLOY_PUBLIC_APP_URL Override the public app URL (default: https://flow.deltaisland.io).
DEPLOY_PUBLIC_API_HEALTH_URL Optional separate public API health URL for two-origin deployments. DEPLOY_PUBLIC_API_HEALTH_URL Optional separate public API health URL for two-origin deployments.
DEPLOY_NATIVE_EDGE_READY Set to 1 to allow native rollouts that include the public web or API edge.
DEPLOY_NATIVE_SYSTEMCTL_PREFIX Override systemctl invocation for native rollouts (default: sudo -n systemctl). DEPLOY_NATIVE_SYSTEMCTL_PREFIX Override systemctl invocation for native rollouts (default: sudo -n systemctl).
DEPLOY_NATIVE_WEB_UNIT Override native web systemd unit name. DEPLOY_NATIVE_WEB_UNIT Override native web systemd unit name.
DEPLOY_NATIVE_API_UNIT Override native api systemd unit name. DEPLOY_NATIVE_API_UNIT Override native api systemd unit name.
DEPLOY_NATIVE_COMPUTE_UNIT Override native compute systemd unit name. DEPLOY_NATIVE_COMPUTE_UNIT Override native compute systemd unit name.
DEPLOY_NATIVE_CANDLES_UNIT Override native candles systemd unit name. DEPLOY_NATIVE_CANDLES_UNIT Override native candles systemd unit name.
DEPLOY_NATIVE_INGEST_OPTIONS_UNIT Override native ingest-options systemd unit name. DEPLOY_NATIVE_INGEST_OPTIONS_UNIT Override native ingest-options systemd unit name.
DEPLOY_NATIVE_INGEST_EQUITIES_UNIT Override native ingest-equities systemd unit name.`); DEPLOY_NATIVE_INGEST_EQUITIES_UNIT Override native ingest-equities systemd unit name.
DEPLOY_NATIVE_INGEST_NEWS_UNIT Override native ingest-news systemd unit name.`);
process.exit(exitCode); process.exit(exitCode);
} }
@ -114,6 +139,32 @@ function section(title: string): void {
console.log(`\n== ${title} ==`); console.log(`\n== ${title} ==`);
} }
function formatDuration(durationMs: number): string {
if (durationMs < 1000) {
return `${durationMs}ms`;
}
return `${(durationMs / 1000).toFixed(2)}s`;
}
function timedPhase<T>(timings: PhaseTiming[], name: string, fn: () => T): T {
const startedAt = Date.now();
try {
return fn();
} finally {
timings.push({ name, durationMs: Date.now() - startedAt });
}
}
function printTimingSummary(timings: PhaseTiming[]): void {
section("Deploy Timings");
const totalMs = timings.reduce((sum, timing) => sum + timing.durationMs, 0);
for (const timing of timings) {
console.log(`[deploy] ${timing.name}: ${formatDuration(timing.durationMs)}`);
}
console.log(`[deploy] total: ${formatDuration(totalMs)}`);
}
function formatCommand(command: string, args: string[]): string { function formatCommand(command: string, args: string[]): string {
return [command, ...args] return [command, ...args]
.map((part) => (/\s/.test(part) ? JSON.stringify(part) : part)) .map((part) => (/\s/.test(part) ? JSON.stringify(part) : part))
@ -180,6 +231,23 @@ function runRemoteScript(
args: string[] = [] args: string[] = []
): void { ): void {
section(title); section(title);
if (isLocalServerExecution) {
const localArgs = ["-s", "--", ...args];
console.log(`$ ${formatCommand("bash", localArgs)} # local server execution`);
const result = spawnSync("bash", localArgs, {
cwd: repoRoot,
input: script,
encoding: "utf8",
stdio: ["pipe", "inherit", "inherit"]
});
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
return;
}
const sshArgs = [...SSH_OPTIONS, REMOTE_HOST, "bash", "-s", "--", ...args]; const sshArgs = [...SSH_OPTIONS, REMOTE_HOST, "bash", "-s", "--", ...args];
console.log(`$ ${formatCommand("ssh", sshArgs)}`); console.log(`$ ${formatCommand("ssh", sshArgs)}`);
const result = spawnSync("ssh", sshArgs, { const result = spawnSync("ssh", sshArgs, {
@ -221,11 +289,14 @@ function parseScope(rawArgs: string[]): DeployScope {
const scopes = [ const scopes = [
rawArgs.includes("--web-only") ? "web" : null, rawArgs.includes("--web-only") ? "web" : null,
rawArgs.includes("--api-only") ? "api" : null, rawArgs.includes("--api-only") ? "api" : null,
rawArgs.includes("--services-only") ? "services" : null rawArgs.includes("--services-only") ? "services" : null,
rawArgs.includes("--workers-only") ? "workers" : null
].filter((value): value is Exclude<DeployScope, "full"> => value !== null); ].filter((value): value is Exclude<DeployScope, "full"> => value !== null);
if (scopes.length > 1) { if (scopes.length > 1) {
console.error("Choose only one deploy scope flag: --web-only, --api-only, or --services-only."); console.error(
"Choose only one deploy scope flag: --web-only, --api-only, --services-only, or --workers-only."
);
process.exit(1); process.exit(1);
} }
@ -250,6 +321,7 @@ function parseArgs(rawArgs: string[]): DeployOptions {
arg !== "--web-only" && arg !== "--web-only" &&
arg !== "--api-only" && arg !== "--api-only" &&
arg !== "--services-only" && arg !== "--services-only" &&
arg !== "--workers-only" &&
arg !== "--runtime" && arg !== "--runtime" &&
rawArgs[index - 1] !== "--runtime" && rawArgs[index - 1] !== "--runtime" &&
!arg.startsWith("--runtime=") !arg.startsWith("--runtime=")
@ -282,8 +354,13 @@ function parseArgs(rawArgs: string[]): DeployOptions {
} }
function assertSshKeyExists(): void { function assertSshKeyExists(): void {
if (isLocalServerExecution) {
return;
}
if (!existsSync(SSH_KEY)) { if (!existsSync(SSH_KEY)) {
console.error(`Missing SSH key: ${SSH_KEY}`); console.error(`Missing SSH key: ${SSH_KEY}`);
console.error("Set DEPLOY_SSH_KEY_PATH or run from the live server checkout without DEPLOY_FORCE_SSH.");
process.exit(1); process.exit(1);
} }
} }
@ -398,14 +475,16 @@ function describeScope(scope: DeployScope): string {
return "api only"; return "api only";
case "services": case "services":
return "api + backend services"; return "api + backend services";
case "workers":
return "worker services only";
default: default:
return "full stack"; return "full stack";
} }
} }
function effectiveScope(scope: DeployScope, fast: boolean): DeployScope { function effectiveScope(scope: DeployScope, runtime: DeployRuntime, fast: boolean): DeployScope {
if (fast && scope === "full") { if (fast && scope === "full") {
return "services"; return runtime === "native" ? "workers" : "services";
} }
return scope; return scope;
} }
@ -418,6 +497,10 @@ function scopeIncludesApi(scope: DeployScope): boolean {
return scope === "full" || scope === "api" || scope === "services"; return scope === "full" || scope === "api" || scope === "services";
} }
function scopeTouchesPublicEdge(scope: DeployScope): boolean {
return scopeIncludesWeb(scope) || scopeIncludesApi(scope);
}
function dockerServicesForScope(scope: DeployScope): string[] { function dockerServicesForScope(scope: DeployScope): string[] {
switch (scope) { switch (scope) {
case "web": case "web":
@ -426,6 +509,8 @@ function dockerServicesForScope(scope: DeployScope): string[] {
return ["api"]; return ["api"];
case "services": case "services":
return [...DOCKER_BACKEND_SERVICES]; return [...DOCKER_BACKEND_SERVICES];
case "workers":
return [...DOCKER_WORKER_SERVICES];
default: default:
return []; return [];
} }
@ -448,6 +533,8 @@ function dockerLogServicesForScope(scope: DeployScope): string[] {
return ["api"]; return ["api"];
case "services": case "services":
return [...DOCKER_BACKEND_SERVICES]; return [...DOCKER_BACKEND_SERVICES];
case "workers":
return [...DOCKER_WORKER_SERVICES];
default: default:
return [...DOCKER_CORE_SERVICES]; return [...DOCKER_CORE_SERVICES];
} }
@ -465,7 +552,16 @@ function nativeUnitsForScope(scope: DeployScope): string[] {
NATIVE_UNITS.compute, NATIVE_UNITS.compute,
NATIVE_UNITS.candles, NATIVE_UNITS.candles,
NATIVE_UNITS.ingestOptions, NATIVE_UNITS.ingestOptions,
NATIVE_UNITS.ingestEquities NATIVE_UNITS.ingestEquities,
NATIVE_UNITS.ingestNews
];
case "workers":
return [
NATIVE_UNITS.compute,
NATIVE_UNITS.candles,
NATIVE_UNITS.ingestOptions,
NATIVE_UNITS.ingestEquities,
NATIVE_UNITS.ingestNews
]; ];
default: default:
return [ return [
@ -474,7 +570,8 @@ function nativeUnitsForScope(scope: DeployScope): string[] {
NATIVE_UNITS.compute, NATIVE_UNITS.compute,
NATIVE_UNITS.candles, NATIVE_UNITS.candles,
NATIVE_UNITS.ingestOptions, NATIVE_UNITS.ingestOptions,
NATIVE_UNITS.ingestEquities NATIVE_UNITS.ingestEquities,
NATIVE_UNITS.ingestNews
]; ];
} }
} }
@ -494,19 +591,46 @@ function localDockerWorkspaceSnapshotPrecheck(): void {
} }
} }
function localRuntimePrecheck(runtime: DeployRuntime, noBuild: boolean): void { function assertNativeEdgeReady(scope: DeployScope): void {
if (!scopeTouchesPublicEdge(scope) || DEPLOY_NATIVE_EDGE_READY) {
return;
}
console.error(
"Refusing native deploy that touches public web/API scope before edge cutover is acknowledged."
);
console.error(
"Set DEPLOY_NATIVE_EDGE_READY=1 only after proxy routing and native units for the public edge are intentionally prepared."
);
console.error(
"For fast iterative backend deploys before cutover, use --runtime native --workers-only or --runtime native --fast."
);
process.exit(1);
}
function localRuntimePrecheck(runtime: DeployRuntime, scope: DeployScope, noBuild: boolean): void {
if (runtime === "docker" && !noBuild) { if (runtime === "docker" && !noBuild) {
localDockerWorkspaceSnapshotPrecheck(); localDockerWorkspaceSnapshotPrecheck();
return;
}
if (runtime === "native") {
assertNativeEdgeReady(scope);
} }
} }
function localMainPrecheck(remote: string, runtime: DeployRuntime, noBuild: boolean): void { function localMainPrecheck(
remote: string,
runtime: DeployRuntime,
scope: DeployScope,
noBuild: boolean
): void {
section("Local Precheck"); section("Local Precheck");
runChecked("git", ["fetch", remote]); runChecked("git", ["fetch", remote]);
runChecked("git", ["status", "--short", "--branch"]); runChecked("git", ["status", "--short", "--branch"]);
runChecked("git", ["rev-parse", "--verify", "HEAD"]); runChecked("git", ["rev-parse", "--verify", "HEAD"]);
runChecked("git", ["rev-parse", `${remote}/main`]); runChecked("git", ["rev-parse", `${remote}/main`]);
localRuntimePrecheck(runtime, noBuild); localRuntimePrecheck(runtime, scope, noBuild);
} }
function currentBranchName(): string { function currentBranchName(): string {
@ -522,6 +646,7 @@ function localBranchPrecheck(
remote: string, remote: string,
branch: string, branch: string,
runtime: DeployRuntime, runtime: DeployRuntime,
scope: DeployScope,
noBuild: boolean noBuild: boolean
): void { ): void {
section("Local Precheck"); section("Local Precheck");
@ -537,7 +662,7 @@ function localBranchPrecheck(
process.exit(1); process.exit(1);
} }
localRuntimePrecheck(runtime, noBuild); localRuntimePrecheck(runtime, scope, noBuild);
} }
function publishCurrentBranch(remote: string, branch: string): void { function publishCurrentBranch(remote: string, branch: string): void {
@ -631,6 +756,10 @@ set -euo pipefail
cd ${shellEscape(REMOTE_REPO)} cd ${shellEscape(REMOTE_REPO)}
if [[ -x "$HOME/.bun/bin/bun" ]]; then
export PATH="$HOME/.bun/bin:$PATH"
fi
if ! command -v bun >/dev/null 2>&1; then if ! command -v bun >/dev/null 2>&1; then
echo "Refusing native rollout: bun is not installed on the server." >&2 echo "Refusing native rollout: bun is not installed on the server." >&2
echo "The current supported VPS path remains --runtime docker." >&2 echo "The current supported VPS path remains --runtime docker." >&2
@ -732,6 +861,10 @@ function remoteNativeRollout(
`#!/usr/bin/env bash `#!/usr/bin/env bash
set -euo pipefail set -euo pipefail
if [[ -x "$HOME/.bun/bin/bun" ]]; then
export PATH="$HOME/.bun/bin:$PATH"
fi
${remoteGitUpdateScript(mode, remote, branch)} ${remoteGitUpdateScript(mode, remote, branch)}
cd ${shellEscape(REMOTE_REPO)} cd ${shellEscape(REMOTE_REPO)}
@ -803,6 +936,10 @@ function remoteNativeVerification(scope: DeployScope, fast: boolean): void {
const units = nativeUnitsForScope(scope).map((value) => shellEscape(value)).join(" "); const units = nativeUnitsForScope(scope).map((value) => shellEscape(value)).join(" ");
const checks: string[] = []; const checks: string[] = [];
if (scope === "full" || scope === "api" || scope === "services" || scope === "workers") {
checks.push("./deployment/native/check-native-infra.sh");
}
if (scopeIncludesApi(scope)) { if (scopeIncludesApi(scope)) {
checks.push('curl -fksS http://127.0.0.1:4000/health'); checks.push('curl -fksS http://127.0.0.1:4000/health');
} }
@ -816,6 +953,12 @@ function remoteNativeVerification(scope: DeployScope, fast: boolean): void {
`#!/usr/bin/env bash `#!/usr/bin/env bash
set -euo pipefail set -euo pipefail
cd ${shellEscape(REMOTE_REPO)}
if [[ -x "$HOME/.bun/bin/bun" ]]; then
export PATH="$HOME/.bun/bin:$PATH"
fi
declare -a units=(${units}) declare -a units=(${units})
for unit in "\${units[@]}"; do for unit in "\${units[@]}"; do
${NATIVE_SYSTEMCTL_PREFIX} is-active --quiet "$unit" ${NATIVE_SYSTEMCTL_PREFIX} is-active --quiet "$unit"
@ -837,10 +980,10 @@ function remoteVerification(runtime: DeployRuntime, scope: DeployScope, fast: bo
function publicVerification(scope: DeployScope, fast: boolean): void { function publicVerification(scope: DeployScope, fast: boolean): void {
section("Public Verification"); section("Public Verification");
if (!fast || scopeIncludesWeb(scope)) { if (scopeIncludesWeb(scope)) {
runChecked("curl", ["-I", "-fksS", PUBLIC_APP_URL]); runChecked("curl", ["-I", "-fksS", PUBLIC_APP_URL]);
} else { } else {
console.log("[deploy] Fast mode: skipping public app HEAD check because web scope is not included."); console.log("[deploy] Skipping public app HEAD check because web scope is not included.");
} }
if (scopeIncludesApi(scope) && PUBLIC_API_HEALTH_URL) { if (scopeIncludesApi(scope) && PUBLIC_API_HEALTH_URL) {
@ -861,7 +1004,8 @@ function publicVerification(scope: DeployScope, fast: boolean): void {
function main(): void { function main(): void {
const options = parseArgs(process.argv.slice(2)); const options = parseArgs(process.argv.slice(2));
const scope = effectiveScope(options.scope, options.fast); const scope = effectiveScope(options.scope, options.runtime, options.fast);
const timings: PhaseTiming[] = [];
const currentBranch = options.mode === "current-branch" ? currentBranchName() : null; const currentBranch = options.mode === "current-branch" ? currentBranchName() : null;
const deployRemote = resolveDeployRemote(options.mode, currentBranch); const deployRemote = resolveDeployRemote(options.mode, currentBranch);
assertSshKeyExists(); assertSshKeyExists();
@ -872,22 +1016,33 @@ function main(): void {
`via ${describeRuntime(options.runtime)} (${describeScope(scope)}${options.fast ? ", fast mode" : ""}).` `via ${describeRuntime(options.runtime)} (${describeScope(scope)}${options.fast ? ", fast mode" : ""}).`
); );
console.log(`[deploy] Using git remote: ${deployRemote}`); console.log(`[deploy] Using git remote: ${deployRemote}`);
console.log(
`[deploy] Execution mode: ${isLocalServerExecution ? "local server checkout" : `ssh to ${REMOTE_HOST}`}`
);
if (options.fast && options.scope === "full") { if (options.fast && options.scope === "full") {
console.log("[deploy] Fast mode changed default full scope to --services-only."); console.log(
`[deploy] Fast mode changed default full scope to ${options.runtime === "native" ? "--workers-only" : "--services-only"}.`
);
} }
if (options.mode === "main") { if (options.mode === "main") {
localMainPrecheck(deployRemote, options.runtime, options.noBuild); timedPhase(timings, "local precheck", () =>
remoteGitPrecheck(); localMainPrecheck(deployRemote, options.runtime, scope, options.noBuild)
remoteRuntimePrecheck(options.runtime, scope); );
remoteRollout( timedPhase(timings, "remote git precheck", () => remoteGitPrecheck());
options.mode, timedPhase(timings, "remote runtime precheck", () =>
deployRemote, remoteRuntimePrecheck(options.runtime, scope)
options.runtime, );
null, timedPhase(timings, "remote rollout", () =>
scope, remoteRollout(
options.forceRecreate, options.mode,
options.noBuild deployRemote,
options.runtime,
null,
scope,
options.forceRecreate,
options.noBuild
)
); );
} else { } else {
const branch = currentBranch; const branch = currentBranch;
@ -895,23 +1050,34 @@ function main(): void {
console.error("Unable to resolve current branch for current-branch deploy mode."); console.error("Unable to resolve current branch for current-branch deploy mode.");
process.exit(1); process.exit(1);
} }
localBranchPrecheck(deployRemote, branch, options.runtime, options.noBuild); timedPhase(timings, "local precheck", () =>
publishCurrentBranch(deployRemote, branch); localBranchPrecheck(deployRemote, branch, options.runtime, scope, options.noBuild)
remoteGitPrecheck(); );
remoteRuntimePrecheck(options.runtime, scope); timedPhase(timings, "local publish", () => publishCurrentBranch(deployRemote, branch));
remoteRollout( timedPhase(timings, "remote git precheck", () => remoteGitPrecheck());
options.mode, timedPhase(timings, "remote runtime precheck", () =>
deployRemote, remoteRuntimePrecheck(options.runtime, scope)
options.runtime, );
branch, timedPhase(timings, "remote rollout", () =>
scope, remoteRollout(
options.forceRecreate, options.mode,
options.noBuild deployRemote,
options.runtime,
branch,
scope,
options.forceRecreate,
options.noBuild
)
); );
} }
remoteVerification(options.runtime, scope, options.fast); timedPhase(timings, "remote verification", () =>
publicVerification(scope, options.fast); remoteVerification(options.runtime, scope, options.fast)
);
timedPhase(timings, "public verification", () =>
publicVerification(scope, options.fast)
);
printTimingSummary(timings);
} }
main(); main();

View file

@ -222,6 +222,7 @@ process.on("SIGHUP", () => handleSignal("SIGHUP"));
const tasks: ChildSpec[] = [ const tasks: ChildSpec[] = [
{ name: "ingest-options", cmd: ["bun", "run", "dev"], cwd: "services/ingest-options" }, { name: "ingest-options", cmd: ["bun", "run", "dev"], cwd: "services/ingest-options" },
{ name: "ingest-equities", cmd: ["bun", "run", "dev"], cwd: "services/ingest-equities" }, { name: "ingest-equities", cmd: ["bun", "run", "dev"], cwd: "services/ingest-equities" },
{ name: "ingest-news", cmd: ["bun", "run", "dev"], cwd: "services/ingest-news" },
{ name: "compute", cmd: ["bun", "run", "dev"], cwd: "services/compute" }, { name: "compute", cmd: ["bun", "run", "dev"], cwd: "services/compute" },
{ name: "candles", cmd: ["bun", "run", "dev"], cwd: "services/candles" }, { name: "candles", cmd: ["bun", "run", "dev"], cwd: "services/candles" },
{ name: "refdata", cmd: ["bun", "run", "dev"], cwd: "services/refdata" }, { name: "refdata", cmd: ["bun", "run", "dev"], cwd: "services/refdata" },

View file

@ -325,6 +325,7 @@ const serviceTasks: ChildSpec[] = [
{ name: "web", cmd: ["bun", "run", "dev"], cwd: "apps/web" }, { name: "web", cmd: ["bun", "run", "dev"], cwd: "apps/web" },
{ name: "ingest-options", cmd: ["bun", "run", "dev"], cwd: "services/ingest-options" }, { name: "ingest-options", cmd: ["bun", "run", "dev"], cwd: "services/ingest-options" },
{ name: "ingest-equities", cmd: ["bun", "run", "dev"], cwd: "services/ingest-equities" }, { name: "ingest-equities", cmd: ["bun", "run", "dev"], cwd: "services/ingest-equities" },
{ name: "ingest-news", cmd: ["bun", "run", "dev"], cwd: "services/ingest-news" },
{ name: "compute", cmd: ["bun", "run", "dev"], cwd: "services/compute" }, { name: "compute", cmd: ["bun", "run", "dev"], cwd: "services/compute" },
{ name: "candles", cmd: ["bun", "run", "dev"], cwd: "services/candles" }, { name: "candles", cmd: ["bun", "run", "dev"], cwd: "services/candles" },
{ name: "refdata", cmd: ["bun", "run", "dev"], cwd: "services/refdata" }, { name: "refdata", cmd: ["bun", "run", "dev"], cwd: "services/refdata" },

View file

@ -0,0 +1,421 @@
import { promises as fs } from "node:fs";
import path from "node:path";
const docsDir = path.resolve(process.cwd(), "docs");
const outputFile = path.join(docsDir, "index.html");
const dateFormatter = new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
timeStyle: "short",
});
function escapeHtml(value) {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function formatBytes(bytes) {
if (bytes < 1024) {
return `${bytes} B`;
}
const units = ["KB", "MB", "GB"];
let size = bytes / 1024;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex += 1;
}
return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unitIndex]}`;
}
function docsHref(relativePath) {
const encoded = relativePath
.split("/")
.map((part) => encodeURIComponent(part))
.join("/");
return `./${encoded}`;
}
async function collectDocsFiles(rootDir, currentDir = rootDir, acc = []) {
const entries = await fs.readdir(currentDir, { withFileTypes: true });
const sortedEntries = entries.sort((a, b) => a.name.localeCompare(b.name));
for (const entry of sortedEntries) {
if (entry.name.startsWith(".")) {
continue;
}
const absolutePath = path.join(currentDir, entry.name);
const relativePath = path.relative(rootDir, absolutePath).replaceAll(path.sep, "/");
if (relativePath === "index.html") {
continue;
}
if (entry.isDirectory()) {
await collectDocsFiles(rootDir, absolutePath, acc);
continue;
}
if (entry.isFile()) {
const stats = await fs.stat(absolutePath);
acc.push({
relativePath,
category: relativePath.includes("/") ? relativePath.split("/")[0] : "root",
sizeBytes: stats.size,
modifiedAt: stats.mtime,
});
}
}
return acc;
}
function groupByCategory(items) {
const groups = new Map();
for (const item of items) {
if (!groups.has(item.category)) {
groups.set(item.category, []);
}
groups.get(item.category).push(item);
}
return groups;
}
function sortedCategories(groups) {
const preferredOrder = ["turns", "daily-git", "general", "plans", "root"];
const groupNames = [...groups.keys()];
return groupNames.sort((a, b) => {
const aIndex = preferredOrder.indexOf(a);
const bIndex = preferredOrder.indexOf(b);
if (aIndex !== -1 || bIndex !== -1) {
if (aIndex === -1) return 1;
if (bIndex === -1) return -1;
return aIndex - bIndex;
}
return a.localeCompare(b);
});
}
function renderDocument(items) {
const sortedItems = [...items].sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
const groups = groupByCategory(sortedItems);
const categories = sortedCategories(groups);
const totalCount = sortedItems.length;
const categoryChips = categories
.map((category) => {
const count = groups.get(category).length;
return `<a class="chip" href="#category-${escapeHtml(category)}">${escapeHtml(
category
)} <span>${count}</span></a>`;
})
.join("\n");
const groupsMarkup = categories
.map((category) => {
const entries = groups.get(category);
const entryMarkup = entries
.map((entry) => {
const extension = path.extname(entry.relativePath).replace(".", "") || "file";
const searchable = `${entry.relativePath} ${category}`.toLowerCase();
return `
<li class="doc-item" data-search="${escapeHtml(searchable)}">
<a class="doc-link" href="${docsHref(entry.relativePath)}">${escapeHtml(
entry.relativePath
)}</a>
<div class="meta">
<span class="tag">${escapeHtml(extension)}</span>
<span>${escapeHtml(formatBytes(entry.sizeBytes))}</span>
<span>${escapeHtml(dateFormatter.format(entry.modifiedAt))}</span>
</div>
</li>
`;
})
.join("\n");
return `
<section class="group" id="category-${escapeHtml(category)}">
<h2>${escapeHtml(category)} <span>${entries.length}</span></h2>
<ul class="doc-list">
${entryMarkup}
</ul>
</section>
`;
})
.join("\n");
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Islandflow Docs</title>
<style>
:root {
--bg: #f4f6f8;
--surface: #ffffff;
--surface-muted: #e8edf2;
--text: #1a2433;
--muted: #5b6a80;
--border: #ccd5df;
--accent: #0f766e;
--accent-soft: #d1fae5;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Avenir Next", "Segoe UI", sans-serif;
background: radial-gradient(circle at top right, #e2f8f2, var(--bg) 35%);
color: var(--text);
}
main {
max-width: 1120px;
margin: 0 auto;
padding: 32px 16px 48px;
}
.header {
display: grid;
gap: 12px;
}
h1 {
margin: 0;
font-size: clamp(1.8rem, 2.3vw, 2.4rem);
font-weight: 760;
}
.subtitle {
margin: 0;
color: var(--muted);
max-width: 60ch;
}
.toolbar {
margin-top: 10px;
padding: 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
display: grid;
gap: 12px;
}
.stats {
font-size: 0.95rem;
color: var(--muted);
}
.search {
width: 100%;
border: 1px solid var(--border);
border-radius: 8px;
font: inherit;
font-size: 1rem;
padding: 10px 12px;
background: #fff;
}
.search:focus {
outline: 2px solid color-mix(in srgb, var(--accent) 30%, white);
outline-offset: 0;
border-color: var(--accent);
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.chip {
text-decoration: none;
color: var(--text);
background: var(--surface-muted);
padding: 6px 10px;
border-radius: 999px;
font-size: 0.85rem;
border: 1px solid transparent;
}
.chip span {
color: var(--muted);
}
.chip:hover {
border-color: var(--accent);
}
.groups {
margin-top: 20px;
display: grid;
gap: 16px;
}
.group {
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
padding: 14px;
}
.group.hidden {
display: none;
}
.group h2 {
margin: 0 0 10px;
font-size: 1.1rem;
}
.group h2 span {
color: var(--muted);
font-weight: 520;
}
.doc-list {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: 6px;
}
.doc-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 8px 10px;
border-radius: 6px;
}
.doc-item.hidden {
display: none;
}
.doc-item:hover {
background: #f5faf8;
}
.doc-link {
color: var(--text);
text-decoration: none;
font-family: "IBM Plex Mono", "SFMono-Regular", Consolas, monospace;
font-size: 0.92rem;
overflow-wrap: anywhere;
}
.doc-link:hover {
color: var(--accent);
text-decoration: underline;
}
.meta {
display: flex;
align-items: center;
gap: 10px;
color: var(--muted);
font-size: 0.82rem;
white-space: nowrap;
}
.tag {
background: var(--accent-soft);
color: #065f46;
border-radius: 999px;
padding: 3px 8px;
font-size: 0.78rem;
}
.empty {
margin-top: 20px;
border: 1px dashed var(--border);
border-radius: 8px;
background: var(--surface);
color: var(--muted);
padding: 20px;
text-align: center;
display: none;
}
</style>
</head>
<body>
<main>
<header class="header">
<h1>Islandflow docs index</h1>
<p class="subtitle">A browsable index of files under <code>docs/</code> with filtering and grouped navigation.</p>
</header>
<section class="toolbar">
<div class="stats"><strong id="visible-count">${totalCount}</strong> of <strong>${totalCount}</strong> files shown</div>
<input id="doc-search" class="search" type="search" placeholder="Filter by filename or folder..." autocomplete="off" />
<nav class="chips">${categoryChips}</nav>
</section>
<section class="groups" id="groups">${groupsMarkup}</section>
<p class="empty" id="empty-state">No files match that filter.</p>
</main>
<script>
const searchInput = document.getElementById("doc-search");
const items = Array.from(document.querySelectorAll(".doc-item"));
const groups = Array.from(document.querySelectorAll(".group"));
const visibleCount = document.getElementById("visible-count");
const emptyState = document.getElementById("empty-state");
function applyFilter(query) {
const normalized = query.trim().toLowerCase();
let shown = 0;
for (const item of items) {
const searchable = item.dataset.search || "";
const isVisible = normalized.length === 0 || searchable.includes(normalized);
item.classList.toggle("hidden", !isVisible);
if (isVisible) shown += 1;
}
for (const group of groups) {
const hasVisibleItems = group.querySelector(".doc-item:not(.hidden)") !== null;
group.classList.toggle("hidden", !hasVisibleItems);
}
visibleCount.textContent = String(shown);
emptyState.style.display = shown === 0 ? "block" : "none";
}
searchInput.addEventListener("input", () => applyFilter(searchInput.value));
applyFilter("");
</script>
</body>
</html>
`;
}
async function main() {
const files = await collectDocsFiles(docsDir);
const html = renderDocument(files);
await fs.writeFile(outputFile, html, "utf8");
console.log(`Generated ${outputFile} with ${files.length} entries.`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

View file

@ -9,6 +9,7 @@ import {
SUBJECT_EQUITY_QUOTES, SUBJECT_EQUITY_QUOTES,
SUBJECT_INFERRED_DARK, SUBJECT_INFERRED_DARK,
SUBJECT_FLOW_PACKETS, SUBJECT_FLOW_PACKETS,
SUBJECT_NEWS,
SUBJECT_SMART_MONEY_EVENTS, SUBJECT_SMART_MONEY_EVENTS,
SUBJECT_OPTION_NBBO, SUBJECT_OPTION_NBBO,
SUBJECT_OPTION_SIGNAL_PRINTS, SUBJECT_OPTION_SIGNAL_PRINTS,
@ -20,6 +21,7 @@ import {
STREAM_EQUITY_QUOTES, STREAM_EQUITY_QUOTES,
STREAM_INFERRED_DARK, STREAM_INFERRED_DARK,
STREAM_FLOW_PACKETS, STREAM_FLOW_PACKETS,
STREAM_NEWS,
STREAM_SMART_MONEY_EVENTS, STREAM_SMART_MONEY_EVENTS,
STREAM_OPTION_NBBO, STREAM_OPTION_NBBO,
STREAM_OPTION_SIGNAL_PRINTS, STREAM_OPTION_SIGNAL_PRINTS,
@ -35,6 +37,7 @@ import {
import { import {
createClickHouseClient, createClickHouseClient,
ensureAlertsTable, ensureAlertsTable,
ensureNewsTable,
ensureClassifierHitsTable, ensureClassifierHitsTable,
ensureEquityCandlesTable, ensureEquityCandlesTable,
ensureEquityPrintJoinsTable, ensureEquityPrintJoinsTable,
@ -48,6 +51,8 @@ import {
fetchAlertsAfter, fetchAlertsAfter,
fetchAlertsBefore, fetchAlertsBefore,
fetchAlertContextByTraceId, fetchAlertContextByTraceId,
fetchNewsAfter,
fetchNewsBefore,
fetchClassifierHitsAfter, fetchClassifierHitsAfter,
fetchClassifierHitsBefore, fetchClassifierHitsBefore,
fetchSmartMoneyEventsAfter, fetchSmartMoneyEventsAfter,
@ -58,6 +63,7 @@ import {
fetchFlowPacketsByMemberTraceIds, fetchFlowPacketsByMemberTraceIds,
fetchFlowPacketsBefore, fetchFlowPacketsBefore,
fetchRecentAlerts, fetchRecentAlerts,
fetchRecentNews,
fetchRecentClassifierHits, fetchRecentClassifierHits,
fetchRecentSmartMoneyEvents, fetchRecentSmartMoneyEvents,
fetchRecentEquityPrintJoins, fetchRecentEquityPrintJoins,
@ -86,7 +92,8 @@ import {
fetchNearestOptionNBBOForPrints, fetchNearestOptionNBBOForPrints,
fetchSmartMoneyEventsByPacketIds, fetchSmartMoneyEventsByPacketIds,
fetchClassifierHitsByPacketIds, fetchClassifierHitsByPacketIds,
fetchRecentOptionPrints fetchRecentOptionPrints,
insertNewsStory
} from "@islandflow/storage"; } from "@islandflow/storage";
import type { EquityPrintQueryFilters } from "@islandflow/storage"; import type { EquityPrintQueryFilters } from "@islandflow/storage";
import { import {
@ -99,6 +106,7 @@ import {
EquityQuoteSchema, EquityQuoteSchema,
FeedSnapshot, FeedSnapshot,
InferredDarkEventSchema, InferredDarkEventSchema,
NewsStorySchema,
LiveClientMessageSchema, LiveClientMessageSchema,
LiveServerMessage, LiveServerMessage,
LiveSubscription, LiveSubscription,
@ -138,6 +146,7 @@ const DeliverPolicySchema = z.enum(["new", "all", "last", "last_per_subject"]);
const envSchema = z.object({ const envSchema = z.object({
API_PORT: z.coerce.number().int().positive().default(4000), API_PORT: z.coerce.number().int().positive().default(4000),
API_HOST: z.string().min(1).default("127.0.0.1"),
NATS_URL: z.string().default("nats://127.0.0.1:4222"), NATS_URL: z.string().default("nats://127.0.0.1:4222"),
CLICKHOUSE_URL: z.string().default("http://127.0.0.1:8123"), CLICKHOUSE_URL: z.string().default("http://127.0.0.1:8123"),
CLICKHOUSE_DATABASE: z.string().default("default"), CLICKHOUSE_DATABASE: z.string().default("default"),
@ -676,7 +685,8 @@ const run = async () => {
STREAM_FLOW_PACKETS, STREAM_FLOW_PACKETS,
STREAM_SMART_MONEY_EVENTS, STREAM_SMART_MONEY_EVENTS,
STREAM_CLASSIFIER_HITS, STREAM_CLASSIFIER_HITS,
STREAM_ALERTS STREAM_ALERTS,
STREAM_NEWS
], ],
{ logger } { logger }
); );
@ -719,6 +729,7 @@ const run = async () => {
await ensureSmartMoneyEventsTable(clickhouse); await ensureSmartMoneyEventsTable(clickhouse);
await ensureClassifierHitsTable(clickhouse); await ensureClassifierHitsTable(clickhouse);
await ensureAlertsTable(clickhouse); await ensureAlertsTable(clickhouse);
await ensureNewsTable(clickhouse);
}); });
let redis: ReturnType<typeof createClient> | null = null; let redis: ReturnType<typeof createClient> | null = null;
@ -843,6 +854,11 @@ const run = async () => {
subject: SUBJECT_ALERTS, subject: SUBJECT_ALERTS,
stream: STREAM_ALERTS, stream: STREAM_ALERTS,
durableName: "api-alerts" durableName: "api-alerts"
},
{
subject: SUBJECT_NEWS,
stream: STREAM_NEWS,
durableName: "api-news"
} }
] as const; ] as const;
@ -991,10 +1007,16 @@ const run = async () => {
consumerBindings[10].durableName consumerBindings[10].durableName
); );
const newsSubscription = await subscribeWithReset(
consumerBindings[11].subject,
consumerBindings[11].stream,
consumerBindings[11].durableName
);
const fanoutLive = async ( const fanoutLive = async (
subscription: LiveSubscription, subscription: LiveSubscription,
item: unknown, item: unknown,
ingestChannel: "options" | "nbbo" | "equities" | "equity-quotes" | "equity-candles" | "equity-overlay" | "equity-joins" | "flow" | "classifier-hits" | "alerts" | "inferred-dark" ingestChannel: "options" | "nbbo" | "equities" | "equity-quotes" | "equity-candles" | "equity-overlay" | "equity-joins" | "flow" | "classifier-hits" | "alerts" | "inferred-dark" | "news"
) => { ) => {
const watermark = await liveState.ingest(ingestChannel, item); const watermark = await liveState.ingest(ingestChannel, item);
@ -1252,6 +1274,22 @@ const run = async () => {
} }
}; };
const pumpNews = async () => {
for await (const msg of newsSubscription.messages) {
try {
const payload = NewsStorySchema.parse(newsSubscription.decode(msg));
await insertNewsStory(clickhouse, payload);
await fanoutLive({ channel: "news" }, payload, "news");
msg.ack();
} catch (error) {
logger.error("failed to process news story", {
error: error instanceof Error ? error.message : String(error)
});
msg.term();
}
}
};
void pumpOptions(); void pumpOptions();
void pumpOptionNbbo(); void pumpOptionNbbo();
void pumpEquities(); void pumpEquities();
@ -1263,6 +1301,7 @@ const run = async () => {
void pumpSmartMoney(); void pumpSmartMoney();
void pumpClassifierHits(); void pumpClassifierHits();
void pumpAlerts(); void pumpAlerts();
void pumpNews();
const buildSyntheticStatusBody = () => { const buildSyntheticStatusBody = () => {
const derived = const derived =
@ -1313,6 +1352,7 @@ const run = async () => {
}; };
const server = Bun.serve<WsData | LiveWsData>({ const server = Bun.serve<WsData | LiveWsData>({
hostname: env.API_HOST,
port: env.API_PORT, port: env.API_PORT,
fetch: async (req: Request, serverRef: any) => { fetch: async (req: Request, serverRef: any) => {
const url = new URL(req.url); const url = new URL(req.url);
@ -1490,6 +1530,12 @@ const run = async () => {
return jsonResponse({ data }); return jsonResponse({ data });
} }
if (req.method === "GET" && url.pathname === "/news") {
const limit = parseLimit(url.searchParams.get("limit") ?? "100");
const data = await fetchRecentNews(clickhouse, limit);
return jsonResponse({ data });
}
if (req.method === "GET" && isAlertContextPath(url.pathname)) { if (req.method === "GET" && isAlertContextPath(url.pathname)) {
try { try {
const traceId = parseAlertContextTraceIdPath(url.pathname); const traceId = parseAlertContextTraceIdPath(url.pathname);
@ -1607,6 +1653,14 @@ const run = async () => {
); );
} }
if (req.method === "GET" && url.pathname === "/history/news") {
const { beforeTs, beforeSeq, limit } = parseBeforeParams(url);
const data = await fetchNewsBefore(clickhouse, beforeTs, beforeSeq, limit);
return jsonResponse(
buildHistoryResponse(data, (item) => ({ ts: item.published_ts, seq: item.seq }))
);
}
if (req.method === "GET" && /^\/flow\/packets\/[^/]+$/.test(url.pathname)) { if (req.method === "GET" && /^\/flow\/packets\/[^/]+$/.test(url.pathname)) {
const id = decodeURIComponent(url.pathname.slice("/flow/packets/".length)); const id = decodeURIComponent(url.pathname.slice("/flow/packets/".length));
const data = await fetchFlowPacketById(clickhouse, id); const data = await fetchFlowPacketById(clickhouse, id);
@ -1995,7 +2049,7 @@ const run = async () => {
} }
}); });
logger.info("api listening", { port: server.port }); logger.info("api listening", { host: env.API_HOST, port: server.port });
const shutdown = async (signal: string) => { const shutdown = async (signal: string) => {
if (state.shutdownPromise) { if (state.shutdownPromise) {

View file

@ -8,6 +8,7 @@ import {
fetchRecentEquityQuotes, fetchRecentEquityQuotes,
fetchRecentFlowPackets, fetchRecentFlowPackets,
fetchRecentInferredDark, fetchRecentInferredDark,
fetchRecentNews,
fetchRecentOptionNBBO, fetchRecentOptionNBBO,
fetchRecentSmartMoneyEvents, fetchRecentSmartMoneyEvents,
type ClickHouseClient type ClickHouseClient
@ -25,6 +26,7 @@ import {
FeedSnapshot, FeedSnapshot,
FlowPacketSchema, FlowPacketSchema,
InferredDarkEventSchema, InferredDarkEventSchema,
NewsStorySchema,
LiveChannelHealth, LiveChannelHealth,
LiveGenericChannel, LiveGenericChannel,
LiveHotChannel, LiveHotChannel,
@ -40,6 +42,7 @@ import {
type EquityCandle, type EquityCandle,
type EquityPrint, type EquityPrint,
type LiveChannel, type LiveChannel,
type NewsStory,
type OptionPrint type OptionPrint
} from "@islandflow/types"; } from "@islandflow/types";
import { createMetrics } from "@islandflow/observability"; import { createMetrics } from "@islandflow/observability";
@ -63,7 +66,8 @@ const GENERIC_LIMIT_ENV_KEYS: Record<LiveGenericChannel, string> = {
"smart-money": "LIVE_LIMIT_SMART_MONEY", "smart-money": "LIVE_LIMIT_SMART_MONEY",
"classifier-hits": "LIVE_LIMIT_CLASSIFIER_HITS", "classifier-hits": "LIVE_LIMIT_CLASSIFIER_HITS",
alerts: "LIVE_LIMIT_ALERTS", alerts: "LIVE_LIMIT_ALERTS",
"inferred-dark": "LIVE_LIMIT_INFERRED_DARK" "inferred-dark": "LIVE_LIMIT_INFERRED_DARK",
news: "LIVE_LIMIT_NEWS"
}; };
const CHART_LIMITS = { const CHART_LIMITS = {
@ -81,7 +85,8 @@ const DEFAULT_LIVE_LIMITS: GenericLiveLimits = {
"smart-money": 300, "smart-money": 300,
"classifier-hits": 300, "classifier-hits": 300,
alerts: 300, alerts: 300,
"inferred-dark": 300 "inferred-dark": 300,
news: 100
}; };
const DEFAULT_SCOPED_CACHE_MAX_KEYS = 32; const DEFAULT_SCOPED_CACHE_MAX_KEYS = 32;
@ -196,16 +201,28 @@ export const resolveGenericLiveLimits = (env: NodeJS.ProcessEnv = process.env):
env, env,
"inferred-dark", "inferred-dark",
env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS["inferred-dark"] env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS["inferred-dark"]
) ),
news: parseGenericLimit(env, "news", env.LIVE_LIMIT_DEFAULT ? liveLimitDefault : DEFAULT_LIVE_LIMITS.news)
}; };
}; };
const parsePositiveInt = (value: string | undefined, fallback: number): number => { const extractFreshnessTs = (channel: LiveGenericChannel, item: any): number | null => {
const parsed = Number(value); switch (channel) {
if (!Number.isFinite(parsed)) { case "options":
return fallback; case "nbbo":
case "equities":
case "equity-quotes":
return typeof item.ts === "number" ? item.ts : null;
case "flow":
case "classifier-hits":
case "alerts":
case "inferred-dark":
return typeof item.source_ts === "number" ? item.source_ts : null;
case "news":
return typeof item.published_ts === "number" ? item.published_ts : null;
default:
return null;
} }
return Math.max(1, Math.floor(parsed));
}; };
export const resolveLiveStateConfig = (env: NodeJS.ProcessEnv = process.env): LiveStateConfig => ({ export const resolveLiveStateConfig = (env: NodeJS.ProcessEnv = process.env): LiveStateConfig => ({
@ -217,6 +234,13 @@ export const resolveLiveStateConfig = (env: NodeJS.ProcessEnv = process.env): Li
), ),
redisFlushMaxItems: parsePositiveInt(env.LIVE_REDIS_FLUSH_MAX_ITEMS, DEFAULT_REDIS_FLUSH_MAX_ITEMS) redisFlushMaxItems: parsePositiveInt(env.LIVE_REDIS_FLUSH_MAX_ITEMS, DEFAULT_REDIS_FLUSH_MAX_ITEMS)
}); });
const parsePositiveInt = (value: string | undefined, fallback: number): number => {
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
return fallback;
}
return Math.max(1, Math.floor(parsed));
};
type RedisLike = Pick< type RedisLike = Pick<
RedisClientType, RedisClientType,
@ -318,6 +342,14 @@ const getGenericConfig = (limits: GenericLiveLimits): {
parse: (value) => InferredDarkEventSchema.parse(value), parse: (value) => InferredDarkEventSchema.parse(value),
cursor: (item) => ({ ts: item.source_ts, seq: item.seq }), cursor: (item) => ({ ts: item.source_ts, seq: item.seq }),
fetchRecent: fetchRecentInferredDark fetchRecent: fetchRecentInferredDark
},
news: {
redisKey: "live:news",
cursorField: "news",
limit: limits.news,
parse: (value) => NewsStorySchema.parse(value),
cursor: (item) => ({ ts: item.published_ts, seq: item.seq }),
fetchRecent: fetchRecentNews
} }
}); });
@ -371,23 +403,6 @@ const normalizeGenericItems = <T>(
return sortGenericItems(items, config.cursor).slice(0, config.limit); return sortGenericItems(items, config.cursor).slice(0, config.limit);
}; };
const extractFreshnessTs = (channel: LiveGenericChannel, item: any): number | null => {
switch (channel) {
case "options":
case "nbbo":
case "equities":
case "equity-quotes":
return typeof item.ts === "number" ? item.ts : null;
case "flow":
case "classifier-hits":
case "alerts":
case "inferred-dark":
return typeof item.source_ts === "number" ? item.source_ts : null;
default:
return null;
}
};
const isWithinLiveFeedLookback = ( const isWithinLiveFeedLookback = (
channel: LiveGenericChannel, channel: LiveGenericChannel,
item: unknown, item: unknown,

View file

@ -1,3 +1,8 @@
import {
buildAlpacaAuthHeaders,
buildAlpacaWebSocketAuthMessage,
type AlpacaCredentials
} from "@islandflow/config";
import { createLogger } from "@islandflow/observability"; import { createLogger } from "@islandflow/observability";
import type { EquityPrint, EquityQuote } from "@islandflow/types"; import type { EquityPrint, EquityQuote } from "@islandflow/types";
import type { EquityIngestAdapter, EquityIngestHandlers } from "./types"; import type { EquityIngestAdapter, EquityIngestHandlers } from "./types";
@ -6,7 +11,7 @@ import WebSocket from "ws";
export type AlpacaEquitiesFeed = "iex" | "sip"; export type AlpacaEquitiesFeed = "iex" | "sip";
export type AlpacaEquitiesAdapterConfig = { export type AlpacaEquitiesAdapterConfig = {
apiKey: string; credentials: AlpacaCredentials;
restUrl: string; restUrl: string;
wsBaseUrl: string; wsBaseUrl: string;
feed: AlpacaEquitiesFeed; feed: AlpacaEquitiesFeed;
@ -62,12 +67,6 @@ const normalizeSymbols = (symbols: string[]): string[] => {
return result; return result;
}; };
const buildHeaders = (config: AlpacaEquitiesAdapterConfig): Record<string, string> => {
return {
Authorization: `Bearer ${config.apiKey}`
};
};
const parseTimestamp = (value: string): number => { const parseTimestamp = (value: string): number => {
const parsed = Date.parse(value); const parsed = Date.parse(value);
if (Number.isFinite(parsed)) { if (Number.isFinite(parsed)) {
@ -157,7 +156,7 @@ const fetchExchangeMeta = async (config: AlpacaEquitiesAdapterConfig): Promise<M
try { try {
const response = await fetch(url.toString(), { const response = await fetch(url.toString(), {
headers: buildHeaders(config) headers: buildAlpacaAuthHeaders(config.credentials)
}); });
if (!response.ok) { if (!response.ok) {
@ -184,8 +183,8 @@ export const createAlpacaEquitiesAdapter = (
return { return {
name: "alpaca", name: "alpaca",
start: async (handlers: EquityIngestHandlers) => { start: async (handlers: EquityIngestHandlers) => {
if (!config.apiKey) { if (!config.credentials.keyId) {
throw new Error("Alpaca equities adapter requires ALPACA_API_KEY."); throw new Error("Alpaca equities adapter requires Alpaca credentials.");
} }
const symbols = normalizeSymbols(config.symbols); const symbols = normalizeSymbols(config.symbols);
@ -196,7 +195,7 @@ export const createAlpacaEquitiesAdapter = (
const exchangeNameMap = await fetchExchangeMeta(config); const exchangeNameMap = await fetchExchangeMeta(config);
const wsUrl = buildWsUrl(config.wsBaseUrl, config.feed); const wsUrl = buildWsUrl(config.wsBaseUrl, config.feed);
const ws = new WebSocket(wsUrl, { const ws = new WebSocket(wsUrl, {
headers: buildHeaders(config) headers: buildAlpacaAuthHeaders(config.credentials)
}); });
let seq = 0; let seq = 0;
@ -204,13 +203,7 @@ export const createAlpacaEquitiesAdapter = (
let authenticated = false; let authenticated = false;
ws.on("open", () => { ws.on("open", () => {
ws.send( ws.send(JSON.stringify(buildAlpacaWebSocketAuthMessage(config.credentials)));
JSON.stringify({
action: "auth",
key: config.apiKey,
secret: ""
})
);
}); });
const subscribe = () => { const subscribe = () => {

Some files were not shown because too many files have changed in this diff Show more