jettypod 4.4.118 → 4.4.121

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 (240) hide show
  1. package/.env +4 -3
  2. package/Cargo.lock +6450 -0
  3. package/Cargo.toml +35 -0
  4. package/README.md +5 -1
  5. package/TAURI-MIGRATION-PLAN.md +840 -0
  6. package/apps/dashboard/app/connect-claude/page.tsx +5 -6
  7. package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
  8. package/apps/dashboard/app/demo/gates/page.tsx +43 -45
  9. package/apps/dashboard/app/design-system/page.tsx +868 -0
  10. package/apps/dashboard/app/globals.css +80 -4
  11. package/apps/dashboard/app/install-claude/page.tsx +4 -6
  12. package/apps/dashboard/app/login/page.tsx +72 -54
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +61 -13
  15. package/apps/dashboard/app/signup/page.tsx +242 -0
  16. package/apps/dashboard/app/subscribe/page.tsx +0 -2
  17. package/apps/dashboard/app/tests/page.tsx +37 -4
  18. package/apps/dashboard/app/welcome/page.tsx +13 -16
  19. package/apps/dashboard/app/work/[id]/page.tsx +117 -118
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +92 -85
  22. package/apps/dashboard/components/CardMenu.tsx +45 -12
  23. package/apps/dashboard/components/ClaudePanel.tsx +771 -850
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
  26. package/apps/dashboard/components/CopyableId.tsx +3 -4
  27. package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
  28. package/apps/dashboard/components/DragContext.tsx +134 -63
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +6 -7
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +26 -7
  34. package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
  35. package/apps/dashboard/components/EpicGroup.tsx +359 -0
  36. package/apps/dashboard/components/GateCard.tsx +79 -17
  37. package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
  39. package/apps/dashboard/components/JettyLoader.tsx +37 -0
  40. package/apps/dashboard/components/KanbanBoard.tsx +368 -958
  41. package/apps/dashboard/components/KanbanCard.tsx +740 -0
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
  44. package/apps/dashboard/components/MainNav.tsx +38 -73
  45. package/apps/dashboard/components/MessageBlock.tsx +468 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -16
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
  53. package/apps/dashboard/components/ReviewFooter.tsx +139 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -19
  55. package/apps/dashboard/components/SubscribeContent.tsx +91 -47
  56. package/apps/dashboard/components/TestTree.tsx +16 -16
  57. package/apps/dashboard/components/TipCard.tsx +16 -17
  58. package/apps/dashboard/components/Toast.tsx +5 -6
  59. package/apps/dashboard/components/TypeIcon.tsx +55 -0
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
  62. package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
  64. package/apps/dashboard/components/WorkItemTree.tsx +11 -32
  65. package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
  66. package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
  67. package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
  68. package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
  72. package/apps/dashboard/components/ui/Button.tsx +104 -0
  73. package/apps/dashboard/components/ui/Input.tsx +78 -0
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
  77. package/apps/dashboard/contexts/UsageContext.tsx +87 -32
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  83. package/apps/dashboard/index.html +73 -0
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/data-bridge.ts +722 -0
  86. package/apps/dashboard/lib/db.ts +69 -1265
  87. package/apps/dashboard/lib/environment-config.ts +173 -0
  88. package/apps/dashboard/lib/environment-verification.ts +119 -0
  89. package/apps/dashboard/lib/kanban-utils.ts +270 -0
  90. package/apps/dashboard/lib/proof-run.ts +495 -0
  91. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  92. package/apps/dashboard/lib/run-migrations.js +27 -2
  93. package/apps/dashboard/lib/service-recovery.ts +326 -0
  94. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  95. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  96. package/apps/dashboard/lib/session-stream-manager.ts +308 -134
  97. package/apps/dashboard/lib/shadows.ts +7 -0
  98. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  99. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  100. package/apps/dashboard/lib/tauri.ts +106 -0
  101. package/apps/dashboard/lib/utils.ts +6 -0
  102. package/apps/dashboard/next-env.d.ts +1 -1
  103. package/apps/dashboard/package.json +21 -32
  104. package/apps/dashboard/public/bug-icon.png +0 -0
  105. package/apps/dashboard/public/buoy-icon.png +0 -0
  106. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  107. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  108. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  109. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  110. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  111. package/apps/dashboard/public/jettypod_logo.png +0 -0
  112. package/apps/dashboard/public/pier-icon.png +0 -0
  113. package/apps/dashboard/public/star-icon.png +0 -0
  114. package/apps/dashboard/public/wrench-icon.png +0 -0
  115. package/apps/dashboard/scripts/tauri-build.js +228 -0
  116. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  117. package/apps/dashboard/scripts/ws-server.js +191 -0
  118. package/apps/dashboard/src/main.tsx +12 -0
  119. package/apps/dashboard/src/router.tsx +107 -0
  120. package/apps/dashboard/src/vite-env.d.ts +1 -0
  121. package/apps/dashboard/tsconfig.json +7 -12
  122. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  123. package/apps/dashboard/vite.config.ts +33 -0
  124. package/apps/update-server/src/index.ts +228 -80
  125. package/claude-hooks/global-guardrails.js +14 -13
  126. package/crates/jettypod-cli/Cargo.toml +19 -0
  127. package/crates/jettypod-cli/src/commands.rs +1249 -0
  128. package/crates/jettypod-cli/src/main.rs +595 -0
  129. package/crates/jettypod-core/Cargo.toml +26 -0
  130. package/crates/jettypod-core/build.rs +98 -0
  131. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  132. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  133. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  134. package/crates/jettypod-core/src/auth.rs +294 -0
  135. package/crates/jettypod-core/src/config.rs +397 -0
  136. package/crates/jettypod-core/src/db/mod.rs +507 -0
  137. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  138. package/crates/jettypod-core/src/db/startup.rs +101 -0
  139. package/crates/jettypod-core/src/db/validate.rs +149 -0
  140. package/crates/jettypod-core/src/error.rs +76 -0
  141. package/crates/jettypod-core/src/git.rs +458 -0
  142. package/crates/jettypod-core/src/lib.rs +20 -0
  143. package/crates/jettypod-core/src/sessions.rs +625 -0
  144. package/crates/jettypod-core/src/skills.rs +556 -0
  145. package/crates/jettypod-core/src/work.rs +1086 -0
  146. package/crates/jettypod-core/src/worktree.rs +628 -0
  147. package/crates/jettypod-core/src/ws.rs +767 -0
  148. package/cucumber-test.cjs +6 -0
  149. package/cucumber.js +9 -3
  150. package/docs/COMMAND_REFERENCE.md +34 -0
  151. package/hooks/post-checkout +32 -75
  152. package/hooks/post-merge +111 -10
  153. package/jest.setup.js +1 -0
  154. package/jettypod.js +145 -116
  155. package/lib/bdd-preflight.js +96 -0
  156. package/lib/chore-taxonomy.js +33 -10
  157. package/lib/database.js +36 -16
  158. package/lib/db-watcher.js +1 -1
  159. package/lib/git-hooks/pre-commit +1 -1
  160. package/lib/jettypod-backup.js +27 -4
  161. package/lib/merge-lock.js +111 -253
  162. package/lib/migrations/027-plan-at-creation-column.js +3 -1
  163. package/lib/migrations/029-remove-autoincrement.js +307 -0
  164. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  165. package/lib/migrations/030-rejection-round-columns.js +54 -0
  166. package/lib/migrations/031-session-isolation-index.js +17 -0
  167. package/lib/migrations/index.js +47 -4
  168. package/lib/schema.js +10 -5
  169. package/lib/seed-onboarding.js +1 -1
  170. package/lib/update-command/index.js +9 -175
  171. package/lib/work-commands/index.js +144 -19
  172. package/lib/work-tracking/index.js +148 -27
  173. package/lib/worktree-diagnostics.js +16 -16
  174. package/lib/worktree-facade.js +1 -1
  175. package/lib/worktree-manager.js +8 -8
  176. package/lib/worktree-reconciler.js +5 -5
  177. package/package.json +9 -2
  178. package/scripts/ndjson-to-cucumber-json.js +152 -0
  179. package/scripts/postinstall.js +25 -0
  180. package/skills-templates/bug-mode/SKILL.md +79 -20
  181. package/skills-templates/bug-planning/SKILL.md +25 -29
  182. package/skills-templates/chore-mode/SKILL.md +171 -69
  183. package/skills-templates/chore-mode/verification.js +51 -10
  184. package/skills-templates/chore-planning/SKILL.md +47 -18
  185. package/skills-templates/design-system-selection/SKILL.md +273 -0
  186. package/skills-templates/epic-planning/SKILL.md +82 -48
  187. package/skills-templates/external-transition/SKILL.md +47 -47
  188. package/skills-templates/feature-planning/SKILL.md +173 -74
  189. package/skills-templates/production-mode/SKILL.md +69 -49
  190. package/skills-templates/request-routing/SKILL.md +4 -4
  191. package/skills-templates/simple-improvement/SKILL.md +74 -29
  192. package/skills-templates/speed-mode/SKILL.md +217 -141
  193. package/skills-templates/stable-mode/SKILL.md +148 -89
  194. package/apps/dashboard/README.md +0 -36
  195. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
  196. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  197. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
  198. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  199. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
  200. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  201. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  202. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  203. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  204. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  205. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  206. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  207. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  208. package/apps/dashboard/app/api/tests/route.ts +0 -9
  209. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  210. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  211. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  212. package/apps/dashboard/app/api/usage/route.ts +0 -17
  213. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  214. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  215. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  216. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
  217. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  218. package/apps/dashboard/app/layout.tsx +0 -43
  219. package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
  220. package/apps/dashboard/electron/ipc-handlers.js +0 -1028
  221. package/apps/dashboard/electron/main.js +0 -2124
  222. package/apps/dashboard/electron/preload.js +0 -123
  223. package/apps/dashboard/electron/session-manager.js +0 -141
  224. package/apps/dashboard/electron-builder.config.js +0 -357
  225. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  226. package/apps/dashboard/lib/claude-process-manager.ts +0 -492
  227. package/apps/dashboard/lib/db-bridge.ts +0 -282
  228. package/apps/dashboard/lib/prototypes.ts +0 -202
  229. package/apps/dashboard/lib/test-results-db.ts +0 -307
  230. package/apps/dashboard/lib/tests.ts +0 -282
  231. package/apps/dashboard/next.config.js +0 -50
  232. package/apps/dashboard/postcss.config.mjs +0 -7
  233. package/apps/dashboard/public/file.svg +0 -1
  234. package/apps/dashboard/public/globe.svg +0 -1
  235. package/apps/dashboard/public/next.svg +0 -1
  236. package/apps/dashboard/public/vercel.svg +0 -1
  237. package/apps/dashboard/public/window.svg +0 -1
  238. package/apps/dashboard/scripts/download-node.js +0 -104
  239. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
  240. package/docs/bdd-guidance.md +0 -390
@@ -1,3 +1,20 @@
1
+ /* Font declarations */
2
+ @font-face {
3
+ font-family: 'Satoshi';
4
+ src: url('/fonts/Satoshi-Variable.woff2') format('woff2');
5
+ font-style: normal;
6
+ font-display: swap;
7
+ font-weight: 100 900;
8
+ }
9
+
10
+ @font-face {
11
+ font-family: 'Satoshi';
12
+ src: url('/fonts/Satoshi-VariableItalic.woff2') format('woff2');
13
+ font-style: italic;
14
+ font-display: swap;
15
+ font-weight: 100 900;
16
+ }
17
+
1
18
  @import "tailwindcss";
2
19
  @import "tw-animate-css";
3
20
 
@@ -6,7 +23,7 @@
6
23
  @theme inline {
7
24
  --color-background: var(--background);
8
25
  --color-foreground: var(--foreground);
9
- --font-sans: var(--font-geist-sans);
26
+ --font-sans: var(--font-satoshi);
10
27
  --font-mono: var(--font-geist-mono);
11
28
  --color-sidebar-ring: var(--sidebar-ring);
12
29
  --color-sidebar-border: var(--sidebar-border);
@@ -45,7 +62,9 @@
45
62
 
46
63
  :root {
47
64
  --radius: 0.625rem;
48
- --background: oklch(1 0 0);
65
+ --font-satoshi: 'Satoshi', system-ui, sans-serif;
66
+ --font-geist-mono: 'Geist Mono Variable', 'Geist Mono', monospace;
67
+ --background: oklch(0.98 0.002 80);
49
68
  --foreground: oklch(0.145 0 0);
50
69
  --card: oklch(1 0 0);
51
70
  --card-foreground: oklch(0.145 0 0);
@@ -113,10 +132,67 @@
113
132
  }
114
133
 
115
134
  @layer base {
116
- * {
117
- @apply border-border outline-ring/50;
135
+ *,
136
+ ::before,
137
+ ::after {
138
+ border-color: var(--color-border);
118
139
  }
119
140
  body {
120
141
  @apply bg-background text-foreground;
142
+ font-family: var(--font-satoshi);
143
+ -webkit-font-smoothing: antialiased;
144
+ -moz-osx-font-smoothing: grayscale;
145
+ }
146
+ }
147
+
148
+ html {
149
+ font-size: 16px;
150
+ }
151
+
152
+ /* Kanban card hover — instant shadow swap, no transition (WebKit-friendly) */
153
+ .kanban-card {
154
+ contain: content;
155
+ content-visibility: auto;
156
+ }
157
+ .kanban-card:hover {
158
+ box-shadow: var(--hover-shadow) !important;
159
+ transform: translateY(-2px);
160
+ }
161
+
162
+ /* Highlight pulse — CSS-only, no framer-motion runtime cost */
163
+ @keyframes highlight-pulse {
164
+ 0%, 100% { outline: 3px solid rgba(129, 157, 159, 0); outline-offset: 0; }
165
+ 50% { outline: 3px solid rgba(129, 157, 159, 0.4); outline-offset: 0; }
166
+ }
167
+
168
+ /* View Transitions API — fast cross-fade for route changes */
169
+ ::view-transition-old(root) {
170
+ animation: vt-fade-out 120ms ease-out;
171
+ }
172
+ ::view-transition-new(root) {
173
+ animation: vt-fade-in 200ms ease-out;
174
+ }
175
+ @keyframes vt-fade-out {
176
+ to { opacity: 0; }
177
+ }
178
+ @keyframes vt-fade-in {
179
+ from { opacity: 0; }
180
+ }
181
+
182
+ /* Skeleton loading pulse */
183
+ @keyframes skeleton-pulse {
184
+ 0%, 100% { opacity: 0.5; }
185
+ 50% { opacity: 0.25; }
186
+ }
187
+
188
+ /* Respect reduced motion globally — disables all animations/transitions */
189
+ @media (prefers-reduced-motion: reduce) {
190
+ *,
191
+ ::before,
192
+ ::after {
193
+ animation-duration: 0.01ms !important;
194
+ animation-iteration-count: 1 !important;
195
+ transition-duration: 0.01ms !important;
196
+ scroll-behavior: auto !important;
121
197
  }
122
198
  }
@@ -1,7 +1,6 @@
1
- 'use client';
2
-
3
1
  import { useState } from 'react';
4
2
  import { InstallClaudeScreen } from '@/components/InstallClaudeScreen';
3
+ import { isTauri, claudeCode } from '@/lib/tauri-bridge';
5
4
 
6
5
  export default function InstallClaudePage() {
7
6
  const [isInstalling, setIsInstalling] = useState(false);
@@ -12,15 +11,14 @@ export default function InstallClaudePage() {
12
11
  setError(null);
13
12
  setIsInstalling(true);
14
13
 
15
- // Check if we're in Electron
16
- if (!window.electronAPI?.isElectron) {
14
+ if (!isTauri()) {
17
15
  setError('Installation is only available in the desktop app.');
18
16
  setIsInstalling(false);
19
17
  return;
20
18
  }
21
19
 
22
20
  try {
23
- const result = await window.electronAPI.claudeCode.install();
21
+ const result = await claudeCode.install();
24
22
 
25
23
  if (!result.success) {
26
24
  setError(result.error || 'Installation failed');
@@ -44,7 +42,7 @@ export default function InstallClaudePage() {
44
42
  return (
45
43
  <>
46
44
  {error && (
47
- <div className="fixed top-4 left-1/2 transform -translate-x-1/2 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded z-50">
45
+ <div className="fixed top-4 left-1/2 transform -translate-x-1/2 bg-red-50 dark:bg-red-900/20 border-2 border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-4 py-3 rounded-xl text-base z-50">
48
46
  {error}
49
47
  </div>
50
48
  )}
@@ -1,25 +1,11 @@
1
- 'use client';
2
-
3
1
  import { useState, useEffect, useRef } from 'react';
4
- import Image from 'next/image';
2
+ import { Link } from 'react-router-dom';
3
+ import { Button } from '@/components/ui/Button';
4
+ import { Input } from '@/components/ui/Input';
5
+ import { isTauri, auth } from '@/lib/tauri-bridge';
5
6
 
6
7
  const API_BASE = 'https://jettypod-update-server.spangbaryn2.workers.dev';
7
8
 
8
- const buttonGradientStyle = {
9
- background: 'linear-gradient(145deg, #ffffff 0%, #faf9f7 10%, #f0f4f4 35%, #c8d9da 55%, #819D9F 90%)',
10
- color: '#3d4d4e',
11
- boxShadow: `
12
- 0 1px 1px rgba(0, 0, 0, 0.02),
13
- 0 2px 4px rgba(0, 0, 0, 0.03),
14
- 0 6px 12px rgba(0, 0, 0, 0.05),
15
- 0 12px 24px rgba(0, 0, 0, 0.06),
16
- 0 20px 40px rgba(129, 157, 159, 0.2),
17
- 0 32px 64px rgba(129, 157, 159, 0.18),
18
- inset 0 2px 4px rgba(255, 255, 255, 1),
19
- inset 0 -2px 4px rgba(129, 157, 159, 0.05)
20
- `,
21
- };
22
-
23
9
  export default function LoginPage() {
24
10
  const [email, setEmail] = useState('');
25
11
  const [otpCode, setOtpCode] = useState('');
@@ -29,10 +15,28 @@ export default function LoginPage() {
29
15
  const [error, setError] = useState<string | null>(null);
30
16
 
31
17
  // Poll for auth completion after Google sign-in.
32
- // The deep link handler in main.js saves the token — this polling detects it
33
- // and navigates to the dashboard even if mainWindow.loadURL doesn't fire.
18
+ // The deep link handler saves the token — this polling detects it
19
+ // and navigates to the dashboard.
34
20
  const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
35
21
 
22
+ // Redirect already-authenticated users to dashboard
23
+ useEffect(() => {
24
+ async function checkIfAlreadyAuthenticated() {
25
+ if (isTauri()) {
26
+ try {
27
+ const status = await auth.getStatus();
28
+ if (status.authenticated) {
29
+ const path = await auth.getPostLoginPath() || '/';
30
+ window.location.href = path;
31
+ }
32
+ } catch {
33
+ // Ignore — stay on login page
34
+ }
35
+ }
36
+ }
37
+ checkIfAlreadyAuthenticated();
38
+ }, []);
39
+
36
40
  useEffect(() => {
37
41
  return () => {
38
42
  if (pollRef.current) clearInterval(pollRef.current);
@@ -40,16 +44,17 @@ export default function LoginPage() {
40
44
  }, []);
41
45
 
42
46
  const handleGoogleSignIn = () => {
43
- if (!window.electronAPI?.isElectron) return;
44
- window.electronAPI.auth.loginWithGoogle();
47
+ if (!isTauri()) return;
48
+ auth.loginWithGoogle();
45
49
 
46
50
  // Start polling for auth status (token saved by deep link handler)
47
51
  pollRef.current = setInterval(async () => {
48
52
  try {
49
- const status = await window.electronAPI!.auth.getStatus();
53
+ const status = await auth.getStatus();
50
54
  if (status.authenticated) {
51
55
  if (pollRef.current) clearInterval(pollRef.current);
52
- window.location.href = '/';
56
+ const path = await auth.getPostLoginPath() || '/';
57
+ window.location.href = path;
53
58
  }
54
59
  } catch {
55
60
  // Ignore — keep polling
@@ -116,12 +121,13 @@ export default function LoginPage() {
116
121
 
117
122
  const data = await res.json() as { token: string; user: { id: string; email: string; plan: string } };
118
123
 
119
- // Save auth state via Electron IPC
120
- if (window.electronAPI?.isElectron) {
121
- await window.electronAPI.auth.saveToken(data.token, data.user);
124
+ // Save auth state via Tauri IPC
125
+ if (isTauri()) {
126
+ await auth.saveToken(data.token, data.user);
122
127
  }
123
128
 
124
- window.location.href = '/';
129
+ const path = await auth.getPostLoginPath() || '/';
130
+ window.location.href = path;
125
131
  } catch {
126
132
  setError('Failed to verify code. Check your connection.');
127
133
  setIsVerifying(false);
@@ -130,15 +136,14 @@ export default function LoginPage() {
130
136
 
131
137
  return (
132
138
  <div className="flex flex-col items-center justify-center min-h-screen bg-white dark:bg-zinc-900 p-8">
133
- <div className="max-w-md w-full space-y-8">
139
+ <div className="max-w-md w-full space-y-10">
134
140
  {/* Logo */}
135
- <div className="flex flex-col items-center space-y-4">
136
- <Image
141
+ <div className="flex flex-col items-center space-y-6">
142
+ <img
137
143
  src="/jettypod_wordmark.png"
138
144
  alt="JettyPod"
139
145
  width={160}
140
146
  height={40}
141
- priority
142
147
  />
143
148
  <h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 text-center">
144
149
  Sign in to JettyPod
@@ -150,54 +155,55 @@ export default function LoginPage() {
150
155
 
151
156
  {/* Error */}
152
157
  {error && (
153
- <div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-4 py-3 rounded-lg text-sm">
158
+ <div className="bg-red-50 dark:bg-red-900/20 border-2 border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-5 py-4 rounded-xl text-base">
154
159
  {error}
155
160
  </div>
156
161
  )}
157
162
 
158
163
  {/* Google Sign-In */}
159
164
  <div className="pt-4">
160
- <button
165
+ <Button
161
166
  onClick={handleGoogleSignIn}
162
- className="w-full py-3 px-6 rounded-xl font-medium transition-all duration-200 hover:-translate-y-1 hover:scale-[1.01] active:translate-y-0 active:scale-100"
163
- style={{ cursor: 'pointer', ...buttonGradientStyle }}
167
+ size="lg"
168
+ fullWidth
164
169
  >
165
170
  Sign in with Google
166
- </button>
171
+ </Button>
167
172
  </div>
168
173
 
169
174
  {/* Divider */}
170
175
  <div className="flex items-center gap-4">
171
176
  <div className="flex-1 h-px bg-zinc-200 dark:bg-zinc-700" />
172
- <span className="text-xs text-zinc-400 dark:text-zinc-500">or</span>
177
+ <span className="text-base text-zinc-400 dark:text-zinc-500">or</span>
173
178
  <div className="flex-1 h-px bg-zinc-200 dark:bg-zinc-700" />
174
179
  </div>
175
180
 
176
181
  {/* Email OTP */}
177
182
  {!otpSent ? (
178
- <form onSubmit={handleSendOTP} className="space-y-4">
179
- <input
183
+ <form onSubmit={handleSendOTP} className="space-y-6">
184
+ <Input
180
185
  type="email"
181
186
  value={email}
182
187
  onChange={(e) => setEmail(e.target.value)}
183
188
  placeholder="Enter your email"
184
189
  disabled={isSending}
185
- className="w-full px-4 py-3 rounded-xl border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 dark:placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-400 dark:focus:ring-zinc-500 disabled:opacity-50"
186
190
  />
187
- <button
191
+ <Button
188
192
  type="submit"
193
+ variant="secondary"
194
+ size="lg"
195
+ fullWidth
189
196
  disabled={isSending || !email.trim()}
190
- className="w-full py-3 px-6 rounded-xl font-medium border border-zinc-300 dark:border-zinc-600 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors disabled:opacity-50 disabled:pointer-events-none"
191
197
  >
192
198
  {isSending ? 'Sending code...' : 'Sign in with email'}
193
- </button>
199
+ </Button>
194
200
  </form>
195
201
  ) : (
196
- <form onSubmit={handleVerifyOTP} className="space-y-4">
197
- <p className="text-sm text-zinc-500 dark:text-zinc-400">
202
+ <form onSubmit={handleVerifyOTP} className="space-y-6">
203
+ <p className="text-base text-zinc-500 dark:text-zinc-400">
198
204
  We sent a 6-digit code to <span className="font-medium text-zinc-700 dark:text-zinc-300">{email}</span>
199
205
  </p>
200
- <input
206
+ <Input
201
207
  type="text"
202
208
  value={otpCode}
203
209
  onChange={(e) => setOtpCode(e.target.value)}
@@ -205,24 +211,36 @@ export default function LoginPage() {
205
211
  maxLength={6}
206
212
  autoFocus
207
213
  disabled={isVerifying}
208
- className="w-full px-4 py-3 rounded-xl border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 dark:placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-400 dark:focus:ring-zinc-500 disabled:opacity-50 text-center text-xl tracking-widest font-mono"
214
+ className="text-center text-xl tracking-widest font-mono"
209
215
  />
210
- <button
216
+ <Button
211
217
  type="submit"
218
+ variant="secondary"
219
+ size="lg"
220
+ fullWidth
212
221
  disabled={isVerifying || !otpCode.trim()}
213
- className="w-full py-3 px-6 rounded-xl font-medium border border-zinc-300 dark:border-zinc-600 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors disabled:opacity-50 disabled:pointer-events-none"
214
222
  >
215
223
  {isVerifying ? 'Verifying...' : 'Verify code'}
216
- </button>
217
- <button
224
+ </Button>
225
+ <Button
218
226
  type="button"
227
+ variant="ghost"
228
+ size="sm"
229
+ fullWidth
219
230
  onClick={() => { setOtpSent(false); setOtpCode(''); setError(null); }}
220
- className="w-full text-sm text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-300 transition-colors"
221
231
  >
222
232
  Use a different email
223
- </button>
233
+ </Button>
224
234
  </form>
225
235
  )}
236
+
237
+ {/* Switch to signup */}
238
+ <p className="text-center text-base text-zinc-500 dark:text-zinc-400">
239
+ Don&apos;t have an account?{' '}
240
+ <Link to="/signup" className="font-medium hover:underline" style={{ color: '#819D9F' }}>
241
+ Create one
242
+ </Link>
243
+ </p>
226
244
  </div>
227
245
  </div>
228
246
  );
@@ -1,61 +1,114 @@
1
- import { redirect } from 'next/navigation';
2
- import { getKanbanData, hasProject, isBlankProject, getProjectRoot } from '@/lib/db';
1
+ import { useState, useEffect } from 'react';
2
+ import { useNavigate, useLocation, Outlet } from 'react-router-dom';
3
3
  import { RealTimeKanbanWrapper } from '@/components/RealTimeKanbanWrapper';
4
-
5
- // Force dynamic rendering - database is only available at runtime
6
- export const dynamic = 'force-dynamic';
4
+ import { dataBridge, prefetch } from '@/lib/data-bridge';
5
+ import type { KanbanData } from '@/lib/data-bridge';
7
6
 
8
7
  export default function Home() {
9
- // Check if a project is selected - if not, redirect to welcome
10
- if (!hasProject()) {
11
- redirect('/welcome');
12
- }
8
+ const navigate = useNavigate();
9
+ const { pathname } = useLocation();
10
+ const isChildRoute = pathname !== '/';
11
+ const [data, setData] = useState<KanbanData | null>(null);
12
+ const [projectPath, setProjectPath] = useState('');
13
+ const [isBlank, setIsBlank] = useState(false);
14
+ const [error, setError] = useState<string | null>(null);
15
+ const [loading, setLoading] = useState(true);
13
16
 
14
- try {
15
- const data = getKanbanData();
16
- const projectRoot = getProjectRoot();
17
- const isBlank = projectRoot ? isBlankProject(projectRoot) : false;
17
+ useEffect(() => {
18
+ async function loadData() {
19
+ try {
20
+ // getProjectRoot() is cached after first call — no redundant IPC
21
+ const root = await dataBridge.getProjectRoot();
22
+ if (!root) {
23
+ navigate('/welcome', { replace: true });
24
+ return;
25
+ }
18
26
 
19
- // Serialize Map data for client component
20
- const serializedData = {
21
- inFlight: data.inFlight,
22
- backlog: Array.from(data.backlog.entries()),
23
- done: Array.from(data.done.entries()),
24
- };
27
+ const kanbanData = await prefetch.backlog();
25
28
 
26
- return (
27
- <div className="h-full flex flex-col min-h-0 overflow-hidden max-w-6xl w-full mx-auto px-4 py-4">
28
- <RealTimeKanbanWrapper initialData={serializedData} isBlank={isBlank} projectPath={projectRoot || ''} />
29
- </div>
30
- );
31
- } catch (error) {
32
- const errorMessage = error instanceof Error ? error.message : String(error);
33
- const errorStack = error instanceof Error ? error.stack : undefined;
29
+ setData(kanbanData);
30
+ setProjectPath(root || '');
31
+ // isBlank detection: fresh project with only the seeded "Project Planning" epic
32
+ const onlyGroup = kanbanData.backlog.size === 1
33
+ ? [...kanbanData.backlog.values()][0]
34
+ : null;
35
+ const hasOnlyOnboarding = kanbanData.inFlight.length === 0
36
+ && kanbanData.done.size === 0
37
+ && onlyGroup?.epicTitle === 'Project Planning';
38
+ setIsBlank(hasOnlyOnboarding);
39
+ } catch (err) {
40
+ setError(err instanceof Error ? err.message : String(err));
41
+ } finally {
42
+ setLoading(false);
43
+ }
44
+ }
45
+ loadData();
46
+ }, [navigate]);
34
47
 
35
- // Log to server console (visible in Electron logs)
36
- console.error('[Home Page Error]', errorMessage);
37
- console.error('[Home Page Stack]', errorStack);
38
- console.error('[JETTYPOD_PROJECT_PATH]', process.env.JETTYPOD_PROJECT_PATH);
48
+ // Serialize Map data for RealTimeKanbanWrapper (it expects this format)
49
+ const serializedData = data ? {
50
+ inFlight: data.inFlight,
51
+ backlog: Array.from(data.backlog.entries()),
52
+ done: Array.from(data.done.entries()),
53
+ } : null;
39
54
 
40
- // Return error UI so user can see what's wrong
41
- return (
42
- <div className="flex-1 flex items-center justify-center p-8">
43
- <div className="max-w-2xl w-full bg-red-50 border border-red-200 rounded-lg p-6">
44
- <h1 className="text-xl font-bold text-red-800 mb-4">Failed to load project</h1>
45
- <div className="bg-white border border-red-100 rounded p-4 mb-4">
46
- <p className="font-mono text-sm text-red-700 whitespace-pre-wrap">{errorMessage}</p>
55
+ // Kanban board content based on loading/error/data state
56
+ const kanbanContent = loading ? (
57
+ <div className="max-w-7xl w-full mx-auto px-4 py-4">
58
+ <div className="flex gap-4" style={{ height: 'calc(var(--main-h, 100vh) - 2rem)' }}>
59
+ {/* Backlog column skeleton */}
60
+ <div className="flex-1 max-w-[600px] flex flex-col min-h-0">
61
+ <div className="bg-zinc-100 dark:bg-zinc-900 rounded-xl p-4 flex-1 min-h-0">
62
+ <div className="flex items-center justify-between mb-4">
63
+ <div className="h-6 w-24 bg-zinc-200 dark:bg-zinc-800 rounded" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
64
+ <div className="h-6 w-10 bg-zinc-200 dark:bg-zinc-800 rounded-full" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
65
+ </div>
66
+ <div className="space-y-3">
67
+ {[1, 2, 3, 4, 5].map(i => (
68
+ <div key={i} className="h-20 bg-zinc-200 dark:bg-zinc-800 rounded-xl" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
69
+ ))}
70
+ </div>
47
71
  </div>
48
- <div className="text-sm text-gray-600 space-y-1">
49
- <p><strong>Project path:</strong> {process.env.JETTYPOD_PROJECT_PATH || '(not set)'}</p>
72
+ </div>
73
+ {/* Done column skeleton */}
74
+ <div className="flex-1 max-w-[600px] flex flex-col min-h-0">
75
+ <div className="bg-zinc-100 dark:bg-zinc-900 rounded-xl p-4 flex-1 min-h-0">
76
+ <div className="flex items-center justify-between mb-4">
77
+ <div className="h-6 w-16 bg-zinc-200 dark:bg-zinc-800 rounded" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
78
+ <div className="h-6 w-10 bg-zinc-200 dark:bg-zinc-800 rounded-full" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
79
+ </div>
80
+ <div className="space-y-3">
81
+ {[1, 2, 3].map(i => (
82
+ <div key={i} className="h-16 bg-zinc-200 dark:bg-zinc-800 rounded-xl" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
83
+ ))}
84
+ </div>
50
85
  </div>
51
- {errorStack && (
52
- <details className="mt-4">
53
- <summary className="text-sm text-gray-500 cursor-pointer">Stack trace</summary>
54
- <pre className="mt-2 text-xs text-gray-500 overflow-auto p-2 bg-gray-50 rounded">{errorStack}</pre>
55
- </details>
56
- )}
57
86
  </div>
58
87
  </div>
59
- );
60
- }
88
+ </div>
89
+ ) : error ? (
90
+ <div className="flex-1 flex items-center justify-center p-8">
91
+ <div className="max-w-2xl w-full bg-red-50 border-2 border-red-200 rounded-lg p-8">
92
+ <h1 className="text-xl font-bold text-red-800 mb-4">Failed to load project</h1>
93
+ <div className="bg-white border-2 border-red-100 rounded p-6 mb-6">
94
+ <p className="font-mono text-base text-red-700 whitespace-pre-wrap">{error}</p>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ ) : serializedData ? (
99
+ <div className="max-w-7xl w-full mx-auto px-4 py-4">
100
+ <RealTimeKanbanWrapper initialData={serializedData} isBlank={isBlank} projectPath={projectPath} />
101
+ </div>
102
+ ) : null;
103
+
104
+ return (
105
+ <>
106
+ {/* Kanban board — always mounted, hidden when viewing a child route */}
107
+ <div style={isChildRoute ? { display: 'none' } : undefined}>
108
+ {kanbanContent}
109
+ </div>
110
+ {/* Child route content (work detail, proof dashboard, decision) */}
111
+ <Outlet />
112
+ </>
113
+ );
61
114
  }
@@ -1,27 +1,75 @@
1
- import { getEnvVars, discoverEnvFiles, getSelectedEnvFile, getMainBranch } from '@/lib/db';
1
+ import { useState, useEffect } from 'react';
2
2
  import { AccountSection } from '@/components/settings/AccountSection';
3
3
  import { EnvVarsSection } from '@/components/settings/EnvVarsSection';
4
4
  import { GeneralSection } from '@/components/settings/GeneralSection';
5
+ import { AiContextSection } from '@/components/settings/AiContextSection';
6
+ import { ProjectStackSection } from '@/components/settings/ProjectStackSection';
5
7
  import { SettingsLayout } from '@/components/settings/SettingsLayout';
6
-
7
- export const dynamic = 'force-dynamic';
8
+ import { dataBridge, prefetch } from '@/lib/data-bridge';
9
+ import type { ContextDocument } from '@/lib/data-bridge';
10
+ import type { EnvironmentConfig } from '@/lib/environment-config';
8
11
 
9
12
  export default function SettingsPage() {
10
- const envFiles = discoverEnvFiles();
11
- const selectedFile = getSelectedEnvFile() || (envFiles.includes('.env') ? '.env' : envFiles[0] || null);
12
- const envVars = selectedFile ? getEnvVars(selectedFile) : [];
13
- const mainBranch = getMainBranch();
13
+ const [envFiles, setEnvFiles] = useState<string[]>([]);
14
+ const [selectedFile, setSelectedFile] = useState<string | null>(null);
15
+ const [envVars, setEnvVars] = useState<Array<{ key: string; value: string }>>([]);
16
+ const [mainBranch, setMainBranch] = useState('main');
17
+ const [claudeModel, setClaudeModel] = useState<string | null>(null);
18
+ const [designSystemDir, setDesignSystemDir] = useState<string | null>(null);
19
+ const [contextDocuments, setContextDocuments] = useState<ContextDocument[]>([]);
20
+ const [environmentConfig, setEnvironmentConfig] = useState<EnvironmentConfig | null>(null);
21
+ const [loading, setLoading] = useState(true);
22
+
23
+ useEffect(() => {
24
+ async function loadSettings() {
25
+ try {
26
+ const { files, selected, branch, claudeModel: model, designSystemDir: dsDir, contextDocuments: ctxDocs, environmentConfig: envConfig } = await prefetch.settings();
27
+ setEnvFiles(files);
28
+ const activeFile = selected || (files.includes('.env') ? '.env' : files[0] || null);
29
+ setSelectedFile(activeFile);
30
+ if (activeFile) {
31
+ const vars = await dataBridge.getEnvVars(activeFile);
32
+ setEnvVars(vars);
33
+ }
34
+ setMainBranch(branch);
35
+ setClaudeModel(model);
36
+ setDesignSystemDir(dsDir);
37
+ setContextDocuments(ctxDocs);
38
+ setEnvironmentConfig(envConfig);
39
+ } catch (err) {
40
+ console.error('Failed to load settings:', err);
41
+ } finally {
42
+ setLoading(false);
43
+ }
44
+ }
45
+ loadSettings();
46
+ }, []);
47
+
48
+ if (loading) return (
49
+ <div className="flex-1 overflow-auto max-w-7xl w-full mx-auto px-4 py-4">
50
+ <div className="h-8 w-40 bg-zinc-200 dark:bg-zinc-800 rounded mb-8" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
51
+ <div className="flex gap-4 mb-6">
52
+ {[1, 2, 3, 4, 5].map(i => (
53
+ <div key={i} className="h-9 w-32 bg-zinc-200 dark:bg-zinc-800 rounded-lg" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
54
+ ))}
55
+ </div>
56
+ <div className="space-y-4">
57
+ <div className="h-12 bg-zinc-200 dark:bg-zinc-800 rounded-lg" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
58
+ <div className="h-12 bg-zinc-200 dark:bg-zinc-800 rounded-lg" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
59
+ </div>
60
+ </div>
61
+ );
14
62
 
15
63
  return (
16
- <div className="flex-1 overflow-auto max-w-6xl w-full mx-auto px-4 py-4">
17
- <h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mb-6">
18
- Settings
19
- </h1>
64
+ <div className="flex-1 overflow-auto max-w-7xl w-full mx-auto px-4 py-4">
65
+ <h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mb-8">Settings</h1>
20
66
  <SettingsLayout
21
67
  tabs={[
22
68
  { id: 'account', label: 'Account', content: <AccountSection /> },
23
- { id: 'general', label: 'General', content: <GeneralSection initialMainBranch={mainBranch} /> },
24
- { id: 'env-vars', label: 'Environment Variables', content: <EnvVarsSection initialEnvVars={envVars} envFiles={envFiles} selectedFile={selectedFile} /> },
69
+ { id: 'general', label: 'General', content: <GeneralSection initialMainBranch={{ branch: mainBranch, source: 'detected' }} initialClaudeModel={claudeModel} /> },
70
+ { id: 'env-vars', label: 'Environment Variables', content: <EnvVarsSection initialEnvVars={envVars.map(v => ({ name: v.key, value: v.value }))} envFiles={envFiles} selectedFile={selectedFile} /> },
71
+ { id: 'ai-context', label: 'AI Context', content: <AiContextSection initialDesignSystemDir={designSystemDir} initialContextDocuments={contextDocuments} /> },
72
+ { id: 'project-stack', label: 'Your Project Stack', content: <ProjectStackSection initialConfig={environmentConfig} /> },
25
73
  ]}
26
74
  />
27
75
  </div>