jettypod 4.4.120 → 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.
- package/.env +2 -1
- package/Cargo.lock +6450 -0
- package/Cargo.toml +35 -0
- package/README.md +5 -1
- package/TAURI-MIGRATION-PLAN.md +840 -0
- package/apps/dashboard/app/connect-claude/page.tsx +5 -6
- package/apps/dashboard/app/decision/[id]/page.tsx +54 -49
- package/apps/dashboard/app/demo/gates/page.tsx +3 -5
- package/apps/dashboard/app/design-system/page.tsx +1 -1
- package/apps/dashboard/app/globals.css +74 -2
- package/apps/dashboard/app/install-claude/page.tsx +3 -5
- package/apps/dashboard/app/login/page.tsx +17 -20
- package/apps/dashboard/app/page.tsx +101 -48
- package/apps/dashboard/app/settings/page.tsx +60 -12
- package/apps/dashboard/app/signup/page.tsx +14 -17
- package/apps/dashboard/app/subscribe/page.tsx +0 -2
- package/apps/dashboard/app/tests/page.tsx +37 -4
- package/apps/dashboard/app/welcome/page.tsx +12 -15
- package/apps/dashboard/app/work/[id]/page.tsx +90 -75
- package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
- package/apps/dashboard/components/AppShell.tsx +70 -61
- package/apps/dashboard/components/CardMenu.tsx +0 -1
- package/apps/dashboard/components/ClaudePanel.tsx +541 -283
- package/apps/dashboard/components/ClaudePanelInput.tsx +23 -4
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +1 -5
- package/apps/dashboard/components/CopyableId.tsx +1 -2
- package/apps/dashboard/components/DetailReviewActions.tsx +11 -20
- package/apps/dashboard/components/DragContext.tsx +132 -62
- package/apps/dashboard/components/DraggableCard.tsx +3 -5
- package/apps/dashboard/components/DropZone.tsx +5 -6
- package/apps/dashboard/components/EditableDetailDescription.tsx +6 -12
- package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
- package/apps/dashboard/components/EditableTitle.tsx +0 -1
- package/apps/dashboard/components/ElapsedTimer.tsx +15 -3
- package/apps/dashboard/components/EpicGroup.tsx +100 -70
- package/apps/dashboard/components/GateCard.tsx +0 -1
- package/apps/dashboard/components/GateChoiceCard.tsx +1 -2
- package/apps/dashboard/components/InstallClaudeScreen.tsx +1 -5
- package/apps/dashboard/components/JettyLoader.tsx +0 -1
- package/apps/dashboard/components/KanbanBoard.tsx +319 -173
- package/apps/dashboard/components/KanbanCard.tsx +341 -107
- package/apps/dashboard/components/LazyCard.tsx +62 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +0 -1
- package/apps/dashboard/components/MainNav.tsx +24 -25
- package/apps/dashboard/components/MessageBlock.tsx +93 -16
- package/apps/dashboard/components/ModeStartCard.tsx +0 -1
- package/apps/dashboard/components/OnboardingWelcome.tsx +0 -1
- package/apps/dashboard/components/PlaceholderCard.tsx +0 -1
- package/apps/dashboard/components/ProjectSwitcher.tsx +20 -20
- package/apps/dashboard/components/PrototypeTimeline.tsx +47 -26
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +308 -223
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +303 -160
- package/apps/dashboard/components/ReviewFooter.tsx +12 -14
- package/apps/dashboard/components/SessionList.tsx +0 -1
- package/apps/dashboard/components/SubscribeContent.tsx +40 -11
- package/apps/dashboard/components/TestTree.tsx +1 -2
- package/apps/dashboard/components/TipCard.tsx +2 -4
- package/apps/dashboard/components/Toast.tsx +0 -1
- package/apps/dashboard/components/TypeIcon.tsx +7 -8
- package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +5 -17
- package/apps/dashboard/components/WelcomeScreen.tsx +2 -6
- package/apps/dashboard/components/WorkItemHeader.tsx +0 -1
- package/apps/dashboard/components/WorkItemTree.tsx +2 -4
- package/apps/dashboard/components/settings/AccountSection.tsx +27 -13
- package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
- package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +20 -73
- package/apps/dashboard/components/settings/GeneralSection.tsx +137 -26
- package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
- package/apps/dashboard/components/settings/SettingsLayout.tsx +0 -1
- package/apps/dashboard/components/ui/Button.tsx +1 -1
- package/apps/dashboard/components/ui/Input.tsx +1 -1
- package/apps/dashboard/components.json +1 -1
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +611 -358
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +0 -1
- package/apps/dashboard/contexts/UsageContext.tsx +62 -31
- package/apps/dashboard/dev.sh +35 -0
- package/apps/dashboard/eslint.config.mjs +9 -9
- package/apps/dashboard/hooks/useWebSocket.ts +138 -83
- package/apps/dashboard/index.html +73 -0
- package/apps/dashboard/lib/data-bridge.ts +722 -0
- package/apps/dashboard/lib/db.ts +69 -1302
- package/apps/dashboard/lib/environment-config.ts +173 -0
- package/apps/dashboard/lib/environment-verification.ts +119 -0
- package/apps/dashboard/lib/kanban-utils.ts +226 -26
- package/apps/dashboard/lib/proof-run.ts +495 -0
- package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
- package/apps/dashboard/lib/service-recovery.ts +326 -0
- package/apps/dashboard/lib/session-state-machine.ts +1 -0
- package/apps/dashboard/lib/session-state-utils.ts +0 -164
- package/apps/dashboard/lib/session-stream-manager.ts +253 -122
- package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
- package/apps/dashboard/lib/tauri-bridge.ts +102 -0
- package/apps/dashboard/lib/tauri.ts +106 -0
- package/apps/dashboard/lib/utils.ts +3 -3
- package/apps/dashboard/next-env.d.ts +1 -1
- package/apps/dashboard/package.json +21 -33
- package/apps/dashboard/public/bug-icon.png +0 -0
- package/apps/dashboard/public/buoy-icon.png +0 -0
- package/apps/dashboard/public/in-flight-seagull.png +0 -0
- package/apps/dashboard/public/pier-icon.png +0 -0
- package/apps/dashboard/public/star-icon.png +0 -0
- package/apps/dashboard/public/wrench-icon.png +0 -0
- package/apps/dashboard/scripts/tauri-build.js +228 -0
- package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
- package/apps/dashboard/src/main.tsx +12 -0
- package/apps/dashboard/src/router.tsx +107 -0
- package/apps/dashboard/src/vite-env.d.ts +1 -0
- package/apps/dashboard/tsconfig.json +7 -12
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
- package/apps/dashboard/vite.config.ts +33 -0
- package/apps/update-server/src/index.ts +167 -30
- package/claude-hooks/global-guardrails.js +14 -13
- package/crates/jettypod-cli/Cargo.toml +19 -0
- package/crates/jettypod-cli/src/commands.rs +1249 -0
- package/crates/jettypod-cli/src/main.rs +595 -0
- package/crates/jettypod-core/Cargo.toml +26 -0
- package/crates/jettypod-core/build.rs +98 -0
- package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
- package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
- package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
- package/crates/jettypod-core/src/auth.rs +294 -0
- package/crates/jettypod-core/src/config.rs +397 -0
- package/crates/jettypod-core/src/db/mod.rs +507 -0
- package/crates/jettypod-core/src/db/recovery.rs +114 -0
- package/crates/jettypod-core/src/db/startup.rs +101 -0
- package/crates/jettypod-core/src/db/validate.rs +149 -0
- package/crates/jettypod-core/src/error.rs +76 -0
- package/crates/jettypod-core/src/git.rs +458 -0
- package/crates/jettypod-core/src/lib.rs +20 -0
- package/crates/jettypod-core/src/sessions.rs +625 -0
- package/crates/jettypod-core/src/skills.rs +556 -0
- package/crates/jettypod-core/src/work.rs +1086 -0
- package/crates/jettypod-core/src/worktree.rs +628 -0
- package/crates/jettypod-core/src/ws.rs +767 -0
- package/cucumber-test.cjs +6 -0
- package/jettypod.js +96 -4
- package/lib/bdd-preflight.js +96 -0
- package/lib/merge-lock.js +111 -253
- package/lib/migrations/030-rejection-round-columns.js +54 -0
- package/lib/migrations/031-session-isolation-index.js +17 -0
- package/lib/work-commands/index.js +58 -16
- package/lib/work-tracking/index.js +108 -8
- package/package.json +1 -1
- package/skills-templates/bug-mode/SKILL.md +43 -1
- package/skills-templates/chore-mode/SKILL.md +40 -1
- package/skills-templates/design-system-selection/SKILL.md +273 -0
- package/skills-templates/epic-planning/SKILL.md +14 -0
- package/skills-templates/feature-planning/SKILL.md +90 -1
- package/skills-templates/production-mode/SKILL.md +20 -0
- package/skills-templates/simple-improvement/SKILL.md +39 -2
- package/skills-templates/speed-mode/SKILL.md +10 -15
- package/skills-templates/stable-mode/SKILL.md +47 -0
- package/apps/dashboard/README.md +0 -36
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -446
- package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -280
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -525
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
- package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
- package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
- package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
- package/apps/dashboard/app/api/kanban/route.ts +0 -15
- package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
- package/apps/dashboard/app/api/settings/general/route.ts +0 -21
- package/apps/dashboard/app/api/tests/route.ts +0 -9
- package/apps/dashboard/app/api/tests/run/route.ts +0 -82
- package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
- package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
- package/apps/dashboard/app/api/usage/route.ts +0 -17
- package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/route.ts +0 -35
- package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -63
- package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
- package/apps/dashboard/app/layout.tsx +0 -55
- package/apps/dashboard/components/UpgradeBanner.tsx +0 -30
- package/apps/dashboard/electron/ipc-handlers.js +0 -1026
- package/apps/dashboard/electron/main.js +0 -2306
- package/apps/dashboard/electron/preload.js +0 -125
- package/apps/dashboard/electron/session-manager.js +0 -163
- package/apps/dashboard/electron-builder.config.js +0 -357
- package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
- package/apps/dashboard/lib/backlog-parser.ts +0 -50
- package/apps/dashboard/lib/claude-process-manager.ts +0 -529
- package/apps/dashboard/lib/db-bridge.ts +0 -283
- package/apps/dashboard/lib/prototypes.ts +0 -202
- package/apps/dashboard/lib/test-results-db.ts +0 -307
- package/apps/dashboard/lib/tests.ts +0 -282
- package/apps/dashboard/next.config.js +0 -66
- package/apps/dashboard/postcss.config.mjs +0 -7
- package/apps/dashboard/public/bug-icon.svg +0 -9
- package/apps/dashboard/public/buoy-icon.svg +0 -9
- package/apps/dashboard/public/file.svg +0 -1
- package/apps/dashboard/public/globe.svg +0 -1
- package/apps/dashboard/public/in-flight-seagull.svg +0 -9
- package/apps/dashboard/public/next.svg +0 -1
- package/apps/dashboard/public/pier-icon.svg +0 -14
- package/apps/dashboard/public/star-icon.svg +0 -9
- package/apps/dashboard/public/vercel.svg +0 -1
- package/apps/dashboard/public/window.svg +0 -1
- package/apps/dashboard/public/wrench-icon.svg +0 -9
- package/apps/dashboard/scripts/download-node.js +0 -104
- package/apps/dashboard/scripts/upload-to-r2.js +0 -89
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
'use client';
|
|
2
1
|
|
|
3
|
-
import { createContext, useContext, useState, useEffect, useCallback, useMemo, type ReactNode } from 'react';
|
|
2
|
+
import { createContext, useContext, useState, useEffect, useCallback, useMemo, useRef, type ReactNode } from 'react';
|
|
4
3
|
import { useWebSocket, type WebSocketMessage } from '../hooks/useWebSocket';
|
|
5
4
|
import { getWebSocketUrl } from '../lib/utils';
|
|
5
|
+
import { isTauri, auth } from '@/lib/tauri-bridge';
|
|
6
|
+
import { invoke } from '@/lib/tauri';
|
|
7
|
+
import { dataBridge } from '@/lib/data-bridge';
|
|
6
8
|
|
|
7
9
|
const FREE_WEEKLY_LIMIT = 20;
|
|
8
10
|
const API_BASE = 'https://jettypod-update-server.spangbaryn2.workers.dev';
|
|
@@ -32,31 +34,43 @@ export function UsageProvider({ children }: { children: ReactNode }) {
|
|
|
32
34
|
loading: true,
|
|
33
35
|
});
|
|
34
36
|
|
|
37
|
+
// Don't hit DB or WS until a project is actually loaded
|
|
38
|
+
const [projectLoaded, setProjectLoaded] = useState(false);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
dataBridge.getProjectRoot().then((root) => {
|
|
41
|
+
if (root) setProjectLoaded(true);
|
|
42
|
+
});
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const lastServerCheckRef = useRef<number>(0);
|
|
46
|
+
|
|
35
47
|
const fetchUsage = useCallback(async () => {
|
|
36
|
-
// Get plan from auth (if
|
|
48
|
+
// Get plan from auth (if Tauri)
|
|
37
49
|
let plan = 'free';
|
|
38
50
|
try {
|
|
39
|
-
if (
|
|
40
|
-
const status = await
|
|
51
|
+
if (isTauri()) {
|
|
52
|
+
const status = await auth.getStatus();
|
|
41
53
|
if (status.authenticated && status.user) {
|
|
42
54
|
plan = status.user.plan || 'free';
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
// Check server-side plan — the local JWT may be stale
|
|
46
|
-
|
|
47
|
-
|
|
58
|
+
// Only check every 10 minutes to avoid unnecessary API calls
|
|
59
|
+
if (plan === 'free' && (!lastServerCheckRef.current || Date.now() - lastServerCheckRef.current > 600_000)) {
|
|
60
|
+
const token = await auth.getToken();
|
|
48
61
|
if (token) {
|
|
49
62
|
try {
|
|
50
63
|
const res = await fetch(`${API_BASE}/auth/me`, {
|
|
51
64
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
52
65
|
});
|
|
66
|
+
lastServerCheckRef.current = Date.now();
|
|
53
67
|
if (res.ok) {
|
|
54
68
|
const data = await res.json() as { user: { plan: string; email: string }; token?: string };
|
|
55
69
|
if (data.user.plan !== 'free') {
|
|
56
70
|
plan = data.user.plan;
|
|
57
71
|
// Server issued a fresh JWT with updated plan — save it locally
|
|
58
72
|
if (data.token) {
|
|
59
|
-
await
|
|
73
|
+
await auth.saveToken(data.token, data.user);
|
|
60
74
|
}
|
|
61
75
|
}
|
|
62
76
|
}
|
|
@@ -83,51 +97,68 @@ export function UsageProvider({ children }: { children: ReactNode }) {
|
|
|
83
97
|
return;
|
|
84
98
|
}
|
|
85
99
|
|
|
86
|
-
// Free plan — count
|
|
100
|
+
// Free plan — count active sessions via Tauri IPC
|
|
87
101
|
try {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
console.error('[usage] UsageContext fetch not ok:', res.status, res.statusText);
|
|
92
|
-
setState(prev => ({ ...prev, allowed: true, loading: false }));
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
const usage = await res.json();
|
|
96
|
-
console.log('[usage] UsageContext received:', usage);
|
|
102
|
+
const count = await invoke<number>('db_get_active_session_count');
|
|
103
|
+
const used = typeof count === 'number' ? count : 0;
|
|
104
|
+
const remaining = Math.max(0, FREE_WEEKLY_LIMIT - used);
|
|
97
105
|
setState({
|
|
98
|
-
used
|
|
99
|
-
limit:
|
|
100
|
-
remaining
|
|
101
|
-
allowed:
|
|
106
|
+
used,
|
|
107
|
+
limit: FREE_WEEKLY_LIMIT,
|
|
108
|
+
remaining,
|
|
109
|
+
allowed: remaining > 0,
|
|
102
110
|
plan,
|
|
103
111
|
loading: false,
|
|
104
112
|
});
|
|
105
113
|
} catch (err) {
|
|
106
|
-
console.error('[usage] UsageContext
|
|
114
|
+
console.error('[usage] UsageContext IPC error:', err);
|
|
107
115
|
setState(prev => ({ ...prev, allowed: true, loading: false }));
|
|
108
116
|
}
|
|
109
117
|
}, []);
|
|
110
118
|
|
|
111
|
-
// Fetch on mount
|
|
119
|
+
// Fetch on mount (only after project is loaded)
|
|
112
120
|
useEffect(() => {
|
|
113
|
-
fetchUsage();
|
|
114
|
-
}, [fetchUsage]);
|
|
121
|
+
if (projectLoaded) fetchUsage();
|
|
122
|
+
}, [projectLoaded, fetchUsage]);
|
|
115
123
|
|
|
116
124
|
// Refresh on window focus
|
|
117
125
|
useEffect(() => {
|
|
126
|
+
if (!projectLoaded) return;
|
|
118
127
|
const handleFocus = () => fetchUsage();
|
|
119
128
|
window.addEventListener('focus', handleFocus);
|
|
120
129
|
return () => window.removeEventListener('focus', handleFocus);
|
|
121
|
-
}, [fetchUsage]);
|
|
130
|
+
}, [projectLoaded, fetchUsage]);
|
|
122
131
|
|
|
123
|
-
// Refresh on WebSocket db_change events
|
|
132
|
+
// Refresh on WebSocket db_change events — debounced and throttled.
|
|
133
|
+
// Skip entirely for paid plans (usage is unlimited).
|
|
134
|
+
// Minimum 30s between db_change-triggered fetches to avoid IPC storms.
|
|
135
|
+
const usageDebounceRef = useRef<NodeJS.Timeout | null>(null);
|
|
136
|
+
const lastDbChangeFetchRef = useRef<number>(0);
|
|
124
137
|
const handleWsMessage = useCallback((message: WebSocketMessage) => {
|
|
125
138
|
if (message.type === 'db_change') {
|
|
126
|
-
|
|
139
|
+
// Paid plans have unlimited usage — no need to refresh
|
|
140
|
+
if (state.plan !== 'free') return;
|
|
141
|
+
|
|
142
|
+
if (usageDebounceRef.current) clearTimeout(usageDebounceRef.current);
|
|
143
|
+
usageDebounceRef.current = setTimeout(() => {
|
|
144
|
+
usageDebounceRef.current = null;
|
|
145
|
+
// Throttle: skip if we fetched recently
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
if (now - lastDbChangeFetchRef.current < 30_000) return;
|
|
148
|
+
lastDbChangeFetchRef.current = now;
|
|
149
|
+
fetchUsage();
|
|
150
|
+
}, 5000);
|
|
127
151
|
}
|
|
128
|
-
}, [fetchUsage]);
|
|
152
|
+
}, [fetchUsage, state.plan]);
|
|
129
153
|
|
|
130
|
-
useWebSocket({ url: getWebSocketUrl(), onMessage: handleWsMessage });
|
|
154
|
+
useWebSocket({ url: projectLoaded ? getWebSocketUrl() : '', onMessage: handleWsMessage });
|
|
155
|
+
|
|
156
|
+
// Cleanup debounce timer
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
return () => {
|
|
159
|
+
if (usageDebounceRef.current) clearTimeout(usageDebounceRef.current);
|
|
160
|
+
};
|
|
161
|
+
}, []);
|
|
131
162
|
|
|
132
163
|
const value = useMemo(() => ({ ...state, refresh: fetchUsage }), [state, fetchUsage]);
|
|
133
164
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Start Vite + Tauri for local development
|
|
3
|
+
# All child processes are killed when this script exits (Ctrl-C, close terminal, etc.)
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
export PATH="$HOME/.cargo/bin:$PATH"
|
|
7
|
+
cd "$(dirname "$0")"
|
|
8
|
+
|
|
9
|
+
# Kill any leftover processes on port 1420
|
|
10
|
+
lsof -ti:1420 | xargs kill 2>/dev/null || true
|
|
11
|
+
|
|
12
|
+
# Swap to dev icon and dev identifier for local builds
|
|
13
|
+
# Dev identifier prevents single-instance plugin from conflicting with installed app
|
|
14
|
+
cp src-tauri/icons/icon-dev.png src-tauri/icons/icon.png
|
|
15
|
+
sed -i '' 's/"identifier": "com.jettypod.app"/"identifier": "com.jettypod.app.dev"/' src-tauri/tauri.conf.json
|
|
16
|
+
sed -i '' 's/"productName": "JettyPod"/"productName": "JettyPod Dev"/' src-tauri/tauri.conf.json
|
|
17
|
+
|
|
18
|
+
# Kill the entire process group on exit — catches Vite, cargo, tauri, rustc, etc.
|
|
19
|
+
# Also restore the production icon and identifier so git stays clean.
|
|
20
|
+
cleanup() {
|
|
21
|
+
trap - INT TERM EXIT
|
|
22
|
+
git checkout src-tauri/icons/icon.png src-tauri/tauri.conf.json 2>/dev/null || true
|
|
23
|
+
kill -- -$$ 2>/dev/null
|
|
24
|
+
}
|
|
25
|
+
trap cleanup INT TERM EXIT
|
|
26
|
+
|
|
27
|
+
npx vite --port 1420 &
|
|
28
|
+
|
|
29
|
+
echo "Waiting for Vite on :1420..."
|
|
30
|
+
while ! curl -s http://localhost:1420 > /dev/null 2>&1; do
|
|
31
|
+
sleep 0.5
|
|
32
|
+
done
|
|
33
|
+
|
|
34
|
+
echo "Vite ready. Starting Tauri..."
|
|
35
|
+
cargo tauri dev
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { defineConfig, globalIgnores } from "eslint/config";
|
|
2
|
-
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
3
|
-
import nextTs from "eslint-config-next/typescript";
|
|
4
2
|
|
|
5
3
|
const eslintConfig = defineConfig([
|
|
6
|
-
...nextVitals,
|
|
7
|
-
...nextTs,
|
|
8
|
-
// Override default ignores of eslint-config-next.
|
|
9
4
|
globalIgnores([
|
|
10
|
-
|
|
11
|
-
".next/**",
|
|
12
|
-
"out/**",
|
|
5
|
+
"dist/**",
|
|
13
6
|
"build/**",
|
|
14
|
-
"
|
|
7
|
+
"src-tauri/**",
|
|
15
8
|
]),
|
|
9
|
+
{
|
|
10
|
+
files: ["**/*.{ts,tsx,js,jsx,mjs}"],
|
|
11
|
+
rules: {
|
|
12
|
+
"no-unused-vars": "off",
|
|
13
|
+
"no-undef": "off",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
16
|
]);
|
|
17
17
|
|
|
18
18
|
export default eslintConfig;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
'use client';
|
|
2
1
|
|
|
3
|
-
import { useEffect, useRef,
|
|
2
|
+
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
4
3
|
|
|
5
4
|
export interface WebSocketMessage {
|
|
6
5
|
type: string;
|
|
7
6
|
timestamp: number;
|
|
8
7
|
event?: string;
|
|
8
|
+
/** Delta fields — present when type === 'db_delta' */
|
|
9
|
+
table?: string;
|
|
10
|
+
rowid?: number;
|
|
11
|
+
action?: 'insert' | 'update' | 'delete';
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
interface UseWebSocketOptions {
|
|
@@ -22,103 +25,155 @@ interface UseWebSocketReturn {
|
|
|
22
25
|
manualReconnect: () => void;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
url,
|
|
27
|
-
onMessage,
|
|
28
|
-
reconnectInterval = 3000,
|
|
29
|
-
maxReconnectAttempts = 5,
|
|
30
|
-
}: UseWebSocketOptions): UseWebSocketReturn {
|
|
31
|
-
const [isConnected, setIsConnected] = useState(false);
|
|
32
|
-
const [isReconnecting, setIsReconnecting] = useState(false);
|
|
33
|
-
const [reconnectionFailed, setReconnectionFailed] = useState(false);
|
|
34
|
-
const wsRef = useRef<WebSocket | null>(null);
|
|
35
|
-
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
36
|
-
const reconnectAttemptsRef = useRef(0);
|
|
37
|
-
const onMessageRef = useRef(onMessage);
|
|
28
|
+
// ---- Shared singleton WebSocket connection ----
|
|
38
29
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
type Subscriber = (message: WebSocketMessage) => void;
|
|
31
|
+
|
|
32
|
+
let sharedWs: WebSocket | null = null;
|
|
33
|
+
let sharedUrl: string | null = null;
|
|
34
|
+
let subscribers = new Set<Subscriber>();
|
|
35
|
+
let connectionState: 'disconnected' | 'connected' | 'reconnecting' | 'failed' = 'disconnected';
|
|
36
|
+
let reconnectAttempts = 0;
|
|
37
|
+
let reconnectTimeout: NodeJS.Timeout | null = null;
|
|
38
|
+
let stateListeners = new Set<() => void>();
|
|
43
39
|
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
const RECONNECT_INTERVAL = 3000;
|
|
41
|
+
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
42
|
+
|
|
43
|
+
function notifyStateChange() {
|
|
44
|
+
for (const listener of stateListeners) listener();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function connectShared(url: string) {
|
|
48
|
+
if (sharedWs?.readyState === WebSocket.OPEN || sharedWs?.readyState === WebSocket.CONNECTING) return;
|
|
49
|
+
|
|
50
|
+
sharedUrl = url;
|
|
51
|
+
const ws = new WebSocket(url);
|
|
52
|
+
|
|
53
|
+
ws.onopen = () => {
|
|
54
|
+
connectionState = 'connected';
|
|
55
|
+
reconnectAttempts = 0;
|
|
56
|
+
// Expose to window for test assertions
|
|
57
|
+
(window as unknown as { __wsConnection: WebSocket }).__wsConnection = ws;
|
|
58
|
+
(window as unknown as { __wsReconnecting: boolean }).__wsReconnecting = false;
|
|
59
|
+
notifyStateChange();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
ws.onmessage = (event) => {
|
|
63
|
+
try {
|
|
64
|
+
const message = JSON.parse(event.data) as WebSocketMessage;
|
|
65
|
+
for (const sub of subscribers) sub(message);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.warn('Received malformed WebSocket message:', e);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
ws.onclose = () => {
|
|
72
|
+
// Only clear if this is still the active connection — prevents a stale
|
|
73
|
+
// WS (closed during CONNECTING by React Strict Mode) from nulling out
|
|
74
|
+
// the newer replacement connection.
|
|
75
|
+
if (sharedWs !== ws) return;
|
|
76
|
+
sharedWs = null;
|
|
77
|
+
|
|
78
|
+
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
79
|
+
connectionState = 'failed';
|
|
80
|
+
notifyStateChange();
|
|
46
81
|
return;
|
|
47
82
|
}
|
|
48
83
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
setIsReconnecting(false);
|
|
54
|
-
setReconnectionFailed(false);
|
|
55
|
-
reconnectAttemptsRef.current = 0;
|
|
56
|
-
// Expose to window for test assertions
|
|
57
|
-
if (typeof window !== 'undefined') {
|
|
58
|
-
(window as unknown as { __wsConnection: WebSocket }).__wsConnection = ws;
|
|
59
|
-
(window as unknown as { __wsReconnecting: boolean }).__wsReconnecting = false;
|
|
60
|
-
}
|
|
61
|
-
};
|
|
84
|
+
reconnectAttempts += 1;
|
|
85
|
+
connectionState = 'reconnecting';
|
|
86
|
+
(window as unknown as { __wsReconnecting: boolean }).__wsReconnecting = true;
|
|
87
|
+
notifyStateChange();
|
|
62
88
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
} catch (e) {
|
|
68
|
-
// Gracefully handle malformed messages - log but don't crash
|
|
69
|
-
console.warn('Received malformed WebSocket message:', e);
|
|
70
|
-
}
|
|
71
|
-
};
|
|
89
|
+
reconnectTimeout = setTimeout(() => {
|
|
90
|
+
if (subscribers.size > 0 && sharedUrl) connectShared(sharedUrl);
|
|
91
|
+
}, RECONNECT_INTERVAL);
|
|
92
|
+
};
|
|
72
93
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
94
|
+
ws.onerror = () => {
|
|
95
|
+
ws.close();
|
|
96
|
+
};
|
|
76
97
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
setIsReconnecting(false);
|
|
80
|
-
setReconnectionFailed(true);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
98
|
+
sharedWs = ws;
|
|
99
|
+
}
|
|
83
100
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
setIsReconnecting(true);
|
|
87
|
-
if (typeof window !== 'undefined') {
|
|
88
|
-
(window as unknown as { __wsReconnecting: boolean }).__wsReconnecting = true;
|
|
89
|
-
}
|
|
101
|
+
function subscribe(url: string, callback: Subscriber): () => void {
|
|
102
|
+
subscribers.add(callback);
|
|
90
103
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
// Start connection if this is the first subscriber
|
|
105
|
+
if (subscribers.size === 1 || (!sharedWs && connectionState !== 'failed')) {
|
|
106
|
+
connectShared(url);
|
|
107
|
+
}
|
|
95
108
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
};
|
|
109
|
+
return () => {
|
|
110
|
+
subscribers.delete(callback);
|
|
99
111
|
|
|
100
|
-
|
|
101
|
-
|
|
112
|
+
// Close connection when last subscriber leaves
|
|
113
|
+
if (subscribers.size === 0) {
|
|
114
|
+
if (reconnectTimeout) clearTimeout(reconnectTimeout);
|
|
115
|
+
if (sharedWs) {
|
|
116
|
+
sharedWs.close();
|
|
117
|
+
sharedWs = null;
|
|
118
|
+
}
|
|
119
|
+
connectionState = 'disconnected';
|
|
120
|
+
reconnectAttempts = 0;
|
|
121
|
+
sharedUrl = null;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
102
125
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
126
|
+
// ---- React hook (thin wrapper over shared connection) ----
|
|
127
|
+
|
|
128
|
+
// Derive a simple state key from the connection state so we only re-render
|
|
129
|
+
// when the user-visible status actually changes — not on every notifyStateChange
|
|
130
|
+
// (which fires on each reconnect attempt, causing cascade re-renders in all
|
|
131
|
+
// components that use this hook).
|
|
132
|
+
function getStateKey(): string {
|
|
133
|
+
return connectionState;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function useWebSocket({
|
|
137
|
+
url,
|
|
138
|
+
onMessage,
|
|
139
|
+
}: UseWebSocketOptions): UseWebSocketReturn {
|
|
140
|
+
const [stateKey, setStateKey] = useState(getStateKey);
|
|
141
|
+
const onMessageRef = useRef(onMessage);
|
|
109
142
|
|
|
110
143
|
useEffect(() => {
|
|
111
|
-
|
|
144
|
+
onMessageRef.current = onMessage;
|
|
145
|
+
}, [onMessage]);
|
|
112
146
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
147
|
+
// Subscribe to state changes — only re-render when the derived state key
|
|
148
|
+
// actually changes. Previous implementation used forceRender(n => n+1) which
|
|
149
|
+
// re-rendered on EVERY notifyStateChange regardless of whether the displayed
|
|
150
|
+
// status changed. With 3+ consumers this caused cascade re-renders.
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
const listener = () => {
|
|
153
|
+
const newKey = getStateKey();
|
|
154
|
+
setStateKey(prev => prev === newKey ? prev : newKey);
|
|
120
155
|
};
|
|
121
|
-
|
|
156
|
+
stateListeners.add(listener);
|
|
157
|
+
return () => { stateListeners.delete(listener); };
|
|
158
|
+
}, []);
|
|
122
159
|
|
|
123
|
-
|
|
160
|
+
// Subscribe to messages (skip when URL is empty — no project loaded)
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (!url) return;
|
|
163
|
+
const callback: Subscriber = (msg) => onMessageRef.current?.(msg);
|
|
164
|
+
return subscribe(url, callback);
|
|
165
|
+
}, [url]);
|
|
166
|
+
|
|
167
|
+
const manualReconnect = useCallback(() => {
|
|
168
|
+
reconnectAttempts = 0;
|
|
169
|
+
connectionState = 'disconnected';
|
|
170
|
+
if (sharedUrl) connectShared(sharedUrl);
|
|
171
|
+
}, []);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
isConnected: stateKey === 'connected',
|
|
175
|
+
isReconnecting: stateKey === 'reconnecting',
|
|
176
|
+
reconnectionFailed: stateKey === 'failed',
|
|
177
|
+
manualReconnect,
|
|
178
|
+
};
|
|
124
179
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>JettyPod</title>
|
|
7
|
+
<link rel="preload" href="/fonts/Satoshi-Variable.woff2" as="font" type="font/woff2" crossorigin />
|
|
8
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
|
9
|
+
<link rel="preconnect" href="ws://localhost:3456" />
|
|
10
|
+
<link rel="dns-prefetch" href="ws://localhost:3456" />
|
|
11
|
+
</head>
|
|
12
|
+
<body class="antialiased">
|
|
13
|
+
<div id="root">
|
|
14
|
+
<!-- Static skeleton — visible in <200ms before React loads, replaced on mount -->
|
|
15
|
+
<style>
|
|
16
|
+
@keyframes sk-pulse { 0%,100% { opacity: 1 } 50% { opacity: .4 } }
|
|
17
|
+
.sk { animation: sk-pulse 1.5s ease-in-out infinite; }
|
|
18
|
+
@media (prefers-color-scheme: dark) {
|
|
19
|
+
.sk-bg { background: #18181b; }
|
|
20
|
+
.sk-nav { background: #18181b; border-color: #27272a; }
|
|
21
|
+
.sk-col { background: #18181b; }
|
|
22
|
+
.sk-el { background: #27272a; }
|
|
23
|
+
}
|
|
24
|
+
</style>
|
|
25
|
+
<div style="display:flex;flex-direction:column;height:100vh;background:#fafafa" class="sk-bg">
|
|
26
|
+
<!-- Nav skeleton -->
|
|
27
|
+
<header style="padding:20px;border-bottom:1px solid #e4e4e7;background:#fff;flex-shrink:0" class="sk-nav">
|
|
28
|
+
<div style="display:flex;align-items:center;gap:16px">
|
|
29
|
+
<div style="width:36px;height:36px;border-radius:50%;background:#e4e4e7" class="sk-el sk"></div>
|
|
30
|
+
<div style="width:100px;height:20px;border-radius:6px;background:#e4e4e7" class="sk-el sk"></div>
|
|
31
|
+
<div style="width:60px;height:20px;border-radius:6px;background:#e4e4e7" class="sk-el sk"></div>
|
|
32
|
+
<div style="width:48px;height:20px;border-radius:6px;background:#e4e4e7" class="sk-el sk"></div>
|
|
33
|
+
</div>
|
|
34
|
+
</header>
|
|
35
|
+
<!-- Kanban skeleton -->
|
|
36
|
+
<div style="flex:1;max-width:80rem;width:100%;margin:0 auto;padding:16px;display:flex;gap:16px;min-height:0">
|
|
37
|
+
<!-- Backlog column -->
|
|
38
|
+
<div style="flex:1;max-width:600px;display:flex;flex-direction:column;min-height:0">
|
|
39
|
+
<div style="background:#f4f4f5;border-radius:12px;padding:16px;flex:1;min-height:0" class="sk-col">
|
|
40
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
|
|
41
|
+
<div style="width:96px;height:24px;border-radius:6px;background:#e4e4e7" class="sk-el sk"></div>
|
|
42
|
+
<div style="width:40px;height:24px;border-radius:12px;background:#e4e4e7" class="sk-el sk"></div>
|
|
43
|
+
</div>
|
|
44
|
+
<div style="display:flex;flex-direction:column;gap:12px">
|
|
45
|
+
<div style="height:80px;border-radius:12px;background:#e4e4e7" class="sk-el sk"></div>
|
|
46
|
+
<div style="height:80px;border-radius:12px;background:#e4e4e7" class="sk-el sk"></div>
|
|
47
|
+
<div style="height:80px;border-radius:12px;background:#e4e4e7" class="sk-el sk"></div>
|
|
48
|
+
<div style="height:80px;border-radius:12px;background:#e4e4e7" class="sk-el sk"></div>
|
|
49
|
+
<div style="height:80px;border-radius:12px;background:#e4e4e7" class="sk-el sk"></div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<!-- Done column -->
|
|
54
|
+
<div style="flex:1;max-width:600px;display:flex;flex-direction:column;min-height:0">
|
|
55
|
+
<div style="background:#f4f4f5;border-radius:12px;padding:16px;flex:1;min-height:0" class="sk-col">
|
|
56
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
|
|
57
|
+
<div style="width:64px;height:24px;border-radius:6px;background:#e4e4e7" class="sk-el sk"></div>
|
|
58
|
+
<div style="width:40px;height:24px;border-radius:12px;background:#e4e4e7" class="sk-el sk"></div>
|
|
59
|
+
</div>
|
|
60
|
+
<div style="display:flex;flex-direction:column;gap:12px">
|
|
61
|
+
<div style="height:64px;border-radius:12px;background:#e4e4e7" class="sk-el sk"></div>
|
|
62
|
+
<div style="height:64px;border-radius:12px;background:#e4e4e7" class="sk-el sk"></div>
|
|
63
|
+
<div style="height:64px;border-radius:12px;background:#e4e4e7" class="sk-el sk"></div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
<div id="tooltip-root" style="position: relative; z-index: 99999"></div>
|
|
71
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
72
|
+
</body>
|
|
73
|
+
</html>
|