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
@@ -0,0 +1,242 @@
1
+ import { useState, useEffect, useRef } from 'react';
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';
6
+
7
+ const API_BASE = 'https://jettypod-update-server.spangbaryn2.workers.dev';
8
+
9
+ export default function SignupPage() {
10
+ const [email, setEmail] = useState('');
11
+ const [otpCode, setOtpCode] = useState('');
12
+ const [otpSent, setOtpSent] = useState(false);
13
+ const [isSending, setIsSending] = useState(false);
14
+ const [isVerifying, setIsVerifying] = useState(false);
15
+ const [error, setError] = useState<string | null>(null);
16
+
17
+ const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
18
+
19
+ // Redirect already-authenticated users to dashboard
20
+ useEffect(() => {
21
+ async function checkIfAlreadyAuthenticated() {
22
+ if (isTauri()) {
23
+ try {
24
+ const status = await auth.getStatus();
25
+ if (status.authenticated) {
26
+ const path = await auth.getPostLoginPath() || '/';
27
+ window.location.href = path;
28
+ }
29
+ } catch {
30
+ // Ignore — stay on signup page
31
+ }
32
+ }
33
+ }
34
+ checkIfAlreadyAuthenticated();
35
+ }, []);
36
+
37
+ useEffect(() => {
38
+ return () => {
39
+ if (pollRef.current) clearInterval(pollRef.current);
40
+ };
41
+ }, []);
42
+
43
+ const handleGoogleSignUp = () => {
44
+ if (!isTauri()) return;
45
+ auth.loginWithGoogle();
46
+
47
+ pollRef.current = setInterval(async () => {
48
+ try {
49
+ const status = await auth.getStatus();
50
+ if (status.authenticated) {
51
+ if (pollRef.current) clearInterval(pollRef.current);
52
+ const path = await auth.getPostLoginPath() || '/';
53
+ window.location.href = path;
54
+ }
55
+ } catch {
56
+ // Ignore — keep polling
57
+ }
58
+ }, 1000);
59
+ };
60
+
61
+ const handleSendOTP = async (e: React.FormEvent) => {
62
+ e.preventDefault();
63
+ setError(null);
64
+
65
+ if (!email.trim() || !email.includes('@')) {
66
+ setError('Please enter a valid email address.');
67
+ return;
68
+ }
69
+
70
+ setIsSending(true);
71
+
72
+ try {
73
+ const res = await fetch(`${API_BASE}/auth/otp/send`, {
74
+ method: 'POST',
75
+ headers: { 'Content-Type': 'application/json' },
76
+ body: JSON.stringify({ email: email.trim().toLowerCase() }),
77
+ });
78
+
79
+ if (!res.ok) {
80
+ const data = await res.json() as { error?: string };
81
+ setError(data.error || 'Failed to send code.');
82
+ return;
83
+ }
84
+
85
+ setOtpSent(true);
86
+ } catch {
87
+ setError('Failed to send code. Check your connection.');
88
+ } finally {
89
+ setIsSending(false);
90
+ }
91
+ };
92
+
93
+ const handleVerifyOTP = async (e: React.FormEvent) => {
94
+ e.preventDefault();
95
+ setError(null);
96
+
97
+ if (!otpCode.trim()) {
98
+ setError('Please enter the code from your email.');
99
+ return;
100
+ }
101
+
102
+ setIsVerifying(true);
103
+
104
+ try {
105
+ const res = await fetch(`${API_BASE}/auth/otp/verify`, {
106
+ method: 'POST',
107
+ headers: { 'Content-Type': 'application/json' },
108
+ body: JSON.stringify({ email: email.trim().toLowerCase(), code: otpCode.trim() }),
109
+ });
110
+
111
+ if (!res.ok) {
112
+ const data = await res.json() as { error?: string };
113
+ setError(data.error || 'Invalid or expired code.');
114
+ setIsVerifying(false);
115
+ return;
116
+ }
117
+
118
+ const data = await res.json() as { token: string; user: { id: string; email: string; plan: string } };
119
+
120
+ if (isTauri()) {
121
+ await auth.saveToken(data.token, data.user);
122
+ }
123
+
124
+ const path = await auth.getPostLoginPath() || '/';
125
+ window.location.href = path;
126
+ } catch {
127
+ setError('Failed to verify code. Check your connection.');
128
+ setIsVerifying(false);
129
+ }
130
+ };
131
+
132
+ return (
133
+ <div className="flex flex-col items-center justify-center min-h-screen bg-white dark:bg-zinc-900 p-8">
134
+ <div className="max-w-md w-full space-y-10">
135
+ {/* Logo */}
136
+ <div className="flex flex-col items-center space-y-6">
137
+ <img
138
+ src="/jettypod_wordmark.png"
139
+ alt="JettyPod"
140
+ width={160}
141
+ height={40}
142
+ />
143
+ <h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 text-center">
144
+ Create your account
145
+ </h1>
146
+ <p className="text-zinc-500 dark:text-zinc-400 text-center">
147
+ Get started free. No credit card required.
148
+ </p>
149
+ </div>
150
+
151
+ {/* Error */}
152
+ {error && (
153
+ <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
+ {error}
155
+ </div>
156
+ )}
157
+
158
+ {/* Google Sign-Up */}
159
+ <div className="pt-4">
160
+ <Button
161
+ onClick={handleGoogleSignUp}
162
+ size="lg"
163
+ fullWidth
164
+ >
165
+ Sign up with Google
166
+ </Button>
167
+ </div>
168
+
169
+ {/* Divider */}
170
+ <div className="flex items-center gap-4">
171
+ <div className="flex-1 h-px bg-zinc-200 dark:bg-zinc-700" />
172
+ <span className="text-base text-zinc-400 dark:text-zinc-500">or</span>
173
+ <div className="flex-1 h-px bg-zinc-200 dark:bg-zinc-700" />
174
+ </div>
175
+
176
+ {/* Email OTP */}
177
+ {!otpSent ? (
178
+ <form onSubmit={handleSendOTP} className="space-y-6">
179
+ <Input
180
+ type="email"
181
+ value={email}
182
+ onChange={(e) => setEmail(e.target.value)}
183
+ placeholder="Enter your email"
184
+ disabled={isSending}
185
+ />
186
+ <Button
187
+ type="submit"
188
+ variant="secondary"
189
+ size="lg"
190
+ fullWidth
191
+ disabled={isSending || !email.trim()}
192
+ >
193
+ {isSending ? 'Sending code...' : 'Sign up with email'}
194
+ </Button>
195
+ </form>
196
+ ) : (
197
+ <form onSubmit={handleVerifyOTP} className="space-y-6">
198
+ <p className="text-base text-zinc-500 dark:text-zinc-400">
199
+ We sent a 6-digit code to <span className="font-medium text-zinc-700 dark:text-zinc-300">{email}</span>
200
+ </p>
201
+ <Input
202
+ type="text"
203
+ value={otpCode}
204
+ onChange={(e) => setOtpCode(e.target.value)}
205
+ placeholder="Enter 6-digit code"
206
+ maxLength={6}
207
+ autoFocus
208
+ disabled={isVerifying}
209
+ className="text-center text-xl tracking-widest font-mono"
210
+ />
211
+ <Button
212
+ type="submit"
213
+ variant="secondary"
214
+ size="lg"
215
+ fullWidth
216
+ disabled={isVerifying || !otpCode.trim()}
217
+ >
218
+ {isVerifying ? 'Verifying...' : 'Verify code'}
219
+ </Button>
220
+ <Button
221
+ type="button"
222
+ variant="ghost"
223
+ size="sm"
224
+ fullWidth
225
+ onClick={() => { setOtpSent(false); setOtpCode(''); setError(null); }}
226
+ >
227
+ Use a different email
228
+ </Button>
229
+ </form>
230
+ )}
231
+
232
+ {/* Switch to login */}
233
+ <p className="text-center text-base text-zinc-500 dark:text-zinc-400">
234
+ Already have an account?{' '}
235
+ <Link to="/login" className="font-medium hover:underline" style={{ color: '#819D9F' }}>
236
+ Sign in
237
+ </Link>
238
+ </p>
239
+ </div>
240
+ </div>
241
+ );
242
+ }
@@ -1,5 +1,3 @@
1
- 'use client';
2
-
3
1
  import { SubscribeContent } from '@/components/SubscribeContent';
4
2
 
5
3
  export default function SubscribePage() {
@@ -1,10 +1,43 @@
1
- import { getTestDashboardData } from '@/lib/tests';
1
+ import { useState, useEffect } from 'react';
2
2
  import { RealTimeTestsWrapper } from '@/components/RealTimeTestsWrapper';
3
-
4
- export const dynamic = 'force-dynamic';
3
+ import { prefetch } from '@/lib/data-bridge';
5
4
 
6
5
  export default function TestsPage() {
7
- const initialData = getTestDashboardData();
6
+ const [initialData, setInitialData] = useState<any>(null);
7
+ const [loading, setLoading] = useState(true);
8
+
9
+ useEffect(() => {
10
+ async function loadData() {
11
+ try {
12
+ const data = await prefetch.tests();
13
+ setInitialData(data);
14
+ } catch (err) {
15
+ console.error('Failed to load test data:', err);
16
+ setInitialData({ suites: [], summary: { total: 0, passing: 0, failing: 0, pending: 0 } });
17
+ } finally {
18
+ setLoading(false);
19
+ }
20
+ }
21
+ loadData();
22
+ }, []);
23
+
24
+ if (loading || !initialData) return (
25
+ <div className="flex-1 max-w-7xl w-full mx-auto px-4 py-4">
26
+ <div className="flex justify-between items-center mb-6">
27
+ <div className="h-8 w-32 bg-zinc-200 dark:bg-zinc-800 rounded" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
28
+ <div className="flex gap-3">
29
+ {[1, 2, 3, 4].map(i => (
30
+ <div key={i} className="h-10 w-20 bg-zinc-200 dark:bg-zinc-800 rounded-lg" style={{ animation: 'skeleton-pulse 1.5s ease-in-out infinite' }} />
31
+ ))}
32
+ </div>
33
+ </div>
34
+ <div className="space-y-3">
35
+ {[1, 2, 3, 4, 5].map(i => (
36
+ <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' }} />
37
+ ))}
38
+ </div>
39
+ </div>
40
+ );
8
41
 
9
42
  return <RealTimeTestsWrapper initialData={initialData} />;
10
43
  }
@@ -1,8 +1,7 @@
1
- 'use client';
2
-
3
1
  import { useState, useEffect } from 'react';
4
2
  import { WelcomeScreen } from '@/components/WelcomeScreen';
5
- import type { RecentProject } from '@/lib/db-bridge';
3
+ import { isTauri, project } from '@/lib/tauri-bridge';
4
+ import type { RecentProject } from '@/lib/tauri-bridge';
6
5
 
7
6
  export default function WelcomePage() {
8
7
  const [error, setError] = useState<string | null>(null);
@@ -11,11 +10,11 @@ export default function WelcomePage() {
11
10
  // Load recent projects on mount
12
11
  useEffect(() => {
13
12
  async function loadRecentProjects() {
14
- if (!window.electronAPI?.isElectron) {
13
+ if (!isTauri()) {
15
14
  return;
16
15
  }
17
16
 
18
- const projects = await window.electronAPI.project.getRecent();
17
+ const projects = await project.getRecent();
19
18
  setRecentProjects(projects);
20
19
  }
21
20
 
@@ -25,12 +24,12 @@ export default function WelcomePage() {
25
24
  const handleNewProject = async () => {
26
25
  setError(null);
27
26
 
28
- if (!window.electronAPI?.isElectron) {
27
+ if (!isTauri()) {
29
28
  setError('Project creation is only available in the desktop app.');
30
29
  return;
31
30
  }
32
31
 
33
- const result = await window.electronAPI.project.newProject();
32
+ const result = await project.newProject();
34
33
 
35
34
  if (result.canceled) {
36
35
  return;
@@ -47,13 +46,12 @@ export default function WelcomePage() {
47
46
  const handleOpenProject = async () => {
48
47
  setError(null);
49
48
 
50
- // Check if we're in Electron
51
- if (!window.electronAPI?.isElectron) {
49
+ if (!isTauri()) {
52
50
  setError('Project selection is only available in the desktop app.');
53
51
  return;
54
52
  }
55
53
 
56
- const result = await window.electronAPI.project.openDialog();
54
+ const result = await project.openDialog();
57
55
 
58
56
  if (result.canceled) {
59
57
  // User canceled - do nothing
@@ -69,19 +67,18 @@ export default function WelcomePage() {
69
67
  window.location.href = '/';
70
68
  };
71
69
 
72
- const handleSelectRecentProject = async (project: RecentProject) => {
70
+ const handleSelectRecentProject = async (p: RecentProject) => {
73
71
  setError(null);
74
72
 
75
- // Check if we're in Electron
76
- if (!window.electronAPI?.isElectron) {
73
+ if (!isTauri()) {
77
74
  setError('Project selection is only available in the desktop app.');
78
75
  return;
79
76
  }
80
77
 
81
- const result = await window.electronAPI.project.openRecent(project.path);
78
+ const result = await project.openRecent(p.path);
82
79
 
83
80
  if (!result.success) {
84
- setError(result.error || 'Failed to open project');
81
+ setError('Failed to open project');
85
82
  return;
86
83
  }
87
84
 
@@ -92,7 +89,7 @@ export default function WelcomePage() {
92
89
  return (
93
90
  <>
94
91
  {error && (
95
- <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">
92
+ <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">
96
93
  {error}
97
94
  </div>
98
95
  )}