jettypod 4.4.118 → 4.4.121
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +4 -3
- 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 +63 -58
- package/apps/dashboard/app/demo/gates/page.tsx +43 -45
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +80 -4
- package/apps/dashboard/app/install-claude/page.tsx +4 -6
- package/apps/dashboard/app/login/page.tsx +72 -54
- package/apps/dashboard/app/page.tsx +101 -48
- package/apps/dashboard/app/settings/page.tsx +61 -13
- package/apps/dashboard/app/signup/page.tsx +242 -0
- 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 +13 -16
- package/apps/dashboard/app/work/[id]/page.tsx +117 -118
- package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
- package/apps/dashboard/components/AppShell.tsx +92 -85
- package/apps/dashboard/components/CardMenu.tsx +45 -12
- package/apps/dashboard/components/ClaudePanel.tsx +771 -850
- package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
- package/apps/dashboard/components/CopyableId.tsx +3 -4
- package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
- package/apps/dashboard/components/DragContext.tsx +134 -63
- package/apps/dashboard/components/DraggableCard.tsx +3 -5
- package/apps/dashboard/components/DropZone.tsx +6 -7
- package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
- package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
- package/apps/dashboard/components/EditableTitle.tsx +26 -7
- package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
- package/apps/dashboard/components/EpicGroup.tsx +359 -0
- package/apps/dashboard/components/GateCard.tsx +79 -17
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
- package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
- package/apps/dashboard/components/JettyLoader.tsx +37 -0
- package/apps/dashboard/components/KanbanBoard.tsx +368 -958
- package/apps/dashboard/components/KanbanCard.tsx +740 -0
- package/apps/dashboard/components/LazyCard.tsx +62 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
- package/apps/dashboard/components/MainNav.tsx +38 -73
- package/apps/dashboard/components/MessageBlock.tsx +468 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -16
- package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
- package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
- package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
- package/apps/dashboard/components/ReviewFooter.tsx +139 -0
- package/apps/dashboard/components/SessionList.tsx +19 -19
- package/apps/dashboard/components/SubscribeContent.tsx +91 -47
- package/apps/dashboard/components/TestTree.tsx +16 -16
- package/apps/dashboard/components/TipCard.tsx +16 -17
- package/apps/dashboard/components/Toast.tsx +5 -6
- package/apps/dashboard/components/TypeIcon.tsx +55 -0
- package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
- package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
- package/apps/dashboard/components/WorkItemTree.tsx +11 -32
- package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
- 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 +74 -152
- package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
- package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/components.json +1 -1
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
- package/apps/dashboard/contexts/UsageContext.tsx +87 -32
- package/apps/dashboard/dev.sh +35 -0
- package/apps/dashboard/eslint.config.mjs +9 -9
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/hooks/useWebSocket.ts +138 -83
- package/apps/dashboard/index.html +73 -0
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/data-bridge.ts +722 -0
- package/apps/dashboard/lib/db.ts +69 -1265
- 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 +270 -0
- package/apps/dashboard/lib/proof-run.ts +495 -0
- package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- 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 +308 -134
- package/apps/dashboard/lib/shadows.ts +7 -0
- 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 +6 -0
- package/apps/dashboard/next-env.d.ts +1 -1
- package/apps/dashboard/package.json +21 -32
- package/apps/dashboard/public/bug-icon.png +0 -0
- package/apps/dashboard/public/buoy-icon.png +0 -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.png +0 -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.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/scripts/ws-server.js +191 -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 +228 -80
- 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/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 +145 -116
- package/lib/bdd-preflight.js +96 -0
- 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/merge-lock.js +111 -253
- package/lib/migrations/027-plan-at-creation-column.js +3 -1
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/030-rejection-round-columns.js +54 -0
- package/lib/migrations/031-session-isolation-index.js +17 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +10 -5
- package/lib/seed-onboarding.js +1 -1
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +144 -19
- package/lib/work-tracking/index.js +148 -27
- 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 +79 -20
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +171 -69
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/design-system-selection/SKILL.md +273 -0
- package/skills-templates/epic-planning/SKILL.md +82 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +173 -74
- package/skills-templates/production-mode/SKILL.md +69 -49
- package/skills-templates/request-routing/SKILL.md +4 -4
- package/skills-templates/simple-improvement/SKILL.md +74 -29
- package/skills-templates/speed-mode/SKILL.md +217 -141
- package/skills-templates/stable-mode/SKILL.md +148 -89
- package/apps/dashboard/README.md +0 -36
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
- package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
- 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 -378
- 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]/status/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
- package/apps/dashboard/app/layout.tsx +0 -43
- package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
- package/apps/dashboard/electron/ipc-handlers.js +0 -1028
- package/apps/dashboard/electron/main.js +0 -2124
- package/apps/dashboard/electron/preload.js +0 -123
- package/apps/dashboard/electron/session-manager.js +0 -141
- package/apps/dashboard/electron-builder.config.js +0 -357
- package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
- package/apps/dashboard/lib/claude-process-manager.ts +0 -492
- package/apps/dashboard/lib/db-bridge.ts +0 -282
- 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 -50
- package/apps/dashboard/postcss.config.mjs +0 -7
- package/apps/dashboard/public/file.svg +0 -1
- package/apps/dashboard/public/globe.svg +0 -1
- package/apps/dashboard/public/next.svg +0 -1
- package/apps/dashboard/public/vercel.svg +0 -1
- package/apps/dashboard/public/window.svg +0 -1
- package/apps/dashboard/scripts/download-node.js +0 -104
- package/apps/dashboard/scripts/upload-to-r2.js +0 -89
- package/docs/bdd-guidance.md +0 -390
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Button } from '@/components/ui/Button';
|
|
4
|
+
import { Input } from '@/components/ui/Input';
|
|
5
|
+
import { dataBridge } from '@/lib/data-bridge';
|
|
6
|
+
|
|
7
|
+
interface ReviewFooterProps {
|
|
8
|
+
workItemId: string;
|
|
9
|
+
onAccepted: () => void;
|
|
10
|
+
onRejected: (reason: string) => void;
|
|
11
|
+
onAskQuestion: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ReviewFooter({ workItemId, onAccepted, onRejected, onAskQuestion }: ReviewFooterProps) {
|
|
15
|
+
const [showRejectInput, setShowRejectInput] = useState(false);
|
|
16
|
+
const [rejectReason, setRejectReason] = useState('');
|
|
17
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
18
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
19
|
+
|
|
20
|
+
const handleAccept = async () => {
|
|
21
|
+
setErrorMessage(null);
|
|
22
|
+
setIsSubmitting(true);
|
|
23
|
+
try {
|
|
24
|
+
await dataBridge.updateStatus(parseInt(workItemId, 10), 'done');
|
|
25
|
+
onAccepted();
|
|
26
|
+
} catch {
|
|
27
|
+
setErrorMessage('Failed to accept. Please try again.');
|
|
28
|
+
setIsSubmitting(false);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const handleRejectClick = () => {
|
|
33
|
+
setShowRejectInput(true);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const handleRejectConfirm = async () => {
|
|
37
|
+
if (!rejectReason.trim()) return;
|
|
38
|
+
setErrorMessage(null);
|
|
39
|
+
setIsSubmitting(true);
|
|
40
|
+
try {
|
|
41
|
+
await dataBridge.updateStatus(parseInt(workItemId, 10), 'in_progress', rejectReason.trim());
|
|
42
|
+
const reason = rejectReason.trim();
|
|
43
|
+
setIsSubmitting(false);
|
|
44
|
+
setShowRejectInput(false);
|
|
45
|
+
setRejectReason('');
|
|
46
|
+
onRejected(reason);
|
|
47
|
+
} catch {
|
|
48
|
+
setErrorMessage('Failed to reject. Please try again.');
|
|
49
|
+
setIsSubmitting(false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleRejectCancel = () => {
|
|
54
|
+
setShowRejectInput(false);
|
|
55
|
+
setRejectReason('');
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="border-t border-zinc-200 px-5 py-5 flex-shrink-0" data-testid="review-footer">
|
|
60
|
+
<p className="text-base font-semibold text-zinc-900 mb-1.5">
|
|
61
|
+
This work is done. How does it look?
|
|
62
|
+
</p>
|
|
63
|
+
<p className="text-base text-zinc-500 mb-4">
|
|
64
|
+
Just a heads up. This chat will close after acceptance.
|
|
65
|
+
</p>
|
|
66
|
+
|
|
67
|
+
{errorMessage && (
|
|
68
|
+
<p className="text-sm text-red-600 mb-3" data-testid="review-error-message">{errorMessage}</p>
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
{!showRejectInput ? (
|
|
72
|
+
<div className="flex items-center gap-2" data-testid="review-actions">
|
|
73
|
+
<Button
|
|
74
|
+
onClick={handleAccept}
|
|
75
|
+
loading={isSubmitting}
|
|
76
|
+
data-testid="review-accept-button"
|
|
77
|
+
>
|
|
78
|
+
Accept
|
|
79
|
+
</Button>
|
|
80
|
+
<Button
|
|
81
|
+
onClick={handleRejectClick}
|
|
82
|
+
variant="secondary"
|
|
83
|
+
data-testid="review-reject-button"
|
|
84
|
+
>
|
|
85
|
+
Reject
|
|
86
|
+
</Button>
|
|
87
|
+
<Button
|
|
88
|
+
onClick={onAskQuestion}
|
|
89
|
+
variant="ghost"
|
|
90
|
+
data-testid="review-ask-question-button"
|
|
91
|
+
>
|
|
92
|
+
Ask a question
|
|
93
|
+
</Button>
|
|
94
|
+
</div>
|
|
95
|
+
) : (
|
|
96
|
+
<div data-testid="review-reject-input-area">
|
|
97
|
+
<Input
|
|
98
|
+
type="text"
|
|
99
|
+
value={rejectReason}
|
|
100
|
+
onChange={(e) => setRejectReason(e.target.value)}
|
|
101
|
+
onKeyDown={(e) => {
|
|
102
|
+
if (e.key === 'Enter' && rejectReason.trim()) {
|
|
103
|
+
handleRejectConfirm();
|
|
104
|
+
}
|
|
105
|
+
if (e.key === 'Escape') {
|
|
106
|
+
handleRejectCancel();
|
|
107
|
+
}
|
|
108
|
+
}}
|
|
109
|
+
placeholder="Rejection reason..."
|
|
110
|
+
size="sm"
|
|
111
|
+
error
|
|
112
|
+
autoFocus
|
|
113
|
+
data-testid="review-reject-reason-input"
|
|
114
|
+
/>
|
|
115
|
+
<div className="flex items-center gap-1.5 mt-2">
|
|
116
|
+
<Button
|
|
117
|
+
onClick={handleRejectConfirm}
|
|
118
|
+
disabled={!rejectReason.trim()}
|
|
119
|
+
loading={isSubmitting}
|
|
120
|
+
variant="destructive"
|
|
121
|
+
size="sm"
|
|
122
|
+
data-testid="review-reject-confirm"
|
|
123
|
+
>
|
|
124
|
+
Reject
|
|
125
|
+
</Button>
|
|
126
|
+
<Button
|
|
127
|
+
onClick={handleRejectCancel}
|
|
128
|
+
variant="ghost"
|
|
129
|
+
size="sm"
|
|
130
|
+
data-testid="review-reject-cancel"
|
|
131
|
+
>
|
|
132
|
+
Cancel
|
|
133
|
+
</Button>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
'use client';
|
|
2
1
|
|
|
3
|
-
import {
|
|
2
|
+
import { m } from 'framer-motion';
|
|
3
|
+
import { Button } from '@/components/ui/Button';
|
|
4
4
|
|
|
5
5
|
export interface SessionItem {
|
|
6
6
|
id: string;
|
|
@@ -26,23 +26,23 @@ export function SessionList({
|
|
|
26
26
|
return (
|
|
27
27
|
<div className="flex-1 flex flex-col" data-testid="session-list">
|
|
28
28
|
{/* Header */}
|
|
29
|
-
<div className="px-
|
|
29
|
+
<div className="px-5 py-4 border-b border-zinc-800">
|
|
30
30
|
<div className="flex items-center justify-between">
|
|
31
|
-
<h2 className="text-
|
|
32
|
-
<
|
|
31
|
+
<h2 className="text-base font-semibold text-white">Sessions</h2>
|
|
32
|
+
<Button
|
|
33
33
|
onClick={onNewSession}
|
|
34
|
-
|
|
34
|
+
size="sm"
|
|
35
35
|
data-testid="new-session-button"
|
|
36
36
|
>
|
|
37
37
|
New Session
|
|
38
|
-
</
|
|
38
|
+
</Button>
|
|
39
39
|
</div>
|
|
40
40
|
</div>
|
|
41
41
|
|
|
42
42
|
{/* Session List */}
|
|
43
43
|
<div className="flex-1 overflow-y-auto">
|
|
44
44
|
{sessions.length === 0 ? (
|
|
45
|
-
<div className="p-
|
|
45
|
+
<div className="p-6 text-center text-zinc-500 text-base" data-testid="empty-session-state">
|
|
46
46
|
No sessions yet. Click New Session to start.
|
|
47
47
|
</div>
|
|
48
48
|
) : (
|
|
@@ -50,39 +50,39 @@ export function SessionList({
|
|
|
50
50
|
{sessions.map((session) => (
|
|
51
51
|
<div
|
|
52
52
|
key={session.id}
|
|
53
|
-
className="flex items-center hover:bg-zinc-800/50 transition-colors"
|
|
53
|
+
className="flex items-center hover:bg-zinc-800/50 transition-colors duration-200 ease-out"
|
|
54
54
|
data-testid={`session-item-${session.id}`}
|
|
55
55
|
>
|
|
56
|
-
<
|
|
56
|
+
<m.button
|
|
57
57
|
onClick={() => onSelectSession(session.id)}
|
|
58
|
-
className="flex-1 px-
|
|
58
|
+
className="flex-1 px-5 py-4 text-left"
|
|
59
59
|
whileHover={{ x: 4 }}
|
|
60
60
|
>
|
|
61
|
-
<div className="flex items-center gap-
|
|
61
|
+
<div className="flex items-center gap-3">
|
|
62
62
|
<SessionIcon hasFeature={!!session.featureId} />
|
|
63
63
|
<div className="flex-1 min-w-0">
|
|
64
|
-
<p className="text-
|
|
64
|
+
<p className="text-base font-medium text-white truncate">
|
|
65
65
|
{session.featureId ? session.featureTitle : session.title}
|
|
66
66
|
</p>
|
|
67
67
|
{session.featureId && (
|
|
68
|
-
<span className="inline-flex items-center px-
|
|
68
|
+
<span className="inline-flex items-center px-2 py-1 mt-1.5 text-xs font-medium bg-[#819D9F]/20 text-[#a3bfc0] rounded">
|
|
69
69
|
#{session.featureId}
|
|
70
70
|
</span>
|
|
71
71
|
)}
|
|
72
72
|
{!session.featureId && (
|
|
73
|
-
<p className="text-
|
|
73
|
+
<p className="text-base text-zinc-500 mt-1">Unlinked session</p>
|
|
74
74
|
)}
|
|
75
75
|
</div>
|
|
76
76
|
<ChevronIcon />
|
|
77
77
|
</div>
|
|
78
|
-
</
|
|
78
|
+
</m.button>
|
|
79
79
|
{onCloseSession && (
|
|
80
80
|
<button
|
|
81
81
|
onClick={(e) => {
|
|
82
82
|
e.stopPropagation();
|
|
83
83
|
onCloseSession(session.id);
|
|
84
84
|
}}
|
|
85
|
-
className="p-2 mr-
|
|
85
|
+
className="p-2.5 mr-3 rounded hover:bg-zinc-700 text-zinc-500 hover:text-zinc-300 transition-colors duration-200 ease-out"
|
|
86
86
|
aria-label="Close session"
|
|
87
87
|
data-testid={`close-session-${session.id}`}
|
|
88
88
|
>
|
|
@@ -101,10 +101,10 @@ export function SessionList({
|
|
|
101
101
|
function SessionIcon({ hasFeature }: { hasFeature: boolean }) {
|
|
102
102
|
return (
|
|
103
103
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
|
104
|
-
hasFeature ? 'bg-
|
|
104
|
+
hasFeature ? 'bg-[#819D9F]/20' : 'bg-zinc-800'
|
|
105
105
|
}`}>
|
|
106
106
|
<svg
|
|
107
|
-
className={`w-4 h-4 ${hasFeature ? 'text-
|
|
107
|
+
className={`w-4 h-4 ${hasFeature ? 'text-[#819D9F]' : 'text-zinc-400'}`}
|
|
108
108
|
fill="none"
|
|
109
109
|
stroke="currentColor"
|
|
110
110
|
viewBox="0 0 24 24"
|
|
@@ -1,25 +1,10 @@
|
|
|
1
|
-
'use client';
|
|
2
1
|
|
|
3
2
|
import { useState, useEffect, useRef } from 'react';
|
|
4
|
-
|
|
3
|
+
import { Button } from '@/components/ui/Button';
|
|
4
|
+
import { isTauri, auth, shell } from '@/lib/tauri-bridge';
|
|
5
5
|
|
|
6
6
|
const API_BASE = 'https://jettypod-update-server.spangbaryn2.workers.dev';
|
|
7
7
|
|
|
8
|
-
const buttonGradientStyle = {
|
|
9
|
-
background: 'linear-gradient(145deg, #ffffff 0%, #faf9f7 10%, #f0f4f4 35%, #c8d9da 55%, #819D9F 90%)',
|
|
10
|
-
color: '#3d4d4e',
|
|
11
|
-
boxShadow: `
|
|
12
|
-
0 1px 1px rgba(0, 0, 0, 0.02),
|
|
13
|
-
0 2px 4px rgba(0, 0, 0, 0.03),
|
|
14
|
-
0 6px 12px rgba(0, 0, 0, 0.05),
|
|
15
|
-
0 12px 24px rgba(0, 0, 0, 0.06),
|
|
16
|
-
0 20px 40px rgba(129, 157, 159, 0.2),
|
|
17
|
-
0 32px 64px rgba(129, 157, 159, 0.18),
|
|
18
|
-
inset 0 2px 4px rgba(255, 255, 255, 1),
|
|
19
|
-
inset 0 -2px 4px rgba(129, 157, 159, 0.05)
|
|
20
|
-
`,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
8
|
type PageState = 'idle' | 'checkout-opened' | 'polling' | 'upgraded';
|
|
24
9
|
type PlanType = 'monthly' | 'lifetime';
|
|
25
10
|
|
|
@@ -35,14 +20,38 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
|
|
|
35
20
|
const [userPlan, setUserPlan] = useState<string | null>(null);
|
|
36
21
|
const [pollingPlan, setPollingPlan] = useState<PlanType | null>(null);
|
|
37
22
|
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
23
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
38
24
|
|
|
39
25
|
useEffect(() => {
|
|
40
26
|
async function loadAuth() {
|
|
41
|
-
if (!
|
|
42
|
-
const status = await
|
|
27
|
+
if (!isTauri()) return;
|
|
28
|
+
const status = await auth.getStatus();
|
|
43
29
|
if (status.authenticated && status.user) {
|
|
44
30
|
setUserEmail(status.user.email);
|
|
45
31
|
setUserPlan(status.user.plan || 'free');
|
|
32
|
+
|
|
33
|
+
// Check server-side plan in case webhook updated it after local JWT was issued
|
|
34
|
+
const token = await auth.getToken();
|
|
35
|
+
if (token) {
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(`${API_BASE}/auth/me`, {
|
|
38
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
39
|
+
});
|
|
40
|
+
if (res.ok) {
|
|
41
|
+
const data = await res.json() as { user: { plan: string; email: string }; token?: string };
|
|
42
|
+
if (data.user.plan !== 'free') {
|
|
43
|
+
if (data.token) {
|
|
44
|
+
await auth.saveToken(data.token, data.user);
|
|
45
|
+
}
|
|
46
|
+
setUserPlan(data.user.plan);
|
|
47
|
+
setPageState('upgraded');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Fall through to show upgrade UI on network error
|
|
53
|
+
}
|
|
54
|
+
}
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
loadAuth();
|
|
@@ -51,19 +60,22 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
|
|
|
51
60
|
useEffect(() => {
|
|
52
61
|
return () => {
|
|
53
62
|
if (pollingRef.current) clearInterval(pollingRef.current);
|
|
63
|
+
abortRef.current?.abort();
|
|
54
64
|
};
|
|
55
65
|
}, []);
|
|
56
66
|
|
|
57
67
|
const startPolling = () => {
|
|
58
68
|
setPageState('polling');
|
|
69
|
+
abortRef.current = new AbortController();
|
|
59
70
|
|
|
60
71
|
pollingRef.current = setInterval(async () => {
|
|
61
72
|
try {
|
|
62
|
-
const token = await
|
|
73
|
+
const token = await auth.getToken();
|
|
63
74
|
if (!token) return;
|
|
64
75
|
|
|
65
76
|
const res = await fetch(`${API_BASE}/auth/me`, {
|
|
66
77
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
78
|
+
signal: abortRef.current?.signal,
|
|
67
79
|
});
|
|
68
80
|
|
|
69
81
|
if (!res.ok) return;
|
|
@@ -72,7 +84,7 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
|
|
|
72
84
|
|
|
73
85
|
if (data.user.plan !== 'free') {
|
|
74
86
|
if (data.token) {
|
|
75
|
-
await
|
|
87
|
+
await auth.saveToken(data.token, data.user);
|
|
76
88
|
}
|
|
77
89
|
setUserPlan(data.user.plan);
|
|
78
90
|
setPageState('upgraded');
|
|
@@ -85,13 +97,38 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
|
|
|
85
97
|
};
|
|
86
98
|
|
|
87
99
|
const handleCheckout = async (plan: 'monthly' | 'lifetime') => {
|
|
88
|
-
if (!
|
|
100
|
+
if (!isTauri()) return;
|
|
89
101
|
setCheckoutPlan(plan);
|
|
90
102
|
setError(null);
|
|
91
103
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
try {
|
|
105
|
+
const token = await auth.getToken();
|
|
106
|
+
if (!token) {
|
|
107
|
+
setError('Not authenticated');
|
|
108
|
+
setCheckoutPlan(null);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const res = await fetch(`${API_BASE}/billing/create-checkout`, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
114
|
+
body: JSON.stringify({ plan }),
|
|
115
|
+
});
|
|
116
|
+
if (res.ok) {
|
|
117
|
+
const data = await res.json() as { url?: string };
|
|
118
|
+
if (data.url) {
|
|
119
|
+
await shell.openUrl(data.url);
|
|
120
|
+
} else {
|
|
121
|
+
setError('Failed to start checkout.');
|
|
122
|
+
setCheckoutPlan(null);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
setError('Failed to start checkout.');
|
|
127
|
+
setCheckoutPlan(null);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
setError('Failed to start checkout.');
|
|
95
132
|
setCheckoutPlan(null);
|
|
96
133
|
return;
|
|
97
134
|
}
|
|
@@ -111,20 +148,16 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
|
|
|
111
148
|
|
|
112
149
|
if (pageState === 'upgraded') {
|
|
113
150
|
return (
|
|
114
|
-
<div className="max-w-md w-full space-y-
|
|
151
|
+
<div className="max-w-md w-full space-y-10 text-center">
|
|
115
152
|
<h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100">
|
|
116
153
|
You're all set!
|
|
117
154
|
</h1>
|
|
118
155
|
<p className="text-zinc-500 dark:text-zinc-400">
|
|
119
156
|
Your plan has been upgraded to <span className="font-medium text-zinc-900 dark:text-zinc-100">{userPlan}</span>.
|
|
120
157
|
</p>
|
|
121
|
-
<
|
|
122
|
-
onClick={handleDone}
|
|
123
|
-
className="w-full py-3 px-6 rounded-xl font-medium transition-all duration-200 hover:-translate-y-1 hover:scale-[1.01] active:translate-y-0 active:scale-100"
|
|
124
|
-
style={buttonGradientStyle}
|
|
125
|
-
>
|
|
158
|
+
<Button onClick={handleDone} size="lg" fullWidth>
|
|
126
159
|
{onClose ? 'Done' : 'Go to Dashboard'}
|
|
127
|
-
</
|
|
160
|
+
</Button>
|
|
128
161
|
</div>
|
|
129
162
|
);
|
|
130
163
|
}
|
|
@@ -132,8 +165,8 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
|
|
|
132
165
|
const isPolling = pageState === 'polling';
|
|
133
166
|
|
|
134
167
|
return (
|
|
135
|
-
<div className="max-w-md w-full space-y-
|
|
136
|
-
<div className="flex flex-col items-center space-y-
|
|
168
|
+
<div className="max-w-md w-full space-y-10">
|
|
169
|
+
<div className="flex flex-col items-center space-y-6">
|
|
137
170
|
<h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 text-center">
|
|
138
171
|
Unlock Unlimited Use
|
|
139
172
|
</h1>
|
|
@@ -143,7 +176,7 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
|
|
|
143
176
|
</div>
|
|
144
177
|
|
|
145
178
|
{userEmail && (
|
|
146
|
-
<div className="bg-zinc-50 dark:bg-zinc-800 rounded-xl px-
|
|
179
|
+
<div className="bg-zinc-50 dark:bg-zinc-800 rounded-xl px-5 py-4 text-base">
|
|
147
180
|
<div className="flex justify-between items-center">
|
|
148
181
|
<span className="text-zinc-500 dark:text-zinc-400">{userEmail}</span>
|
|
149
182
|
<span className="text-zinc-400 dark:text-zinc-500 capitalize">{userPlan}</span>
|
|
@@ -152,39 +185,50 @@ export function SubscribeContent({ onClose }: SubscribeContentProps) {
|
|
|
152
185
|
)}
|
|
153
186
|
|
|
154
187
|
{error && (
|
|
155
|
-
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-
|
|
188
|
+
<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">
|
|
156
189
|
{error}
|
|
157
190
|
</div>
|
|
158
191
|
)}
|
|
159
192
|
|
|
160
|
-
<div className="pt-
|
|
161
|
-
|
|
193
|
+
<div className="pt-6 space-y-4">
|
|
194
|
+
{!onClose && (
|
|
195
|
+
<Button
|
|
196
|
+
variant="ghost"
|
|
197
|
+
size="sm"
|
|
198
|
+
fullWidth
|
|
199
|
+
className="mb-2"
|
|
200
|
+
onClick={() => window.history.back()}
|
|
201
|
+
>
|
|
202
|
+
← Go back
|
|
203
|
+
</Button>
|
|
204
|
+
)}
|
|
205
|
+
<Button
|
|
162
206
|
onClick={() => handleCheckout('monthly')}
|
|
163
207
|
disabled={checkoutPlan !== null || isPolling}
|
|
164
|
-
|
|
165
|
-
|
|
208
|
+
size="lg"
|
|
209
|
+
fullWidth
|
|
166
210
|
>
|
|
167
211
|
{pollingPlan === 'monthly' ? (
|
|
168
|
-
<span className="inline-flex items-center gap-
|
|
212
|
+
<span className="inline-flex items-center gap-3">
|
|
169
213
|
<span className="animate-spin inline-block h-4 w-4 border-2 border-current border-t-transparent rounded-full" />
|
|
170
214
|
Upgrading...
|
|
171
215
|
</span>
|
|
172
216
|
) : checkoutPlan === 'monthly' ? 'Opening checkout...' : 'Monthly'}
|
|
173
|
-
</
|
|
217
|
+
</Button>
|
|
174
218
|
|
|
175
|
-
<
|
|
219
|
+
<Button
|
|
176
220
|
onClick={() => handleCheckout('lifetime')}
|
|
177
221
|
disabled={checkoutPlan !== null || isPolling}
|
|
178
|
-
|
|
179
|
-
|
|
222
|
+
size="lg"
|
|
223
|
+
fullWidth
|
|
180
224
|
>
|
|
181
225
|
{pollingPlan === 'lifetime' ? (
|
|
182
|
-
<span className="inline-flex items-center gap-
|
|
226
|
+
<span className="inline-flex items-center gap-3">
|
|
183
227
|
<span className="animate-spin inline-block h-4 w-4 border-2 border-current border-t-transparent rounded-full" />
|
|
184
228
|
Upgrading...
|
|
185
229
|
</span>
|
|
186
230
|
) : checkoutPlan === 'lifetime' ? 'Opening checkout...' : 'Lifetime Access'}
|
|
187
|
-
</
|
|
231
|
+
</Button>
|
|
188
232
|
</div>
|
|
189
233
|
</div>
|
|
190
234
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
'use client';
|
|
2
1
|
|
|
3
2
|
import { useState } from 'react';
|
|
4
|
-
import type { TestDashboardData, TestEpic, TestFeature, TestScenario } from '@/lib/
|
|
3
|
+
import type { TestDashboardData, TestEpic, TestFeature, TestScenario } from '@/lib/db';
|
|
4
|
+
import { TypeIcon } from './TypeIcon';
|
|
5
5
|
|
|
6
6
|
const statusIcons: Record<string, string> = {
|
|
7
7
|
pass: '✅',
|
|
@@ -27,7 +27,7 @@ function ScenarioNode({ scenario, depth }: ScenarioNodeProps) {
|
|
|
27
27
|
return (
|
|
28
28
|
<div className="select-none">
|
|
29
29
|
<div
|
|
30
|
-
className={`flex items-center gap-2 py-
|
|
30
|
+
className={`flex items-center gap-2 py-2 px-3 rounded hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors duration-200 ease-out ${hasError ? 'cursor-pointer' : ''}`}
|
|
31
31
|
style={{ paddingLeft: `${depth * 20 + 8}px` }}
|
|
32
32
|
onClick={() => hasError && setExpanded(!expanded)}
|
|
33
33
|
>
|
|
@@ -38,8 +38,8 @@ function ScenarioNode({ scenario, depth }: ScenarioNodeProps) {
|
|
|
38
38
|
) : (
|
|
39
39
|
<span className="w-4" />
|
|
40
40
|
)}
|
|
41
|
-
<span className="text-
|
|
42
|
-
<span className={`flex-1 text-
|
|
41
|
+
<span className="text-base">{statusIcons[scenario.status]}</span>
|
|
42
|
+
<span className={`flex-1 text-base ${statusColors[scenario.status]}`}>
|
|
43
43
|
{scenario.title}
|
|
44
44
|
</span>
|
|
45
45
|
<span className="text-xs text-zinc-400 font-mono">{scenario.duration}</span>
|
|
@@ -48,11 +48,11 @@ function ScenarioNode({ scenario, depth }: ScenarioNodeProps) {
|
|
|
48
48
|
{/* Error details panel */}
|
|
49
49
|
{expanded && hasError && (
|
|
50
50
|
<div
|
|
51
|
-
className="mt-1 mb-
|
|
51
|
+
className="mt-1.5 mb-3 p-4 bg-red-50 dark:bg-red-900/20 border-2 border-red-200 dark:border-red-800 rounded text-base"
|
|
52
52
|
style={{ marginLeft: `${depth * 20 + 28}px` }}
|
|
53
53
|
>
|
|
54
54
|
{scenario.failedStep && (
|
|
55
|
-
<div className="mb-
|
|
55
|
+
<div className="mb-3">
|
|
56
56
|
<span className="font-semibold text-red-700 dark:text-red-300">Failed step: </span>
|
|
57
57
|
<span className="text-red-600 dark:text-red-400">{scenario.failedStep}</span>
|
|
58
58
|
</div>
|
|
@@ -60,7 +60,7 @@ function ScenarioNode({ scenario, depth }: ScenarioNodeProps) {
|
|
|
60
60
|
{scenario.error && (
|
|
61
61
|
<div>
|
|
62
62
|
<span className="font-semibold text-red-700 dark:text-red-300">Error: </span>
|
|
63
|
-
<pre className="mt-1 p-
|
|
63
|
+
<pre className="mt-1.5 p-3 bg-red-100 dark:bg-red-900/40 rounded text-xs text-red-800 dark:text-red-200 overflow-x-auto whitespace-pre-wrap">
|
|
64
64
|
{scenario.error}
|
|
65
65
|
</pre>
|
|
66
66
|
</div>
|
|
@@ -87,7 +87,7 @@ function FeatureNode({ feature, depth }: FeatureNodeProps) {
|
|
|
87
87
|
return (
|
|
88
88
|
<div className="select-none">
|
|
89
89
|
<div
|
|
90
|
-
className="flex items-center gap-2 py-
|
|
90
|
+
className="flex items-center gap-2 py-2 px-3 rounded hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors duration-200 ease-out cursor-pointer"
|
|
91
91
|
style={{ paddingLeft: `${depth * 20 + 8}px` }}
|
|
92
92
|
onClick={() => setExpanded(!expanded)}
|
|
93
93
|
>
|
|
@@ -98,12 +98,12 @@ function FeatureNode({ feature, depth }: FeatureNodeProps) {
|
|
|
98
98
|
) : (
|
|
99
99
|
<span className="w-4" />
|
|
100
100
|
)}
|
|
101
|
-
<span className="text-
|
|
101
|
+
<span className="text-base"><TypeIcon type="feature" /></span>
|
|
102
102
|
<span className={`flex-1 font-medium ${allPassing ? 'text-zinc-900 dark:text-zinc-100' : 'text-red-600 dark:text-red-400'}`}>
|
|
103
103
|
{feature.title}
|
|
104
104
|
</span>
|
|
105
105
|
{/* Health badge */}
|
|
106
|
-
<span className="text-xs px-
|
|
106
|
+
<span className="text-xs px-2 py-1 rounded bg-zinc-100 dark:bg-zinc-800">
|
|
107
107
|
<span className="text-green-600 dark:text-green-400">{passingCount}</span>
|
|
108
108
|
<span className="text-zinc-400 mx-1">/</span>
|
|
109
109
|
<span className={failingCount > 0 ? 'text-red-600 dark:text-red-400' : 'text-zinc-400'}>
|
|
@@ -135,7 +135,7 @@ function EpicNode({ epic }: EpicNodeProps) {
|
|
|
135
135
|
return (
|
|
136
136
|
<div className="select-none">
|
|
137
137
|
<div
|
|
138
|
-
className="flex items-center gap-2 py-
|
|
138
|
+
className="flex items-center gap-2 py-3 px-3 rounded hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors duration-200 ease-out cursor-pointer"
|
|
139
139
|
onClick={() => setExpanded(!expanded)}
|
|
140
140
|
>
|
|
141
141
|
{hasFeatures ? (
|
|
@@ -145,12 +145,12 @@ function EpicNode({ epic }: EpicNodeProps) {
|
|
|
145
145
|
) : (
|
|
146
146
|
<span className="w-4" />
|
|
147
147
|
)}
|
|
148
|
-
<span className="text-base"
|
|
148
|
+
<span className="text-base"><TypeIcon type="epic" /></span>
|
|
149
149
|
<span className={`flex-1 font-semibold ${allPassing ? 'text-zinc-900 dark:text-zinc-100' : 'text-red-600 dark:text-red-400'}`}>
|
|
150
150
|
{epic.title}
|
|
151
151
|
</span>
|
|
152
152
|
{/* Health badge */}
|
|
153
|
-
<span className="text-xs px-
|
|
153
|
+
<span className="text-xs px-3 py-1.5 rounded bg-zinc-100 dark:bg-zinc-800 font-mono">
|
|
154
154
|
<span className="text-green-600 dark:text-green-400">{epic.healthBadge.passing}</span>
|
|
155
155
|
<span className="text-zinc-400 mx-1">/</span>
|
|
156
156
|
<span className={epic.healthBadge.failing > 0 ? 'text-red-600 dark:text-red-400' : 'text-zinc-400'}>
|
|
@@ -184,7 +184,7 @@ export function TestTree({ data }: TestTreeProps) {
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
return (
|
|
187
|
-
<div className="font-mono text-
|
|
187
|
+
<div className="font-mono text-base space-y-2">
|
|
188
188
|
{/* Epics */}
|
|
189
189
|
{data.epics.map(epic => (
|
|
190
190
|
<EpicNode key={epic.id} epic={epic} />
|
|
@@ -194,7 +194,7 @@ export function TestTree({ data }: TestTreeProps) {
|
|
|
194
194
|
{data.standaloneFeatures.length > 0 && (
|
|
195
195
|
<div className="mt-4">
|
|
196
196
|
{data.epics.length > 0 && (
|
|
197
|
-
<div className="text-
|
|
197
|
+
<div className="text-base text-zinc-400 uppercase tracking-wider mb-2 px-2">
|
|
198
198
|
Standalone Features
|
|
199
199
|
</div>
|
|
200
200
|
)}
|