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.
- package/.env +7 -0
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +124 -48
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +171 -58
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +161 -10
- package/apps/dashboard/app/api/tests/run/stream/route.ts +13 -1
- package/apps/dashboard/app/api/usage/route.ts +17 -0
- package/apps/dashboard/app/api/work/[id]/route.ts +35 -0
- package/apps/dashboard/app/api/work/[id]/status/route.ts +43 -1
- package/apps/dashboard/app/connect-claude/page.tsx +24 -0
- package/apps/dashboard/app/decision/[id]/page.tsx +14 -14
- package/apps/dashboard/app/demo/gates/page.tsx +42 -42
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +6 -2
- package/apps/dashboard/app/install-claude/page.tsx +9 -7
- package/apps/dashboard/app/layout.tsx +17 -5
- package/apps/dashboard/app/login/page.tsx +250 -0
- package/apps/dashboard/app/page.tsx +11 -9
- package/apps/dashboard/app/settings/page.tsx +4 -2
- package/apps/dashboard/app/signup/page.tsx +245 -0
- package/apps/dashboard/app/subscribe/page.tsx +11 -0
- package/apps/dashboard/app/welcome/page.tsx +24 -1
- package/apps/dashboard/app/work/[id]/page.tsx +34 -50
- package/apps/dashboard/components/AppShell.tsx +95 -55
- package/apps/dashboard/components/CardMenu.tsx +56 -13
- package/apps/dashboard/components/ClaudePanel.tsx +301 -582
- package/apps/dashboard/components/ClaudePanelInput.tsx +23 -14
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +210 -0
- package/apps/dashboard/components/CopyableId.tsx +3 -3
- package/apps/dashboard/components/DetailReviewActions.tsx +109 -0
- package/apps/dashboard/components/DragContext.tsx +75 -65
- package/apps/dashboard/components/DraggableCard.tsx +6 -46
- package/apps/dashboard/components/DropZone.tsx +2 -2
- package/apps/dashboard/components/EditableDetailDescription.tsx +1 -1
- package/apps/dashboard/components/EditableTitle.tsx +26 -6
- package/apps/dashboard/components/ElapsedTimer.tsx +54 -0
- package/apps/dashboard/components/EpicGroup.tsx +329 -0
- package/apps/dashboard/components/GateCard.tsx +100 -16
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -17
- package/apps/dashboard/components/InstallClaudeScreen.tsx +140 -51
- package/apps/dashboard/components/JettyLoader.tsx +38 -0
- package/apps/dashboard/components/KanbanBoard.tsx +147 -766
- package/apps/dashboard/components/KanbanCard.tsx +506 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +12 -0
- package/apps/dashboard/components/MainNav.tsx +20 -54
- package/apps/dashboard/components/MessageBlock.tsx +391 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -15
- package/apps/dashboard/components/OnboardingWelcome.tsx +214 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +11 -21
- package/apps/dashboard/components/ProjectSwitcher.tsx +36 -8
- package/apps/dashboard/components/PrototypeTimeline.tsx +25 -25
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +265 -301
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +97 -74
- package/apps/dashboard/components/ReviewFooter.tsx +141 -0
- package/apps/dashboard/components/SessionList.tsx +19 -18
- package/apps/dashboard/components/SubscribeContent.tsx +206 -0
- package/apps/dashboard/components/TestTree.tsx +15 -14
- package/apps/dashboard/components/TipCard.tsx +177 -0
- package/apps/dashboard/components/Toast.tsx +5 -5
- package/apps/dashboard/components/TypeIcon.tsx +56 -0
- package/apps/dashboard/components/UpgradeBanner.tsx +30 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +61 -62
- package/apps/dashboard/components/WelcomeScreen.tsx +25 -27
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -4
- package/apps/dashboard/components/WorkItemTree.tsx +9 -28
- package/apps/dashboard/components/settings/AccountSection.tsx +169 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +54 -79
- package/apps/dashboard/components/settings/GeneralSection.tsx +26 -31
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -4
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +408 -105
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -4
- package/apps/dashboard/contexts/UsageContext.tsx +155 -0
- package/apps/dashboard/contexts/usageHelpers.js +9 -0
- package/apps/dashboard/electron/ipc-handlers.js +281 -88
- package/apps/dashboard/electron/main.js +691 -131
- package/apps/dashboard/electron/preload.js +25 -4
- package/apps/dashboard/electron/session-manager.js +163 -0
- package/apps/dashboard/electron-builder.config.js +3 -5
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/lib/backlog-parser.ts +50 -0
- package/apps/dashboard/lib/claude-process-manager.ts +50 -11
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/db-bridge.ts +33 -0
- package/apps/dashboard/lib/db.ts +136 -20
- package/apps/dashboard/lib/kanban-utils.ts +70 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- package/apps/dashboard/lib/session-state-machine.ts +3 -0
- package/apps/dashboard/lib/session-stream-manager.ts +144 -38
- package/apps/dashboard/lib/shadows.ts +7 -0
- package/apps/dashboard/lib/tests.ts +3 -1
- package/apps/dashboard/lib/utils.ts +6 -0
- package/apps/dashboard/next.config.js +35 -14
- package/apps/dashboard/package.json +6 -3
- package/apps/dashboard/public/bug-icon.svg +9 -0
- package/apps/dashboard/public/buoy-icon.svg +9 -0
- package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
- package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
- package/apps/dashboard/public/in-flight-seagull.svg +9 -0
- package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
- package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
- package/apps/dashboard/public/jettypod_logo.png +0 -0
- package/apps/dashboard/public/pier-icon.svg +14 -0
- package/apps/dashboard/public/star-icon.svg +9 -0
- package/apps/dashboard/public/wrench-icon.svg +9 -0
- package/apps/dashboard/scripts/upload-to-r2.js +89 -0
- package/apps/dashboard/scripts/ws-server.js +191 -0
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -0
- package/apps/update-server/package.json +16 -0
- package/apps/update-server/schema.sql +31 -0
- package/apps/update-server/src/index.ts +1085 -0
- package/apps/update-server/tsconfig.json +16 -0
- package/apps/update-server/wrangler.toml +35 -0
- package/cucumber.js +9 -3
- package/docs/COMMAND_REFERENCE.md +34 -0
- package/hooks/post-checkout +32 -75
- package/hooks/post-merge +111 -10
- package/jest.setup.js +1 -0
- package/jettypod.js +54 -116
- package/lib/chore-taxonomy.js +33 -10
- package/lib/database.js +36 -16
- package/lib/db-watcher.js +1 -1
- package/lib/git-hooks/pre-commit +1 -1
- package/lib/jettypod-backup.js +27 -4
- package/lib/migrations/027-plan-at-creation-column.js +33 -0
- package/lib/migrations/028-ready-for-review-column.js +27 -0
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +13 -6
- package/lib/seed-onboarding.js +101 -69
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +129 -16
- package/lib/work-tracking/index.js +86 -46
- package/lib/worktree-diagnostics.js +16 -16
- package/lib/worktree-facade.js +1 -1
- package/lib/worktree-manager.js +8 -8
- package/lib/worktree-reconciler.js +5 -5
- package/package.json +9 -2
- package/scripts/ndjson-to-cucumber-json.js +152 -0
- package/scripts/postinstall.js +25 -0
- package/skills-templates/bug-mode/SKILL.md +39 -28
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +131 -68
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/epic-planning/SKILL.md +68 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +83 -73
- package/skills-templates/production-mode/SKILL.md +49 -49
- package/skills-templates/request-routing/SKILL.md +27 -14
- package/skills-templates/simple-improvement/SKILL.md +68 -44
- package/skills-templates/speed-mode/SKILL.md +209 -128
- package/skills-templates/stable-mode/SKILL.md +105 -94
- package/templates/bdd-guidance.md +139 -0
- package/templates/bdd-scaffolding/wait.js +18 -0
- package/templates/bdd-scaffolding/world.js +19 -0
- package/.jettypod-backup/work.db +0 -0
- package/apps/dashboard/app/access-code/page.tsx +0 -110
- package/lib/discovery-checkpoint.js +0 -123
- 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-
|
|
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(
|
|
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 [
|
|
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
|
-
|
|
31
|
+
// Show success animation
|
|
32
|
+
setIsSuccess(true);
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
|
|
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-
|
|
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
|
-
|
|
54
|
+
isSuccess={isSuccess}
|
|
53
55
|
/>
|
|
54
56
|
</>
|
|
55
57
|
);
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
|
-
import
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
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={`${
|
|
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'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="
|
|
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-
|
|
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-
|
|
44
|
-
<p className="font-mono text-
|
|
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-
|
|
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-
|
|
52
|
-
<pre className="mt-2 text-
|
|
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-
|
|
16
|
-
<h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mb-
|
|
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
|
+
}
|