@assistkick/create 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/bin/create.d.ts +2 -0
  2. package/dist/bin/create.js +25 -0
  3. package/dist/bin/create.js.map +1 -0
  4. package/dist/src/scaffolder.d.ts +22 -0
  5. package/dist/src/scaffolder.js +120 -0
  6. package/dist/src/scaffolder.js.map +1 -0
  7. package/package.json +24 -0
  8. package/templates/product-system/.env.example +8 -0
  9. package/templates/product-system/CLAUDE.md +45 -0
  10. package/templates/product-system/package.json +32 -0
  11. package/templates/product-system/packages/backend/package.json +37 -0
  12. package/templates/product-system/packages/backend/src/middleware/auth_middleware.test.ts +86 -0
  13. package/templates/product-system/packages/backend/src/middleware/auth_middleware.ts +35 -0
  14. package/templates/product-system/packages/backend/src/routes/auth.ts +463 -0
  15. package/templates/product-system/packages/backend/src/routes/coherence.ts +187 -0
  16. package/templates/product-system/packages/backend/src/routes/graph.ts +67 -0
  17. package/templates/product-system/packages/backend/src/routes/kanban.ts +201 -0
  18. package/templates/product-system/packages/backend/src/routes/pipeline.ts +41 -0
  19. package/templates/product-system/packages/backend/src/routes/projects.ts +122 -0
  20. package/templates/product-system/packages/backend/src/routes/users.ts +97 -0
  21. package/templates/product-system/packages/backend/src/server.ts +159 -0
  22. package/templates/product-system/packages/backend/src/services/auth_service.test.ts +115 -0
  23. package/templates/product-system/packages/backend/src/services/auth_service.ts +82 -0
  24. package/templates/product-system/packages/backend/src/services/coherence-review.ts +339 -0
  25. package/templates/product-system/packages/backend/src/services/email_service.ts +75 -0
  26. package/templates/product-system/packages/backend/src/services/init.ts +80 -0
  27. package/templates/product-system/packages/backend/src/services/invitation_service.test.ts +235 -0
  28. package/templates/product-system/packages/backend/src/services/invitation_service.ts +193 -0
  29. package/templates/product-system/packages/backend/src/services/password_reset_service.test.ts +151 -0
  30. package/templates/product-system/packages/backend/src/services/password_reset_service.ts +135 -0
  31. package/templates/product-system/packages/backend/src/services/project_service.test.ts +215 -0
  32. package/templates/product-system/packages/backend/src/services/project_service.ts +171 -0
  33. package/templates/product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -0
  34. package/templates/product-system/packages/backend/src/services/pty_session_manager.ts +279 -0
  35. package/templates/product-system/packages/backend/src/services/terminal_ws_handler.ts +133 -0
  36. package/templates/product-system/packages/backend/src/services/user_management_service.test.ts +158 -0
  37. package/templates/product-system/packages/backend/src/services/user_management_service.ts +128 -0
  38. package/templates/product-system/packages/backend/tsconfig.json +22 -0
  39. package/templates/product-system/packages/frontend/index.html +13 -0
  40. package/templates/product-system/packages/frontend/package-lock.json +2666 -0
  41. package/templates/product-system/packages/frontend/package.json +30 -0
  42. package/templates/product-system/packages/frontend/public/favicon.svg +16 -0
  43. package/templates/product-system/packages/frontend/src/App.tsx +29 -0
  44. package/templates/product-system/packages/frontend/src/api/client.ts +386 -0
  45. package/templates/product-system/packages/frontend/src/api/client_projects.test.ts +104 -0
  46. package/templates/product-system/packages/frontend/src/api/client_refresh.test.ts +145 -0
  47. package/templates/product-system/packages/frontend/src/components/CoherenceView.tsx +414 -0
  48. package/templates/product-system/packages/frontend/src/components/GraphLegend.tsx +124 -0
  49. package/templates/product-system/packages/frontend/src/components/GraphSettings.tsx +112 -0
  50. package/templates/product-system/packages/frontend/src/components/GraphView.tsx +370 -0
  51. package/templates/product-system/packages/frontend/src/components/InviteUserDialog.tsx +85 -0
  52. package/templates/product-system/packages/frontend/src/components/KanbanView.tsx +470 -0
  53. package/templates/product-system/packages/frontend/src/components/LoginPage.tsx +116 -0
  54. package/templates/product-system/packages/frontend/src/components/ProjectSelector.tsx +187 -0
  55. package/templates/product-system/packages/frontend/src/components/QaIssueSheet.tsx +192 -0
  56. package/templates/product-system/packages/frontend/src/components/SidePanel.tsx +231 -0
  57. package/templates/product-system/packages/frontend/src/components/TerminalView.tsx +200 -0
  58. package/templates/product-system/packages/frontend/src/components/Toolbar.tsx +84 -0
  59. package/templates/product-system/packages/frontend/src/components/UsersView.tsx +249 -0
  60. package/templates/product-system/packages/frontend/src/constants/graph.ts +191 -0
  61. package/templates/product-system/packages/frontend/src/hooks/useAuth.tsx +54 -0
  62. package/templates/product-system/packages/frontend/src/hooks/useGraph.ts +27 -0
  63. package/templates/product-system/packages/frontend/src/hooks/useKanban.ts +21 -0
  64. package/templates/product-system/packages/frontend/src/hooks/useProjects.ts +86 -0
  65. package/templates/product-system/packages/frontend/src/hooks/useTheme.ts +26 -0
  66. package/templates/product-system/packages/frontend/src/hooks/useToast.tsx +62 -0
  67. package/templates/product-system/packages/frontend/src/hooks/use_projects_logic.test.ts +61 -0
  68. package/templates/product-system/packages/frontend/src/main.tsx +12 -0
  69. package/templates/product-system/packages/frontend/src/pages/accept_invitation_page.tsx +167 -0
  70. package/templates/product-system/packages/frontend/src/pages/forgot_password_page.tsx +100 -0
  71. package/templates/product-system/packages/frontend/src/pages/register_page.tsx +137 -0
  72. package/templates/product-system/packages/frontend/src/pages/reset_password_page.tsx +146 -0
  73. package/templates/product-system/packages/frontend/src/routes/ProtectedRoute.tsx +12 -0
  74. package/templates/product-system/packages/frontend/src/routes/accept_invitation.tsx +14 -0
  75. package/templates/product-system/packages/frontend/src/routes/dashboard.tsx +221 -0
  76. package/templates/product-system/packages/frontend/src/routes/forgot_password.tsx +13 -0
  77. package/templates/product-system/packages/frontend/src/routes/login.tsx +14 -0
  78. package/templates/product-system/packages/frontend/src/routes/register.tsx +14 -0
  79. package/templates/product-system/packages/frontend/src/routes/reset_password.tsx +13 -0
  80. package/templates/product-system/packages/frontend/src/styles/index.css +3358 -0
  81. package/templates/product-system/packages/frontend/src/utils/auth_validation.test.ts +51 -0
  82. package/templates/product-system/packages/frontend/src/utils/auth_validation.ts +19 -0
  83. package/templates/product-system/packages/frontend/src/utils/login_validation.test.ts +61 -0
  84. package/templates/product-system/packages/frontend/src/utils/login_validation.ts +24 -0
  85. package/templates/product-system/packages/frontend/src/utils/logout.test.ts +63 -0
  86. package/templates/product-system/packages/frontend/src/utils/node_sizing.test.ts +62 -0
  87. package/templates/product-system/packages/frontend/src/utils/node_sizing.ts +24 -0
  88. package/templates/product-system/packages/frontend/src/utils/task_status.test.ts +53 -0
  89. package/templates/product-system/packages/frontend/src/utils/task_status.ts +14 -0
  90. package/templates/product-system/packages/frontend/tsconfig.json +21 -0
  91. package/templates/product-system/packages/frontend/vite.config.ts +20 -0
  92. package/templates/product-system/packages/shared/.env.example +3 -0
  93. package/templates/product-system/packages/shared/README.md +1 -0
  94. package/templates/product-system/packages/shared/db/migrate.ts +32 -0
  95. package/templates/product-system/packages/shared/db/migrations/0000_dashing_gorgon.sql +128 -0
  96. package/templates/product-system/packages/shared/db/migrations/meta/0000_snapshot.json +819 -0
  97. package/templates/product-system/packages/shared/db/migrations/meta/_journal.json +13 -0
  98. package/templates/product-system/packages/shared/db/schema.ts +137 -0
  99. package/templates/product-system/packages/shared/drizzle.config.js +14 -0
  100. package/templates/product-system/packages/shared/lib/claude-service.ts +215 -0
  101. package/templates/product-system/packages/shared/lib/coherence.ts +278 -0
  102. package/templates/product-system/packages/shared/lib/completeness.ts +30 -0
  103. package/templates/product-system/packages/shared/lib/constants.ts +327 -0
  104. package/templates/product-system/packages/shared/lib/db.ts +81 -0
  105. package/templates/product-system/packages/shared/lib/git_workflow.ts +110 -0
  106. package/templates/product-system/packages/shared/lib/graph.ts +186 -0
  107. package/templates/product-system/packages/shared/lib/kanban.ts +161 -0
  108. package/templates/product-system/packages/shared/lib/markdown.ts +205 -0
  109. package/templates/product-system/packages/shared/lib/pipeline-state-store.ts +124 -0
  110. package/templates/product-system/packages/shared/lib/pipeline.ts +489 -0
  111. package/templates/product-system/packages/shared/lib/prompt_builder.ts +170 -0
  112. package/templates/product-system/packages/shared/lib/relevance_search.ts +159 -0
  113. package/templates/product-system/packages/shared/lib/session.ts +152 -0
  114. package/templates/product-system/packages/shared/lib/validator.ts +117 -0
  115. package/templates/product-system/packages/shared/lib/work_summary_parser.ts +130 -0
  116. package/templates/product-system/packages/shared/package.json +30 -0
  117. package/templates/product-system/packages/shared/scripts/assign-project.ts +52 -0
  118. package/templates/product-system/packages/shared/tools/add_edge.ts +61 -0
  119. package/templates/product-system/packages/shared/tools/add_node.ts +101 -0
  120. package/templates/product-system/packages/shared/tools/end_session.ts +87 -0
  121. package/templates/product-system/packages/shared/tools/get_gaps.ts +87 -0
  122. package/templates/product-system/packages/shared/tools/get_kanban.ts +125 -0
  123. package/templates/product-system/packages/shared/tools/get_node.ts +78 -0
  124. package/templates/product-system/packages/shared/tools/get_status.ts +98 -0
  125. package/templates/product-system/packages/shared/tools/migrate_to_turso.ts +385 -0
  126. package/templates/product-system/packages/shared/tools/move_card.ts +143 -0
  127. package/templates/product-system/packages/shared/tools/rebuild_index.ts +77 -0
  128. package/templates/product-system/packages/shared/tools/remove_edge.ts +59 -0
  129. package/templates/product-system/packages/shared/tools/remove_node.ts +96 -0
  130. package/templates/product-system/packages/shared/tools/resolve_question.ts +75 -0
  131. package/templates/product-system/packages/shared/tools/search_nodes.ts +106 -0
  132. package/templates/product-system/packages/shared/tools/start_session.ts +144 -0
  133. package/templates/product-system/packages/shared/tools/update_node.ts +133 -0
  134. package/templates/product-system/packages/shared/tsconfig.json +24 -0
  135. package/templates/product-system/pnpm-workspace.yaml +2 -0
  136. package/templates/product-system/smoke_test.ts +219 -0
  137. package/templates/product-system/tests/coherence_review.test.ts +562 -0
  138. package/templates/product-system/tests/db_sqlite_fallback.test.ts +75 -0
  139. package/templates/product-system/tests/edge_type_color_coding.test.ts +147 -0
  140. package/templates/product-system/tests/emit-tool-use-events.test.ts +85 -0
  141. package/templates/product-system/tests/feature_kind.test.ts +139 -0
  142. package/templates/product-system/tests/gap_indicators.test.ts +199 -0
  143. package/templates/product-system/tests/graceful_init.test.ts +142 -0
  144. package/templates/product-system/tests/graph_legend.test.ts +314 -0
  145. package/templates/product-system/tests/graph_settings_sheet.test.ts +804 -0
  146. package/templates/product-system/tests/hide_defined_filter.test.ts +205 -0
  147. package/templates/product-system/tests/kanban.test.ts +529 -0
  148. package/templates/product-system/tests/neighborhood_focus.test.ts +132 -0
  149. package/templates/product-system/tests/node_search.test.ts +340 -0
  150. package/templates/product-system/tests/node_sizing.test.ts +170 -0
  151. package/templates/product-system/tests/node_type_toggle_filters.test.ts +285 -0
  152. package/templates/product-system/tests/node_type_visual_encoding.test.ts +103 -0
  153. package/templates/product-system/tests/pipeline-state-store.test.ts +268 -0
  154. package/templates/product-system/tests/pipeline-unit.test.ts +593 -0
  155. package/templates/product-system/tests/pipeline.test.ts +195 -0
  156. package/templates/product-system/tests/pipeline_stats_all_cards.test.ts +193 -0
  157. package/templates/product-system/tests/play_all.test.ts +296 -0
  158. package/templates/product-system/tests/qa_issue_sheet.test.ts +464 -0
  159. package/templates/product-system/tests/relevance_search.test.ts +186 -0
  160. package/templates/product-system/tests/search_reorder.test.ts +88 -0
  161. package/templates/product-system/tests/serve_ui.test.ts +281 -0
  162. package/templates/product-system/tests/serve_ui_drizzle.test.ts +114 -0
  163. package/templates/product-system/tests/session_context_recall.test.ts +135 -0
  164. package/templates/product-system/tests/side_panel.test.ts +345 -0
  165. package/templates/product-system/tests/spec_completeness_label.test.ts +69 -0
  166. package/templates/product-system/tests/url_routing_test.ts +122 -0
  167. package/templates/product-system/tests/user_login.test.ts +150 -0
  168. package/templates/product-system/tests/user_registration.test.ts +205 -0
  169. package/templates/product-system/tests/web_terminal.test.ts +572 -0
  170. package/templates/product-system/tests/work_summary.test.ts +211 -0
  171. package/templates/product-system/tests/zoom_pan.test.ts +43 -0
  172. package/templates/product-system/tsconfig.json +24 -0
  173. package/templates/skills/product-bootstrap/SKILL.md +312 -0
  174. package/templates/skills/product-code-reviewer/SKILL.md +147 -0
  175. package/templates/skills/product-debugger/SKILL.md +206 -0
  176. package/templates/skills/product-debugger/references/agent-browser.md +1156 -0
  177. package/templates/skills/product-developer/SKILL.md +182 -0
  178. package/templates/skills/product-interview/SKILL.md +220 -0
@@ -0,0 +1,470 @@
1
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
2
+ import { apiClient } from '../api/client';
3
+ import { COLUMNS, PIPELINE_STATUS_LABELS } from '../constants/graph';
4
+ import { useToast } from '../hooks/useToast';
5
+
6
+
7
+ interface KanbanViewProps {
8
+ graphData: any;
9
+ projectId?: string | null;
10
+ onCardClick: (node: { id: string; name: string }) => void;
11
+ onIssuesClick: (featureId: string, featureName: string, column: string, notes: any[], reviews: any[]) => void;
12
+ }
13
+
14
+ interface CardData {
15
+ id: string;
16
+ name: string;
17
+ completeness: number;
18
+ kind: string;
19
+ rejectionCount: number;
20
+ notes: any[];
21
+ reviews: any[];
22
+ column: string;
23
+ devBlocked: boolean;
24
+ movedAt: string | null;
25
+ }
26
+
27
+ const ALL_COLUMNS = COLUMNS.map(c => c.id);
28
+
29
+ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }: KanbanViewProps) {
30
+ const [kanbanData, setKanbanData] = useState<any>(null);
31
+ const [error, setError] = useState<string | null>(null);
32
+ const [draggedCard, setDraggedCard] = useState<string | null>(null);
33
+ const [dragOverColumn, setDragOverColumn] = useState<string | null>(null);
34
+ const [pipelineStatuses, setPipelineStatuses] = useState<Record<string, any>>({});
35
+ const [playAllRunning, setPlayAllRunning] = useState(false);
36
+ const [playAllCurrentFeature, setPlayAllCurrentFeature] = useState<string | null>(null);
37
+ const [copiedId, setCopiedId] = useState<string | null>(null);
38
+ const { showToast } = useToast();
39
+
40
+ const pollersRef = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map());
41
+ const playAllAbortedRef = useRef(false);
42
+ const kanbanDataRef = useRef(kanbanData);
43
+ kanbanDataRef.current = kanbanData;
44
+
45
+ const fetchKanban = useCallback(async () => {
46
+ try {
47
+ const data = await apiClient.fetchKanban(projectId ?? undefined);
48
+ setKanbanData(data);
49
+ setError(null);
50
+ return data;
51
+ } catch {
52
+ setError('Failed to load kanban data.');
53
+ return null;
54
+ }
55
+ }, [projectId]);
56
+
57
+ useEffect(() => {
58
+ fetchKanban();
59
+ return () => {
60
+ // Cleanup all pollers
61
+ for (const intervalId of pollersRef.current.values()) {
62
+ clearInterval(intervalId);
63
+ }
64
+ pollersRef.current.clear();
65
+ };
66
+ }, [fetchKanban]);
67
+
68
+ const getCardsForColumn = useCallback((columnId: string): CardData[] => {
69
+ if (!kanbanData || !graphData) return [];
70
+ const featureNodes = new Map<string, any>();
71
+ graphData.nodes
72
+ .filter((n: any) => n.type === 'feature')
73
+ .forEach((n: any) => featureNodes.set(n.id, n));
74
+
75
+ const cards: CardData[] = Object.entries(kanbanData)
76
+ .filter(([id, entry]: [string, any]) => entry.column === columnId && featureNodes.has(id))
77
+ .map(([id, entry]: [string, any]) => ({
78
+ id,
79
+ name: featureNodes.get(id).name,
80
+ completeness: featureNodes.get(id).completeness,
81
+ kind: featureNodes.get(id).kind || 'new',
82
+ rejectionCount: entry.rejection_count || 0,
83
+ notes: entry.notes || [],
84
+ reviews: entry.reviews || [],
85
+ column: entry.column,
86
+ devBlocked: entry.dev_blocked || false,
87
+ movedAt: entry.moved_at || null,
88
+ }));
89
+
90
+ if (columnId === 'todo') {
91
+ cards.sort((a, b) => (b.completeness || 0) - (a.completeness || 0));
92
+ } else if (columnId === 'done') {
93
+ cards.sort((a, b) => (b.movedAt || '').localeCompare(a.movedAt || ''));
94
+ } else {
95
+ cards.sort((a, b) => (a.movedAt || '').localeCompare(b.movedAt || ''));
96
+ }
97
+
98
+ return cards;
99
+ }, [kanbanData, graphData]);
100
+
101
+ const getPipelineBadgeText = (status: string, rejectionCount: number) => {
102
+ const label = (PIPELINE_STATUS_LABELS as any)[status] || status;
103
+ if (rejectionCount >= 1) return `Retry ${rejectionCount + 1}`;
104
+ return label;
105
+ };
106
+
107
+ const startPipelinePolling = useCallback((featureId: string) => {
108
+ if (pollersRef.current.has(featureId)) return;
109
+ const intervalId = setInterval(async () => {
110
+ try {
111
+ const status = await apiClient.getPipelineStatus(featureId);
112
+ setPipelineStatuses(prev => ({ ...prev, [featureId]: status }));
113
+ if (['idle', 'completed', 'blocked', 'failed', 'interrupted'].includes(status.status)) {
114
+ clearInterval(pollersRef.current.get(featureId)!);
115
+ pollersRef.current.delete(featureId);
116
+ await fetchKanban();
117
+ }
118
+ } catch {
119
+ clearInterval(pollersRef.current.get(featureId)!);
120
+ pollersRef.current.delete(featureId);
121
+ }
122
+ }, 5000);
123
+ pollersRef.current.set(featureId, intervalId);
124
+ }, [fetchKanban]);
125
+
126
+ // Fetch initial pipeline statuses for all cards
127
+ useEffect(() => {
128
+ if (!kanbanData || !graphData) return;
129
+ const featureIds = Object.keys(kanbanData);
130
+ featureIds.forEach(async (id) => {
131
+ try {
132
+ const status = await apiClient.getPipelineStatus(id);
133
+ if (status.status !== 'idle') {
134
+ setPipelineStatuses(prev => ({ ...prev, [id]: status }));
135
+ if (!['completed', 'failed', 'blocked', 'interrupted'].includes(status.status)) {
136
+ startPipelinePolling(id);
137
+ }
138
+ }
139
+ } catch { /* ignore */ }
140
+ });
141
+ }, [kanbanData, graphData, startPipelinePolling]);
142
+
143
+ const checkDependencies = useCallback((featureId: string) => {
144
+ if (!graphData || !kanbanData) return { blocked: false, blockedBy: [] as string[] };
145
+ const deps = graphData.edges
146
+ .filter((e: any) => e.from === featureId && e.relation === 'depends_on')
147
+ .map((e: any) => e.to)
148
+ .filter((depId: string) => graphData.nodes.some((n: any) => n.id === depId && n.type === 'feature'));
149
+ const blockedBy = deps.filter((depId: string) => {
150
+ const entry = kanbanData[depId];
151
+ return !entry || entry.column !== 'done';
152
+ });
153
+ return { blocked: blockedBy.length > 0, blockedBy };
154
+ }, [graphData, kanbanData]);
155
+
156
+ // Drag handlers
157
+ const handleDragStart = (e: React.DragEvent, featureId: string) => {
158
+ setDraggedCard(featureId);
159
+ e.dataTransfer.effectAllowed = 'move';
160
+ e.dataTransfer.setData('text/plain', featureId);
161
+ (e.target as HTMLElement).classList.add('dragging');
162
+ };
163
+
164
+ const handleDragEnd = (e: React.DragEvent) => {
165
+ (e.target as HTMLElement).classList.remove('dragging');
166
+ setDraggedCard(null);
167
+ setDragOverColumn(null);
168
+ };
169
+
170
+ const handleDragOver = (e: React.DragEvent, columnId: string) => {
171
+ if (!draggedCard) return;
172
+ const sourceColumn = kanbanData?.[draggedCard]?.column;
173
+ if (columnId !== sourceColumn) {
174
+ e.preventDefault();
175
+ setDragOverColumn(columnId);
176
+ }
177
+ };
178
+
179
+ const handleDragLeave = () => {
180
+ setDragOverColumn(null);
181
+ };
182
+
183
+ const handleDrop = async (e: React.DragEvent, targetColumn: string) => {
184
+ e.preventDefault();
185
+ setDragOverColumn(null);
186
+ const featureId = e.dataTransfer.getData('text/plain');
187
+ if (!featureId || !targetColumn) return;
188
+ try {
189
+ await apiClient.moveCard(featureId, targetColumn);
190
+ await fetchKanban();
191
+ } catch (err) {
192
+ console.error('Failed to move card:', err);
193
+ }
194
+ };
195
+
196
+ const handleStartPipeline = async (featureId: string) => {
197
+ try {
198
+ await apiClient.startPipeline(featureId);
199
+ startPipelinePolling(featureId);
200
+ await fetchKanban();
201
+ } catch (err: any) {
202
+ console.error('Failed to start pipeline:', err);
203
+ showToast(err?.message || 'Failed to start pipeline');
204
+ }
205
+ };
206
+
207
+ const handleUnblock = async (featureId: string) => {
208
+ try {
209
+ await apiClient.unblockCard(featureId);
210
+ await fetchKanban();
211
+ } catch (err) {
212
+ console.error('Failed to unblock card:', err);
213
+ }
214
+ };
215
+
216
+ const handleCopy = async (card: CardData) => {
217
+ const text = `${card.id} ${card.name}`;
218
+ try {
219
+ await navigator.clipboard.writeText(text);
220
+ setCopiedId(card.id);
221
+ setTimeout(() => setCopiedId(null), 1500);
222
+ } catch { /* ignore */ }
223
+ };
224
+
225
+ // Play All
226
+ const startPlayAll = async () => {
227
+ if (playAllRunning) return;
228
+ setPlayAllRunning(true);
229
+ playAllAbortedRef.current = false;
230
+ setPlayAllCurrentFeature(null);
231
+
232
+ try {
233
+ await playAllLoop();
234
+ } finally {
235
+ setPlayAllRunning(false);
236
+ setPlayAllCurrentFeature(null);
237
+ }
238
+ };
239
+
240
+ const stopPlayAll = () => {
241
+ playAllAbortedRef.current = true;
242
+ };
243
+
244
+ const playAllLoop = async () => {
245
+ while (!playAllAbortedRef.current) {
246
+ let freshKanban: any;
247
+ let freshGraph: any;
248
+ try {
249
+ freshKanban = await apiClient.fetchKanban(projectId ?? undefined);
250
+ freshGraph = await apiClient.fetchGraph(projectId ?? undefined);
251
+ setKanbanData(freshKanban);
252
+ } catch { break; }
253
+
254
+ const featureNodes = new Map<string, any>();
255
+ freshGraph.nodes
256
+ .filter((n: any) => n.type === 'feature')
257
+ .forEach((n: any) => featureNodes.set(n.id, n));
258
+
259
+ const todoCards = Object.entries(freshKanban)
260
+ .filter(([id, entry]: [string, any]) => entry.column === 'todo' && featureNodes.has(id) && !entry.dev_blocked)
261
+ .map(([id, entry]: [string, any]) => ({
262
+ id,
263
+ completeness: featureNodes.get(id).completeness || 0,
264
+ }))
265
+ .sort((a, b) => b.completeness - a.completeness);
266
+
267
+ if (todoCards.length === 0) break;
268
+
269
+ let processed = false;
270
+ for (const card of todoCards) {
271
+ if (playAllAbortedRef.current) return;
272
+
273
+ // Check deps
274
+ const deps = freshGraph.edges
275
+ .filter((e: any) => e.from === card.id && e.relation === 'depends_on')
276
+ .map((e: any) => e.to)
277
+ .filter((depId: string) => freshGraph.nodes.some((n: any) => n.id === depId && n.type === 'feature'));
278
+ const blocked = deps.some((depId: string) => !freshKanban[depId] || freshKanban[depId].column !== 'done');
279
+ if (blocked) continue;
280
+
281
+ setPlayAllCurrentFeature(card.id);
282
+
283
+ try {
284
+ await apiClient.startPipeline(card.id);
285
+ } catch (err: any) {
286
+ console.error(`Play All: failed to start pipeline for ${card.id}:`, err);
287
+ showToast(err?.message || `Failed to start pipeline for ${card.id}`);
288
+ break;
289
+ }
290
+
291
+ // Wait for pipeline completion
292
+ await new Promise<void>((resolve) => {
293
+ const poll = async () => {
294
+ if (playAllAbortedRef.current) { resolve(); return; }
295
+ try {
296
+ const status = await apiClient.getPipelineStatus(card.id);
297
+ setPipelineStatuses(prev => ({ ...prev, [card.id]: status }));
298
+ if (['idle', 'completed', 'blocked', 'failed', 'interrupted'].includes(status.status)) {
299
+ resolve();
300
+ return;
301
+ }
302
+ } catch { resolve(); return; }
303
+ setTimeout(poll, 5000);
304
+ };
305
+ poll();
306
+ });
307
+
308
+ if (playAllAbortedRef.current) return;
309
+ processed = true;
310
+ break;
311
+ }
312
+
313
+ if (!processed) break;
314
+ }
315
+ };
316
+
317
+ if (error) return <div className="error-msg">{error}</div>;
318
+ if (!kanbanData) return <div className="kanban-loading">Loading...</div>;
319
+
320
+ return (
321
+ <div className="kanban-board">
322
+ {COLUMNS.map(col => {
323
+ const cards = getCardsForColumn(col.id);
324
+ const sourceColumn = draggedCard ? kanbanData?.[draggedCard]?.column : null;
325
+ const isDropTarget = draggedCard && col.id !== sourceColumn;
326
+ const isDragOver = dragOverColumn === col.id;
327
+
328
+ return (
329
+ <div key={col.id} className="kanban-column" data-column={col.id}>
330
+ <div className="kanban-column-header">
331
+ <div className="kanban-column-header-left">
332
+ <span className="kanban-column-title">{col.label}</span>
333
+ <span className="kanban-column-count">{cards.length}</span>
334
+ </div>
335
+ {col.id === 'todo' && (
336
+ playAllRunning ? (
337
+ <button className="kanban-play-all-btn stop" title="Stop processing" onClick={stopPlayAll}>
338
+ {'\u25A0'}
339
+ </button>
340
+ ) : (
341
+ <button className="kanban-play-all-btn" title="Start automated development for all TODO features" onClick={startPlayAll}>
342
+ {'\u25B6\u25B6'}
343
+ </button>
344
+ )
345
+ )}
346
+ </div>
347
+ <div
348
+ className={`kanban-column-body${isDropTarget ? ' drop-target' : ''}${isDragOver ? ' drag-over' : ''}`}
349
+ data-column={col.id}
350
+ onDragOver={(e) => handleDragOver(e, col.id)}
351
+ onDragLeave={handleDragLeave}
352
+ onDrop={(e) => handleDrop(e, col.id)}
353
+ >
354
+ {cards.map(card => {
355
+ const pct = Math.round((card.completeness || 0) * 100);
356
+ const pStatus = pipelineStatuses[card.id];
357
+ const isActive = pStatus && !['idle', 'completed', 'blocked', 'failed', 'interrupted'].includes(pStatus.status);
358
+ const isTerminal = pStatus && ['completed', 'failed', 'blocked', 'interrupted'].includes(pStatus.status);
359
+
360
+ return (
361
+ <div
362
+ key={card.id}
363
+ className={`kanban-card${card.rejectionCount >= 3 ? ' problematic' : ''}${card.devBlocked ? ' dev-blocked' : ''}${playAllCurrentFeature === card.id ? ' play-all-active' : ''}`}
364
+ data-feature-id={card.id}
365
+ draggable
366
+ onDragStart={(e) => handleDragStart(e, card.id)}
367
+ onDragEnd={handleDragEnd}
368
+ onClick={(e) => {
369
+ if ((e.target as HTMLElement).closest('button, textarea, .kanban-note-form')) return;
370
+ onCardClick({ id: card.id, name: card.name });
371
+ }}
372
+ style={{ cursor: 'pointer' }}
373
+ >
374
+ <div className="kanban-card-header">
375
+ <span className="kanban-card-id">
376
+ {card.id}
377
+ {card.kind && card.kind !== 'new' && (
378
+ <> <span className={`kanban-card-kind kind-${card.kind}`}>{card.kind}</span></>
379
+ )}
380
+ </span>
381
+ <button
382
+ className={`kanban-copy-btn${copiedId === card.id ? ' copied' : ''}`}
383
+ title="Copy feature ID and name"
384
+ onClick={(e) => { e.stopPropagation(); handleCopy(card); }}
385
+ >
386
+ {copiedId === card.id ? '\u2713' : '\uD83D\uDCCB'}
387
+ </button>
388
+ <span className="kanban-card-header-right">
389
+ {card.rejectionCount > 0 && (
390
+ <span className={`kanban-card-rejections${card.rejectionCount >= 3 ? ' high' : ''}`}>
391
+ {card.rejectionCount}x rejected
392
+ </span>
393
+ )}
394
+ {card.column === 'todo' && !card.devBlocked && (
395
+ <button
396
+ className="kanban-play-btn"
397
+ title="Start automated development pipeline"
398
+ onClick={(e) => { e.stopPropagation(); handleStartPipeline(card.id); }}
399
+ >
400
+ {'\u25B6'}
401
+ </button>
402
+ )}
403
+ {card.devBlocked && (
404
+ <span className="kanban-blocked-badge">Blocked</span>
405
+ )}
406
+ </span>
407
+ </div>
408
+
409
+ <div className="kanban-card-name">{card.name}</div>
410
+
411
+ <div className="kanban-card-completeness-row">
412
+ <span className="kanban-card-completeness-prefix">Spec</span>
413
+ <div className="kanban-card-completeness">
414
+ <div className="kanban-card-completeness-fill" style={{ width: `${pct}%` }} />
415
+ </div>
416
+ <span className="kanban-card-completeness-label">{pct}%</span>
417
+ </div>
418
+
419
+ {pStatus && pStatus.status !== 'idle' && (
420
+ <div className={`kanban-pipeline-status${isActive ? ' pipeline-active' : ''}${isTerminal ? ` pipeline-${pStatus.status}` : ''}`}>
421
+ {getPipelineBadgeText(pStatus.status, card.rejectionCount)}
422
+ </div>
423
+ )}
424
+
425
+ {pStatus?.toolCalls?.total > 0 && (
426
+ <div className="kanban-tool-calls" style={{ display: 'flex' }}>
427
+ {[
428
+ { label: 'Write', count: pStatus.toolCalls.write },
429
+ { label: 'Edit', count: pStatus.toolCalls.edit },
430
+ { label: 'Read', count: pStatus.toolCalls.read },
431
+ { label: 'Bash', count: pStatus.toolCalls.bash },
432
+ ].filter(i => i.count > 0).map(i => (
433
+ <span key={i.label} className="kanban-tool-badge">{i.label}: {i.count}</span>
434
+ ))}
435
+ </div>
436
+ )}
437
+
438
+ {card.devBlocked && (
439
+ <button
440
+ className="kanban-unblock-btn"
441
+ onClick={(e) => { e.stopPropagation(); handleUnblock(card.id); }}
442
+ >
443
+ Unblock
444
+ </button>
445
+ )}
446
+
447
+ <button
448
+ className={`kanban-issues-btn${card.notes.length > 0 ? ' has-issues' : ''}`}
449
+ onClick={(e) => {
450
+ e.stopPropagation();
451
+ onIssuesClick(card.id, card.name, card.column, card.notes, card.reviews);
452
+ }}
453
+ >
454
+ {card.notes.length > 0
455
+ ? `${card.notes.length} issue${card.notes.length !== 1 ? 's' : ''} reported`
456
+ : card.column === 'qa'
457
+ ? '+ Report Issue'
458
+ : 'No issues'}
459
+ </button>
460
+ </div>
461
+ );
462
+ })}
463
+ </div>
464
+ </div>
465
+ );
466
+ })}
467
+
468
+ </div>
469
+ );
470
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Login Page — email/password form for unauthenticated users.
3
+ * Calls POST /api/auth/login, redirects to main app on success.
4
+ * Minimal developer tool aesthetic, dark/light theme support.
5
+ */
6
+
7
+ import React, { useState, useCallback } from 'react';
8
+ import { Link } from 'react-router-dom';
9
+ import { useTheme } from '../hooks/useTheme';
10
+ import { validateLoginForm, parseLoginResponse } from '../utils/login_validation';
11
+
12
+ interface LoginPageProps {
13
+ onLoginSuccess: () => void;
14
+ }
15
+
16
+ export function LoginPage({ onLoginSuccess }: LoginPageProps) {
17
+ const { theme, toggleTheme } = useTheme();
18
+ const [email, setEmail] = useState('');
19
+ const [password, setPassword] = useState('');
20
+ const [error, setError] = useState('');
21
+ const [submitting, setSubmitting] = useState(false);
22
+
23
+ const handleSubmit = useCallback(async (e: React.FormEvent) => {
24
+ e.preventDefault();
25
+ setError('');
26
+
27
+ const validationError = validateLoginForm(email, password);
28
+ if (validationError) {
29
+ setError(validationError);
30
+ return;
31
+ }
32
+
33
+ setSubmitting(true);
34
+ try {
35
+ const resp = await fetch('/api/auth/login', {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ credentials: 'same-origin',
39
+ body: JSON.stringify({ email: email.trim(), password }),
40
+ });
41
+
42
+ const result = await parseLoginResponse(resp);
43
+ if ('error' in result) {
44
+ setError(result.error);
45
+ return;
46
+ }
47
+
48
+ onLoginSuccess();
49
+ } catch {
50
+ setError('Unable to connect to server');
51
+ } finally {
52
+ setSubmitting(false);
53
+ }
54
+ }, [email, password, onLoginSuccess]);
55
+
56
+ return (
57
+ <div className="login-page">
58
+ <div className="login-card">
59
+ <div className="login-header">
60
+ <span className="login-title">login</span>
61
+ <button
62
+ className="login-theme-toggle"
63
+ onClick={toggleTheme}
64
+ title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
65
+ >
66
+ {theme === 'dark' ? '☀' : '☾'}
67
+ </button>
68
+ </div>
69
+
70
+ <form className="login-form" onSubmit={handleSubmit}>
71
+ <label className="login-label" htmlFor="login-email">email</label>
72
+ <input
73
+ id="login-email"
74
+ className="login-input"
75
+ type="email"
76
+ value={email}
77
+ onChange={e => setEmail(e.target.value)}
78
+ placeholder="user@example.com"
79
+ autoComplete="email"
80
+ autoFocus
81
+ disabled={submitting}
82
+ />
83
+
84
+ <label className="login-label" htmlFor="login-password">password</label>
85
+ <input
86
+ id="login-password"
87
+ className="login-input"
88
+ type="password"
89
+ value={password}
90
+ onChange={e => setPassword(e.target.value)}
91
+ placeholder="••••••••"
92
+ autoComplete="current-password"
93
+ disabled={submitting}
94
+ />
95
+
96
+ {error && <div className="login-error">{error}</div>}
97
+
98
+ <button
99
+ className="login-submit"
100
+ type="submit"
101
+ disabled={submitting}
102
+ >
103
+ {submitting ? 'signing in...' : 'sign in'}
104
+ </button>
105
+ </form>
106
+
107
+ <p className="login-register-link">
108
+ <Link to="/forgot-password">Forgot password?</Link>
109
+ </p>
110
+ <p className="login-register-link">
111
+ No account yet? <Link to="/register">Register</Link>
112
+ </p>
113
+ </div>
114
+ </div>
115
+ );
116
+ }