Add Electron desktop shell workspace

This commit is contained in:
dirtydishes 2026-05-13 09:21:06 -04:00
parent b803d10836
commit 5d8e5ea44a
16 changed files with 1652 additions and 21 deletions

View file

@ -1,27 +1,11 @@
{"_type":"issue","id":"islandflow-ebp","title":"Implement JetStream retention reconciliation and admin rollout command","description":"Implement shared JetStream stream catalog and reconciliation logic so retention cap changes take effect on existing streams without deleting them.\n\nScope:\n- Centralize known stream definitions in packages/bus\n- Change retention defaults to raw=60m/512MiB and derived=12h/256MiB\n- Update ensureStream() to reconcile allowed retention drift in place and fail on structural mismatch\n- Add a Bun CLI entrypoint to audit/apply stream reconciliation\n- Reuse the same helpers from startup and CLI paths\n- Document Docker rollout and verification flow\n- Add unit tests for defaults, drift detection, safe updates, and CLI behavior\n","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-08T19:47:23Z","created_by":"dirtydishes","updated_at":"2026-05-08T19:52:08Z","started_at":"2026-05-08T19:47:29Z","closed_at":"2026-05-08T19:52:08Z","close_reason":"Implemented shared JetStream retention reconciliation, startup drift correction, admin CLI, docs, and tests","dependencies":[{"issue_id":"islandflow-ebp","depends_on_id":"islandflow-1ln","type":"discovered-from","created_at":"2026-05-08T15:47:22Z","created_by":"dirtydishes","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-9ug","title":"Electron desktop shell for hosted Islandflow","description":"Build a macOS-first Electron desktop shell workspace that loads hosted Islandflow in a locked-down BrowserWindow, adds Bun-first dev/package scripts, documents the workflow, and preserves the existing remote API/WS contract.\n","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-13T13:11:40Z","created_by":"dirtydishes","updated_at":"2026-05-13T13:20:57Z","started_at":"2026-05-13T13:12:03Z","closed_at":"2026-05-13T13:20:57Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-vnq","title":"Fix deploy verification for same-origin host","description":"Remove the hardcoded separate API host assumption from deployment tooling and docs. Make deploy verification and documentation match the current flow.deltaisland.io setup, using same-origin verification where appropriate instead of forcing api.flow.deltaisland.io.\n","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-08T11:34:49Z","created_by":"dirtydishes","updated_at":"2026-05-08T11:37:35Z","started_at":"2026-05-08T11:35:37Z","closed_at":"2026-05-08T11:37:35Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-762","title":"Fix public API hostname TLS/proxy path","description":"Debug and fix the public API hostname so https://api.flow.deltaisland.io/health works again. Determine whether the failure is in Cloudflare, Nginx Proxy Manager, DNS, or the API proxy host definition, then apply the smallest safe fix and verify the public endpoint.\n","status":"in_progress","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-08T11:21:41Z","created_by":"dirtydishes","updated_at":"2026-05-08T11:21:52Z","started_at":"2026-05-08T11:21:52Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-33c","title":"Investigate public API TLS handshake failure","description":"Investigate the public TLS handshake failure on https://api.flow.deltaisland.io/health. After the compose network fix, the app host is healthy and nginx-proxy-manager can reach islandflow-vps-api-1 internally, but both local and server-side HTTPS requests to api.flow.deltaisland.io fail during TLS handshake at the public edge. This likely needs proxy or Cloudflare inspection outside the app stack.\n","status":"open","priority":1,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-08T11:13:36Z","created_by":"dirtydishes","updated_at":"2026-05-08T11:13:36Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-xsi","title":"Fix deploy precheck shell pattern generation","description":"Fix the deploy precheck shell-pattern generation introduced while allowing known untracked server paths. The generated remote bash case statement needs a valid combined pattern so ./deploy main can complete on the live server.\n","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-08T11:11:37Z","created_by":"dirtydishes","updated_at":"2026-05-08T11:12:02Z","started_at":"2026-05-08T11:11:53Z","closed_at":"2026-05-08T11:12:02Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-kda","title":"Fix production compose shared-network topology","description":"Restore the production Docker topology so the merged deploy workflow actually matches the live proxy setup. Update deployment/docker/docker-compose.yml on the working branch so web and api attach to the shared npm-shared network instead of relying on loopback host port bindings, then validate the compose config and document any rollout implications.\n","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-08T11:08:48Z","created_by":"dirtydishes","updated_at":"2026-05-08T11:10:46Z","started_at":"2026-05-08T11:09:02Z","closed_at":"2026-05-08T11:10:46Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-43i","title":"Implement safe VPS deploy modes","description":"Implement a safe local deploy entrypoint for the existing Islandflow VPS checkout. Add two rollout modes: deploy origin/main and deploy the current local branch. Use explicit SSH identity flags, preserve the shared npm-shared network topology, avoid destructive git cleanup on the server, allow the known untracked signal-cli tarball, and run standard remote plus public verification checks after compose rebuilds. Keep compatibility wrappers for the existing deployment helper scripts and document the workflow.\n","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-08T07:56:03Z","created_by":"dirtydishes","updated_at":"2026-05-08T08:01:32Z","started_at":"2026-05-08T07:56:08Z","closed_at":"2026-05-08T08:01:32Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-dil","title":"Run production baseline and post-rollout verification for load reduction","description":"Run the production verification checklist from the load-reduction plan on the VPS, capture baseline container/resource stats, validate replay remains disabled, and confirm JetStream/Redis behavior after rollout.\n\nThis follow-up is operational rather than code-local and could not be executed from the current workspace. It should compare pre/post CPU, RSS, Redis memory, and retention growth using the documented commands.\n","status":"open","priority":1,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-08T06:45:06Z","created_by":"dirtydishes","updated_at":"2026-05-08T06:45:06Z","dependencies":[{"issue_id":"islandflow-dil","depends_on_id":"islandflow-1ln","type":"discovered-from","created_at":"2026-05-08T02:45:06Z","created_by":"dirtydishes","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-1ln","title":"Implement VPS load reduction plan","description":"Implement load-reduction plan across API, compute, logging, retention, and cache pruning.\n\nThis issue tracks the first-pass implementation of VPS load mitigations: lower live cache limits, async Redis write-behind in API live state, scoped cache eviction, reduced hot-path logging, bounded JetStream retention via shared config, in-memory rolling stats with async Redis snapshots, batched ClickHouse inserts for derived tables, and TTL/cardinality pruning for long-lived in-process maps.\n\nAcceptance:\n- Config surface for live limits, logging, rolling cache, and stream retention added\n- API live ingest avoids per-event full resort in monotonic case and avoids synchronous Redis writes per event\n- Compute rolling stats leave Redis hot path and derived ClickHouse writes batch\n- Long-lived caches/maps are pruned by TTL/cardinality\n- Tests cover monotonic/out-of-order live ingest, scoped eviction, rolling stats, and pruning behavior\n","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-08T06:27:41Z","created_by":"dirtydishes","updated_at":"2026-05-08T06:46:23Z","started_at":"2026-05-08T06:27:54Z","closed_at":"2026-05-08T06:46:23Z","close_reason":"Implemented in code; rollout verification follow-up is islandflow-dil and Redis durability decision follow-up is islandflow-ybs","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-pre","title":"Fix contract-focused options tape hydration","description":"Implement contract-focused options tape hydration so focused contract views preserve the clicked seed row, stop reapplying broad flow filters in the Options pane, and use raw contract-scoped ClickHouse queries consistently across live snapshots, history, and replay. Includes frontend replay source-grouping changes and regression tests for focus seed durability, focused filtering, and contract-scoped API behavior.\n","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-08T03:27:31Z","created_by":"dirtydishes","updated_at":"2026-05-08T03:37:18Z","started_at":"2026-05-08T03:27:35Z","closed_at":"2026-05-08T03:37:18Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-9xs","title":"Fix terminal hydration and virtual row measurement crash","description":"Fix client crash caused by options-support hydration on non-JSON/404 responses and satisfy tanstack virtual measured-row data-index requirement across virtualized tables.\n","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-07T06:14:33Z","created_by":"dirtydishes","updated_at":"2026-05-07T06:17:09Z","started_at":"2026-05-07T06:14:43Z","closed_at":"2026-05-07T06:17:09Z","close_reason":"Completed: added data-index attributes on measured virtual rows, hardened options-support hydration error handling/content-type validation, and guarded trace-id hydration loops against malformed payload entries.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-35g","title":"Fix Docker deployment workspace lockfile drift","description":"Refresh deployment/docker workspace lockfile for Docker builds, add a drift guard for Docker-built workspaces, and document the separate deployment snapshot so frozen Bun installs cannot fail when repo dependencies change.\n","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-07T06:02:06Z","created_by":"dirtydishes","updated_at":"2026-05-07T06:07:50Z","started_at":"2026-05-07T06:02:15Z","closed_at":"2026-05-07T06:07:50Z","close_reason":"Completed: synced deployment Docker workspace snapshot from repo root, refreshed deployment bun.lock, added sync/check scripts, and documented maintenance workflow. Local docker compose build validation is blocked here because Docker daemon is unavailable.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-2ij","title":"Harden tape virtualization, scoped focus, and live feed health","description":"Implement the coordinated tape stability plan across web and API.\n\nScope:\n- replace fixed-height tape virtualization with measured virtualization and virtual-end history loading\n- replace scrollHeight anchoring with key-based anchor restore\n- compose canonical tape lists across seed/live/history sources\n- preserve clicked contract/ticker context during scoped focus transitions\n- separate backend hot-channel health from scoped quiet empty states\n- shrink browser hot windows and modestly reduce server cache limits\n- add regression tests and development instrumentation\n\nAcceptance:\n- no giant blank spacer gaps during tape scrolling\n- scroll remains stable while live data and history mutate the list\n- clicked deep-history option/equity rows remain visible immediately after focus\n- narrow scopes do not surface Feed behind unless backend channel health is stale\n","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-07T05:35:18Z","created_by":"dirtydishes","updated_at":"2026-05-07T05:52:14Z","started_at":"2026-05-07T05:35:21Z","closed_at":"2026-05-07T05:52:14Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-uj7","title":"Fix home to tape navigation","description":"Home rail Tape navigation was not reliably switching to the tape route. Use browser-native top-level navigation for Home/Tape rail links so /tape remains reachable even if client router handling stalls.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-07T03:18:14Z","created_by":"dirtydishes","updated_at":"2026-05-07T03:18:21Z","started_at":"2026-05-07T03:18:20Z","closed_at":"2026-05-07T03:18:21Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-84s","title":"Implement seamless /tape live-to-history scroll gate","description":"Implement seamless live-to-ClickHouse scroll-gated history for /tape panes, including split live/history buffers in the web client, snapshot_limit support on live subscriptions, a bundled options support lookup endpoint, ClickHouse helpers for parity hydration, and test coverage for live head retention, background history loading, scoped options deep-hydration, and historical options decor restoration.\n","status":"in_progress","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-07T02:10:43Z","created_by":"dirtydishes","updated_at":"2026-05-07T02:10:47Z","started_at":"2026-05-07T02:10:47Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-sh1","title":"Fix live websocket stale lag and reconnect loop","description":"Investigate and fix API live consumer lag causing stale timestamps, feed-behind status, and reconnect loops. Optimize live cache persistence path, add lag telemetry/alerts, and validate in runtime.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-04T17:04:34Z","created_by":"dirtydishes","updated_at":"2026-05-04T17:09:44Z","started_at":"2026-05-04T17:04:38Z","closed_at":"2026-05-04T17:09:44Z","close_reason":"Completed: optimized live cache persistence path, added lag telemetry, deployed api via docker compose on di, verified ws freshness and low hotFeedLagMs","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-sh1","title":"Fix live websocket stale lag and reconnect loop","description":"Investigate and fix API live consumer lag causing stale timestamps, feed-behind status, and reconnect loops. Optimize live cache persistence path, add lag telemetry/alerts, and validate in runtime.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-04T17:04:34Z","created_by":"dirtydishes","updated_at":"2026-05-04T17:09:44Z","started_at":"2026-05-04T17:04:38Z","closed_at":"2026-05-04T17:09:44Z","close_reason":"Completed: optimized live cache persistence path, added lag telemetry, deployed api via docker compose on di, verified ws freshness and low hotFeedLagMs","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-b3o","title":"Implement options tape table with execution spot","description":"Redesign OptionsPane into a dense classifier-colored table and preserve execution-time underlying spot on option prints from equity quote mid.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-04T04:41:59Z","created_by":"dirtydishes","updated_at":"2026-05-04T05:14:26Z","started_at":"2026-05-04T04:42:08Z","closed_at":"2026-05-04T05:14:26Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-b3o","title":"Implement options tape table with execution spot","description":"Redesign OptionsPane into a dense classifier-colored table and preserve execution-time underlying spot on option prints from equity quote mid.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-04T04:41:59Z","created_by":"dirtydishes","updated_at":"2026-05-04T05:14:26Z","started_at":"2026-05-04T04:42:08Z","closed_at":"2026-05-04T05:14:26Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-ug1","title":"Fix false NBBO-missing badges in live Options tape","description":"Investigate and fix client-side cases where Options rows show NBBO missing/stale even when a fresh NBBO quote exists in the live nbbo map. Update rendering logic to prefer fresh quote-derived status and add regression tests.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-29T15:58:31Z","created_by":"dirtydishes","updated_at":"2026-04-29T16:01:28Z","started_at":"2026-04-29T15:58:35Z","closed_at":"2026-04-29T16:01:28Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-ug1","title":"Fix false NBBO-missing badges in live Options tape","description":"Investigate and fix client-side cases where Options rows show NBBO missing/stale even when a fresh NBBO quote exists in the live nbbo map. Update rendering logic to prefer fresh quote-derived status and add regression tests.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-29T15:58:31Z","created_by":"dirtydishes","updated_at":"2026-04-29T16:01:28Z","started_at":"2026-04-29T15:58:35Z","closed_at":"2026-04-29T16:01:28Z","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-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-4sr","title":"Remove deprecated NPM deployment path","description":"The repo still carries a deprecated Nginx Proxy Manager deployment path under deployment/npm, and the Docker deployment docs/config still assume an external NPM shared network. Remove the obsolete NPM deployment path and update the Docker deployment to be the supported way to run Islandflow, including docs and compose/env defaults.\n","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-08T08:12:30Z","created_by":"dirtydishes","updated_at":"2026-05-08T08:17:05Z","started_at":"2026-05-08T08:12:38Z","closed_at":"2026-05-08T08:17:05Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-932","title":"Desktop follow-up native features","description":"Track deferred native desktop features after the thin hosted-wrapper v1 lands: notifications, keyboard shortcuts, local preferences storage, remembered window state, signed/notarized macOS distribution, auto-update evaluation, and optional local frontend bundling.\n","status":"open","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-13T13:20:12Z","created_by":"dirtydishes","updated_at":"2026-05-13T13:20:12Z","dependencies":[{"issue_id":"islandflow-932","depends_on_id":"islandflow-9ug","type":"discovered-from","created_at":"2026-05-13T09:20:12Z","created_by":"dirtydishes","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-dga","title":"Remove obsolete deploy wrappers","description":"Remove the legacy deployment helper wrappers now that the repo-standard local deploy entrypoint exists. Delete the obsolete deployment/docker/deploy.sh and deployment/docker/deploy-branch.sh scripts, update documentation to point only at ./deploy, and verify there are no remaining references to the old helpers.\n","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-08T08:07:43Z","created_by":"dirtydishes","updated_at":"2026-05-08T08:08:12Z","started_at":"2026-05-08T08:07:52Z","closed_at":"2026-05-08T08:08:12Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-ybs","title":"Decide Redis AOF and cache/durable split after load rollout","description":"Decide whether the deployment Redis should keep AOF enabled or be split into cache vs durable roles after the first rollout data is available.\n\nThe current code changes reduce cache churn, but the operational durability/caching tradeoff still needs a production decision.\n","status":"open","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-08T06:45:05Z","created_by":"dirtydishes","updated_at":"2026-05-08T06:45:05Z","dependencies":[{"issue_id":"islandflow-ybs","depends_on_id":"islandflow-1ln","type":"discovered-from","created_at":"2026-05-08T02:45:04Z","created_by":"dirtydishes","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-vbk","title":"Remove deprecated Alpaca key-pair auth","description":"Remove legacy Alpaca key-pair authentication support and keep ALPACA_API_KEY as the only supported auth method across options/equities ingest and docs.\n","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-05T07:19:51Z","created_by":"dirtydishes","updated_at":"2026-05-05T07:21:10Z","started_at":"2026-05-05T07:19:54Z","closed_at":"2026-05-05T07:21:10Z","close_reason":"Removed key-pair auth and kept ALPACA_API_KEY only","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-vbk","title":"Remove deprecated Alpaca key-pair auth","description":"Remove legacy Alpaca key-pair authentication support and keep ALPACA_API_KEY as the only supported auth method across options/equities ingest and docs.\n","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-05T07:19:51Z","created_by":"dirtydishes","updated_at":"2026-05-05T07:21:10Z","started_at":"2026-05-05T07:19:54Z","closed_at":"2026-05-05T07:21:10Z","close_reason":"Removed key-pair auth and kept ALPACA_API_KEY only","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-h47","title":"Support single-token Alpaca auth","description":"Support single-token Alpaca authentication across ingest adapters using ALPACA_API_KEY with fallback to ALPACA_KEY_ID/ALPACA_SECRET_KEY, and document env usage.\n","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-05T07:12:22Z","created_by":"dirtydishes","updated_at":"2026-05-05T07:13:54Z","started_at":"2026-05-05T07:12:25Z","closed_at":"2026-05-05T07:13:54Z","close_reason":"Added ALPACA_API_KEY support with key-pair fallback","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-h47","title":"Support single-token Alpaca auth","description":"Support single-token Alpaca authentication across ingest adapters using ALPACA_API_KEY with fallback to ALPACA_KEY_ID/ALPACA_SECRET_KEY, and document env usage.\n","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-05T07:12:22Z","created_by":"dirtydishes","updated_at":"2026-05-05T07:13:54Z","started_at":"2026-05-05T07:12:25Z","closed_at":"2026-05-05T07:13:54Z","close_reason":"Added ALPACA_API_KEY support with key-pair fallback","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-neu","title":"Add Alpha Vantage event calendar provider","description":"Add an Alpha Vantage earnings-calendar provider to services/refdata that fetches CSV, normalizes entries, writes the JSON cache consumed by compute, and documents the required env variables.\n","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-05T07:00:31Z","created_by":"dirtydishes","updated_at":"2026-05-05T07:02:30Z","started_at":"2026-05-05T07:00:37Z","closed_at":"2026-05-05T07:02:30Z","close_reason":"Added Alpha Vantage event-calendar provider","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-neu","title":"Add Alpha Vantage event calendar provider","description":"Add an Alpha Vantage earnings-calendar provider to services/refdata that fetches CSV, normalizes entries, writes the JSON cache consumed by compute, and documents the required env variables.\n","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-05T07:00:31Z","created_by":"dirtydishes","updated_at":"2026-05-05T07:02:30Z","started_at":"2026-05-05T07:00:37Z","closed_at":"2026-05-05T07:02:30Z","close_reason":"Added Alpha Vantage event-calendar provider","dependency_count":0,"dependent_count":0,"comment_count":0}

1
.gitignore vendored
View file

@ -12,6 +12,7 @@ logs/
.tmp/ .tmp/
apps/web/.next/ apps/web/.next/
apps/web/.next-dev/ apps/web/.next-dev/
apps/desktop/out/
# Local assistant artifacts # Local assistant artifacts
session-ses_*.md session-ses_*.md

View file

@ -75,6 +75,7 @@ Planned / not yet complete:
## 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.
- `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/compute` — clustering, structures, classifiers, alerts, inferred dark.
@ -115,6 +116,48 @@ Start web only:
- `bun run dev:web` - `bun run dev:web`
## Desktop Shell
Islandflow also includes a thin Electron desktop shell in `apps/desktop`.
What it is:
- a macOS-first wrapper around the hosted app at `https://flow.deltaisland.io`,
- a native app window plus packaging/distribution shell,
- a way to run the existing web UI inside Electron without local backend services.
What it is not:
- a bundled backend runtime,
- a packaged local Next.js frontend in v1,
- a desktop feature layer with notifications, preferences, or auto-updates yet.
Run the desktop shell against a local web UI:
- `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:
- `bun run dev:desktop:remote`
Package the desktop shell:
- `bun run package:desktop`
- `bun run make:desktop`
Desktop-specific environment:
- `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.
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
All runtime configuration comes from `.env`. All runtime configuration comes from `.env`.

View file

@ -1,3 +1,6 @@
# Apps # Apps
Next.js app(s) live here. Scaffold pending. User-facing app workspaces live here.
- `web` contains the hosted Next.js UI.
- `desktop` contains the thin Electron shell for macOS-first internal distribution.

29
apps/desktop/README.md Normal file
View file

@ -0,0 +1,29 @@
# Islandflow Desktop Shell
This workspace packages a thin Electron shell around the hosted Islandflow app.
## What It Does
- Loads `https://flow.deltaisland.io` by default.
- Supports local UI development against `http://127.0.0.1:3000`.
- Preserves the existing remote API and WebSocket behavior from the web app.
- Keeps Electron privileges locked down for remote content.
## What It Does Not Do
- Bundle a local backend.
- Ship a packaged local Next.js renderer in v1.
- Add desktop-native features beyond launch, windowing, and packaging.
## Workspace Commands
- `bun run start` builds the main process and launches Electron Forge in dev mode.
- `bun run package` creates a packaged unsigned macOS app bundle.
- `bun run make` creates a macOS zip distributable for the current host architecture.
- `bun run test` runs the desktop URL-policy tests.
## Development Notes
- `ISLANDFLOW_DESKTOP_START_URL` controls which trusted app URL Electron loads.
- `NEXT_PUBLIC_API_URL` remains a web-app setting and should typically be `https://flow.deltaisland.io` when developing the local UI inside Electron.
- `assets/` currently contains placeholders only; a real `.icns` icon is deferred.

View file

@ -0,0 +1,6 @@
# Desktop Asset Placeholders
This folder is reserved for the Electron shell's packaged app assets.
- `icon-placeholder.svg` is a visual stub only.
- A real macOS release icon should eventually be added as `.icns` and then wired into `forge.config.ts`.

View file

@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-labelledby="title">
<title>Islandflow desktop placeholder icon</title>
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#081017" />
<stop offset="100%" stop-color="#05070a" />
</linearGradient>
<linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#f5a623" />
<stop offset="100%" stop-color="#ffd89a" />
</linearGradient>
</defs>
<rect width="256" height="256" rx="56" fill="url(#bg)" />
<path
d="M48 96h160v20H48zm0 44h114v20H48zm0 44h160v20H48z"
fill="url(#accent)"
opacity="0.94"
/>
<circle cx="188" cy="150" r="22" fill="#25c17a" opacity="0.95" />
</svg>

After

Width:  |  Height:  |  Size: 791 B

View file

@ -0,0 +1,17 @@
export default {
packagerConfig: {
appBundleId: "io.deltaisland.islandflow",
appCategoryType: "public.app-category.finance",
asar: true,
executableName: "Islandflow",
name: "Islandflow",
ignore: [/^\/node_modules($|\/)/],
prune: false
},
makers: [
{
name: "@electron-forge/maker-zip",
platforms: ["darwin"]
}
]
};

23
apps/desktop/package.json Normal file
View file

@ -0,0 +1,23 @@
{
"name": "@islandflow/desktop",
"private": true,
"type": "module",
"version": "0.1.0",
"main": "dist/main.js",
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "bun test src",
"start": "bun run build && electron-forge start",
"package": "bun run build && electron-forge package",
"make": "bun run build && electron-forge make"
},
"devDependencies": {
"@electron-forge/cli": "^7.8.1",
"@electron-forge/core": "^7.11.1",
"@electron-forge/maker-zip": "^7.8.1",
"@types/node": "^24.10.1",
"electron": "^39.2.0",
"typescript": "^5.9.3"
}
}

117
apps/desktop/src/main.ts Normal file
View file

@ -0,0 +1,117 @@
import { app, BrowserWindow, shell } from "electron";
import type { Event as ElectronEvent } from "electron";
import {
DESKTOP_PRODUCTION_URL,
isSafeExternalUrl,
isTrustedAppUrl,
resolveDesktopStartUrl
} from "./security.js";
const WINDOW_BACKGROUND_COLOR = "#06080b";
const WINDOW_TITLE = "Islandflow";
let mainWindow: BrowserWindow | null = null;
const canOpenExternalUrl = (sourceUrl: string, targetUrl: string): boolean => {
return isTrustedAppUrl(sourceUrl) && isSafeExternalUrl(targetUrl);
};
const openExternalUrl = async (sourceUrl: string, targetUrl: string): Promise<void> => {
if (!canOpenExternalUrl(sourceUrl, targetUrl)) {
return;
}
await shell.openExternal(targetUrl);
};
const installNavigationGuards = (window: BrowserWindow): void => {
const { webContents } = window;
const { session } = webContents;
session.setPermissionRequestHandler((_webContents, _permission, callback) => {
callback(false);
});
const handleNavigationAttempt = (event: ElectronEvent, targetUrl: string) => {
if (isTrustedAppUrl(targetUrl)) {
return;
}
event.preventDefault();
void openExternalUrl(webContents.getURL(), targetUrl);
};
webContents.on("will-navigate", handleNavigationAttempt);
webContents.on("will-redirect", handleNavigationAttempt);
webContents.setWindowOpenHandler(({ url }) => {
void openExternalUrl(webContents.getURL(), url);
return { action: "deny" };
});
};
const createMainWindow = (): BrowserWindow => {
const window = new BrowserWindow({
width: 1440,
height: 960,
minWidth: 1200,
minHeight: 800,
show: false,
title: WINDOW_TITLE,
backgroundColor: WINDOW_BACKGROUND_COLOR,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
webSecurity: true,
webviewTag: false
}
});
installNavigationGuards(window);
window.once("ready-to-show", () => {
window.show();
});
window.on("closed", () => {
if (mainWindow === window) {
mainWindow = null;
}
});
const startUrl = resolveDesktopStartUrl(process.env.ISLANDFLOW_DESKTOP_START_URL);
if (process.env.ISLANDFLOW_DESKTOP_START_URL && startUrl === DESKTOP_PRODUCTION_URL) {
console.warn(
`[desktop] Refused untrusted ISLANDFLOW_DESKTOP_START_URL; falling back to ${DESKTOP_PRODUCTION_URL}`
);
}
void window.loadURL(startUrl);
return window;
};
const ensureMainWindow = (): void => {
if (mainWindow) {
return;
}
mainWindow = createMainWindow();
};
app.whenReady().then(() => {
ensureMainWindow();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
ensureMainWindow();
}
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});

View file

@ -0,0 +1,41 @@
import { describe, expect, it } from "bun:test";
import {
DESKTOP_PRODUCTION_URL,
isSafeExternalUrl,
isTrustedAppUrl,
resolveDesktopStartUrl
} from "./security.js";
describe("desktop URL policy", () => {
it("allows the hosted production origin", () => {
expect(isTrustedAppUrl("https://flow.deltaisland.io/tape?symbol=SPY")).toBe(true);
});
it("allows local dev origins", () => {
expect(isTrustedAppUrl("http://127.0.0.1:3000/signals")).toBe(true);
expect(isTrustedAppUrl("http://localhost:3000/charts")).toBe(true);
});
it("rejects untrusted origins", () => {
expect(isTrustedAppUrl("https://example.com")).toBe(false);
expect(isTrustedAppUrl("http://127.0.0.1:4000")).toBe(false);
});
it("rejects malformed URLs", () => {
expect(isTrustedAppUrl("not a url")).toBe(false);
expect(isTrustedAppUrl("javascript:alert('xss')")).toBe(false);
});
it("treats third-party http targets as external-only", () => {
expect(isSafeExternalUrl("https://deltaisland.io/about")).toBe(true);
expect(isSafeExternalUrl("mailto:support@deltaisland.io")).toBe(false);
expect(isSafeExternalUrl("https://flow.deltaisland.io/help")).toBe(false);
});
it("falls back to production when the desktop start URL is invalid", () => {
expect(resolveDesktopStartUrl(undefined)).toBe(DESKTOP_PRODUCTION_URL);
expect(resolveDesktopStartUrl("https://example.com")).toBe(DESKTOP_PRODUCTION_URL);
expect(resolveDesktopStartUrl("http://127.0.0.1:3000")).toBe("http://127.0.0.1:3000");
});
});

View file

@ -0,0 +1,44 @@
export const DESKTOP_PRODUCTION_URL = "https://flow.deltaisland.io";
export const DESKTOP_LOCAL_DEV_URL = "http://127.0.0.1:3000";
const TRUSTED_ORIGINS = new Set([
new URL(DESKTOP_PRODUCTION_URL).origin,
new URL(DESKTOP_LOCAL_DEV_URL).origin,
"http://localhost:3000"
]);
const HTTP_PROTOCOLS = new Set(["http:", "https:"]);
const parseUrl = (candidate: string): URL | null => {
try {
return new URL(candidate);
} catch {
return null;
}
};
export const isTrustedAppUrl = (candidate: string): boolean => {
const url = parseUrl(candidate);
if (!url || !HTTP_PROTOCOLS.has(url.protocol)) {
return false;
}
return TRUSTED_ORIGINS.has(url.origin);
};
export const isSafeExternalUrl = (candidate: string): boolean => {
const url = parseUrl(candidate);
if (!url || !HTTP_PROTOCOLS.has(url.protocol)) {
return false;
}
return !TRUSTED_ORIGINS.has(url.origin);
};
export const resolveDesktopStartUrl = (candidate: string | undefined): string => {
if (candidate && isTrustedAppUrl(candidate)) {
return candidate;
}
return DESKTOP_PRODUCTION_URL;
};

View file

@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"types": ["node"],
"rootDir": "src",
"outDir": "dist",
"noEmit": false,
"sourceMap": true,
"declaration": false
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts"]
}

1000
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -11,8 +11,12 @@
"dev": "bun run scripts/dev.ts", "dev": "bun run scripts/dev.ts",
"dev:infra": "docker compose up", "dev:infra": "docker compose up",
"dev:infra:down": "docker compose down", "dev:infra:down": "docker compose down",
"dev:desktop": "bun run scripts/dev-desktop.ts",
"dev:desktop:remote": "bun run scripts/dev-desktop.ts --remote",
"dev:web": "bun --cwd=apps/web run dev", "dev:web": "bun --cwd=apps/web run dev",
"dev:services": "bun run scripts/dev-services.ts", "dev:services": "bun run scripts/dev-services.ts",
"package:desktop": "bun --cwd=apps/desktop run package",
"make:desktop": "bun --cwd=apps/desktop run make",
"deploy": "bun run scripts/deploy.ts", "deploy": "bun run scripts/deploy.ts",
"deploy:main": "./deploy main", "deploy:main": "./deploy main",
"deploy:current-branch": "./deploy current-branch", "deploy:current-branch": "./deploy current-branch",

286
scripts/dev-desktop.ts Normal file
View file

@ -0,0 +1,286 @@
import net from "node:net";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
const DESKTOP_REMOTE_URL = "https://flow.deltaisland.io";
const DESKTOP_LOCAL_URL = "http://127.0.0.1:3000";
const WEB_PORT = 3000;
type ChildSpec = {
name: string;
cmd: string[];
cwd: string;
env?: Record<string, string>;
};
type Child = {
name: string;
process: Bun.Subprocess;
};
const children: Child[] = [];
let shuttingDown = false;
let shutdownPromise: Promise<void> | null = null;
let forceShutdownPromise: Promise<void> | null = null;
const stateDir = path.join(process.cwd(), ".tmp");
const pidFile = path.join(stateDir, "dev-desktop-runner-pids.json");
const remoteMode = process.argv.includes("--remote");
const sleep = (delayMs: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, delayMs));
};
const isPidRunning = (pid: number): boolean => {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
};
const waitForPidExit = async (pid: number, timeoutMs: number): Promise<boolean> => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (!isPidRunning(pid)) {
return true;
}
await sleep(100);
}
return !isPidRunning(pid);
};
const signalProcess = (pid: number, signal: NodeJS.Signals): boolean => {
try {
process.kill(-pid, signal);
return true;
} catch {
try {
process.kill(pid, signal);
return true;
} catch {
return false;
}
}
};
const stopPid = async (pid: number, timeoutMs = 5000): Promise<void> => {
if (!signalProcess(pid, "SIGINT")) {
return;
}
if (await waitForPidExit(pid, timeoutMs)) {
return;
}
if (!signalProcess(pid, "SIGKILL")) {
return;
}
await waitForPidExit(pid, 2000);
};
const stopChild = async (child: Child, timeoutMs = 5000): Promise<void> => {
const pid = child.process.pid;
if (!pid) {
return;
}
await stopPid(pid, timeoutMs);
};
const persistChildren = async (): Promise<void> => {
await mkdir(stateDir, { recursive: true });
const payload = children
.map((child) => {
const pid = child.process.pid;
return pid ? { name: child.name, pid } : null;
})
.filter((value): value is { name: string; pid: number } => value !== null);
await writeFile(pidFile, JSON.stringify(payload, null, 2));
};
const clearPersistedChildren = async (): Promise<void> => {
await rm(pidFile, { force: true });
};
const cleanupStaleChildren = async (): Promise<void> => {
try {
const raw = await readFile(pidFile, "utf8");
const recorded = JSON.parse(raw) as Array<{ name?: string; pid?: number }>;
const stale = recorded.filter(
(entry): entry is { name: string; pid: number } =>
typeof entry?.name === "string" && typeof entry?.pid === "number" && isPidRunning(entry.pid)
);
if (stale.length > 0) {
console.log(
`[dev:desktop] Cleaning up stale processes from previous run: ${stale
.map((entry) => `${entry.name}(${entry.pid})`)
.join(", ")}`
);
}
for (const entry of stale) {
await stopPid(entry.pid, 3000);
}
} catch {
// No persisted children from a prior run.
} finally {
await clearPersistedChildren();
}
};
const spawnChild = ({ name, cmd, cwd, env }: ChildSpec): void => {
const proc = Bun.spawn(cmd, {
cwd,
detached: true,
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
env: {
...Bun.env,
...env
}
});
children.push({ name, process: proc });
void persistChildren();
proc.exited.then((code) => {
if (shuttingDown) {
return;
}
const exitCode = code ?? 0;
const statusLabel = exitCode === 0 ? "exited" : "failed";
console.error(`[dev:desktop] ${name} ${statusLabel} (${exitCode})`);
void shutdown(exitCode);
});
};
const shutdown = async (code: number): Promise<void> => {
if (shutdownPromise) {
return shutdownPromise;
}
shuttingDown = true;
shutdownPromise = (async () => {
await Promise.all(children.map((child) => stopChild(child)));
await clearPersistedChildren();
process.exit(code);
})();
return shutdownPromise;
};
const forceShutdown = async (code: number): Promise<void> => {
if (forceShutdownPromise) {
return forceShutdownPromise;
}
shuttingDown = true;
forceShutdownPromise = (async () => {
await Promise.all(
children.map(async (child) => {
const pid = child.process.pid;
if (!pid) {
return;
}
if (!signalProcess(pid, "SIGKILL")) {
return;
}
await waitForPidExit(pid, 2000);
})
);
await clearPersistedChildren();
process.exit(code);
})();
return forceShutdownPromise;
};
const handleSignal = (signal: NodeJS.Signals) => {
if (shuttingDown) {
if (signal === "SIGINT") {
console.error("[dev:desktop] Force shutdown requested. Terminating remaining processes.");
void forceShutdown(130);
}
return;
}
void shutdown(0);
};
const checkTcp = (host: string, port: number, timeoutMs = 1000): Promise<boolean> => {
return new Promise((resolve) => {
const socket = net.connect({ host, port });
const finalize = (ok: boolean) => {
socket.removeAllListeners();
socket.destroy();
resolve(ok);
};
socket.setTimeout(timeoutMs);
socket.once("connect", () => finalize(true));
socket.once("error", () => finalize(false));
socket.once("timeout", () => finalize(false));
});
};
const waitForWebPort = async (): Promise<void> => {
const deadline = Date.now() + 90_000;
let lastLog = 0;
while (Date.now() < deadline) {
if (await checkTcp("127.0.0.1", WEB_PORT)) {
console.log(`[dev:desktop] Web UI ready on ${DESKTOP_LOCAL_URL}`);
return;
}
const now = Date.now();
if (now - lastLog > 5000) {
console.log(`[dev:desktop] Waiting for local web UI on ${DESKTOP_LOCAL_URL}...`);
lastLog = now;
}
await sleep(1000);
}
console.error("[dev:desktop] Web UI did not open port 3000 within 90s.");
void shutdown(1);
};
process.on("SIGINT", () => handleSignal("SIGINT"));
process.on("SIGTERM", () => handleSignal("SIGTERM"));
process.on("SIGHUP", () => handleSignal("SIGHUP"));
await cleanupStaleChildren();
if (!remoteMode) {
spawnChild({
name: "web",
cmd: ["bun", "run", "dev"],
cwd: "apps/web",
env: {
NEXT_PUBLIC_API_URL: Bun.env.NEXT_PUBLIC_API_URL ?? DESKTOP_REMOTE_URL
}
});
await waitForWebPort();
}
spawnChild({
name: "desktop",
cmd: ["bun", "run", "start"],
cwd: "apps/desktop",
env: {
ISLANDFLOW_DESKTOP_START_URL: remoteMode ? DESKTOP_REMOTE_URL : DESKTOP_LOCAL_URL
}
});
await new Promise(() => {});