jettypod 4.4.116 → 4.4.120

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 (162) hide show
  1. package/.env +7 -0
  2. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +124 -48
  3. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +171 -58
  4. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +161 -10
  5. package/apps/dashboard/app/api/tests/run/stream/route.ts +13 -1
  6. package/apps/dashboard/app/api/usage/route.ts +17 -0
  7. package/apps/dashboard/app/api/work/[id]/route.ts +35 -0
  8. package/apps/dashboard/app/api/work/[id]/status/route.ts +43 -1
  9. package/apps/dashboard/app/connect-claude/page.tsx +24 -0
  10. package/apps/dashboard/app/decision/[id]/page.tsx +14 -14
  11. package/apps/dashboard/app/demo/gates/page.tsx +42 -42
  12. package/apps/dashboard/app/design-system/page.tsx +868 -0
  13. package/apps/dashboard/app/globals.css +6 -2
  14. package/apps/dashboard/app/install-claude/page.tsx +9 -7
  15. package/apps/dashboard/app/layout.tsx +17 -5
  16. package/apps/dashboard/app/login/page.tsx +250 -0
  17. package/apps/dashboard/app/page.tsx +11 -9
  18. package/apps/dashboard/app/settings/page.tsx +4 -2
  19. package/apps/dashboard/app/signup/page.tsx +245 -0
  20. package/apps/dashboard/app/subscribe/page.tsx +11 -0
  21. package/apps/dashboard/app/welcome/page.tsx +24 -1
  22. package/apps/dashboard/app/work/[id]/page.tsx +34 -50
  23. package/apps/dashboard/components/AppShell.tsx +95 -55
  24. package/apps/dashboard/components/CardMenu.tsx +56 -13
  25. package/apps/dashboard/components/ClaudePanel.tsx +301 -582
  26. package/apps/dashboard/components/ClaudePanelInput.tsx +23 -14
  27. package/apps/dashboard/components/ConnectClaudeScreen.tsx +210 -0
  28. package/apps/dashboard/components/CopyableId.tsx +3 -3
  29. package/apps/dashboard/components/DetailReviewActions.tsx +109 -0
  30. package/apps/dashboard/components/DragContext.tsx +75 -65
  31. package/apps/dashboard/components/DraggableCard.tsx +6 -46
  32. package/apps/dashboard/components/DropZone.tsx +2 -2
  33. package/apps/dashboard/components/EditableDetailDescription.tsx +1 -1
  34. package/apps/dashboard/components/EditableTitle.tsx +26 -6
  35. package/apps/dashboard/components/ElapsedTimer.tsx +54 -0
  36. package/apps/dashboard/components/EpicGroup.tsx +329 -0
  37. package/apps/dashboard/components/GateCard.tsx +100 -16
  38. package/apps/dashboard/components/GateChoiceCard.tsx +15 -17
  39. package/apps/dashboard/components/InstallClaudeScreen.tsx +140 -51
  40. package/apps/dashboard/components/JettyLoader.tsx +38 -0
  41. package/apps/dashboard/components/KanbanBoard.tsx +147 -766
  42. package/apps/dashboard/components/KanbanCard.tsx +506 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +12 -0
  44. package/apps/dashboard/components/MainNav.tsx +20 -54
  45. package/apps/dashboard/components/MessageBlock.tsx +391 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -15
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +214 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +11 -21
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +36 -8
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +25 -25
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +265 -301
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +97 -74
  53. package/apps/dashboard/components/ReviewFooter.tsx +141 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -18
  55. package/apps/dashboard/components/SubscribeContent.tsx +206 -0
  56. package/apps/dashboard/components/TestTree.tsx +15 -14
  57. package/apps/dashboard/components/TipCard.tsx +177 -0
  58. package/apps/dashboard/components/Toast.tsx +5 -5
  59. package/apps/dashboard/components/TypeIcon.tsx +56 -0
  60. package/apps/dashboard/components/UpgradeBanner.tsx +30 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +61 -62
  62. package/apps/dashboard/components/WelcomeScreen.tsx +25 -27
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -4
  64. package/apps/dashboard/components/WorkItemTree.tsx +9 -28
  65. package/apps/dashboard/components/settings/AccountSection.tsx +169 -0
  66. package/apps/dashboard/components/settings/EnvVarsSection.tsx +54 -79
  67. package/apps/dashboard/components/settings/GeneralSection.tsx +26 -31
  68. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -4
  69. package/apps/dashboard/components/ui/Button.tsx +104 -0
  70. package/apps/dashboard/components/ui/Input.tsx +78 -0
  71. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +408 -105
  72. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -4
  73. package/apps/dashboard/contexts/UsageContext.tsx +155 -0
  74. package/apps/dashboard/contexts/usageHelpers.js +9 -0
  75. package/apps/dashboard/electron/ipc-handlers.js +281 -88
  76. package/apps/dashboard/electron/main.js +691 -131
  77. package/apps/dashboard/electron/preload.js +25 -4
  78. package/apps/dashboard/electron/session-manager.js +163 -0
  79. package/apps/dashboard/electron-builder.config.js +3 -5
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/lib/backlog-parser.ts +50 -0
  83. package/apps/dashboard/lib/claude-process-manager.ts +50 -11
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/db-bridge.ts +33 -0
  86. package/apps/dashboard/lib/db.ts +136 -20
  87. package/apps/dashboard/lib/kanban-utils.ts +70 -0
  88. package/apps/dashboard/lib/run-migrations.js +27 -2
  89. package/apps/dashboard/lib/session-state-machine.ts +3 -0
  90. package/apps/dashboard/lib/session-stream-manager.ts +144 -38
  91. package/apps/dashboard/lib/shadows.ts +7 -0
  92. package/apps/dashboard/lib/tests.ts +3 -1
  93. package/apps/dashboard/lib/utils.ts +6 -0
  94. package/apps/dashboard/next.config.js +35 -14
  95. package/apps/dashboard/package.json +6 -3
  96. package/apps/dashboard/public/bug-icon.svg +9 -0
  97. package/apps/dashboard/public/buoy-icon.svg +9 -0
  98. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  99. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  100. package/apps/dashboard/public/in-flight-seagull.svg +9 -0
  101. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  102. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  103. package/apps/dashboard/public/jettypod_logo.png +0 -0
  104. package/apps/dashboard/public/pier-icon.svg +14 -0
  105. package/apps/dashboard/public/star-icon.svg +9 -0
  106. package/apps/dashboard/public/wrench-icon.svg +9 -0
  107. package/apps/dashboard/scripts/upload-to-r2.js +89 -0
  108. package/apps/dashboard/scripts/ws-server.js +191 -0
  109. package/apps/dashboard/tsconfig.tsbuildinfo +1 -0
  110. package/apps/update-server/package.json +16 -0
  111. package/apps/update-server/schema.sql +31 -0
  112. package/apps/update-server/src/index.ts +1085 -0
  113. package/apps/update-server/tsconfig.json +16 -0
  114. package/apps/update-server/wrangler.toml +35 -0
  115. package/cucumber.js +9 -3
  116. package/docs/COMMAND_REFERENCE.md +34 -0
  117. package/hooks/post-checkout +32 -75
  118. package/hooks/post-merge +111 -10
  119. package/jest.setup.js +1 -0
  120. package/jettypod.js +54 -116
  121. package/lib/chore-taxonomy.js +33 -10
  122. package/lib/database.js +36 -16
  123. package/lib/db-watcher.js +1 -1
  124. package/lib/git-hooks/pre-commit +1 -1
  125. package/lib/jettypod-backup.js +27 -4
  126. package/lib/migrations/027-plan-at-creation-column.js +33 -0
  127. package/lib/migrations/028-ready-for-review-column.js +27 -0
  128. package/lib/migrations/029-remove-autoincrement.js +307 -0
  129. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  130. package/lib/migrations/index.js +47 -4
  131. package/lib/schema.js +13 -6
  132. package/lib/seed-onboarding.js +101 -69
  133. package/lib/update-command/index.js +9 -175
  134. package/lib/work-commands/index.js +129 -16
  135. package/lib/work-tracking/index.js +86 -46
  136. package/lib/worktree-diagnostics.js +16 -16
  137. package/lib/worktree-facade.js +1 -1
  138. package/lib/worktree-manager.js +8 -8
  139. package/lib/worktree-reconciler.js +5 -5
  140. package/package.json +9 -2
  141. package/scripts/ndjson-to-cucumber-json.js +152 -0
  142. package/scripts/postinstall.js +25 -0
  143. package/skills-templates/bug-mode/SKILL.md +39 -28
  144. package/skills-templates/bug-planning/SKILL.md +25 -29
  145. package/skills-templates/chore-mode/SKILL.md +131 -68
  146. package/skills-templates/chore-mode/verification.js +51 -10
  147. package/skills-templates/chore-planning/SKILL.md +47 -18
  148. package/skills-templates/epic-planning/SKILL.md +68 -48
  149. package/skills-templates/external-transition/SKILL.md +47 -47
  150. package/skills-templates/feature-planning/SKILL.md +83 -73
  151. package/skills-templates/production-mode/SKILL.md +49 -49
  152. package/skills-templates/request-routing/SKILL.md +27 -14
  153. package/skills-templates/simple-improvement/SKILL.md +68 -44
  154. package/skills-templates/speed-mode/SKILL.md +209 -128
  155. package/skills-templates/stable-mode/SKILL.md +105 -94
  156. package/templates/bdd-guidance.md +139 -0
  157. package/templates/bdd-scaffolding/wait.js +18 -0
  158. package/templates/bdd-scaffolding/world.js +19 -0
  159. package/.jettypod-backup/work.db +0 -0
  160. package/apps/dashboard/app/access-code/page.tsx +0 -110
  161. package/lib/discovery-checkpoint.js +0 -123
  162. package/skills-templates/project-discovery/SKILL.md +0 -372
@@ -6,7 +6,7 @@
6
6
  @theme inline {
7
7
  --color-background: var(--background);
8
8
  --color-foreground: var(--foreground);
9
- --font-sans: var(--font-geist-sans);
9
+ --font-sans: var(--font-satoshi);
10
10
  --font-mono: var(--font-geist-mono);
11
11
  --color-sidebar-ring: var(--sidebar-ring);
12
12
  --color-sidebar-border: var(--sidebar-border);
@@ -45,7 +45,7 @@
45
45
 
46
46
  :root {
47
47
  --radius: 0.625rem;
48
- --background: oklch(1 0 0);
48
+ --background: oklch(0.98 0.002 80);
49
49
  --foreground: oklch(0.145 0 0);
50
50
  --card: oklch(1 0 0);
51
51
  --card-foreground: oklch(0.145 0 0);
@@ -120,3 +120,7 @@
120
120
  @apply bg-background text-foreground;
121
121
  }
122
122
  }
123
+
124
+ html {
125
+ font-size: 16px;
126
+ }
@@ -5,13 +5,12 @@ import { InstallClaudeScreen } from '@/components/InstallClaudeScreen';
5
5
 
6
6
  export default function InstallClaudePage() {
7
7
  const [isInstalling, setIsInstalling] = useState(false);
8
- const [installProgress, setInstallProgress] = useState<string>('');
8
+ const [isSuccess, setIsSuccess] = useState(false);
9
9
  const [error, setError] = useState<string | null>(null);
10
10
 
11
11
  const handleInstall = async () => {
12
12
  setError(null);
13
13
  setIsInstalling(true);
14
- setInstallProgress('Starting installation...\n');
15
14
 
16
15
  // Check if we're in Electron
17
16
  if (!window.electronAPI?.isElectron) {
@@ -29,10 +28,13 @@ export default function InstallClaudePage() {
29
28
  return;
30
29
  }
31
30
 
32
- setInstallProgress(prev => prev + 'Installation complete!\n');
31
+ // Show success animation
32
+ setIsSuccess(true);
33
33
 
34
- // Navigate to dashboard after successful install
35
- window.location.href = '/';
34
+ // After success animation plays, navigate to connect-claude
35
+ setTimeout(() => {
36
+ window.location.href = '/connect-claude';
37
+ }, 2000);
36
38
  } catch (err) {
37
39
  setError(err instanceof Error ? err.message : 'Installation failed');
38
40
  setIsInstalling(false);
@@ -42,14 +44,14 @@ export default function InstallClaudePage() {
42
44
  return (
43
45
  <>
44
46
  {error && (
45
- <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">
47
+ <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">
46
48
  {error}
47
49
  </div>
48
50
  )}
49
51
  <InstallClaudeScreen
50
52
  onInstall={handleInstall}
51
53
  isInstalling={isInstalling}
52
- installProgress={installProgress}
54
+ isSuccess={isSuccess}
53
55
  />
54
56
  </>
55
57
  );
@@ -1,12 +1,23 @@
1
1
  import type { Metadata } from "next";
2
- import { Geist, Geist_Mono } from "next/font/google";
2
+ import localFont from "next/font/local";
3
+ import { Geist_Mono } from "next/font/google";
3
4
  import "./globals.css";
4
5
  import { getProjectName } from "@/lib/db";
5
6
  import { AppShell } from "@/components/AppShell";
6
7
 
7
- const geistSans = Geist({
8
- variable: "--font-geist-sans",
9
- subsets: ["latin"],
8
+ const satoshi = localFont({
9
+ src: [
10
+ {
11
+ path: "../public/fonts/Satoshi-Variable.woff2",
12
+ style: "normal",
13
+ },
14
+ {
15
+ path: "../public/fonts/Satoshi-VariableItalic.woff2",
16
+ style: "italic",
17
+ },
18
+ ],
19
+ variable: "--font-satoshi",
20
+ display: "swap",
10
21
  });
11
22
 
12
23
  const geistMono = Geist_Mono({
@@ -32,11 +43,12 @@ export default function RootLayout({
32
43
  return (
33
44
  <html lang="en" suppressHydrationWarning>
34
45
  <body
35
- className={`${geistSans.variable} ${geistMono.variable} antialiased`}
46
+ className={`${satoshi.className} ${satoshi.variable} ${geistMono.variable} antialiased`}
36
47
  >
37
48
  <AppShell projectName={projectName}>
38
49
  {children}
39
50
  </AppShell>
51
+ <div id="tooltip-root" style={{ position: 'relative', zIndex: 99999 }} />
40
52
  </body>
41
53
  </html>
42
54
  );
@@ -0,0 +1,250 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef } from 'react';
4
+ import Image from 'next/image';
5
+ import Link from 'next/link';
6
+ import { Button } from '@/components/ui/Button';
7
+ import { Input } from '@/components/ui/Input';
8
+
9
+ const API_BASE = 'https://jettypod-update-server.spangbaryn2.workers.dev';
10
+
11
+ export default function LoginPage() {
12
+ const [email, setEmail] = useState('');
13
+ const [otpCode, setOtpCode] = useState('');
14
+ const [otpSent, setOtpSent] = useState(false);
15
+ const [isSending, setIsSending] = useState(false);
16
+ const [isVerifying, setIsVerifying] = useState(false);
17
+ const [error, setError] = useState<string | null>(null);
18
+
19
+ // Poll for auth completion after Google sign-in.
20
+ // The deep link handler in main.js saves the token — this polling detects it
21
+ // and navigates to the dashboard even if mainWindow.loadURL doesn't fire.
22
+ const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
23
+
24
+ // Redirect already-authenticated users to dashboard
25
+ useEffect(() => {
26
+ async function checkIfAlreadyAuthenticated() {
27
+ if (window.electronAPI?.isElectron) {
28
+ try {
29
+ const status = await window.electronAPI.auth.getStatus();
30
+ if (status.authenticated) {
31
+ const path = await window.electronAPI.auth.getPostLoginPath?.() || '/';
32
+ window.location.href = path;
33
+ }
34
+ } catch {
35
+ // Ignore — stay on login page
36
+ }
37
+ }
38
+ }
39
+ checkIfAlreadyAuthenticated();
40
+ }, []);
41
+
42
+ useEffect(() => {
43
+ return () => {
44
+ if (pollRef.current) clearInterval(pollRef.current);
45
+ };
46
+ }, []);
47
+
48
+ const handleGoogleSignIn = () => {
49
+ if (!window.electronAPI?.isElectron) return;
50
+ window.electronAPI.auth.loginWithGoogle();
51
+
52
+ // Start polling for auth status (token saved by deep link handler)
53
+ pollRef.current = setInterval(async () => {
54
+ try {
55
+ const status = await window.electronAPI!.auth.getStatus();
56
+ if (status.authenticated) {
57
+ if (pollRef.current) clearInterval(pollRef.current);
58
+ const path = await window.electronAPI!.auth.getPostLoginPath?.() || '/';
59
+ window.location.href = path;
60
+ }
61
+ } catch {
62
+ // Ignore — keep polling
63
+ }
64
+ }, 1000);
65
+ };
66
+
67
+ const handleSendOTP = async (e: React.FormEvent) => {
68
+ e.preventDefault();
69
+ setError(null);
70
+
71
+ if (!email.trim() || !email.includes('@')) {
72
+ setError('Please enter a valid email address.');
73
+ return;
74
+ }
75
+
76
+ setIsSending(true);
77
+
78
+ try {
79
+ const res = await fetch(`${API_BASE}/auth/otp/send`, {
80
+ method: 'POST',
81
+ headers: { 'Content-Type': 'application/json' },
82
+ body: JSON.stringify({ email: email.trim().toLowerCase() }),
83
+ });
84
+
85
+ if (!res.ok) {
86
+ const data = await res.json() as { error?: string };
87
+ setError(data.error || 'Failed to send code.');
88
+ return;
89
+ }
90
+
91
+ setOtpSent(true);
92
+ } catch {
93
+ setError('Failed to send code. Check your connection.');
94
+ } finally {
95
+ setIsSending(false);
96
+ }
97
+ };
98
+
99
+ const handleVerifyOTP = async (e: React.FormEvent) => {
100
+ e.preventDefault();
101
+ setError(null);
102
+
103
+ if (!otpCode.trim()) {
104
+ setError('Please enter the code from your email.');
105
+ return;
106
+ }
107
+
108
+ setIsVerifying(true);
109
+
110
+ try {
111
+ const res = await fetch(`${API_BASE}/auth/otp/verify`, {
112
+ method: 'POST',
113
+ headers: { 'Content-Type': 'application/json' },
114
+ body: JSON.stringify({ email: email.trim().toLowerCase(), code: otpCode.trim() }),
115
+ });
116
+
117
+ if (!res.ok) {
118
+ const data = await res.json() as { error?: string };
119
+ setError(data.error || 'Invalid or expired code.');
120
+ setIsVerifying(false);
121
+ return;
122
+ }
123
+
124
+ const data = await res.json() as { token: string; user: { id: string; email: string; plan: string } };
125
+
126
+ // Save auth state via Electron IPC
127
+ if (window.electronAPI?.isElectron) {
128
+ await window.electronAPI.auth.saveToken(data.token, data.user);
129
+ }
130
+
131
+ const path = await window.electronAPI?.auth.getPostLoginPath?.() || '/';
132
+ window.location.href = path;
133
+ } catch {
134
+ setError('Failed to verify code. Check your connection.');
135
+ setIsVerifying(false);
136
+ }
137
+ };
138
+
139
+ return (
140
+ <div className="flex flex-col items-center justify-center min-h-screen bg-white dark:bg-zinc-900 p-8">
141
+ <div className="max-w-md w-full space-y-10">
142
+ {/* Logo */}
143
+ <div className="flex flex-col items-center space-y-6">
144
+ <Image
145
+ src="/jettypod_wordmark.png"
146
+ alt="JettyPod"
147
+ width={160}
148
+ height={40}
149
+ priority
150
+ />
151
+ <h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 text-center">
152
+ Sign in to JettyPod
153
+ </h1>
154
+ <p className="text-zinc-500 dark:text-zinc-400 text-center">
155
+ Sign in to get started. Free plan included.
156
+ </p>
157
+ </div>
158
+
159
+ {/* Error */}
160
+ {error && (
161
+ <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">
162
+ {error}
163
+ </div>
164
+ )}
165
+
166
+ {/* Google Sign-In */}
167
+ <div className="pt-4">
168
+ <Button
169
+ onClick={handleGoogleSignIn}
170
+ size="lg"
171
+ fullWidth
172
+ >
173
+ Sign in with Google
174
+ </Button>
175
+ </div>
176
+
177
+ {/* Divider */}
178
+ <div className="flex items-center gap-4">
179
+ <div className="flex-1 h-px bg-zinc-200 dark:bg-zinc-700" />
180
+ <span className="text-base text-zinc-400 dark:text-zinc-500">or</span>
181
+ <div className="flex-1 h-px bg-zinc-200 dark:bg-zinc-700" />
182
+ </div>
183
+
184
+ {/* Email OTP */}
185
+ {!otpSent ? (
186
+ <form onSubmit={handleSendOTP} className="space-y-6">
187
+ <Input
188
+ type="email"
189
+ value={email}
190
+ onChange={(e) => setEmail(e.target.value)}
191
+ placeholder="Enter your email"
192
+ disabled={isSending}
193
+ />
194
+ <Button
195
+ type="submit"
196
+ variant="secondary"
197
+ size="lg"
198
+ fullWidth
199
+ disabled={isSending || !email.trim()}
200
+ >
201
+ {isSending ? 'Sending code...' : 'Sign in with email'}
202
+ </Button>
203
+ </form>
204
+ ) : (
205
+ <form onSubmit={handleVerifyOTP} className="space-y-6">
206
+ <p className="text-base text-zinc-500 dark:text-zinc-400">
207
+ We sent a 6-digit code to <span className="font-medium text-zinc-700 dark:text-zinc-300">{email}</span>
208
+ </p>
209
+ <Input
210
+ type="text"
211
+ value={otpCode}
212
+ onChange={(e) => setOtpCode(e.target.value)}
213
+ placeholder="Enter 6-digit code"
214
+ maxLength={6}
215
+ autoFocus
216
+ disabled={isVerifying}
217
+ className="text-center text-xl tracking-widest font-mono"
218
+ />
219
+ <Button
220
+ type="submit"
221
+ variant="secondary"
222
+ size="lg"
223
+ fullWidth
224
+ disabled={isVerifying || !otpCode.trim()}
225
+ >
226
+ {isVerifying ? 'Verifying...' : 'Verify code'}
227
+ </Button>
228
+ <Button
229
+ type="button"
230
+ variant="ghost"
231
+ size="sm"
232
+ fullWidth
233
+ onClick={() => { setOtpSent(false); setOtpCode(''); setError(null); }}
234
+ >
235
+ Use a different email
236
+ </Button>
237
+ </form>
238
+ )}
239
+
240
+ {/* Switch to signup */}
241
+ <p className="text-center text-base text-zinc-500 dark:text-zinc-400">
242
+ Don&apos;t have an account?{' '}
243
+ <Link href="/signup" className="font-medium hover:underline" style={{ color: '#819D9F' }}>
244
+ Create one
245
+ </Link>
246
+ </p>
247
+ </div>
248
+ </div>
249
+ );
250
+ }
@@ -1,5 +1,5 @@
1
1
  import { redirect } from 'next/navigation';
2
- import { getKanbanData, hasProject } from '@/lib/db';
2
+ import { getKanbanData, hasProject, isBlankProject, hasOnboardingStarted, getProjectRoot } from '@/lib/db';
3
3
  import { RealTimeKanbanWrapper } from '@/components/RealTimeKanbanWrapper';
4
4
 
5
5
  // Force dynamic rendering - database is only available at runtime
@@ -13,6 +13,8 @@ export default function Home() {
13
13
 
14
14
  try {
15
15
  const data = getKanbanData();
16
+ const projectRoot = getProjectRoot();
17
+ const isBlank = projectRoot ? isBlankProject(projectRoot) && !hasOnboardingStarted() : false;
16
18
 
17
19
  // Serialize Map data for client component
18
20
  const serializedData = {
@@ -22,8 +24,8 @@ export default function Home() {
22
24
  };
23
25
 
24
26
  return (
25
- <div className="flex-1 flex flex-col min-h-0 overflow-hidden max-w-6xl w-full mx-auto px-4 py-4">
26
- <RealTimeKanbanWrapper initialData={serializedData} />
27
+ <div className="h-full flex flex-col min-h-0 overflow-hidden max-w-7xl w-full mx-auto px-4 py-4">
28
+ <RealTimeKanbanWrapper initialData={serializedData} isBlank={isBlank} projectPath={projectRoot || ''} />
27
29
  </div>
28
30
  );
29
31
  } catch (error) {
@@ -38,18 +40,18 @@ export default function Home() {
38
40
  // Return error UI so user can see what's wrong
39
41
  return (
40
42
  <div className="flex-1 flex items-center justify-center p-8">
41
- <div className="max-w-2xl w-full bg-red-50 border border-red-200 rounded-lg p-6">
43
+ <div className="max-w-2xl w-full bg-red-50 border-2 border-red-200 rounded-lg p-8">
42
44
  <h1 className="text-xl font-bold text-red-800 mb-4">Failed to load project</h1>
43
- <div className="bg-white border border-red-100 rounded p-4 mb-4">
44
- <p className="font-mono text-sm text-red-700 whitespace-pre-wrap">{errorMessage}</p>
45
+ <div className="bg-white border-2 border-red-100 rounded p-6 mb-6">
46
+ <p className="font-mono text-base text-red-700 whitespace-pre-wrap">{errorMessage}</p>
45
47
  </div>
46
- <div className="text-sm text-gray-600 space-y-1">
48
+ <div className="text-base text-gray-600 space-y-1.5">
47
49
  <p><strong>Project path:</strong> {process.env.JETTYPOD_PROJECT_PATH || '(not set)'}</p>
48
50
  </div>
49
51
  {errorStack && (
50
52
  <details className="mt-4">
51
- <summary className="text-sm text-gray-500 cursor-pointer">Stack trace</summary>
52
- <pre className="mt-2 text-xs text-gray-500 overflow-auto p-2 bg-gray-50 rounded">{errorStack}</pre>
53
+ <summary className="text-base text-gray-500 cursor-pointer">Stack trace</summary>
54
+ <pre className="mt-2 text-base text-gray-500 overflow-auto p-3 bg-gray-50 rounded">{errorStack}</pre>
53
55
  </details>
54
56
  )}
55
57
  </div>
@@ -1,4 +1,5 @@
1
1
  import { getEnvVars, discoverEnvFiles, getSelectedEnvFile, getMainBranch } from '@/lib/db';
2
+ import { AccountSection } from '@/components/settings/AccountSection';
2
3
  import { EnvVarsSection } from '@/components/settings/EnvVarsSection';
3
4
  import { GeneralSection } from '@/components/settings/GeneralSection';
4
5
  import { SettingsLayout } from '@/components/settings/SettingsLayout';
@@ -12,12 +13,13 @@ export default function SettingsPage() {
12
13
  const mainBranch = getMainBranch();
13
14
 
14
15
  return (
15
- <div className="flex-1 overflow-auto max-w-6xl w-full mx-auto px-4 py-4">
16
- <h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mb-6">
16
+ <div className="flex-1 overflow-auto max-w-7xl w-full mx-auto px-4 py-4">
17
+ <h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mb-8">
17
18
  Settings
18
19
  </h1>
19
20
  <SettingsLayout
20
21
  tabs={[
22
+ { id: 'account', label: 'Account', content: <AccountSection /> },
21
23
  { id: 'general', label: 'General', content: <GeneralSection initialMainBranch={mainBranch} /> },
22
24
  { id: 'env-vars', label: 'Environment Variables', content: <EnvVarsSection initialEnvVars={envVars} envFiles={envFiles} selectedFile={selectedFile} /> },
23
25
  ]}
@@ -0,0 +1,245 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef } from 'react';
4
+ import Image from 'next/image';
5
+ import Link from 'next/link';
6
+ import { Button } from '@/components/ui/Button';
7
+ import { Input } from '@/components/ui/Input';
8
+
9
+ const API_BASE = 'https://jettypod-update-server.spangbaryn2.workers.dev';
10
+
11
+ export default function SignupPage() {
12
+ const [email, setEmail] = useState('');
13
+ const [otpCode, setOtpCode] = useState('');
14
+ const [otpSent, setOtpSent] = useState(false);
15
+ const [isSending, setIsSending] = useState(false);
16
+ const [isVerifying, setIsVerifying] = useState(false);
17
+ const [error, setError] = useState<string | null>(null);
18
+
19
+ const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
20
+
21
+ // Redirect already-authenticated users to dashboard
22
+ useEffect(() => {
23
+ async function checkIfAlreadyAuthenticated() {
24
+ if (window.electronAPI?.isElectron) {
25
+ try {
26
+ const status = await window.electronAPI.auth.getStatus();
27
+ if (status.authenticated) {
28
+ const path = await window.electronAPI.auth.getPostLoginPath?.() || '/';
29
+ window.location.href = path;
30
+ }
31
+ } catch {
32
+ // Ignore — stay on signup page
33
+ }
34
+ }
35
+ }
36
+ checkIfAlreadyAuthenticated();
37
+ }, []);
38
+
39
+ useEffect(() => {
40
+ return () => {
41
+ if (pollRef.current) clearInterval(pollRef.current);
42
+ };
43
+ }, []);
44
+
45
+ const handleGoogleSignUp = () => {
46
+ if (!window.electronAPI?.isElectron) return;
47
+ window.electronAPI.auth.loginWithGoogle();
48
+
49
+ pollRef.current = setInterval(async () => {
50
+ try {
51
+ const status = await window.electronAPI!.auth.getStatus();
52
+ if (status.authenticated) {
53
+ if (pollRef.current) clearInterval(pollRef.current);
54
+ const path = await window.electronAPI!.auth.getPostLoginPath?.() || '/';
55
+ window.location.href = path;
56
+ }
57
+ } catch {
58
+ // Ignore — keep polling
59
+ }
60
+ }, 1000);
61
+ };
62
+
63
+ const handleSendOTP = async (e: React.FormEvent) => {
64
+ e.preventDefault();
65
+ setError(null);
66
+
67
+ if (!email.trim() || !email.includes('@')) {
68
+ setError('Please enter a valid email address.');
69
+ return;
70
+ }
71
+
72
+ setIsSending(true);
73
+
74
+ try {
75
+ const res = await fetch(`${API_BASE}/auth/otp/send`, {
76
+ method: 'POST',
77
+ headers: { 'Content-Type': 'application/json' },
78
+ body: JSON.stringify({ email: email.trim().toLowerCase() }),
79
+ });
80
+
81
+ if (!res.ok) {
82
+ const data = await res.json() as { error?: string };
83
+ setError(data.error || 'Failed to send code.');
84
+ return;
85
+ }
86
+
87
+ setOtpSent(true);
88
+ } catch {
89
+ setError('Failed to send code. Check your connection.');
90
+ } finally {
91
+ setIsSending(false);
92
+ }
93
+ };
94
+
95
+ const handleVerifyOTP = async (e: React.FormEvent) => {
96
+ e.preventDefault();
97
+ setError(null);
98
+
99
+ if (!otpCode.trim()) {
100
+ setError('Please enter the code from your email.');
101
+ return;
102
+ }
103
+
104
+ setIsVerifying(true);
105
+
106
+ try {
107
+ const res = await fetch(`${API_BASE}/auth/otp/verify`, {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/json' },
110
+ body: JSON.stringify({ email: email.trim().toLowerCase(), code: otpCode.trim() }),
111
+ });
112
+
113
+ if (!res.ok) {
114
+ const data = await res.json() as { error?: string };
115
+ setError(data.error || 'Invalid or expired code.');
116
+ setIsVerifying(false);
117
+ return;
118
+ }
119
+
120
+ const data = await res.json() as { token: string; user: { id: string; email: string; plan: string } };
121
+
122
+ if (window.electronAPI?.isElectron) {
123
+ await window.electronAPI.auth.saveToken(data.token, data.user);
124
+ }
125
+
126
+ const path = await window.electronAPI?.auth.getPostLoginPath?.() || '/';
127
+ window.location.href = path;
128
+ } catch {
129
+ setError('Failed to verify code. Check your connection.');
130
+ setIsVerifying(false);
131
+ }
132
+ };
133
+
134
+ return (
135
+ <div className="flex flex-col items-center justify-center min-h-screen bg-white dark:bg-zinc-900 p-8">
136
+ <div className="max-w-md w-full space-y-10">
137
+ {/* Logo */}
138
+ <div className="flex flex-col items-center space-y-6">
139
+ <Image
140
+ src="/jettypod_wordmark.png"
141
+ alt="JettyPod"
142
+ width={160}
143
+ height={40}
144
+ priority
145
+ />
146
+ <h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 text-center">
147
+ Create your account
148
+ </h1>
149
+ <p className="text-zinc-500 dark:text-zinc-400 text-center">
150
+ Get started free. No credit card required.
151
+ </p>
152
+ </div>
153
+
154
+ {/* Error */}
155
+ {error && (
156
+ <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">
157
+ {error}
158
+ </div>
159
+ )}
160
+
161
+ {/* Google Sign-Up */}
162
+ <div className="pt-4">
163
+ <Button
164
+ onClick={handleGoogleSignUp}
165
+ size="lg"
166
+ fullWidth
167
+ >
168
+ Sign up with Google
169
+ </Button>
170
+ </div>
171
+
172
+ {/* Divider */}
173
+ <div className="flex items-center gap-4">
174
+ <div className="flex-1 h-px bg-zinc-200 dark:bg-zinc-700" />
175
+ <span className="text-base text-zinc-400 dark:text-zinc-500">or</span>
176
+ <div className="flex-1 h-px bg-zinc-200 dark:bg-zinc-700" />
177
+ </div>
178
+
179
+ {/* Email OTP */}
180
+ {!otpSent ? (
181
+ <form onSubmit={handleSendOTP} className="space-y-6">
182
+ <Input
183
+ type="email"
184
+ value={email}
185
+ onChange={(e) => setEmail(e.target.value)}
186
+ placeholder="Enter your email"
187
+ disabled={isSending}
188
+ />
189
+ <Button
190
+ type="submit"
191
+ variant="secondary"
192
+ size="lg"
193
+ fullWidth
194
+ disabled={isSending || !email.trim()}
195
+ >
196
+ {isSending ? 'Sending code...' : 'Sign up with email'}
197
+ </Button>
198
+ </form>
199
+ ) : (
200
+ <form onSubmit={handleVerifyOTP} className="space-y-6">
201
+ <p className="text-base text-zinc-500 dark:text-zinc-400">
202
+ We sent a 6-digit code to <span className="font-medium text-zinc-700 dark:text-zinc-300">{email}</span>
203
+ </p>
204
+ <Input
205
+ type="text"
206
+ value={otpCode}
207
+ onChange={(e) => setOtpCode(e.target.value)}
208
+ placeholder="Enter 6-digit code"
209
+ maxLength={6}
210
+ autoFocus
211
+ disabled={isVerifying}
212
+ className="text-center text-xl tracking-widest font-mono"
213
+ />
214
+ <Button
215
+ type="submit"
216
+ variant="secondary"
217
+ size="lg"
218
+ fullWidth
219
+ disabled={isVerifying || !otpCode.trim()}
220
+ >
221
+ {isVerifying ? 'Verifying...' : 'Verify code'}
222
+ </Button>
223
+ <Button
224
+ type="button"
225
+ variant="ghost"
226
+ size="sm"
227
+ fullWidth
228
+ onClick={() => { setOtpSent(false); setOtpCode(''); setError(null); }}
229
+ >
230
+ Use a different email
231
+ </Button>
232
+ </form>
233
+ )}
234
+
235
+ {/* Switch to login */}
236
+ <p className="text-center text-base text-zinc-500 dark:text-zinc-400">
237
+ Already have an account?{' '}
238
+ <Link href="/login" className="font-medium hover:underline" style={{ color: '#819D9F' }}>
239
+ Sign in
240
+ </Link>
241
+ </p>
242
+ </div>
243
+ </div>
244
+ );
245
+ }