shipwright-cli 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/.claude/agents/code-reviewer.md +2 -0
  2. package/.claude/agents/devops-engineer.md +2 -0
  3. package/.claude/agents/doc-fleet-agent.md +2 -0
  4. package/.claude/agents/pipeline-agent.md +2 -0
  5. package/.claude/agents/shell-script-specialist.md +2 -0
  6. package/.claude/agents/test-specialist.md +2 -0
  7. package/.claude/hooks/agent-crash-capture.sh +32 -0
  8. package/.claude/hooks/post-tool-use.sh +3 -2
  9. package/.claude/hooks/pre-tool-use.sh +35 -3
  10. package/README.md +4 -4
  11. package/claude-code/hooks/config-change.sh +18 -0
  12. package/claude-code/hooks/instructions-reloaded.sh +7 -0
  13. package/claude-code/hooks/worktree-create.sh +25 -0
  14. package/claude-code/hooks/worktree-remove.sh +20 -0
  15. package/config/code-constitution.json +130 -0
  16. package/dashboard/middleware/auth.ts +134 -0
  17. package/dashboard/middleware/constants.ts +21 -0
  18. package/dashboard/public/index.html +2 -6
  19. package/dashboard/public/styles.css +100 -97
  20. package/dashboard/routes/auth.ts +38 -0
  21. package/dashboard/server.ts +66 -25
  22. package/dashboard/services/config.ts +26 -0
  23. package/dashboard/services/db.ts +118 -0
  24. package/dashboard/src/canvas/pixel-agent.ts +298 -0
  25. package/dashboard/src/canvas/pixel-sprites.ts +440 -0
  26. package/dashboard/src/canvas/shipyard-effects.ts +367 -0
  27. package/dashboard/src/canvas/shipyard-scene.ts +616 -0
  28. package/dashboard/src/canvas/submarine-layout.ts +267 -0
  29. package/dashboard/src/components/header.ts +8 -7
  30. package/dashboard/src/core/router.ts +1 -0
  31. package/dashboard/src/design/submarine-theme.ts +253 -0
  32. package/dashboard/src/main.ts +2 -0
  33. package/dashboard/src/types/api.ts +2 -1
  34. package/dashboard/src/views/activity.ts +2 -1
  35. package/dashboard/src/views/shipyard.ts +39 -0
  36. package/dashboard/types/index.ts +166 -0
  37. package/docs/plans/2026-02-28-compound-audit-and-shipyard-design.md +186 -0
  38. package/docs/plans/2026-02-28-skipper-shipwright-implementation-plan.md +1182 -0
  39. package/docs/plans/2026-02-28-skipper-shipwright-integration-design.md +531 -0
  40. package/docs/plans/2026-03-01-ai-powered-skill-injection-design.md +298 -0
  41. package/docs/plans/2026-03-01-ai-powered-skill-injection-plan.md +1109 -0
  42. package/docs/plans/2026-03-01-capabilities-cleanup-plan.md +658 -0
  43. package/docs/plans/2026-03-01-clean-architecture-plan.md +924 -0
  44. package/docs/plans/2026-03-01-compound-audit-cascade-design.md +191 -0
  45. package/docs/plans/2026-03-01-compound-audit-cascade-plan.md +921 -0
  46. package/docs/plans/2026-03-01-deep-integration-plan.md +851 -0
  47. package/docs/plans/2026-03-01-pipeline-audit-trail-design.md +145 -0
  48. package/docs/plans/2026-03-01-pipeline-audit-trail-plan.md +770 -0
  49. package/docs/plans/2026-03-01-refined-depths-brand-design.md +382 -0
  50. package/docs/plans/2026-03-01-refined-depths-implementation.md +599 -0
  51. package/docs/plans/2026-03-01-skipper-kernel-integration-design.md +203 -0
  52. package/docs/plans/2026-03-01-unified-platform-design.md +272 -0
  53. package/docs/plans/2026-03-07-claude-code-feature-integration-design.md +189 -0
  54. package/docs/plans/2026-03-07-claude-code-feature-integration-plan.md +1165 -0
  55. package/docs/research/BACKLOG_QUICK_REFERENCE.md +352 -0
  56. package/docs/research/CUTTING_EDGE_RESEARCH_2026.md +546 -0
  57. package/docs/research/RESEARCH_INDEX.md +439 -0
  58. package/docs/research/RESEARCH_SOURCES.md +440 -0
  59. package/docs/research/RESEARCH_SUMMARY.txt +275 -0
  60. package/docs/superpowers/specs/2026-03-10-pipeline-quality-revolution-design.md +341 -0
  61. package/package.json +2 -2
  62. package/scripts/lib/adaptive-model.sh +427 -0
  63. package/scripts/lib/adaptive-timeout.sh +316 -0
  64. package/scripts/lib/audit-trail.sh +309 -0
  65. package/scripts/lib/auto-recovery.sh +471 -0
  66. package/scripts/lib/bandit-selector.sh +431 -0
  67. package/scripts/lib/bootstrap.sh +104 -2
  68. package/scripts/lib/causal-graph.sh +455 -0
  69. package/scripts/lib/compat.sh +126 -0
  70. package/scripts/lib/compound-audit.sh +337 -0
  71. package/scripts/lib/constitutional.sh +454 -0
  72. package/scripts/lib/context-budget.sh +359 -0
  73. package/scripts/lib/convergence.sh +594 -0
  74. package/scripts/lib/cost-optimizer.sh +634 -0
  75. package/scripts/lib/daemon-adaptive.sh +10 -0
  76. package/scripts/lib/daemon-dispatch.sh +106 -17
  77. package/scripts/lib/daemon-failure.sh +34 -4
  78. package/scripts/lib/daemon-patrol.sh +23 -2
  79. package/scripts/lib/daemon-poll-github.sh +361 -0
  80. package/scripts/lib/daemon-poll-health.sh +299 -0
  81. package/scripts/lib/daemon-poll.sh +27 -611
  82. package/scripts/lib/daemon-state.sh +112 -66
  83. package/scripts/lib/daemon-triage.sh +10 -0
  84. package/scripts/lib/dod-scorecard.sh +442 -0
  85. package/scripts/lib/error-actionability.sh +300 -0
  86. package/scripts/lib/formal-spec.sh +461 -0
  87. package/scripts/lib/helpers.sh +177 -4
  88. package/scripts/lib/intent-analysis.sh +409 -0
  89. package/scripts/lib/loop-convergence.sh +350 -0
  90. package/scripts/lib/loop-iteration.sh +682 -0
  91. package/scripts/lib/loop-progress.sh +48 -0
  92. package/scripts/lib/loop-restart.sh +185 -0
  93. package/scripts/lib/memory-effectiveness.sh +506 -0
  94. package/scripts/lib/mutation-executor.sh +352 -0
  95. package/scripts/lib/outcome-feedback.sh +521 -0
  96. package/scripts/lib/pipeline-cli.sh +336 -0
  97. package/scripts/lib/pipeline-commands.sh +1216 -0
  98. package/scripts/lib/pipeline-detection.sh +100 -2
  99. package/scripts/lib/pipeline-execution.sh +897 -0
  100. package/scripts/lib/pipeline-github.sh +28 -3
  101. package/scripts/lib/pipeline-intelligence-compound.sh +431 -0
  102. package/scripts/lib/pipeline-intelligence-scoring.sh +407 -0
  103. package/scripts/lib/pipeline-intelligence-skip.sh +181 -0
  104. package/scripts/lib/pipeline-intelligence.sh +100 -1136
  105. package/scripts/lib/pipeline-quality-bash-compat.sh +182 -0
  106. package/scripts/lib/pipeline-quality-checks.sh +17 -715
  107. package/scripts/lib/pipeline-quality-gates.sh +563 -0
  108. package/scripts/lib/pipeline-stages-build.sh +730 -0
  109. package/scripts/lib/pipeline-stages-delivery.sh +965 -0
  110. package/scripts/lib/pipeline-stages-intake.sh +1133 -0
  111. package/scripts/lib/pipeline-stages-monitor.sh +407 -0
  112. package/scripts/lib/pipeline-stages-review.sh +1022 -0
  113. package/scripts/lib/pipeline-stages.sh +59 -2929
  114. package/scripts/lib/pipeline-state.sh +36 -5
  115. package/scripts/lib/pipeline-util.sh +487 -0
  116. package/scripts/lib/policy-learner.sh +438 -0
  117. package/scripts/lib/process-reward.sh +493 -0
  118. package/scripts/lib/project-detect.sh +649 -0
  119. package/scripts/lib/quality-profile.sh +334 -0
  120. package/scripts/lib/recruit-commands.sh +885 -0
  121. package/scripts/lib/recruit-learning.sh +739 -0
  122. package/scripts/lib/recruit-roles.sh +648 -0
  123. package/scripts/lib/reward-aggregator.sh +458 -0
  124. package/scripts/lib/rl-optimizer.sh +362 -0
  125. package/scripts/lib/root-cause.sh +427 -0
  126. package/scripts/lib/scope-enforcement.sh +445 -0
  127. package/scripts/lib/session-restart.sh +493 -0
  128. package/scripts/lib/skill-memory.sh +300 -0
  129. package/scripts/lib/skill-registry.sh +775 -0
  130. package/scripts/lib/spec-driven.sh +476 -0
  131. package/scripts/lib/test-helpers.sh +18 -7
  132. package/scripts/lib/test-holdout.sh +429 -0
  133. package/scripts/lib/test-optimizer.sh +511 -0
  134. package/scripts/shipwright-file-suggest.sh +45 -0
  135. package/scripts/skills/adversarial-quality.md +61 -0
  136. package/scripts/skills/api-design.md +44 -0
  137. package/scripts/skills/architecture-design.md +50 -0
  138. package/scripts/skills/brainstorming.md +43 -0
  139. package/scripts/skills/data-pipeline.md +44 -0
  140. package/scripts/skills/deploy-safety.md +64 -0
  141. package/scripts/skills/documentation.md +38 -0
  142. package/scripts/skills/frontend-design.md +45 -0
  143. package/scripts/skills/generated/.gitkeep +0 -0
  144. package/scripts/skills/generated/_refinements/.gitkeep +0 -0
  145. package/scripts/skills/generated/_refinements/adversarial-quality.patch.md +3 -0
  146. package/scripts/skills/generated/_refinements/architecture-design.patch.md +3 -0
  147. package/scripts/skills/generated/_refinements/brainstorming.patch.md +3 -0
  148. package/scripts/skills/generated/cli-version-management.md +29 -0
  149. package/scripts/skills/generated/collection-system-validation.md +99 -0
  150. package/scripts/skills/generated/large-scale-c-refactoring-coordination.md +97 -0
  151. package/scripts/skills/generated/pattern-matching-similarity-scoring.md +195 -0
  152. package/scripts/skills/generated/test-parallelization-detection.md +65 -0
  153. package/scripts/skills/observability.md +79 -0
  154. package/scripts/skills/performance.md +48 -0
  155. package/scripts/skills/pr-quality.md +49 -0
  156. package/scripts/skills/product-thinking.md +43 -0
  157. package/scripts/skills/security-audit.md +49 -0
  158. package/scripts/skills/systematic-debugging.md +40 -0
  159. package/scripts/skills/testing-strategy.md +47 -0
  160. package/scripts/skills/two-stage-review.md +52 -0
  161. package/scripts/skills/validation-thoroughness.md +55 -0
  162. package/scripts/sw +9 -3
  163. package/scripts/sw-activity.sh +9 -2
  164. package/scripts/sw-adaptive.sh +2 -1
  165. package/scripts/sw-adversarial.sh +2 -1
  166. package/scripts/sw-architecture-enforcer.sh +3 -1
  167. package/scripts/sw-auth.sh +12 -2
  168. package/scripts/sw-autonomous.sh +5 -1
  169. package/scripts/sw-changelog.sh +4 -1
  170. package/scripts/sw-checkpoint.sh +2 -1
  171. package/scripts/sw-ci.sh +5 -1
  172. package/scripts/sw-cleanup.sh +4 -26
  173. package/scripts/sw-code-review.sh +10 -4
  174. package/scripts/sw-connect.sh +2 -1
  175. package/scripts/sw-context.sh +2 -1
  176. package/scripts/sw-cost.sh +48 -3
  177. package/scripts/sw-daemon.sh +66 -9
  178. package/scripts/sw-dashboard.sh +3 -1
  179. package/scripts/sw-db.sh +59 -16
  180. package/scripts/sw-decide.sh +8 -2
  181. package/scripts/sw-decompose.sh +360 -17
  182. package/scripts/sw-deps.sh +4 -1
  183. package/scripts/sw-developer-simulation.sh +4 -1
  184. package/scripts/sw-discovery.sh +325 -2
  185. package/scripts/sw-doc-fleet.sh +4 -1
  186. package/scripts/sw-docs-agent.sh +3 -1
  187. package/scripts/sw-docs.sh +2 -1
  188. package/scripts/sw-doctor.sh +453 -2
  189. package/scripts/sw-dora.sh +4 -1
  190. package/scripts/sw-durable.sh +4 -3
  191. package/scripts/sw-e2e-orchestrator.sh +17 -16
  192. package/scripts/sw-eventbus.sh +7 -1
  193. package/scripts/sw-evidence.sh +364 -12
  194. package/scripts/sw-feedback.sh +550 -9
  195. package/scripts/sw-fix.sh +20 -1
  196. package/scripts/sw-fleet-discover.sh +6 -2
  197. package/scripts/sw-fleet-viz.sh +4 -1
  198. package/scripts/sw-fleet.sh +5 -1
  199. package/scripts/sw-github-app.sh +16 -3
  200. package/scripts/sw-github-checks.sh +3 -2
  201. package/scripts/sw-github-deploy.sh +3 -2
  202. package/scripts/sw-github-graphql.sh +18 -7
  203. package/scripts/sw-guild.sh +5 -1
  204. package/scripts/sw-heartbeat.sh +5 -30
  205. package/scripts/sw-hello.sh +67 -0
  206. package/scripts/sw-hygiene.sh +6 -1
  207. package/scripts/sw-incident.sh +265 -1
  208. package/scripts/sw-init.sh +18 -2
  209. package/scripts/sw-instrument.sh +10 -2
  210. package/scripts/sw-intelligence.sh +42 -6
  211. package/scripts/sw-jira.sh +5 -1
  212. package/scripts/sw-launchd.sh +2 -1
  213. package/scripts/sw-linear.sh +4 -1
  214. package/scripts/sw-logs.sh +4 -1
  215. package/scripts/sw-loop.sh +432 -1128
  216. package/scripts/sw-memory.sh +356 -2
  217. package/scripts/sw-mission-control.sh +6 -1
  218. package/scripts/sw-model-router.sh +481 -26
  219. package/scripts/sw-otel.sh +13 -4
  220. package/scripts/sw-oversight.sh +14 -5
  221. package/scripts/sw-patrol-meta.sh +334 -0
  222. package/scripts/sw-pipeline-composer.sh +5 -1
  223. package/scripts/sw-pipeline-vitals.sh +2 -1
  224. package/scripts/sw-pipeline.sh +53 -2664
  225. package/scripts/sw-pm.sh +12 -5
  226. package/scripts/sw-pr-lifecycle.sh +2 -1
  227. package/scripts/sw-predictive.sh +7 -1
  228. package/scripts/sw-prep.sh +185 -2
  229. package/scripts/sw-ps.sh +5 -25
  230. package/scripts/sw-public-dashboard.sh +15 -3
  231. package/scripts/sw-quality.sh +2 -1
  232. package/scripts/sw-reaper.sh +8 -25
  233. package/scripts/sw-recruit.sh +156 -2303
  234. package/scripts/sw-regression.sh +19 -12
  235. package/scripts/sw-release-manager.sh +3 -1
  236. package/scripts/sw-release.sh +4 -1
  237. package/scripts/sw-remote.sh +3 -1
  238. package/scripts/sw-replay.sh +7 -1
  239. package/scripts/sw-retro.sh +158 -1
  240. package/scripts/sw-review-rerun.sh +3 -1
  241. package/scripts/sw-scale.sh +10 -3
  242. package/scripts/sw-security-audit.sh +6 -1
  243. package/scripts/sw-self-optimize.sh +6 -3
  244. package/scripts/sw-session.sh +9 -3
  245. package/scripts/sw-setup.sh +3 -1
  246. package/scripts/sw-stall-detector.sh +406 -0
  247. package/scripts/sw-standup.sh +15 -7
  248. package/scripts/sw-status.sh +3 -1
  249. package/scripts/sw-strategic.sh +4 -1
  250. package/scripts/sw-stream.sh +7 -1
  251. package/scripts/sw-swarm.sh +18 -6
  252. package/scripts/sw-team-stages.sh +13 -6
  253. package/scripts/sw-templates.sh +5 -29
  254. package/scripts/sw-testgen.sh +7 -1
  255. package/scripts/sw-tmux-pipeline.sh +4 -1
  256. package/scripts/sw-tmux-role-color.sh +2 -0
  257. package/scripts/sw-tmux-status.sh +1 -1
  258. package/scripts/sw-tmux.sh +3 -1
  259. package/scripts/sw-trace.sh +3 -1
  260. package/scripts/sw-tracker-github.sh +3 -0
  261. package/scripts/sw-tracker-jira.sh +3 -0
  262. package/scripts/sw-tracker-linear.sh +3 -0
  263. package/scripts/sw-tracker.sh +3 -1
  264. package/scripts/sw-triage.sh +2 -1
  265. package/scripts/sw-upgrade.sh +3 -1
  266. package/scripts/sw-ux.sh +5 -2
  267. package/scripts/sw-webhook.sh +3 -1
  268. package/scripts/sw-widgets.sh +3 -1
  269. package/scripts/sw-worktree.sh +15 -3
  270. package/scripts/test-skill-injection.sh +1233 -0
  271. package/templates/pipelines/autonomous.json +27 -3
  272. package/templates/pipelines/cost-aware.json +34 -8
  273. package/templates/pipelines/deployed.json +12 -0
  274. package/templates/pipelines/enterprise.json +12 -0
  275. package/templates/pipelines/fast.json +6 -0
  276. package/templates/pipelines/full.json +27 -3
  277. package/templates/pipelines/hotfix.json +6 -0
  278. package/templates/pipelines/standard.json +12 -0
  279. package/templates/pipelines/tdd.json +12 -0
@@ -8,39 +8,47 @@
8
8
  }
9
9
 
10
10
  :root {
11
- --abyss: #060a14;
12
- --deep: #0a1628;
13
- --ocean: #0d1f3c;
14
- --surface: #132d56;
15
- --foam: #1a3a6a;
16
-
11
+ /* Ocean Depths — quieted */
12
+ --abyss: #050508;
13
+ --deep: #0a0d14;
14
+ --ocean: #111520;
15
+ --surface: #171c28;
16
+ --foam: #1e2536;
17
+
18
+ /* Accent — cyan primary */
17
19
  --cyan: #00d4ff;
18
- --cyan-glow: rgba(0, 212, 255, 0.15);
19
- --cyan-dim: rgba(0, 212, 255, 0.4);
20
+ --cyan-subtle: rgba(0, 212, 255, 0.08);
21
+ --cyan-muted: rgba(0, 212, 255, 0.25);
20
22
  --purple: #7c3aed;
21
- --purple-glow: rgba(124, 58, 237, 0.15);
22
23
  --blue: #0066ff;
23
24
 
25
+ /* Status */
24
26
  --green: #4ade80;
25
27
  --amber: #fbbf24;
26
28
  --rose: #f43f5e;
27
29
 
28
- --text-primary: #e8ecf4;
29
- --text-secondary: #8899b8;
30
- --text-muted: #5a6d8a;
31
-
32
- --card-bg: rgba(10, 22, 40, 0.8);
33
- --card-border: rgba(0, 212, 255, 0.08);
34
- --card-hover-border: rgba(0, 212, 255, 0.2);
35
- --card-radius: 16px;
36
-
37
- --font-display: "Instrument Serif", Georgia, serif;
38
- --font-body: "Plus Jakarta Sans", system-ui, sans-serif;
39
- --font-mono: "JetBrains Mono", "SF Mono", monospace;
40
-
30
+ /* Text */
31
+ --text-primary: #e8eaed;
32
+ --text-secondary: #8b8f9a;
33
+ --text-muted: #555a66;
34
+
35
+ /* Cards */
36
+ --card-bg: rgba(10, 13, 20, 0.8);
37
+ --card-border: rgba(255, 255, 255, 0.06);
38
+ --card-hover-border: rgba(0, 212, 255, 0.15);
39
+ --card-radius: 14px;
40
+
41
+ /* Typography system fonts */
42
+ --font-body:
43
+ -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, "Helvetica Neue",
44
+ sans-serif;
45
+ --font-mono:
46
+ "SF Mono", ui-monospace, "Cascadia Code", "Geist Mono", monospace;
47
+
48
+ /* Transitions */
41
49
  --transition-fast: 0.15s ease;
42
- --transition-base: 0.3s ease;
43
- --transition-slow: 0.5s ease;
50
+ --transition-base: 0.25s ease;
51
+ --transition-slow: 0.4s ease;
44
52
 
45
53
  /* Spacing scale */
46
54
  --space-1: 4px;
@@ -55,18 +63,17 @@
55
63
  --space-16: 64px;
56
64
 
57
65
  /* Border radius */
58
- --radius-sm: 4px;
59
- --radius-md: 8px;
60
- --radius-lg: 12px;
61
- --radius-xl: 16px;
66
+ --radius-sm: 6px;
67
+ --radius-md: 10px;
68
+ --radius-lg: 14px;
69
+ --radius-xl: 20px;
62
70
  --radius-full: 9999px;
63
71
 
64
72
  /* Shadows */
65
- --shadow-glow-cyan: 0 0 20px rgba(0, 212, 255, 0.15);
66
- --shadow-glow-purple: 0 0 20px rgba(124, 58, 237, 0.15);
67
- --shadow-glow-success: 0 0 12px rgba(74, 222, 128, 0.2);
68
- --shadow-glow-error: 0 0 12px rgba(244, 63, 94, 0.2);
69
- --shadow-elevated: 0 8px 32px rgba(0, 0, 0, 0.4);
73
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.1);
74
+ --shadow-md: 0 4px 8px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.1);
75
+ --shadow-lg: 0 12px 24px rgba(0, 0, 0, 0.4), 0 0 1px rgba(0, 0, 0, 0.1);
76
+ --shadow-elevated: 0 24px 48px rgba(0, 0, 0, 0.5), 0 0 1px rgba(0, 0, 0, 0.1);
70
77
 
71
78
  /* Z-index */
72
79
  --z-base: 1;
@@ -78,46 +85,42 @@
78
85
 
79
86
  /* Easing */
80
87
  --ease-smooth: cubic-bezier(0.4, 0, 0.2, 1);
81
- --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
88
+ --ease-spring: cubic-bezier(0.34, 1.2, 0.64, 1);
82
89
  }
83
90
 
84
91
  /* ── Light Mode ───────────────────────────────── */
85
92
  :root[data-theme="light"] {
86
- --abyss: #f5f7fa;
87
- --deep: #ebeef3;
88
- --ocean: #dde2ea;
89
- --surface: #cdd4de;
90
- --foam: #bcc5d3;
91
-
92
- --cyan: #0077b6;
93
- --cyan-glow: rgba(0, 119, 182, 0.1);
94
- --cyan-dim: rgba(0, 119, 182, 0.3);
93
+ --abyss: #f8f9fa;
94
+ --deep: #ffffff;
95
+ --ocean: #f1f3f5;
96
+ --surface: #e9ecef;
97
+ --foam: #dee2e6;
98
+
99
+ --cyan: #0091b3;
100
+ --cyan-subtle: rgba(0, 145, 179, 0.08);
101
+ --cyan-muted: rgba(0, 145, 179, 0.25);
95
102
  --purple: #6d28d9;
96
- --purple-glow: rgba(109, 40, 217, 0.1);
97
103
  --blue: #0055cc;
98
104
 
99
105
  --green: #16a34a;
100
106
  --amber: #d97706;
101
107
  --rose: #dc2626;
102
108
 
103
- --text-primary: #1a1a2e;
104
- --text-secondary: #475569;
105
- --text-muted: #94a3b8;
109
+ --text-primary: #1a1d21;
110
+ --text-secondary: #495057;
111
+ --text-muted: #868e96;
106
112
 
107
113
  --card-bg: rgba(255, 255, 255, 0.9);
108
- --card-border: rgba(0, 119, 182, 0.12);
109
- --card-hover-border: rgba(0, 119, 182, 0.25);
114
+ --card-border: rgba(0, 0, 0, 0.06);
115
+ --card-hover-border: rgba(0, 145, 179, 0.15);
110
116
 
111
- --shadow-elevated: 0 4px 16px rgba(0, 0, 0, 0.08);
112
- --shadow-glow-cyan: 0 0 12px rgba(0, 119, 182, 0.1);
113
- --shadow-glow-purple: 0 0 12px rgba(109, 40, 217, 0.1);
114
- --shadow-glow-success: 0 0 8px rgba(22, 163, 74, 0.15);
115
- --shadow-glow-error: 0 0 8px rgba(220, 38, 38, 0.15);
117
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08), 0 0 1px rgba(0, 0, 0, 0.04);
118
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
119
+ --shadow-lg: 0 12px 28px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.04);
120
+ --shadow-elevated:
121
+ 0 24px 48px rgba(0, 0, 0, 0.12), 0 8px 16px rgba(0, 0, 0, 0.06);
116
122
 
117
- --bg-abyss: #f5f7fa;
118
- --bg-deep: #ebeef3;
119
- --bg-foam: #dde2ea;
120
- --border: rgba(0, 0, 0, 0.1);
123
+ --border: rgba(0, 0, 0, 0.08);
121
124
  }
122
125
 
123
126
  ::selection {
@@ -136,7 +139,7 @@ body {
136
139
  overflow-x: hidden;
137
140
  -webkit-font-smoothing: antialiased;
138
141
  -moz-osx-font-smoothing: grayscale;
139
- scrollbar-color: var(--cyan-dim) var(--deep);
142
+ scrollbar-color: var(--cyan-muted) var(--deep);
140
143
  }
141
144
 
142
145
  body::-webkit-scrollbar {
@@ -146,7 +149,7 @@ body::-webkit-scrollbar-track {
146
149
  background: var(--deep);
147
150
  }
148
151
  body::-webkit-scrollbar-thumb {
149
- background: var(--cyan-dim);
152
+ background: var(--cyan-muted);
150
153
  border-radius: 4px;
151
154
  }
152
155
 
@@ -184,7 +187,7 @@ body::-webkit-scrollbar-thumb {
184
187
  }
185
188
 
186
189
  .header-title {
187
- font-family: var(--font-display);
190
+ font-family: var(--font-body);
188
191
  font-size: 1.25rem;
189
192
  color: var(--text-primary);
190
193
  line-height: 1;
@@ -265,7 +268,7 @@ body::-webkit-scrollbar-thumb {
265
268
 
266
269
  .user-avatar:hover {
267
270
  border-color: var(--card-hover-border);
268
- box-shadow: 0 0 12px var(--cyan-glow);
271
+ box-shadow: 0 0 12px var(--cyan-subtle);
269
272
  }
270
273
 
271
274
  .user-avatar img {
@@ -460,7 +463,7 @@ body::-webkit-scrollbar-thumb {
460
463
  }
461
464
 
462
465
  .stat-value {
463
- font-family: var(--font-display);
466
+ font-family: var(--font-body);
464
467
  font-size: 1.75rem;
465
468
  font-weight: 400;
466
469
  line-height: 1.2;
@@ -661,7 +664,7 @@ body::-webkit-scrollbar-thumb {
661
664
  @keyframes stage-glow {
662
665
  0%,
663
666
  100% {
664
- box-shadow: 0 0 6px var(--cyan-glow);
667
+ box-shadow: 0 0 6px var(--cyan-subtle);
665
668
  }
666
669
  50% {
667
670
  box-shadow: 0 0 14px rgba(0, 212, 255, 0.35);
@@ -775,7 +778,7 @@ body::-webkit-scrollbar-thumb {
775
778
  .activity-feed {
776
779
  max-height: 400px;
777
780
  overflow-y: auto;
778
- scrollbar-color: var(--cyan-dim) var(--deep);
781
+ scrollbar-color: var(--cyan-muted) var(--deep);
779
782
  }
780
783
 
781
784
  .activity-feed::-webkit-scrollbar {
@@ -785,7 +788,7 @@ body::-webkit-scrollbar-thumb {
785
788
  background: transparent;
786
789
  }
787
790
  .activity-feed::-webkit-scrollbar-thumb {
788
- background: var(--cyan-dim);
791
+ background: var(--cyan-muted);
789
792
  border-radius: 3px;
790
793
  }
791
794
 
@@ -802,7 +805,7 @@ body::-webkit-scrollbar-thumb {
802
805
  }
803
806
 
804
807
  .activity-row:hover {
805
- border-left-color: var(--cyan-glow);
808
+ border-left-color: var(--cyan-subtle);
806
809
  background: rgba(0, 212, 255, 0.02);
807
810
  }
808
811
 
@@ -1022,7 +1025,7 @@ body::-webkit-scrollbar-thumb {
1022
1025
  .issue-filter-input:focus {
1023
1026
  outline: none;
1024
1027
  border-color: var(--cyan);
1025
- box-shadow: 0 0 12px var(--cyan-glow);
1028
+ box-shadow: 0 0 12px var(--cyan-subtle);
1026
1029
  }
1027
1030
 
1028
1031
  /* ── Segmented Control ────────────────────────── */
@@ -1267,7 +1270,7 @@ body::-webkit-scrollbar-thumb {
1267
1270
  .stage-timeline-dot.active {
1268
1271
  background: var(--cyan);
1269
1272
  border-color: var(--cyan);
1270
- box-shadow: 0 0 8px var(--cyan-glow);
1273
+ box-shadow: 0 0 8px var(--cyan-subtle);
1271
1274
  }
1272
1275
  .stage-timeline-dot.failed {
1273
1276
  background: var(--rose);
@@ -1325,7 +1328,7 @@ body::-webkit-scrollbar-thumb {
1325
1328
  background: transparent;
1326
1329
  }
1327
1330
  .detail-plan-content::-webkit-scrollbar-thumb {
1328
- background: var(--cyan-dim);
1331
+ background: var(--cyan-muted);
1329
1332
  border-radius: 3px;
1330
1333
  }
1331
1334
 
@@ -1359,7 +1362,7 @@ body::-webkit-scrollbar-thumb {
1359
1362
  .activity-timeline {
1360
1363
  max-height: calc(100vh - 260px);
1361
1364
  overflow-y: auto;
1362
- scrollbar-color: var(--cyan-dim) var(--deep);
1365
+ scrollbar-color: var(--cyan-muted) var(--deep);
1363
1366
  }
1364
1367
 
1365
1368
  .activity-timeline::-webkit-scrollbar {
@@ -1369,7 +1372,7 @@ body::-webkit-scrollbar-thumb {
1369
1372
  background: transparent;
1370
1373
  }
1371
1374
  .activity-timeline::-webkit-scrollbar-thumb {
1372
- background: var(--cyan-dim);
1375
+ background: var(--cyan-muted);
1373
1376
  border-radius: 3px;
1374
1377
  }
1375
1378
 
@@ -1388,7 +1391,7 @@ body::-webkit-scrollbar-thumb {
1388
1391
 
1389
1392
  .timeline-row:hover {
1390
1393
  background: rgba(0, 212, 255, 0.03);
1391
- border-left-color: var(--cyan-glow);
1394
+ border-left-color: var(--cyan-subtle);
1392
1395
  }
1393
1396
 
1394
1397
  .timeline-ts {
@@ -1502,7 +1505,7 @@ body::-webkit-scrollbar-thumb {
1502
1505
  }
1503
1506
 
1504
1507
  .metric-big-value {
1505
- font-family: var(--font-display);
1508
+ font-family: var(--font-body);
1506
1509
  font-size: 2rem;
1507
1510
  font-weight: 400;
1508
1511
  line-height: 1.2;
@@ -1552,7 +1555,7 @@ body::-webkit-scrollbar-thumb {
1552
1555
  .donut-value {
1553
1556
  position: relative;
1554
1557
  z-index: 1;
1555
- font-family: var(--font-display);
1558
+ font-family: var(--font-body);
1556
1559
  font-size: 1.5rem;
1557
1560
  font-weight: 400;
1558
1561
  color: var(--text-primary);
@@ -1596,7 +1599,7 @@ body::-webkit-scrollbar-thumb {
1596
1599
  }
1597
1600
 
1598
1601
  .svg-donut text {
1599
- font-family: var(--font-display);
1602
+ font-family: var(--font-body);
1600
1603
  fill: var(--text-primary);
1601
1604
  text-anchor: middle;
1602
1605
  dominant-baseline: central;
@@ -2304,7 +2307,7 @@ body::-webkit-scrollbar-thumb {
2304
2307
  .modal-textarea:focus {
2305
2308
  outline: none;
2306
2309
  border-color: var(--cyan);
2307
- box-shadow: 0 0 12px var(--cyan-glow);
2310
+ box-shadow: 0 0 12px var(--cyan-subtle);
2308
2311
  }
2309
2312
 
2310
2313
  .modal-footer {
@@ -2485,7 +2488,7 @@ body::-webkit-scrollbar-thumb {
2485
2488
  @keyframes stage-node-pulse {
2486
2489
  0%,
2487
2490
  100% {
2488
- filter: drop-shadow(0 0 4px var(--cyan-glow));
2491
+ filter: drop-shadow(0 0 4px var(--cyan-subtle));
2489
2492
  }
2490
2493
  50% {
2491
2494
  filter: drop-shadow(0 0 12px rgba(0, 212, 255, 0.4));
@@ -2515,7 +2518,7 @@ body::-webkit-scrollbar-thumb {
2515
2518
  width: 48px;
2516
2519
  height: 48px;
2517
2520
  border-radius: 12px;
2518
- font-family: var(--font-display);
2521
+ font-family: var(--font-body);
2519
2522
  font-size: 1.25rem;
2520
2523
  font-weight: 400;
2521
2524
  letter-spacing: 0.02em;
@@ -2676,7 +2679,7 @@ body::-webkit-scrollbar-thumb {
2676
2679
 
2677
2680
  /* ── KPI Cards ────────────────────────────────── */
2678
2681
  .kpi-value {
2679
- font-family: var(--font-display);
2682
+ font-family: var(--font-body);
2680
2683
  font-size: 2.5rem;
2681
2684
  font-weight: 400;
2682
2685
  line-height: 1;
@@ -2762,7 +2765,7 @@ body::-webkit-scrollbar-thumb {
2762
2765
  .artifact-content {
2763
2766
  max-height: 400px;
2764
2767
  overflow-y: auto;
2765
- scrollbar-color: var(--cyan-dim) var(--deep);
2768
+ scrollbar-color: var(--cyan-muted) var(--deep);
2766
2769
  }
2767
2770
 
2768
2771
  .artifact-content::-webkit-scrollbar {
@@ -2772,7 +2775,7 @@ body::-webkit-scrollbar-thumb {
2772
2775
  background: transparent;
2773
2776
  }
2774
2777
  .artifact-content::-webkit-scrollbar-thumb {
2775
- background: var(--cyan-dim);
2778
+ background: var(--cyan-muted);
2776
2779
  border-radius: 3px;
2777
2780
  }
2778
2781
 
@@ -2790,7 +2793,7 @@ body::-webkit-scrollbar-thumb {
2790
2793
  overflow-y: auto;
2791
2794
  white-space: pre-wrap;
2792
2795
  word-break: break-all;
2793
- scrollbar-color: var(--cyan-dim) var(--abyss);
2796
+ scrollbar-color: var(--cyan-muted) var(--abyss);
2794
2797
  }
2795
2798
 
2796
2799
  .log-viewer::-webkit-scrollbar {
@@ -2800,7 +2803,7 @@ body::-webkit-scrollbar-thumb {
2800
2803
  background: transparent;
2801
2804
  }
2802
2805
  .log-viewer::-webkit-scrollbar-thumb {
2803
- background: var(--cyan-dim);
2806
+ background: var(--cyan-muted);
2804
2807
  border-radius: 3px;
2805
2808
  }
2806
2809
 
@@ -3324,7 +3327,7 @@ body::-webkit-scrollbar-thumb {
3324
3327
  }
3325
3328
 
3326
3329
  .capacity-value {
3327
- font-family: var(--font-display);
3330
+ font-family: var(--font-body);
3328
3331
  font-size: 1.5rem;
3329
3332
  color: var(--text-primary);
3330
3333
  }
@@ -5010,7 +5013,7 @@ select.form-input {
5010
5013
 
5011
5014
  .theater-pipeline-item.selected {
5012
5015
  background: var(--surface);
5013
- border: 1px solid var(--cyan-dim);
5016
+ border: 1px solid var(--cyan-muted);
5014
5017
  }
5015
5018
 
5016
5019
  .theater-issue {
@@ -5156,7 +5159,7 @@ select.form-input {
5156
5159
 
5157
5160
  .cockpit-agent-btn:hover {
5158
5161
  background: var(--surface);
5159
- border-color: var(--cyan-dim);
5162
+ border-color: var(--cyan-muted);
5160
5163
  }
5161
5164
 
5162
5165
  .cockpit-agent-btn.selected {
@@ -5269,7 +5272,7 @@ select.form-input {
5269
5272
  }
5270
5273
 
5271
5274
  .sound-toggle:hover {
5272
- border-color: var(--cyan-dim);
5275
+ border-color: var(--cyan-muted);
5273
5276
  color: var(--text-secondary);
5274
5277
  }
5275
5278
 
@@ -5378,7 +5381,7 @@ select.form-input {
5378
5381
  gap: 0.5rem;
5379
5382
  }
5380
5383
  .learning-card {
5381
- background: var(--bg-foam);
5384
+ background: var(--foam);
5382
5385
  border: 1px solid var(--border);
5383
5386
  border-radius: var(--radius-md);
5384
5387
  padding: 0.75rem 1rem;
@@ -5392,7 +5395,7 @@ select.form-input {
5392
5395
  }
5393
5396
  .learning-category {
5394
5397
  background: var(--cyan);
5395
- color: var(--bg-abyss);
5398
+ color: var(--abyss);
5396
5399
  padding: 0.125rem 0.5rem;
5397
5400
  border-radius: var(--radius-sm);
5398
5401
  font-weight: 600;
@@ -5434,7 +5437,7 @@ select.form-input {
5434
5437
  display: flex;
5435
5438
  align-items: center;
5436
5439
  gap: 0.5rem;
5437
- background: var(--bg-foam);
5440
+ background: var(--foam);
5438
5441
  border: 1px solid var(--border);
5439
5442
  border-radius: var(--radius-md);
5440
5443
  padding: 0.75rem 1rem;
@@ -5445,7 +5448,7 @@ select.form-input {
5445
5448
  white-space: nowrap;
5446
5449
  }
5447
5450
  .invite-url {
5448
- background: var(--bg-deep);
5451
+ background: var(--deep);
5449
5452
  padding: 0.25rem 0.5rem;
5450
5453
  border-radius: var(--radius-sm);
5451
5454
  font-size: 0.8rem;
@@ -5472,7 +5475,7 @@ select.form-input {
5472
5475
  align-items: center;
5473
5476
  gap: 0.5rem;
5474
5477
  padding: 0.5rem 0.75rem;
5475
- background: var(--bg-foam);
5478
+ background: var(--foam);
5476
5479
  border-bottom: 1px solid var(--border);
5477
5480
  font-size: 0.85rem;
5478
5481
  font-weight: 600;
@@ -5541,7 +5544,7 @@ select.form-input {
5541
5544
  .changes-diff {
5542
5545
  margin-top: 0.25rem;
5543
5546
  padding: 0.5rem;
5544
- background: var(--bg-abyss);
5547
+ background: var(--abyss);
5545
5548
  border-radius: var(--radius-sm);
5546
5549
  font-size: 0.75rem;
5547
5550
  max-height: 300px;
@@ -5558,7 +5561,7 @@ select.form-input {
5558
5561
  }
5559
5562
  .reasoning-entry,
5560
5563
  .failure-entry {
5561
- background: var(--bg-foam);
5564
+ background: var(--foam);
5562
5565
  border: 1px solid var(--border);
5563
5566
  border-radius: var(--radius-md);
5564
5567
  padding: 0.75rem 1rem;
@@ -5684,7 +5687,7 @@ select.form-input {
5684
5687
  display: flex;
5685
5688
  align-items: center;
5686
5689
  gap: 0.5rem;
5687
- background: var(--bg-foam);
5690
+ background: var(--foam);
5688
5691
  border: 1px solid var(--border);
5689
5692
  border-radius: var(--radius-md);
5690
5693
  padding: 0.5rem 0.75rem;
@@ -5825,7 +5828,7 @@ select.form-input {
5825
5828
  overflow: auto;
5826
5829
  }
5827
5830
  .admin-debug-pre {
5828
- background: var(--bg-deep);
5831
+ background: var(--deep);
5829
5832
  border: 1px solid var(--card-border);
5830
5833
  border-radius: var(--radius-md);
5831
5834
  padding: 1rem;
@@ -0,0 +1,38 @@
1
+ import type { Session } from "../types/index.js";
2
+ import { CORS_HEADERS } from "../middleware/constants.js";
3
+ import { getAuthMode, isAuthEnabled, createSession, sessionCookie, clearSessionCookie } from "../middleware/auth.js";
4
+
5
+ export function handleAuthRoutes(req: Request, pathname: string, url: URL): Response | null {
6
+ // POST /auth/pat-login — PAT-based login (returns session)
7
+ if (pathname === "/auth/pat-login" && req.method === "POST") {
8
+ // This endpoint requires body parsing which is done in server.ts
9
+ // Returning null to signal server.ts to handle this
10
+ return null;
11
+ }
12
+
13
+ // GET /auth/logout — Clear session and redirect
14
+ if (pathname === "/auth/logout") {
15
+ return new Response("Redirecting to login...", {
16
+ status: 303,
17
+ headers: {
18
+ Location: "/login",
19
+ "Set-Cookie": clearSessionCookie(),
20
+ },
21
+ });
22
+ }
23
+
24
+ // GET /auth/github — OAuth redirect (requires server.ts to handle)
25
+ if (pathname === "/auth/github") {
26
+ if (getAuthMode() !== "oauth") {
27
+ return new Response("OAuth not configured", { status: 500 });
28
+ }
29
+ return null; // server.ts handles
30
+ }
31
+
32
+ // GET /auth/callback — OAuth callback (requires server.ts to handle)
33
+ if (pathname === "/auth/callback") {
34
+ return null; // server.ts handles with query params
35
+ }
36
+
37
+ return null;
38
+ }
@@ -41,6 +41,10 @@ const SESSION_SECRET = process.env.SESSION_SECRET || crypto.randomUUID();
41
41
  const SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
42
42
  const ALLOWED_PERMISSIONS = ["admin", "write"];
43
43
 
44
+ // ─── WebSocket Security ──────────────────────────────────────────────
45
+ const MAX_WS_CLIENTS = 50;
46
+ const WS_CONNECTION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
47
+
44
48
  // ─── SQLite Database (optional) ──────────────────────────────────────
45
49
  const DB_FILE = join(HOME, ".shipwright", "shipwright.db");
46
50
  let db: Database | null = null;
@@ -333,6 +337,15 @@ function sessionCookie(sessionId: string): string {
333
337
  return `fleet_session=${sessionId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${Math.floor(SESSION_TTL_MS / 1000)}`;
334
338
  }
335
339
 
340
+ function isLocalConnection(req: Request): boolean {
341
+ const host = req.headers.get("host") || "";
342
+ return (
343
+ host.startsWith("localhost:") ||
344
+ host.startsWith("127.0.0.1:") ||
345
+ host.startsWith("[::1]:")
346
+ );
347
+ }
348
+
336
349
  function clearSessionCookie(): string {
337
350
  return "fleet_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0";
338
351
  }
@@ -932,18 +945,19 @@ function broadcastNewEvents(): void {
932
945
  }
933
946
 
934
947
  // ─── Data Collection ─────────────────────────────────────────────────
935
- function readEvents(): DaemonEvent[] {
948
+ function readEvents(limit = 200): DaemonEvent[] {
936
949
  // Try SQLite first (faster for large event logs)
937
- const dbEvents = dbQueryEvents(0, 10000);
950
+ const dbEvents = dbQueryEvents(0, limit);
938
951
  if (dbEvents.length > 0) return dbEvents;
939
952
 
940
- // Fallback to JSONL
953
+ // Fallback to JSONL — only read last 1000 lines to avoid OOM on large files
941
954
  if (!existsSync(EVENTS_FILE)) return [];
942
955
  try {
943
956
  const content = readFileSync(EVENTS_FILE, "utf-8").trim();
944
957
  if (!content) return [];
945
- return content
946
- .split("\n")
958
+ const lines = content.split("\n");
959
+ const recent = lines.length > 1000 ? lines.slice(-1000) : lines;
960
+ return recent
947
961
  .filter((l) => l.trim())
948
962
  .map((l) => {
949
963
  try {
@@ -2204,6 +2218,12 @@ function ghCached<T>(key: string, fn: () => T): T {
2204
2218
  const now = Date.now();
2205
2219
  const cached = ghCache.get(key);
2206
2220
  if (cached && now - cached.ts < GH_CACHE_TTL_MS) return cached.data as T;
2221
+ // Evict expired entries to prevent unbounded memory growth
2222
+ if (ghCache.size > 50) {
2223
+ for (const [k, v] of ghCache) {
2224
+ if (now - v.ts > GH_CACHE_TTL_MS) ghCache.delete(k);
2225
+ }
2226
+ }
2207
2227
  const data = fn();
2208
2228
  ghCache.set(key, { data, ts: now });
2209
2229
  return data;
@@ -2689,15 +2709,43 @@ const server = Bun.serve({
2689
2709
  return handleAuthLogout(req);
2690
2710
  }
2691
2711
 
2692
- // ── Auth gate ─────────────────────────────────────────────────
2693
- // If auth is enabled, enforce it on all remaining routes
2712
+ // ── WebSocket Auth gate ──────────────────────────────────────
2713
+ // WebSocket always requires authentication (unless local connection)
2714
+ if (pathname === "/ws" || pathname === "/ws/events") {
2715
+ const isLocal = isLocalConnection(req);
2716
+ if (!isLocal) {
2717
+ const session = getSession(req);
2718
+ if (!session) {
2719
+ return new Response("Unauthorized", { status: 401 });
2720
+ }
2721
+ }
2722
+
2723
+ // Check connection limit
2724
+ const totalClients = wsClients.size + eventClients.size;
2725
+ if (totalClients >= MAX_WS_CLIENTS && !isLocal) {
2726
+ return new Response("Too Many Connections", { status: 429 });
2727
+ }
2728
+
2729
+ // WebSocket upgrade — /ws/events streams raw events, /ws streams aggregated state
2730
+ if (pathname === "/ws/events") {
2731
+ const upgraded = server.upgrade(req, {
2732
+ data: { type: "events", lastEventId: 0 },
2733
+ });
2734
+ if (upgraded) return undefined as unknown as Response;
2735
+ return new Response("WebSocket upgrade failed", { status: 400 });
2736
+ }
2737
+ if (pathname === "/ws") {
2738
+ const upgraded = server.upgrade(req);
2739
+ if (upgraded) return undefined as unknown as Response;
2740
+ return new Response("WebSocket upgrade failed", { status: 400 });
2741
+ }
2742
+ }
2743
+
2744
+ // ── HTTP Auth gate ───────────────────────────────────────────
2745
+ // If auth is enabled, enforce it on all other remaining routes
2694
2746
  if (isAuthEnabled()) {
2695
2747
  const session = getSession(req);
2696
2748
  if (!session) {
2697
- // WebSocket upgrade attempt without auth
2698
- if (pathname === "/ws" || pathname === "/ws/events") {
2699
- return new Response("Unauthorized", { status: 401 });
2700
- }
2701
2749
  return new Response(null, {
2702
2750
  status: 302,
2703
2751
  headers: { Location: "/login" },
@@ -2707,20 +2755,6 @@ const server = Bun.serve({
2707
2755
 
2708
2756
  // ── Protected routes ──────────────────────────────────────────
2709
2757
 
2710
- // WebSocket upgrade — /ws/events streams raw events, /ws streams aggregated state
2711
- if (pathname === "/ws/events") {
2712
- const upgraded = server.upgrade(req, {
2713
- data: { type: "events", lastEventId: 0 },
2714
- });
2715
- if (upgraded) return undefined as unknown as Response;
2716
- return new Response("WebSocket upgrade failed", { status: 400 });
2717
- }
2718
- if (pathname === "/ws") {
2719
- const upgraded = server.upgrade(req);
2720
- if (upgraded) return undefined as unknown as Response;
2721
- return new Response("WebSocket upgrade failed", { status: 400 });
2722
- }
2723
-
2724
2758
  // REST: fleet state
2725
2759
  if (pathname === "/api/status") {
2726
2760
  return new Response(JSON.stringify(getFleetState()), {
@@ -5897,6 +5931,13 @@ process.on("SIGINT", () => {
5897
5931
  clearInterval(staleClaimInterval);
5898
5932
  clearInterval(inviteCleanupInterval);
5899
5933
  if (eventsWatcher) eventsWatcher.close();
5934
+ if (db) {
5935
+ try {
5936
+ db.close();
5937
+ } catch {
5938
+ /* ignore */
5939
+ }
5940
+ }
5900
5941
  for (const ws of wsClients) {
5901
5942
  try {
5902
5943
  ws.close(1001, "Server shutting down");