@hustle-together/api-dev-tools 3.12.3 → 4.5.1
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/.claude/adr-requests/.gitkeep +10 -0
- package/.claude/agents/adr-researcher.md +109 -0
- package/.claude/agents/visual-analyzer.md +183 -0
- package/.claude/api-dev-state.json +7 -463
- package/.claude/documentation-audit.json +114 -0
- package/.claude/registry.json +289 -0
- package/.claude/settings.json +45 -1
- package/.claude/workflow-logs/None.json +49 -0
- package/.claude/workflow-logs/session-20251230-143727.json +106 -0
- package/.skills/adr-deep-research/SKILL.md +351 -0
- package/.skills/api-create/SKILL.md +116 -17
- package/.skills/api-research/SKILL.md +130 -0
- package/.skills/docs-sync/SKILL.md +260 -0
- package/.skills/docs-update/SKILL.md +205 -0
- package/.skills/hustle-brand/SKILL.md +368 -0
- package/.skills/hustle-build/SKILL.md +786 -0
- package/.skills/hustle-build-review/SKILL.md +518 -0
- package/.skills/parallel-spawn/SKILL.md +212 -0
- package/.skills/ralph-continue/SKILL.md +151 -0
- package/.skills/ralph-loop/SKILL.md +341 -0
- package/.skills/ralph-status/SKILL.md +87 -0
- package/.skills/refactor/SKILL.md +59 -0
- package/.skills/shadcn/SKILL.md +522 -0
- package/.skills/test-all/SKILL.md +210 -0
- package/.skills/test-builds/SKILL.md +208 -0
- package/.skills/test-debug/SKILL.md +212 -0
- package/.skills/test-e2e/SKILL.md +168 -0
- package/.skills/test-review/SKILL.md +707 -0
- package/.skills/test-unit/SKILL.md +143 -0
- package/.skills/test-visual/SKILL.md +301 -0
- package/.skills/token-report/SKILL.md +132 -0
- package/CHANGELOG.md +575 -0
- package/README.md +426 -56
- package/bin/cli.js +1538 -88
- package/commands/hustle-api-create.md +22 -0
- package/commands/hustle-build.md +259 -0
- package/commands/hustle-combine.md +81 -2
- package/commands/hustle-ui-create-page.md +84 -2
- package/commands/hustle-ui-create.md +82 -2
- package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
- package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
- package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
- package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
- package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
- package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
- package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
- package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
- package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
- package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
- package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
- package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
- package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
- package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
- package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
- package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
- package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
- package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
- package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
- package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
- package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
- package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
- package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
- package/hooks/api-workflow-check.py +34 -0
- package/hooks/auto-answer.py +305 -0
- package/hooks/check-update.py +132 -0
- package/hooks/completion-promise-detector.py +293 -0
- package/hooks/context-capacity-warning.py +171 -0
- package/hooks/docs-update-check.py +120 -0
- package/hooks/enforce-dry-run.py +134 -0
- package/hooks/enforce-external-research.py +25 -0
- package/hooks/enforce-interview.py +20 -0
- package/hooks/generate-adr-options.py +282 -0
- package/hooks/hook_utils.py +609 -0
- package/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
- package/hooks/ntfy-on-question.py +240 -0
- package/hooks/orchestrator-completion.py +313 -0
- package/hooks/orchestrator-handoff.py +267 -0
- package/hooks/orchestrator-session-startup.py +146 -0
- package/hooks/parallel-orchestrator.py +451 -0
- package/hooks/periodic-reground.py +270 -67
- package/hooks/project-document-prompt.py +302 -0
- package/hooks/remote-question-proxy.py +284 -0
- package/hooks/remote-question-server.py +1224 -0
- package/hooks/run-code-review.py +176 -29
- package/hooks/run-visual-qa.py +338 -0
- package/hooks/session-logger.py +27 -1
- package/hooks/session-startup.py +113 -0
- package/hooks/update-adr-decision.py +236 -0
- package/hooks/update-api-showcase.py +13 -1
- package/hooks/update-testing-checklist.py +195 -0
- package/hooks/update-ui-showcase.py +13 -1
- package/package.json +7 -3
- package/scripts/extract-schema-docs.cjs +322 -0
- package/templates/.skills/hustle-interview/SKILL.md +174 -0
- package/templates/CLAUDE-SECTION.md +89 -64
- package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
- package/templates/api-dev-state.json +33 -1
- package/templates/api-showcase/_components/APIModal.tsx +100 -8
- package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
- package/templates/api-showcase/_components/APITester.tsx +367 -58
- package/templates/brand-page/page.tsx +645 -0
- package/templates/component/Component.visual.spec.ts +30 -24
- package/templates/docs/page.tsx +230 -0
- package/templates/eslint-plugin-zod-schema/index.js +446 -0
- package/templates/eslint-plugin-zod-schema/package.json +26 -0
- package/templates/github-workflows/security.yml +274 -0
- package/templates/hustle-build-defaults.json +136 -0
- package/templates/hustle-dev-dashboard/page.tsx +365 -0
- package/templates/page/page.e2e.test.ts +30 -26
- package/templates/performance-budgets.json +63 -5
- package/templates/playwright-report/page.tsx +258 -0
- package/templates/registry.json +279 -3
- package/templates/review-dashboard/page.tsx +510 -0
- package/templates/settings.json +155 -7
- package/templates/test-results/page.tsx +237 -0
- package/templates/typedoc.json +19 -0
- package/templates/ui-showcase/_components/UIShowcase.tsx +48 -1
- package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
- package/templates/ui-showcase/page.tsx +1 -1
|
@@ -0,0 +1,1224 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Remote Question Server with Build Dashboard
|
|
4
|
+
|
|
5
|
+
A comprehensive HTTP server that displays:
|
|
6
|
+
- Build progress and phase status
|
|
7
|
+
- Current question interface
|
|
8
|
+
- Build queue with dependencies
|
|
9
|
+
- Recent activity log
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
python remote-question-server.py [port]
|
|
13
|
+
|
|
14
|
+
Access:
|
|
15
|
+
- Local: http://localhost:8765
|
|
16
|
+
- Same network (phone/tablet): http://<your-computer-ip>:8765
|
|
17
|
+
|
|
18
|
+
Environment:
|
|
19
|
+
REMOTE_QUESTIONS_PORT - Port to run on (default: 8765)
|
|
20
|
+
CLAUDE_PROJECT_DIR - Project directory to monitor (default: .)
|
|
21
|
+
|
|
22
|
+
Version: 4.6.1
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from datetime import datetime
|
|
31
|
+
from urllib.parse import parse_qs
|
|
32
|
+
import threading
|
|
33
|
+
import time
|
|
34
|
+
|
|
35
|
+
DEFAULT_PORT = 8765
|
|
36
|
+
QUESTION_FILE = ".claude/current-question.json"
|
|
37
|
+
ANSWER_FILE = ".claude/pending-answer.json"
|
|
38
|
+
STATE_FILE = ".claude/hustle-build-state.json"
|
|
39
|
+
ACTIVITY_LOG_FILE = ".claude/workflow-logs/activity.json"
|
|
40
|
+
|
|
41
|
+
# Full dashboard HTML template
|
|
42
|
+
HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
43
|
+
<html lang="en">
|
|
44
|
+
<head>
|
|
45
|
+
<meta charset="UTF-8">
|
|
46
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
47
|
+
<title>Hustle Build Dashboard</title>
|
|
48
|
+
<style>
|
|
49
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
50
|
+
body {
|
|
51
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
52
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
53
|
+
min-height: 100vh;
|
|
54
|
+
color: #e0e0e0;
|
|
55
|
+
padding: 15px;
|
|
56
|
+
}
|
|
57
|
+
.container {
|
|
58
|
+
max-width: 900px;
|
|
59
|
+
margin: 0 auto;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Header */
|
|
63
|
+
.header {
|
|
64
|
+
background: rgba(255,255,255,0.05);
|
|
65
|
+
border-radius: 12px;
|
|
66
|
+
padding: 20px;
|
|
67
|
+
margin-bottom: 15px;
|
|
68
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
69
|
+
}
|
|
70
|
+
.header-top {
|
|
71
|
+
display: flex;
|
|
72
|
+
justify-content: space-between;
|
|
73
|
+
align-items: flex-start;
|
|
74
|
+
flex-wrap: wrap;
|
|
75
|
+
gap: 10px;
|
|
76
|
+
}
|
|
77
|
+
.header h1 {
|
|
78
|
+
color: #00d4ff;
|
|
79
|
+
font-size: 1.4rem;
|
|
80
|
+
margin-bottom: 5px;
|
|
81
|
+
}
|
|
82
|
+
.header .subtitle {
|
|
83
|
+
color: #888;
|
|
84
|
+
font-size: 0.85rem;
|
|
85
|
+
}
|
|
86
|
+
.build-status {
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
gap: 8px;
|
|
90
|
+
padding: 8px 16px;
|
|
91
|
+
border-radius: 20px;
|
|
92
|
+
font-size: 0.85rem;
|
|
93
|
+
font-weight: 600;
|
|
94
|
+
}
|
|
95
|
+
.build-status.in_progress {
|
|
96
|
+
background: rgba(0,212,255,0.2);
|
|
97
|
+
color: #00d4ff;
|
|
98
|
+
}
|
|
99
|
+
.build-status.complete {
|
|
100
|
+
background: rgba(0,255,128,0.2);
|
|
101
|
+
color: #00ff80;
|
|
102
|
+
}
|
|
103
|
+
.build-status.pending {
|
|
104
|
+
background: rgba(255,204,0,0.2);
|
|
105
|
+
color: #ffcc00;
|
|
106
|
+
}
|
|
107
|
+
.build-status.no-build {
|
|
108
|
+
background: rgba(255,255,255,0.1);
|
|
109
|
+
color: #888;
|
|
110
|
+
}
|
|
111
|
+
.build-meta {
|
|
112
|
+
display: flex;
|
|
113
|
+
gap: 20px;
|
|
114
|
+
margin-top: 15px;
|
|
115
|
+
flex-wrap: wrap;
|
|
116
|
+
}
|
|
117
|
+
.build-meta-item {
|
|
118
|
+
font-size: 0.8rem;
|
|
119
|
+
}
|
|
120
|
+
.build-meta-item .label {
|
|
121
|
+
color: #666;
|
|
122
|
+
}
|
|
123
|
+
.build-meta-item .value {
|
|
124
|
+
color: #aaa;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Phase Progress */
|
|
128
|
+
.card {
|
|
129
|
+
background: rgba(255,255,255,0.05);
|
|
130
|
+
border-radius: 12px;
|
|
131
|
+
padding: 20px;
|
|
132
|
+
margin-bottom: 15px;
|
|
133
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
134
|
+
}
|
|
135
|
+
.card-title {
|
|
136
|
+
color: #888;
|
|
137
|
+
font-size: 0.8rem;
|
|
138
|
+
text-transform: uppercase;
|
|
139
|
+
letter-spacing: 1px;
|
|
140
|
+
margin-bottom: 15px;
|
|
141
|
+
}
|
|
142
|
+
.phases {
|
|
143
|
+
display: flex;
|
|
144
|
+
flex-direction: column;
|
|
145
|
+
gap: 6px;
|
|
146
|
+
}
|
|
147
|
+
.phase-item {
|
|
148
|
+
display: flex;
|
|
149
|
+
align-items: center;
|
|
150
|
+
gap: 10px;
|
|
151
|
+
padding: 8px 12px;
|
|
152
|
+
background: rgba(255,255,255,0.03);
|
|
153
|
+
border-radius: 6px;
|
|
154
|
+
font-size: 0.9rem;
|
|
155
|
+
}
|
|
156
|
+
.phase-icon {
|
|
157
|
+
width: 20px;
|
|
158
|
+
height: 20px;
|
|
159
|
+
border-radius: 50%;
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
justify-content: center;
|
|
163
|
+
font-size: 0.7rem;
|
|
164
|
+
flex-shrink: 0;
|
|
165
|
+
}
|
|
166
|
+
.phase-icon.completed {
|
|
167
|
+
background: #00ff80;
|
|
168
|
+
color: #1a1a2e;
|
|
169
|
+
}
|
|
170
|
+
.phase-icon.in_progress {
|
|
171
|
+
background: #00d4ff;
|
|
172
|
+
color: #1a1a2e;
|
|
173
|
+
animation: pulse 1.5s infinite;
|
|
174
|
+
}
|
|
175
|
+
.phase-icon.pending {
|
|
176
|
+
background: rgba(255,255,255,0.1);
|
|
177
|
+
color: #666;
|
|
178
|
+
}
|
|
179
|
+
@keyframes pulse {
|
|
180
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
181
|
+
50% { opacity: 0.7; transform: scale(0.95); }
|
|
182
|
+
}
|
|
183
|
+
.phase-name {
|
|
184
|
+
flex: 1;
|
|
185
|
+
}
|
|
186
|
+
.phase-name.active {
|
|
187
|
+
color: #00d4ff;
|
|
188
|
+
font-weight: 600;
|
|
189
|
+
}
|
|
190
|
+
.phase-name.completed {
|
|
191
|
+
color: #00ff80;
|
|
192
|
+
}
|
|
193
|
+
.phase-name.pending {
|
|
194
|
+
color: #666;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Question Card */
|
|
198
|
+
.question-card {
|
|
199
|
+
background: linear-gradient(135deg, rgba(0,212,255,0.1) 0%, rgba(0,168,204,0.05) 100%);
|
|
200
|
+
border-color: rgba(0,212,255,0.3);
|
|
201
|
+
}
|
|
202
|
+
.phase-badge {
|
|
203
|
+
background: #00d4ff;
|
|
204
|
+
color: #1a1a2e;
|
|
205
|
+
padding: 4px 12px;
|
|
206
|
+
border-radius: 20px;
|
|
207
|
+
font-size: 0.75rem;
|
|
208
|
+
font-weight: 600;
|
|
209
|
+
display: inline-block;
|
|
210
|
+
margin-bottom: 12px;
|
|
211
|
+
}
|
|
212
|
+
.question-text {
|
|
213
|
+
font-size: 1.05rem;
|
|
214
|
+
line-height: 1.5;
|
|
215
|
+
margin-bottom: 15px;
|
|
216
|
+
}
|
|
217
|
+
.options {
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-direction: column;
|
|
220
|
+
gap: 8px;
|
|
221
|
+
}
|
|
222
|
+
.option {
|
|
223
|
+
background: rgba(255,255,255,0.08);
|
|
224
|
+
border: 2px solid transparent;
|
|
225
|
+
border-radius: 8px;
|
|
226
|
+
padding: 12px 15px;
|
|
227
|
+
cursor: pointer;
|
|
228
|
+
transition: all 0.2s;
|
|
229
|
+
}
|
|
230
|
+
.option:hover {
|
|
231
|
+
background: rgba(0,212,255,0.1);
|
|
232
|
+
border-color: #00d4ff;
|
|
233
|
+
}
|
|
234
|
+
.option.selected {
|
|
235
|
+
background: rgba(0,212,255,0.2);
|
|
236
|
+
border-color: #00d4ff;
|
|
237
|
+
}
|
|
238
|
+
.option-label {
|
|
239
|
+
font-weight: 600;
|
|
240
|
+
font-size: 0.95rem;
|
|
241
|
+
}
|
|
242
|
+
.option-description {
|
|
243
|
+
font-size: 0.85rem;
|
|
244
|
+
color: #aaa;
|
|
245
|
+
margin-top: 3px;
|
|
246
|
+
}
|
|
247
|
+
.custom-input {
|
|
248
|
+
display: none;
|
|
249
|
+
margin-top: 12px;
|
|
250
|
+
}
|
|
251
|
+
.custom-input.visible {
|
|
252
|
+
display: block;
|
|
253
|
+
}
|
|
254
|
+
.custom-input textarea {
|
|
255
|
+
width: 100%;
|
|
256
|
+
padding: 12px;
|
|
257
|
+
background: rgba(255,255,255,0.08);
|
|
258
|
+
border: 1px solid rgba(255,255,255,0.2);
|
|
259
|
+
border-radius: 8px;
|
|
260
|
+
color: #e0e0e0;
|
|
261
|
+
font-family: inherit;
|
|
262
|
+
font-size: 0.95rem;
|
|
263
|
+
resize: vertical;
|
|
264
|
+
min-height: 70px;
|
|
265
|
+
}
|
|
266
|
+
.custom-input textarea:focus {
|
|
267
|
+
outline: none;
|
|
268
|
+
border-color: #00d4ff;
|
|
269
|
+
}
|
|
270
|
+
.submit-btn {
|
|
271
|
+
width: 100%;
|
|
272
|
+
background: linear-gradient(135deg, #00d4ff 0%, #00a8cc 100%);
|
|
273
|
+
color: #1a1a2e;
|
|
274
|
+
border: none;
|
|
275
|
+
border-radius: 8px;
|
|
276
|
+
padding: 14px;
|
|
277
|
+
font-size: 0.95rem;
|
|
278
|
+
font-weight: 600;
|
|
279
|
+
cursor: pointer;
|
|
280
|
+
margin-top: 15px;
|
|
281
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
282
|
+
}
|
|
283
|
+
.submit-btn:hover {
|
|
284
|
+
transform: translateY(-2px);
|
|
285
|
+
box-shadow: 0 5px 20px rgba(0,212,255,0.3);
|
|
286
|
+
}
|
|
287
|
+
.submit-btn:disabled {
|
|
288
|
+
background: #444;
|
|
289
|
+
cursor: not-allowed;
|
|
290
|
+
transform: none;
|
|
291
|
+
box-shadow: none;
|
|
292
|
+
}
|
|
293
|
+
.no-question {
|
|
294
|
+
text-align: center;
|
|
295
|
+
padding: 30px 20px;
|
|
296
|
+
color: #666;
|
|
297
|
+
}
|
|
298
|
+
.no-question .icon {
|
|
299
|
+
font-size: 2.5rem;
|
|
300
|
+
margin-bottom: 10px;
|
|
301
|
+
}
|
|
302
|
+
.success-msg {
|
|
303
|
+
background: rgba(0,255,128,0.1);
|
|
304
|
+
color: #00ff80;
|
|
305
|
+
border-radius: 8px;
|
|
306
|
+
padding: 15px;
|
|
307
|
+
text-align: center;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* Build Queue */
|
|
311
|
+
.queue-table {
|
|
312
|
+
width: 100%;
|
|
313
|
+
border-collapse: collapse;
|
|
314
|
+
font-size: 0.85rem;
|
|
315
|
+
}
|
|
316
|
+
.queue-table th {
|
|
317
|
+
text-align: left;
|
|
318
|
+
padding: 10px;
|
|
319
|
+
color: #666;
|
|
320
|
+
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
321
|
+
font-weight: 500;
|
|
322
|
+
}
|
|
323
|
+
.queue-table td {
|
|
324
|
+
padding: 10px;
|
|
325
|
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
326
|
+
}
|
|
327
|
+
.queue-type {
|
|
328
|
+
padding: 3px 8px;
|
|
329
|
+
border-radius: 4px;
|
|
330
|
+
font-size: 0.75rem;
|
|
331
|
+
font-weight: 600;
|
|
332
|
+
}
|
|
333
|
+
.queue-type.api { background: rgba(255,107,107,0.2); color: #ff6b6b; }
|
|
334
|
+
.queue-type.component { background: rgba(78,205,196,0.2); color: #4ecdc4; }
|
|
335
|
+
.queue-type.page { background: rgba(255,230,109,0.2); color: #ffe66d; }
|
|
336
|
+
.queue-type.combined { background: rgba(168,130,255,0.2); color: #a882ff; }
|
|
337
|
+
|
|
338
|
+
.queue-status {
|
|
339
|
+
display: flex;
|
|
340
|
+
align-items: center;
|
|
341
|
+
gap: 6px;
|
|
342
|
+
}
|
|
343
|
+
.status-dot {
|
|
344
|
+
width: 8px;
|
|
345
|
+
height: 8px;
|
|
346
|
+
border-radius: 50%;
|
|
347
|
+
}
|
|
348
|
+
.status-dot.done { background: #00ff80; }
|
|
349
|
+
.status-dot.building { background: #00d4ff; animation: pulse 1.5s infinite; }
|
|
350
|
+
.status-dot.pending { background: #666; }
|
|
351
|
+
.status-dot.failed { background: #ff6b6b; }
|
|
352
|
+
|
|
353
|
+
/* Activity Log */
|
|
354
|
+
.activity-list {
|
|
355
|
+
display: flex;
|
|
356
|
+
flex-direction: column;
|
|
357
|
+
gap: 8px;
|
|
358
|
+
}
|
|
359
|
+
.activity-item {
|
|
360
|
+
display: flex;
|
|
361
|
+
gap: 10px;
|
|
362
|
+
padding: 10px;
|
|
363
|
+
background: rgba(255,255,255,0.03);
|
|
364
|
+
border-radius: 6px;
|
|
365
|
+
font-size: 0.85rem;
|
|
366
|
+
}
|
|
367
|
+
.activity-time {
|
|
368
|
+
color: #666;
|
|
369
|
+
white-space: nowrap;
|
|
370
|
+
font-family: monospace;
|
|
371
|
+
}
|
|
372
|
+
.activity-msg {
|
|
373
|
+
flex: 1;
|
|
374
|
+
color: #aaa;
|
|
375
|
+
}
|
|
376
|
+
.activity-msg strong {
|
|
377
|
+
color: #e0e0e0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* Footer */
|
|
381
|
+
.footer {
|
|
382
|
+
text-align: center;
|
|
383
|
+
padding: 15px;
|
|
384
|
+
color: #444;
|
|
385
|
+
font-size: 0.75rem;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/* Grid layout for larger screens */
|
|
389
|
+
@media (min-width: 768px) {
|
|
390
|
+
.grid-2 {
|
|
391
|
+
display: grid;
|
|
392
|
+
grid-template-columns: 1fr 1fr;
|
|
393
|
+
gap: 15px;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/* Small mobile adjustments */
|
|
398
|
+
@media (max-width: 480px) {
|
|
399
|
+
body { padding: 10px; }
|
|
400
|
+
.header { padding: 15px; }
|
|
401
|
+
.card { padding: 15px; }
|
|
402
|
+
.header h1 { font-size: 1.2rem; }
|
|
403
|
+
}
|
|
404
|
+
</style>
|
|
405
|
+
</head>
|
|
406
|
+
<body>
|
|
407
|
+
<div class="container">
|
|
408
|
+
<!-- Header -->
|
|
409
|
+
<div class="header">
|
|
410
|
+
<div class="header-top">
|
|
411
|
+
<div>
|
|
412
|
+
<h1>HUSTLE BUILD DASHBOARD</h1>
|
|
413
|
+
<div class="subtitle" id="buildName">No active build</div>
|
|
414
|
+
</div>
|
|
415
|
+
<div style="display: flex; gap: 10px; align-items: center;">
|
|
416
|
+
<button id="notifyBtn" onclick="requestNotificationPermission()"
|
|
417
|
+
style="padding: 8px 12px; border-radius: 20px; border: none; cursor: pointer; font-size: 0.8rem; transition: all 0.2s;">
|
|
418
|
+
🔕 Enable
|
|
419
|
+
</button>
|
|
420
|
+
<div class="build-status no-build" id="buildStatus">
|
|
421
|
+
<span id="buildStatusText">IDLE</span>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
<div class="build-meta" id="buildMeta" style="display: none;">
|
|
426
|
+
<div class="build-meta-item">
|
|
427
|
+
<span class="label">Started:</span>
|
|
428
|
+
<span class="value" id="buildStarted">-</span>
|
|
429
|
+
</div>
|
|
430
|
+
<div class="build-meta-item">
|
|
431
|
+
<span class="label">Phase:</span>
|
|
432
|
+
<span class="value" id="buildPhase">-</span>
|
|
433
|
+
</div>
|
|
434
|
+
<div class="build-meta-item">
|
|
435
|
+
<span class="label">Mode:</span>
|
|
436
|
+
<span class="value" id="buildMode">-</span>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
|
|
441
|
+
<!-- Phase Progress -->
|
|
442
|
+
<div class="card" id="phaseCard">
|
|
443
|
+
<div class="card-title">Phase Progress</div>
|
|
444
|
+
<div class="phases" id="phaseList">
|
|
445
|
+
<div class="phase-item">
|
|
446
|
+
<div class="phase-icon pending">1</div>
|
|
447
|
+
<div class="phase-name pending">Document Intake & Parsing</div>
|
|
448
|
+
</div>
|
|
449
|
+
<div class="phase-item">
|
|
450
|
+
<div class="phase-icon pending">2</div>
|
|
451
|
+
<div class="phase-name pending">Parse Request</div>
|
|
452
|
+
</div>
|
|
453
|
+
<div class="phase-item">
|
|
454
|
+
<div class="phase-icon pending">3</div>
|
|
455
|
+
<div class="phase-name pending">Decompose Into Workflows</div>
|
|
456
|
+
</div>
|
|
457
|
+
<div class="phase-item">
|
|
458
|
+
<div class="phase-icon pending">4</div>
|
|
459
|
+
<div class="phase-name pending">Orchestrator Interview</div>
|
|
460
|
+
</div>
|
|
461
|
+
<div class="phase-item">
|
|
462
|
+
<div class="phase-icon pending">5</div>
|
|
463
|
+
<div class="phase-name pending">Create Orchestration State</div>
|
|
464
|
+
</div>
|
|
465
|
+
<div class="phase-item">
|
|
466
|
+
<div class="phase-icon pending">6</div>
|
|
467
|
+
<div class="phase-name pending">Execute Workflows</div>
|
|
468
|
+
</div>
|
|
469
|
+
<div class="phase-item">
|
|
470
|
+
<div class="phase-icon pending">7</div>
|
|
471
|
+
<div class="phase-name pending">Cross-Workflow Wiring</div>
|
|
472
|
+
</div>
|
|
473
|
+
<div class="phase-item">
|
|
474
|
+
<div class="phase-icon pending">8</div>
|
|
475
|
+
<div class="phase-name pending">Final Verification</div>
|
|
476
|
+
</div>
|
|
477
|
+
<div class="phase-item">
|
|
478
|
+
<div class="phase-icon pending">9</div>
|
|
479
|
+
<div class="phase-name pending">Documentation Rollup</div>
|
|
480
|
+
</div>
|
|
481
|
+
<div class="phase-item">
|
|
482
|
+
<div class="phase-icon pending">10</div>
|
|
483
|
+
<div class="phase-name pending">Completion</div>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
|
|
488
|
+
<!-- Current Question -->
|
|
489
|
+
<div class="card question-card" id="questionCard">
|
|
490
|
+
<div class="card-title">Current Question</div>
|
|
491
|
+
<div id="questionContent">
|
|
492
|
+
<div class="no-question">
|
|
493
|
+
<div class="icon">💭</div>
|
|
494
|
+
<div>No pending questions</div>
|
|
495
|
+
<div style="font-size: 0.8rem; margin-top: 5px;">Polling every 2 seconds...</div>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
|
|
500
|
+
<div class="grid-2">
|
|
501
|
+
<!-- Build Queue -->
|
|
502
|
+
<div class="card" id="queueCard">
|
|
503
|
+
<div class="card-title">Build Queue</div>
|
|
504
|
+
<div id="queueContent">
|
|
505
|
+
<div class="no-question" style="padding: 20px;">
|
|
506
|
+
<div>No items in queue</div>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
510
|
+
|
|
511
|
+
<!-- Activity Log -->
|
|
512
|
+
<div class="card">
|
|
513
|
+
<div class="card-title">Recent Activity</div>
|
|
514
|
+
<div class="activity-list" id="activityList">
|
|
515
|
+
<div class="activity-item">
|
|
516
|
+
<span class="activity-time">--:--</span>
|
|
517
|
+
<span class="activity-msg">Waiting for build activity...</span>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
|
|
523
|
+
<div class="footer">
|
|
524
|
+
Hustle Build v4.6.1 | Polling every 2 seconds | <span id="lastUpdate">-</span>
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
|
|
528
|
+
<script>
|
|
529
|
+
const PHASES = [
|
|
530
|
+
"Document Intake & Parsing",
|
|
531
|
+
"Parse Request",
|
|
532
|
+
"Decompose Into Workflows",
|
|
533
|
+
"Orchestrator Interview",
|
|
534
|
+
"Create Orchestration State",
|
|
535
|
+
"Execute Workflows",
|
|
536
|
+
"Cross-Workflow Wiring",
|
|
537
|
+
"Final Verification",
|
|
538
|
+
"Documentation Rollup",
|
|
539
|
+
"Completion"
|
|
540
|
+
];
|
|
541
|
+
|
|
542
|
+
let currentQuestion = null;
|
|
543
|
+
let selectedOption = null;
|
|
544
|
+
let answerHistory = JSON.parse(localStorage.getItem('answerHistory') || '[]');
|
|
545
|
+
let notificationsEnabled = false;
|
|
546
|
+
let lastPhase = null;
|
|
547
|
+
let lastBuildStatus = null;
|
|
548
|
+
|
|
549
|
+
// Browser Notification Support
|
|
550
|
+
async function requestNotificationPermission() {
|
|
551
|
+
if (!('Notification' in window)) {
|
|
552
|
+
console.log('Browser does not support notifications');
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
const permission = await Notification.requestPermission();
|
|
558
|
+
notificationsEnabled = permission === 'granted';
|
|
559
|
+
updateNotificationButton();
|
|
560
|
+
|
|
561
|
+
if (notificationsEnabled) {
|
|
562
|
+
showNotification('Notifications Enabled', 'You will receive alerts for questions and phase changes', 'setup');
|
|
563
|
+
}
|
|
564
|
+
} catch (e) {
|
|
565
|
+
console.error('Notification permission error:', e);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function updateNotificationButton() {
|
|
570
|
+
const btn = document.getElementById('notifyBtn');
|
|
571
|
+
if (btn) {
|
|
572
|
+
if (notificationsEnabled) {
|
|
573
|
+
btn.innerHTML = '🔔 ON';
|
|
574
|
+
btn.style.background = 'rgba(0,255,128,0.2)';
|
|
575
|
+
btn.style.color = '#00ff80';
|
|
576
|
+
} else {
|
|
577
|
+
btn.innerHTML = '🔕 Enable';
|
|
578
|
+
btn.style.background = 'rgba(255,255,255,0.1)';
|
|
579
|
+
btn.style.color = '#888';
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function showNotification(title, body, tag) {
|
|
585
|
+
if (!notificationsEnabled) return;
|
|
586
|
+
|
|
587
|
+
// Only show if page is not visible (user is away)
|
|
588
|
+
if (document.visibilityState === 'hidden' || !document.hasFocus()) {
|
|
589
|
+
try {
|
|
590
|
+
const notification = new Notification(title, {
|
|
591
|
+
body: body,
|
|
592
|
+
icon: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🔨</text></svg>',
|
|
593
|
+
tag: tag || 'hustle-build',
|
|
594
|
+
requireInteraction: tag === 'question'
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
notification.onclick = () => {
|
|
598
|
+
window.focus();
|
|
599
|
+
notification.close();
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
// Auto-close non-question notifications after 5 seconds
|
|
603
|
+
if (tag !== 'question') {
|
|
604
|
+
setTimeout(() => notification.close(), 5000);
|
|
605
|
+
}
|
|
606
|
+
} catch (e) {
|
|
607
|
+
console.error('Notification error:', e);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Check for phase/status changes and notify
|
|
613
|
+
function checkForChanges(data) {
|
|
614
|
+
if (!data.build) return;
|
|
615
|
+
|
|
616
|
+
const currentPhase = data.build.current_phase;
|
|
617
|
+
const currentStatus = data.build.status;
|
|
618
|
+
|
|
619
|
+
// Notify on phase change
|
|
620
|
+
if (lastPhase !== null && currentPhase !== lastPhase) {
|
|
621
|
+
const phaseName = PHASES[currentPhase - 1] || `Phase ${currentPhase}`;
|
|
622
|
+
showNotification(
|
|
623
|
+
`Phase ${currentPhase}/10: ${phaseName}`,
|
|
624
|
+
'Build progressed to next phase',
|
|
625
|
+
'phase-change'
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Notify on build completion
|
|
630
|
+
if (lastBuildStatus !== 'complete' && currentStatus === 'complete') {
|
|
631
|
+
showNotification(
|
|
632
|
+
'Build Complete! ✅',
|
|
633
|
+
data.build.name || 'Your build has finished',
|
|
634
|
+
'build-complete'
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
lastPhase = currentPhase;
|
|
639
|
+
lastBuildStatus = currentStatus;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Initialize notifications on page load
|
|
643
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
644
|
+
// Check if already granted
|
|
645
|
+
if ('Notification' in window && Notification.permission === 'granted') {
|
|
646
|
+
notificationsEnabled = true;
|
|
647
|
+
}
|
|
648
|
+
updateNotificationButton();
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// Fetch build status
|
|
652
|
+
async function fetchStatus() {
|
|
653
|
+
try {
|
|
654
|
+
const response = await fetch('/api/status');
|
|
655
|
+
const data = await response.json();
|
|
656
|
+
updateBuildStatus(data);
|
|
657
|
+
checkForChanges(data);
|
|
658
|
+
updateLastUpdate();
|
|
659
|
+
} catch (e) {
|
|
660
|
+
console.error('Error fetching status:', e);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Fetch current question
|
|
665
|
+
async function fetchQuestion() {
|
|
666
|
+
try {
|
|
667
|
+
const response = await fetch('/api/question');
|
|
668
|
+
const data = await response.json();
|
|
669
|
+
|
|
670
|
+
if (data.question && data.question !== currentQuestion?.question) {
|
|
671
|
+
currentQuestion = data;
|
|
672
|
+
renderQuestion(data);
|
|
673
|
+
// Notify about new question
|
|
674
|
+
showNotification(
|
|
675
|
+
'❓ Question Needs Answer',
|
|
676
|
+
data.question.substring(0, 100),
|
|
677
|
+
'question'
|
|
678
|
+
);
|
|
679
|
+
} else if (!data.question && currentQuestion) {
|
|
680
|
+
currentQuestion = null;
|
|
681
|
+
renderNoQuestion();
|
|
682
|
+
}
|
|
683
|
+
} catch (e) {
|
|
684
|
+
console.error('Error fetching question:', e);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function updateBuildStatus(data) {
|
|
689
|
+
const nameEl = document.getElementById('buildName');
|
|
690
|
+
const statusEl = document.getElementById('buildStatus');
|
|
691
|
+
const statusTextEl = document.getElementById('buildStatusText');
|
|
692
|
+
const metaEl = document.getElementById('buildMeta');
|
|
693
|
+
const startedEl = document.getElementById('buildStarted');
|
|
694
|
+
const phaseEl = document.getElementById('buildPhase');
|
|
695
|
+
const modeEl = document.getElementById('buildMode');
|
|
696
|
+
|
|
697
|
+
if (data.build) {
|
|
698
|
+
const build = data.build;
|
|
699
|
+
nameEl.textContent = build.name || build.build_id || 'Active Build';
|
|
700
|
+
|
|
701
|
+
statusEl.className = 'build-status ' + (build.status || 'pending');
|
|
702
|
+
statusTextEl.textContent = (build.status || 'PENDING').toUpperCase();
|
|
703
|
+
|
|
704
|
+
metaEl.style.display = 'flex';
|
|
705
|
+
startedEl.textContent = formatTime(build.created_at);
|
|
706
|
+
phaseEl.textContent = build.current_phase ? `${build.current_phase}/10` : '-';
|
|
707
|
+
modeEl.textContent = build.mode || 'interactive';
|
|
708
|
+
|
|
709
|
+
// Update phase progress
|
|
710
|
+
updatePhaseProgress(build.current_phase || 0, build.phase_statuses || {});
|
|
711
|
+
|
|
712
|
+
// Update build queue
|
|
713
|
+
updateBuildQueue(build.decomposition);
|
|
714
|
+
|
|
715
|
+
// Update activity
|
|
716
|
+
updateActivity(build.activity || data.activity || []);
|
|
717
|
+
} else {
|
|
718
|
+
nameEl.textContent = 'No active build';
|
|
719
|
+
statusEl.className = 'build-status no-build';
|
|
720
|
+
statusTextEl.textContent = 'IDLE';
|
|
721
|
+
metaEl.style.display = 'none';
|
|
722
|
+
resetPhaseProgress();
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function updatePhaseProgress(currentPhase, statuses) {
|
|
727
|
+
const phaseItems = document.querySelectorAll('.phase-item');
|
|
728
|
+
phaseItems.forEach((item, i) => {
|
|
729
|
+
const icon = item.querySelector('.phase-icon');
|
|
730
|
+
const name = item.querySelector('.phase-name');
|
|
731
|
+
const phaseNum = i + 1;
|
|
732
|
+
const status = statuses[phaseNum] || (phaseNum < currentPhase ? 'completed' : (phaseNum === currentPhase ? 'in_progress' : 'pending'));
|
|
733
|
+
|
|
734
|
+
icon.className = 'phase-icon ' + status;
|
|
735
|
+
icon.textContent = status === 'completed' ? '\\u2713' : phaseNum;
|
|
736
|
+
|
|
737
|
+
name.className = 'phase-name ' + (status === 'in_progress' ? 'active' : status);
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function resetPhaseProgress() {
|
|
742
|
+
const phaseItems = document.querySelectorAll('.phase-item');
|
|
743
|
+
phaseItems.forEach((item, i) => {
|
|
744
|
+
const icon = item.querySelector('.phase-icon');
|
|
745
|
+
const name = item.querySelector('.phase-name');
|
|
746
|
+
icon.className = 'phase-icon pending';
|
|
747
|
+
icon.textContent = i + 1;
|
|
748
|
+
name.className = 'phase-name pending';
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function updateBuildQueue(decomposition) {
|
|
753
|
+
const container = document.getElementById('queueContent');
|
|
754
|
+
if (!decomposition) {
|
|
755
|
+
container.innerHTML = '<div class="no-question" style="padding: 20px;"><div>No items in queue</div></div>';
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const items = [];
|
|
760
|
+
['apis', 'components', 'combined_apis', 'pages'].forEach(type => {
|
|
761
|
+
const typeItems = decomposition[type] || [];
|
|
762
|
+
typeItems.forEach(item => {
|
|
763
|
+
items.push({...item, type: type.replace('_', ' ').replace(/s$/, '')});
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
if (items.length === 0) {
|
|
768
|
+
container.innerHTML = '<div class="no-question" style="padding: 20px;"><div>No items in queue</div></div>';
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const rows = items.map(item => {
|
|
773
|
+
const typeClass = item.type.toLowerCase().split(' ')[0];
|
|
774
|
+
const statusClass = item.status === 'complete' ? 'done' : (item.status === 'in_progress' ? 'building' : (item.status === 'failed' ? 'failed' : 'pending'));
|
|
775
|
+
const deps = (item.depends_on || []).join(', ') || '-';
|
|
776
|
+
return `
|
|
777
|
+
<tr>
|
|
778
|
+
<td><span class="queue-type ${typeClass}">${item.type.toUpperCase()}</span></td>
|
|
779
|
+
<td>${item.name}</td>
|
|
780
|
+
<td>
|
|
781
|
+
<div class="queue-status">
|
|
782
|
+
<div class="status-dot ${statusClass}"></div>
|
|
783
|
+
<span>${item.status || 'pending'}</span>
|
|
784
|
+
</div>
|
|
785
|
+
</td>
|
|
786
|
+
<td style="color: #666; font-size: 0.8rem;">${deps}</td>
|
|
787
|
+
</tr>
|
|
788
|
+
`;
|
|
789
|
+
}).join('');
|
|
790
|
+
|
|
791
|
+
container.innerHTML = `
|
|
792
|
+
<table class="queue-table">
|
|
793
|
+
<thead>
|
|
794
|
+
<tr><th>Type</th><th>Name</th><th>Status</th><th>Deps</th></tr>
|
|
795
|
+
</thead>
|
|
796
|
+
<tbody>${rows}</tbody>
|
|
797
|
+
</table>
|
|
798
|
+
`;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function updateActivity(activity) {
|
|
802
|
+
const container = document.getElementById('activityList');
|
|
803
|
+
if (!activity || activity.length === 0) {
|
|
804
|
+
container.innerHTML = `
|
|
805
|
+
<div class="activity-item">
|
|
806
|
+
<span class="activity-time">--:--</span>
|
|
807
|
+
<span class="activity-msg">Waiting for build activity...</span>
|
|
808
|
+
</div>
|
|
809
|
+
`;
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
container.innerHTML = activity.slice(0, 8).map(item => `
|
|
814
|
+
<div class="activity-item">
|
|
815
|
+
<span class="activity-time">${formatTime(item.time)}</span>
|
|
816
|
+
<span class="activity-msg">${item.message}</span>
|
|
817
|
+
</div>
|
|
818
|
+
`).join('');
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function formatTime(timestamp) {
|
|
822
|
+
if (!timestamp) return '-';
|
|
823
|
+
try {
|
|
824
|
+
const date = new Date(timestamp);
|
|
825
|
+
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
|
826
|
+
} catch (e) {
|
|
827
|
+
return timestamp;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function updateLastUpdate() {
|
|
832
|
+
document.getElementById('lastUpdate').textContent = 'Updated: ' + new Date().toLocaleTimeString();
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function renderQuestion(data) {
|
|
836
|
+
const content = document.getElementById('questionContent');
|
|
837
|
+
const optionsHtml = data.options.map((opt, i) => {
|
|
838
|
+
const label = typeof opt === 'string' ? opt : opt.label;
|
|
839
|
+
const desc = typeof opt === 'object' ? opt.description : '';
|
|
840
|
+
const isOther = label.toLowerCase().includes('other');
|
|
841
|
+
return `
|
|
842
|
+
<div class="option" data-index="${i}" data-other="${isOther}" onclick="selectOption(${i}, ${isOther})">
|
|
843
|
+
<div class="option-label">${label}</div>
|
|
844
|
+
${desc ? `<div class="option-description">${desc}</div>` : ''}
|
|
845
|
+
</div>
|
|
846
|
+
`;
|
|
847
|
+
}).join('');
|
|
848
|
+
|
|
849
|
+
content.innerHTML = `
|
|
850
|
+
<span class="phase-badge">${data.phase || data.header || 'Question'}</span>
|
|
851
|
+
<div class="question-text">${data.question}</div>
|
|
852
|
+
<div class="options">${optionsHtml}</div>
|
|
853
|
+
<div class="custom-input" id="customInput">
|
|
854
|
+
<textarea id="customAnswer" placeholder="Enter your custom answer..."></textarea>
|
|
855
|
+
</div>
|
|
856
|
+
<button class="submit-btn" id="submitBtn" onclick="submitAnswer()" disabled>
|
|
857
|
+
Select an option to continue
|
|
858
|
+
</button>
|
|
859
|
+
`;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function renderNoQuestion() {
|
|
863
|
+
document.getElementById('questionContent').innerHTML = `
|
|
864
|
+
<div class="no-question">
|
|
865
|
+
<div class="icon">💭</div>
|
|
866
|
+
<div>No pending questions</div>
|
|
867
|
+
<div style="font-size: 0.8rem; margin-top: 5px;">Polling every 2 seconds...</div>
|
|
868
|
+
</div>
|
|
869
|
+
`;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function selectOption(index, isOther) {
|
|
873
|
+
selectedOption = index;
|
|
874
|
+
document.querySelectorAll('.option').forEach((el, i) => {
|
|
875
|
+
el.classList.toggle('selected', i === index);
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
const customInput = document.getElementById('customInput');
|
|
879
|
+
if (customInput) {
|
|
880
|
+
if (isOther) {
|
|
881
|
+
customInput.classList.add('visible');
|
|
882
|
+
} else {
|
|
883
|
+
customInput.classList.remove('visible');
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const btn = document.getElementById('submitBtn');
|
|
888
|
+
if (btn) {
|
|
889
|
+
btn.disabled = false;
|
|
890
|
+
btn.textContent = 'Submit Answer';
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
async function submitAnswer() {
|
|
895
|
+
if (selectedOption === null || !currentQuestion) return;
|
|
896
|
+
|
|
897
|
+
const btn = document.getElementById('submitBtn');
|
|
898
|
+
btn.disabled = true;
|
|
899
|
+
btn.textContent = 'Submitting...';
|
|
900
|
+
|
|
901
|
+
const opt = currentQuestion.options[selectedOption];
|
|
902
|
+
let answer = typeof opt === 'string' ? opt : opt.label;
|
|
903
|
+
|
|
904
|
+
if (answer.toLowerCase().includes('other')) {
|
|
905
|
+
const customText = document.getElementById('customAnswer')?.value;
|
|
906
|
+
if (customText) answer = customText;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
try {
|
|
910
|
+
const response = await fetch('/api/answer', {
|
|
911
|
+
method: 'POST',
|
|
912
|
+
headers: {'Content-Type': 'application/json'},
|
|
913
|
+
body: JSON.stringify({
|
|
914
|
+
question: currentQuestion.question,
|
|
915
|
+
answer: answer,
|
|
916
|
+
option_index: selectedOption,
|
|
917
|
+
phase: currentQuestion.phase || currentQuestion.header
|
|
918
|
+
})
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
if (response.ok) {
|
|
922
|
+
answerHistory.unshift({
|
|
923
|
+
time: new Date().toISOString(),
|
|
924
|
+
question: currentQuestion.question.substring(0, 50) + '...',
|
|
925
|
+
answer: answer
|
|
926
|
+
});
|
|
927
|
+
answerHistory = answerHistory.slice(0, 10);
|
|
928
|
+
localStorage.setItem('answerHistory', JSON.stringify(answerHistory));
|
|
929
|
+
|
|
930
|
+
document.getElementById('questionContent').innerHTML = `
|
|
931
|
+
<div class="success-msg">
|
|
932
|
+
<div style="font-size: 1.5rem; margin-bottom: 8px;">✓</div>
|
|
933
|
+
<div>Answer submitted!</div>
|
|
934
|
+
<div style="margin-top: 8px; color: #aaa; font-size: 0.85rem;">"${answer}"</div>
|
|
935
|
+
</div>
|
|
936
|
+
`;
|
|
937
|
+
currentQuestion = null;
|
|
938
|
+
selectedOption = null;
|
|
939
|
+
|
|
940
|
+
setTimeout(renderNoQuestion, 2000);
|
|
941
|
+
}
|
|
942
|
+
} catch (e) {
|
|
943
|
+
console.error('Error submitting:', e);
|
|
944
|
+
btn.disabled = false;
|
|
945
|
+
btn.textContent = 'Error - Try Again';
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Start polling
|
|
950
|
+
setInterval(fetchStatus, 2000);
|
|
951
|
+
setInterval(fetchQuestion, 2000);
|
|
952
|
+
fetchStatus();
|
|
953
|
+
fetchQuestion();
|
|
954
|
+
</script>
|
|
955
|
+
</body>
|
|
956
|
+
</html>
|
|
957
|
+
'''
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
class QuestionHandler(BaseHTTPRequestHandler):
|
|
961
|
+
"""HTTP handler for the build dashboard and remote question interface."""
|
|
962
|
+
|
|
963
|
+
def get_project_dir(self):
|
|
964
|
+
return Path(os.environ.get("CLAUDE_PROJECT_DIR", "."))
|
|
965
|
+
|
|
966
|
+
def load_state(self):
|
|
967
|
+
"""Load the hustle-build state file."""
|
|
968
|
+
state_file = self.get_project_dir() / STATE_FILE
|
|
969
|
+
if state_file.exists():
|
|
970
|
+
try:
|
|
971
|
+
return json.loads(state_file.read_text())
|
|
972
|
+
except Exception:
|
|
973
|
+
pass
|
|
974
|
+
return None
|
|
975
|
+
|
|
976
|
+
def load_activity(self):
|
|
977
|
+
"""Load activity log."""
|
|
978
|
+
activity_file = self.get_project_dir() / ACTIVITY_LOG_FILE
|
|
979
|
+
if activity_file.exists():
|
|
980
|
+
try:
|
|
981
|
+
data = json.loads(activity_file.read_text())
|
|
982
|
+
return data.get('entries', data) if isinstance(data, dict) else data
|
|
983
|
+
except Exception:
|
|
984
|
+
pass
|
|
985
|
+
return []
|
|
986
|
+
|
|
987
|
+
def do_GET(self):
|
|
988
|
+
if self.path == '/' or self.path == '/index.html':
|
|
989
|
+
self.send_response(200)
|
|
990
|
+
self.send_header('Content-type', 'text/html; charset=utf-8')
|
|
991
|
+
self.send_header('Cache-Control', 'no-cache')
|
|
992
|
+
self.end_headers()
|
|
993
|
+
self.wfile.write(HTML_TEMPLATE.encode('utf-8'))
|
|
994
|
+
|
|
995
|
+
elif self.path == '/api/question':
|
|
996
|
+
self.send_response(200)
|
|
997
|
+
self.send_header('Content-type', 'application/json')
|
|
998
|
+
self.send_header('Cache-Control', 'no-cache')
|
|
999
|
+
self.end_headers()
|
|
1000
|
+
|
|
1001
|
+
question_file = self.get_project_dir() / QUESTION_FILE
|
|
1002
|
+
if question_file.exists():
|
|
1003
|
+
try:
|
|
1004
|
+
data = json.loads(question_file.read_text())
|
|
1005
|
+
self.wfile.write(json.dumps(data).encode('utf-8'))
|
|
1006
|
+
except Exception:
|
|
1007
|
+
self.wfile.write(json.dumps({}).encode('utf-8'))
|
|
1008
|
+
else:
|
|
1009
|
+
self.wfile.write(json.dumps({}).encode('utf-8'))
|
|
1010
|
+
|
|
1011
|
+
elif self.path == '/api/status':
|
|
1012
|
+
self.send_response(200)
|
|
1013
|
+
self.send_header('Content-type', 'application/json')
|
|
1014
|
+
self.send_header('Cache-Control', 'no-cache')
|
|
1015
|
+
self.end_headers()
|
|
1016
|
+
|
|
1017
|
+
# Load build state
|
|
1018
|
+
state = self.load_state()
|
|
1019
|
+
activity = self.load_activity()
|
|
1020
|
+
|
|
1021
|
+
# Check for pending question
|
|
1022
|
+
question_file = self.get_project_dir() / QUESTION_FILE
|
|
1023
|
+
has_question = question_file.exists()
|
|
1024
|
+
|
|
1025
|
+
response = {
|
|
1026
|
+
"status": "ready",
|
|
1027
|
+
"has_question": has_question,
|
|
1028
|
+
"timestamp": datetime.now().isoformat()
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
if state:
|
|
1032
|
+
# Extract build info from state
|
|
1033
|
+
response["build"] = {
|
|
1034
|
+
"build_id": state.get("build_id"),
|
|
1035
|
+
"name": state.get("request", {}).get("original", "")[:50] if state.get("request") else state.get("build_id"),
|
|
1036
|
+
"status": state.get("status", "pending"),
|
|
1037
|
+
"mode": state.get("mode", "interactive"),
|
|
1038
|
+
"created_at": state.get("created_at"),
|
|
1039
|
+
"current_phase": self._get_current_phase(state),
|
|
1040
|
+
"phase_statuses": self._get_phase_statuses(state),
|
|
1041
|
+
"decomposition": state.get("decomposition"),
|
|
1042
|
+
"activity": activity[-10:] if activity else []
|
|
1043
|
+
}
|
|
1044
|
+
else:
|
|
1045
|
+
response["build"] = None
|
|
1046
|
+
response["activity"] = activity[-10:] if activity else []
|
|
1047
|
+
|
|
1048
|
+
self.wfile.write(json.dumps(response).encode('utf-8'))
|
|
1049
|
+
|
|
1050
|
+
else:
|
|
1051
|
+
self.send_response(404)
|
|
1052
|
+
self.end_headers()
|
|
1053
|
+
|
|
1054
|
+
def _get_current_phase(self, state):
|
|
1055
|
+
"""Determine current phase from state."""
|
|
1056
|
+
# Check for active sub-workflow (Phase 6)
|
|
1057
|
+
if state.get("active_sub_workflow"):
|
|
1058
|
+
return 6
|
|
1059
|
+
|
|
1060
|
+
# Check decomposition status
|
|
1061
|
+
decomp = state.get("decomposition", {})
|
|
1062
|
+
has_decomp = any(decomp.get(k) for k in ["apis", "components", "pages", "combined_apis"])
|
|
1063
|
+
|
|
1064
|
+
# Check interview status
|
|
1065
|
+
interview = state.get("orchestrator_interview", {})
|
|
1066
|
+
interview_complete = interview.get("status") == "complete"
|
|
1067
|
+
|
|
1068
|
+
if state.get("status") == "complete":
|
|
1069
|
+
return 10
|
|
1070
|
+
elif has_decomp and interview_complete:
|
|
1071
|
+
# Check if any workflows are done
|
|
1072
|
+
all_done = True
|
|
1073
|
+
for category in ["apis", "components", "pages", "combined_apis"]:
|
|
1074
|
+
for item in decomp.get(category, []):
|
|
1075
|
+
if item.get("status") != "complete":
|
|
1076
|
+
all_done = False
|
|
1077
|
+
break
|
|
1078
|
+
if all_done and has_decomp:
|
|
1079
|
+
return 7 # Wiring phase
|
|
1080
|
+
return 6 # Still executing
|
|
1081
|
+
elif interview_complete:
|
|
1082
|
+
return 5 # Creating state
|
|
1083
|
+
elif has_decomp:
|
|
1084
|
+
return 4 # Interview
|
|
1085
|
+
elif state.get("project_spec", {}).get("extracted"):
|
|
1086
|
+
return 3 # Decomposition
|
|
1087
|
+
elif state.get("project_spec", {}).get("raw_content"):
|
|
1088
|
+
return 1 # Document parsing
|
|
1089
|
+
|
|
1090
|
+
return 2 # Parse request
|
|
1091
|
+
|
|
1092
|
+
def _get_phase_statuses(self, state):
|
|
1093
|
+
"""Get status of each phase."""
|
|
1094
|
+
statuses = {}
|
|
1095
|
+
current = self._get_current_phase(state)
|
|
1096
|
+
for i in range(1, 11):
|
|
1097
|
+
if i < current:
|
|
1098
|
+
statuses[i] = "completed"
|
|
1099
|
+
elif i == current:
|
|
1100
|
+
statuses[i] = "in_progress"
|
|
1101
|
+
else:
|
|
1102
|
+
statuses[i] = "pending"
|
|
1103
|
+
return statuses
|
|
1104
|
+
|
|
1105
|
+
def do_POST(self):
|
|
1106
|
+
if self.path == '/api/answer':
|
|
1107
|
+
content_length = int(self.headers.get('Content-Length', 0))
|
|
1108
|
+
post_data = self.rfile.read(content_length)
|
|
1109
|
+
|
|
1110
|
+
try:
|
|
1111
|
+
answer_data = json.loads(post_data.decode('utf-8'))
|
|
1112
|
+
answer_data['submitted_at'] = datetime.now().isoformat()
|
|
1113
|
+
|
|
1114
|
+
# Write answer to pending file
|
|
1115
|
+
answer_file = self.get_project_dir() / ANSWER_FILE
|
|
1116
|
+
answer_file.parent.mkdir(parents=True, exist_ok=True)
|
|
1117
|
+
answer_file.write_text(json.dumps(answer_data, indent=2))
|
|
1118
|
+
|
|
1119
|
+
# Clear the question file
|
|
1120
|
+
question_file = self.get_project_dir() / QUESTION_FILE
|
|
1121
|
+
if question_file.exists():
|
|
1122
|
+
question_file.unlink()
|
|
1123
|
+
|
|
1124
|
+
# Log activity
|
|
1125
|
+
self._log_activity(f"Answer submitted: {answer_data.get('answer', '')[:50]}")
|
|
1126
|
+
|
|
1127
|
+
self.send_response(200)
|
|
1128
|
+
self.send_header('Content-type', 'application/json')
|
|
1129
|
+
self.end_headers()
|
|
1130
|
+
self.wfile.write(json.dumps({"status": "success"}).encode('utf-8'))
|
|
1131
|
+
|
|
1132
|
+
except Exception as e:
|
|
1133
|
+
self.send_response(500)
|
|
1134
|
+
self.send_header('Content-type', 'application/json')
|
|
1135
|
+
self.end_headers()
|
|
1136
|
+
self.wfile.write(json.dumps({"error": str(e)}).encode('utf-8'))
|
|
1137
|
+
else:
|
|
1138
|
+
self.send_response(404)
|
|
1139
|
+
self.end_headers()
|
|
1140
|
+
|
|
1141
|
+
def _log_activity(self, message):
|
|
1142
|
+
"""Add entry to activity log."""
|
|
1143
|
+
activity_file = self.get_project_dir() / ACTIVITY_LOG_FILE
|
|
1144
|
+
activity_file.parent.mkdir(parents=True, exist_ok=True)
|
|
1145
|
+
|
|
1146
|
+
entries = []
|
|
1147
|
+
if activity_file.exists():
|
|
1148
|
+
try:
|
|
1149
|
+
data = json.loads(activity_file.read_text())
|
|
1150
|
+
entries = data.get('entries', data) if isinstance(data, dict) else data
|
|
1151
|
+
except Exception:
|
|
1152
|
+
pass
|
|
1153
|
+
|
|
1154
|
+
entries.append({
|
|
1155
|
+
"time": datetime.now().isoformat(),
|
|
1156
|
+
"message": message
|
|
1157
|
+
})
|
|
1158
|
+
entries = entries[-50:] # Keep last 50
|
|
1159
|
+
|
|
1160
|
+
activity_file.write_text(json.dumps({"entries": entries}, indent=2))
|
|
1161
|
+
|
|
1162
|
+
def log_message(self, format, *args):
|
|
1163
|
+
# Custom log format
|
|
1164
|
+
print(f"[{datetime.now().strftime('%H:%M:%S')}] {args[0]}")
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
def main():
|
|
1168
|
+
# Get port from argument or environment
|
|
1169
|
+
port = DEFAULT_PORT
|
|
1170
|
+
if len(sys.argv) > 1:
|
|
1171
|
+
try:
|
|
1172
|
+
port = int(sys.argv[1])
|
|
1173
|
+
except ValueError:
|
|
1174
|
+
pass
|
|
1175
|
+
port = int(os.environ.get('REMOTE_QUESTIONS_PORT', port))
|
|
1176
|
+
|
|
1177
|
+
project_dir = os.environ.get('CLAUDE_PROJECT_DIR', '.')
|
|
1178
|
+
|
|
1179
|
+
# Ensure directories exist
|
|
1180
|
+
claude_dir = Path(project_dir) / ".claude"
|
|
1181
|
+
claude_dir.mkdir(parents=True, exist_ok=True)
|
|
1182
|
+
(Path(project_dir) / ".claude/workflow-logs").mkdir(parents=True, exist_ok=True)
|
|
1183
|
+
|
|
1184
|
+
server = HTTPServer(('0.0.0.0', port), QuestionHandler)
|
|
1185
|
+
|
|
1186
|
+
print(f"""
|
|
1187
|
+
================================================================================
|
|
1188
|
+
HUSTLE BUILD DASHBOARD SERVER v4.6.1
|
|
1189
|
+
================================================================================
|
|
1190
|
+
|
|
1191
|
+
Dashboard running at: http://localhost:{port}
|
|
1192
|
+
|
|
1193
|
+
Features:
|
|
1194
|
+
- Build progress & phase status
|
|
1195
|
+
- Question interface for remote answering
|
|
1196
|
+
- Build queue with dependencies
|
|
1197
|
+
- Activity log
|
|
1198
|
+
- Browser notifications (click Enable in header)
|
|
1199
|
+
|
|
1200
|
+
Access from phone/tablet on same network:
|
|
1201
|
+
http://<your-computer-ip>:{port}
|
|
1202
|
+
(Find IP: ipconfig/ifconfig, look for 192.168.x.x)
|
|
1203
|
+
|
|
1204
|
+
API Endpoints:
|
|
1205
|
+
GET / - Full dashboard HTML
|
|
1206
|
+
GET /api/status - Build state JSON
|
|
1207
|
+
GET /api/question - Current question JSON
|
|
1208
|
+
POST /api/answer - Submit answer
|
|
1209
|
+
|
|
1210
|
+
Project directory: {os.path.abspath(project_dir)}
|
|
1211
|
+
|
|
1212
|
+
Press Ctrl+C to stop.
|
|
1213
|
+
================================================================================
|
|
1214
|
+
""")
|
|
1215
|
+
|
|
1216
|
+
try:
|
|
1217
|
+
server.serve_forever()
|
|
1218
|
+
except KeyboardInterrupt:
|
|
1219
|
+
print("\nShutting down...")
|
|
1220
|
+
server.shutdown()
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
if __name__ == "__main__":
|
|
1224
|
+
main()
|