@opensassi/opencode 0.1.2 → 0.1.4
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/dashboard/dashboard.e2e.test.ts +247 -0
- package/dashboard/dist/index.d.ts +9 -0
- package/dashboard/dist/index.js +36 -0
- package/dashboard/dist/routes/api.d.ts +2 -0
- package/dashboard/dist/routes/api.js +215 -0
- package/dashboard/dist/services/cache.d.ts +13 -0
- package/dashboard/dist/services/cache.js +29 -0
- package/dashboard/dist/services/experiments.d.ts +11 -0
- package/dashboard/dist/services/experiments.js +108 -0
- package/dashboard/dist/services/git.d.ts +12 -0
- package/dashboard/dist/services/git.js +149 -0
- package/dashboard/dist/services/sessions.d.ts +25 -0
- package/dashboard/dist/services/sessions.js +208 -0
- package/dashboard/dist/services/specs.d.ts +9 -0
- package/dashboard/dist/services/specs.js +102 -0
- package/dashboard/dist/types.d.ts +173 -0
- package/dashboard/dist/types.js +1 -0
- package/dashboard/opencode.e2e.test.ts +100 -0
- package/dashboard/playwright.config.ts +11 -0
- package/dashboard/public/app.js +961 -0
- package/dashboard/public/index.html +29 -0
- package/dashboard/public/style.css +231 -0
- package/dashboard/src/index.ts +53 -0
- package/dashboard/src/routes/api.ts +235 -0
- package/dashboard/src/services/cache.ts +38 -0
- package/dashboard/src/services/experiments.ts +117 -0
- package/dashboard/src/services/git.ts +139 -0
- package/dashboard/src/services/sessions.ts +216 -0
- package/dashboard/src/services/specs.ts +95 -0
- package/dashboard/src/types.ts +168 -0
- package/dashboard/technical-specification.md +414 -0
- package/dashboard/test-api.sh +127 -0
- package/dashboard/tsconfig.json +16 -0
- package/lib/util/paths.js +9 -1
- package/package.json +9 -1
- package/scripts/dashboard.js +17 -0
- package/scripts/generate-daily-summaries.js +190 -0
- package/skills/opensassi/SKILL.md +150 -56
- package/skills/todo/SKILL.md +45 -63
- package/skills-index.json +10 -7
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# Technical Specification: deepenc-harness Dashboard
|
|
2
|
+
|
|
3
|
+
## 1. Overview
|
|
4
|
+
|
|
5
|
+
The dashboard is a localhost-only HTTP web application that visualizes session data from `../sessions/daily/`, provides drill-down into individual session transcripts (`.json.bz2`), and overlays Git history for context. It runs as a subcommand of `deepenc-harness` and is isolated from the core harness code.
|
|
6
|
+
|
|
7
|
+
**Key design constraints:**
|
|
8
|
+
- Localhost only — zero security considerations, no auth, no CORS
|
|
9
|
+
- No bundler — vanilla HTML/CSS/JS served directly by Express
|
|
10
|
+
- CDN-loaded libraries only (Chart.js)
|
|
11
|
+
- Zero coupling to the core harness — `dashboard/` is a self-contained module
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 2. Directory Structure
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
deepenc-harness/
|
|
19
|
+
dashboard/
|
|
20
|
+
technical-specification.md
|
|
21
|
+
src/
|
|
22
|
+
index.ts # Server entry point (Express setup, listen)
|
|
23
|
+
routes/
|
|
24
|
+
api.ts # All REST API route handlers
|
|
25
|
+
services/
|
|
26
|
+
sessions.ts # Daily file reader, session detail (.bz2) reader
|
|
27
|
+
git.ts # Git log/diff/stats via child_process
|
|
28
|
+
types.ts # Dashboard-specific type definitions
|
|
29
|
+
public/
|
|
30
|
+
index.html # SPA shell
|
|
31
|
+
style.css # All dashboard styles
|
|
32
|
+
app.js # Client-side router, chart rendering, fetch
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 3. CLI Integration
|
|
38
|
+
|
|
39
|
+
Add `dashboard` as a new top-level command alongside `build`, `test`, and `ml`:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
deepenc-harness dashboard [options]
|
|
43
|
+
|
|
44
|
+
Options:
|
|
45
|
+
--port <n> HTTP server port (default: 3000)
|
|
46
|
+
--sessions <path> Path to sessions directory (default: ../sessions)
|
|
47
|
+
--host <addr> Bind address (default: 127.0.0.1)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Implementation approach in `src/index.ts`
|
|
51
|
+
|
|
52
|
+
The existing CLI parser (`parseArgs`) handles `build`, `test`, and `ml`. A new branch for `dashboard` imports and starts the Express server:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// In src/index.ts parseArgs()
|
|
56
|
+
if (arg === 'dashboard' && !command) {
|
|
57
|
+
command = 'dashboard';
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// In main():
|
|
62
|
+
} else if (command === 'dashboard') {
|
|
63
|
+
const { startDashboard } = await import('../dashboard/src/index.js');
|
|
64
|
+
startDashboard(port, sessionsPath, host);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The dashboard module is imported lazily (dynamic `import()`) so its dependencies (express, etc.) are only loaded when the dashboard command is used. This keeps the core CLI fast.
|
|
69
|
+
|
|
70
|
+
Alternatively, to keep the dashboard fully isolated, the dashboard can be a separate npm workspace or even a standalone script. But for simplicity, lazy dynamic import from the core harness is sufficient.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 4. HTTP Server (Express)
|
|
75
|
+
|
|
76
|
+
### Dependencies
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"dependencies": {
|
|
81
|
+
"express": "^5.1.0"
|
|
82
|
+
},
|
|
83
|
+
"devDependencies": {
|
|
84
|
+
"@types/express": "^5.0.0"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Using Express 5.x (latest for Node 18+ ESM support). Only `express` is needed as a runtime dependency — `@types/express` is a dev dependency.
|
|
90
|
+
|
|
91
|
+
### Server Setup (`dashboard/src/index.ts`)
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
Express static middleware → dashboard/public/
|
|
95
|
+
REST API routes → /api/*
|
|
96
|
+
SPA catch-all → index.html for unknown routes (hash-based routing)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Key server configuration:
|
|
100
|
+
- Static files from `dashboard/public/` served at `/`
|
|
101
|
+
- JSON response compression via Express built-in compression (or `compression` middleware)
|
|
102
|
+
- No CORS headers needed (localhost only)
|
|
103
|
+
- Error middleware returning JSON for API, HTML for static (or just JSON everywhere since SPA)
|
|
104
|
+
- Graceful shutdown on SIGINT/SIGTERM
|
|
105
|
+
|
|
106
|
+
### Port resolution
|
|
107
|
+
|
|
108
|
+
1. Check `--port` CLI arg
|
|
109
|
+
2. Fallback to `DEEPENC_DASHBOARD_PORT` env var
|
|
110
|
+
3. Default to `3000`
|
|
111
|
+
|
|
112
|
+
Print the URL on startup:
|
|
113
|
+
```
|
|
114
|
+
=> deepenc-harness dashboard running at http://127.0.0.1:3000
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 5. Data Layer
|
|
120
|
+
|
|
121
|
+
### 5.1 Sessions Directory Layout
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
../sessions/
|
|
125
|
+
daily/
|
|
126
|
+
YYYY-MM-DD.json # Daily summary + session breakdown
|
|
127
|
+
YYYY-MM-DD-<slug>-<id>.json.bz2 # Full session transcript (bzip2)
|
|
128
|
+
YYYY-MM-DD-<slug>-<id>.md # Human-readable session summary
|
|
129
|
+
YYYY-MM-DD-<slug>-<id>.sha256 # Integrity hash
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 5.2 Daily File Schema (two formats exist)
|
|
133
|
+
|
|
134
|
+
Format A (wrapped in `dashboard` key):
|
|
135
|
+
```typescript
|
|
136
|
+
interface DailyFileFormatA {
|
|
137
|
+
dashboard: {
|
|
138
|
+
metadata: { generated_at: string; audited: boolean; audit_note: string };
|
|
139
|
+
daily_summary: DailySummary;
|
|
140
|
+
session_breakdown: SessionBreakdownEntry[];
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Format B (flat):
|
|
146
|
+
```typescript
|
|
147
|
+
interface DailyFileFormatB {
|
|
148
|
+
date: string;
|
|
149
|
+
total_prompter_time_hours: number;
|
|
150
|
+
total_sme_time_hours: number;
|
|
151
|
+
ai_multiplier: number;
|
|
152
|
+
total_sessions: number;
|
|
153
|
+
top_subject_areas: SubjectArea[];
|
|
154
|
+
session_breakdown: SessionBreakdownEntry[];
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Both are normalized into a single `NormalizedDay` type on read.
|
|
159
|
+
|
|
160
|
+
### 5.3 Types (`dashboard/src/types.ts`)
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
export interface NormalizedDay {
|
|
164
|
+
date: string;
|
|
165
|
+
metadata?: { generated_at: string; audited: boolean; audit_note: string };
|
|
166
|
+
total_prompter_time_hours: number;
|
|
167
|
+
total_sme_time_hours: number;
|
|
168
|
+
ai_multiplier: number;
|
|
169
|
+
total_sessions: number;
|
|
170
|
+
top_subject_areas: SubjectArea[];
|
|
171
|
+
session_breakdown: SessionBreakdownEntry[];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface SubjectArea {
|
|
175
|
+
name: string;
|
|
176
|
+
prompter_time_hours: number;
|
|
177
|
+
sme_time_hours: number;
|
|
178
|
+
ai_multiplier: number;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface SessionBreakdownEntry {
|
|
182
|
+
session_id: string;
|
|
183
|
+
duration_minutes: number;
|
|
184
|
+
prompter_time_minutes: number;
|
|
185
|
+
sme_time_minutes: number;
|
|
186
|
+
top_component_summary: string;
|
|
187
|
+
tags: string[];
|
|
188
|
+
human_confidence: "high" | "medium" | "low";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface SessionDetail {
|
|
192
|
+
info: {
|
|
193
|
+
id: string;
|
|
194
|
+
slug: string;
|
|
195
|
+
title: string;
|
|
196
|
+
agent: string;
|
|
197
|
+
model: { id: string; providerID: string };
|
|
198
|
+
summary: { additions: number; deletions: number; files: number };
|
|
199
|
+
time: { created: number; updated: number };
|
|
200
|
+
};
|
|
201
|
+
messages: SessionMessage[];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface SessionMessage {
|
|
205
|
+
info: {
|
|
206
|
+
role: string;
|
|
207
|
+
time: { created: number };
|
|
208
|
+
agent: string;
|
|
209
|
+
model: { providerID: string; modelID: string };
|
|
210
|
+
summary: { diffs: Array<{ path: string; type: string; lines: Record<string, number> }> };
|
|
211
|
+
id: string;
|
|
212
|
+
};
|
|
213
|
+
parts: Array<{ type: string; text?: string }>;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface GitLogEntry {
|
|
217
|
+
commit: string;
|
|
218
|
+
author: string;
|
|
219
|
+
date: string;
|
|
220
|
+
message: string;
|
|
221
|
+
files_changed: number;
|
|
222
|
+
insertions: number;
|
|
223
|
+
deletions: number;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface GitStats {
|
|
227
|
+
total_commits: number;
|
|
228
|
+
total_files_changed: number;
|
|
229
|
+
total_insertions: number;
|
|
230
|
+
total_deletions: number;
|
|
231
|
+
per_date: Record<string, { commits: number; insertions: number; deletions: number }>;
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### 5.4 Session Data Service (`dashboard/src/services/sessions.ts`)
|
|
236
|
+
|
|
237
|
+
**Reading daily files:**
|
|
238
|
+
- `listDays(): Promise<string[]>` — readdir on `../sessions/daily/`, filter `*.json`, extract `YYYY-MM-DD`
|
|
239
|
+
- `getDay(date: string): Promise<NormalizedDay>` — read JSON, normalize from either Format A or Format B, cache result
|
|
240
|
+
|
|
241
|
+
**Reading session detail:**
|
|
242
|
+
- `getSession(sessionId: string): Promise<SessionDetail>` — find `{sessionId}.json.bz2` in `../sessions/`, decompress with `brotli`/`bzip2` via `child_process` or a pure-JS bzip2 library, parse JSON
|
|
243
|
+
|
|
244
|
+
**Search:**
|
|
245
|
+
- `search(query: string): Promise<SearchResult[]>` — iterate across all daily files and session details, match against `top_component_summary`, `tags`, and message text
|
|
246
|
+
|
|
247
|
+
### 5.5 Git Service (`dashboard/src/services/git.ts`)
|
|
248
|
+
|
|
249
|
+
All Git operations shell out via `child_process.execFile` to `git` in the project root (`../` or `process.cwd()`):
|
|
250
|
+
|
|
251
|
+
- `getGitLog(since?: string, until?: string): Promise<GitLogEntry[]>` — `git log --oneline --stat --since=... --until=...`
|
|
252
|
+
- `getGitStats(): Promise<GitStats>` — `git shortlog -sn` and `git diff --stat` aggregated by day
|
|
253
|
+
- `getCommitDiff(commit: string): Promise<string>` — `git show <commit>` for full diff
|
|
254
|
+
|
|
255
|
+
### 5.6 Caching
|
|
256
|
+
|
|
257
|
+
Simple in-memory Map-based cache in `dashboard/src/services/cache.ts`:
|
|
258
|
+
- TTL-based expiry (default 60s for daily files, 300s for session details, no cache for git to keep it live)
|
|
259
|
+
- Manual invalidation when a new daily file appears (detected via fs.watch on `../sessions/daily/`)
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## 6. REST API Endpoints
|
|
264
|
+
|
|
265
|
+
### 6.1 Sessions & Daily
|
|
266
|
+
|
|
267
|
+
| Method | Path | Description | Response |
|
|
268
|
+
|--------|------|-------------|----------|
|
|
269
|
+
| GET | `/api/days` | List available daily dates | `{ days: string[] }` |
|
|
270
|
+
| GET | `/api/days/:date` | Full normalized day payload | `NormalizedDay` |
|
|
271
|
+
| GET | `/api/days/latest` | Most recent day | `NormalizedDay` |
|
|
272
|
+
| GET | `/api/sessions` | All sessions across all days (flat, paginated) | `{ sessions: SessionBreakdownEntry[], total: number }` |
|
|
273
|
+
| GET | `/api/sessions/:sessionId` | Session detail + transcript | `{ summary: SessionBreakdownEntry, detail: SessionDetail }` |
|
|
274
|
+
| GET | `/api/stats` | Cross-day aggregate statistics | `{ total_days, total_sessions, total_prompter_time, total_sme_time, avg_multiplier, per_day: NormalizedDay[] }` |
|
|
275
|
+
| GET | `/api/search?q=string` | Full-text search | `{ results: SearchResult[] }` |
|
|
276
|
+
|
|
277
|
+
### 6.2 Git
|
|
278
|
+
|
|
279
|
+
| Method | Path | Description | Response |
|
|
280
|
+
|--------|------|-------------|----------|
|
|
281
|
+
| GET | `/api/git/log?since=YYYY-MM-DD&until=YYYY-MM-DD` | Git commit log | `{ commits: GitLogEntry[] }` |
|
|
282
|
+
| GET | `/api/git/stats` | Aggregated git contribution stats | `GitStats` |
|
|
283
|
+
| GET | `/api/git/commit/:hash` | Single commit diff | `{ diff: string }` |
|
|
284
|
+
|
|
285
|
+
### 6.3 Health
|
|
286
|
+
|
|
287
|
+
| Method | Path | Description | Response |
|
|
288
|
+
|--------|------|-------------|----------|
|
|
289
|
+
| GET | `/api/health` | Liveness check | `{ status: "ok", days_count: number, sessions_count: number }` |
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## 7. Frontend (Vanilla HTML/CSS/JS)
|
|
294
|
+
|
|
295
|
+
### 7.1 Architecture
|
|
296
|
+
|
|
297
|
+
Single-page application using hash-based routing:
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
#/ → Overview dashboard
|
|
301
|
+
#/daily/2026-05-11 → Single day detail
|
|
302
|
+
#/session/<id> → Session transcript reader
|
|
303
|
+
#/git → Git timeline overlay
|
|
304
|
+
#/search?q=... → Search results
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Routing implemented in `app.js` via `hashchange` event. No framework — pure DOM manipulation.
|
|
308
|
+
|
|
309
|
+
### 7.2 Pages
|
|
310
|
+
|
|
311
|
+
**Overview Dashboard (`#/`):**
|
|
312
|
+
- Summary cards: total sessions, total prompter hours, total SME hours, avg AI multiplier
|
|
313
|
+
- AI multiplier trend chart (line chart, Chart.js)
|
|
314
|
+
- Top subject areas bar chart (top 15 by SME hours)
|
|
315
|
+
- Session count per day bar chart
|
|
316
|
+
- Latest sessions table (10 most recent)
|
|
317
|
+
|
|
318
|
+
**Daily Detail (`#/daily/:date`):**
|
|
319
|
+
- Date header with aggregate numbers
|
|
320
|
+
- Session breakdown table (sortable by duration, prompter time, SME time)
|
|
321
|
+
- Pie chart of session time distribution
|
|
322
|
+
- Tag cloud (top tags by frequency)
|
|
323
|
+
- Subject area breakdown bar chart
|
|
324
|
+
|
|
325
|
+
**Session Detail (`#/session/:id`):**
|
|
326
|
+
- Session metadata card (title, agent, model, duration, file stats)
|
|
327
|
+
- Tag badges
|
|
328
|
+
- Summary description
|
|
329
|
+
- Expandable message transcript
|
|
330
|
+
- File diff list (from `messages[].info.summary.diffs`)
|
|
331
|
+
- Link to git commit if applicable
|
|
332
|
+
|
|
333
|
+
**Git Timeline (`#/git`):**
|
|
334
|
+
- Commits per day bar chart overlaid on session time
|
|
335
|
+
- Commit list with expandable diffs
|
|
336
|
+
|
|
337
|
+
**Search (`#/search?q=...`):**
|
|
338
|
+
- Results grouped by session
|
|
339
|
+
- Highlighted matching text in summaries
|
|
340
|
+
|
|
341
|
+
### 7.3 Chart.js Integration
|
|
342
|
+
|
|
343
|
+
Loaded from CDN:
|
|
344
|
+
```html
|
|
345
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Charts used:
|
|
349
|
+
- **Line chart** — AI multiplier trend, SME time trend
|
|
350
|
+
- **Bar chart** — top subject areas, session count per day, daily prompter/SME comparison
|
|
351
|
+
- **Pie/doughnut** — session time distribution, tag composition
|
|
352
|
+
- **Stacked bar** — prompter vs SME time per session
|
|
353
|
+
|
|
354
|
+
Chart instances are destroyed and recreated on page navigation to avoid canvas conflicts.
|
|
355
|
+
|
|
356
|
+
### 7.4 Styling
|
|
357
|
+
|
|
358
|
+
- Dark theme (matching a development tool aesthetic)
|
|
359
|
+
- CSS custom properties for theming
|
|
360
|
+
- Grid/flexbox layout
|
|
361
|
+
- Responsive within reason (desktop-first, localhost)
|
|
362
|
+
- No CSS framework — lightweight, minimal styles
|
|
363
|
+
|
|
364
|
+
### 7.5 Client-Side Data Fetching
|
|
365
|
+
|
|
366
|
+
All API calls via `fetch()`. Helper function:
|
|
367
|
+
```javascript
|
|
368
|
+
async function api(path) {
|
|
369
|
+
const res = await fetch(path);
|
|
370
|
+
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
|
371
|
+
return res.json();
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## 8. Implementation Plan
|
|
378
|
+
|
|
379
|
+
### Phase 1: Scaffolding
|
|
380
|
+
1. Create `dashboard/` directory structure
|
|
381
|
+
2. Add `express` and `@types/express` to root `package.json` (or workspace)
|
|
382
|
+
3. Implement `dashboard/src/index.ts` — Express server with static + route registration
|
|
383
|
+
4. Integrate `dashboard` command into `src/index.ts` CLI parser
|
|
384
|
+
|
|
385
|
+
### Phase 2: Data Layer
|
|
386
|
+
5. Implement `dashboard/src/types.ts`
|
|
387
|
+
6. Implement `dashboard/src/services/sessions.ts` — daily file reader + normalizer + bz2 reader
|
|
388
|
+
7. Implement `dashboard/src/services/git.ts` — git log/stats via child_process
|
|
389
|
+
8. Implement `dashboard/src/services/cache.ts` — in-memory TTL cache
|
|
390
|
+
|
|
391
|
+
### Phase 3: API
|
|
392
|
+
9. Implement `dashboard/src/routes/api.ts` — all endpoints
|
|
393
|
+
|
|
394
|
+
### Phase 4: Frontend
|
|
395
|
+
10. Create `dashboard/public/index.html` — SPA shell, hash router, page container
|
|
396
|
+
11. Create `dashboard/public/style.css` — dark theme, layout, components
|
|
397
|
+
12. Create `dashboard/public/app.js` — routing, API client, Chart.js rendering, all pages
|
|
398
|
+
|
|
399
|
+
### Phase 5: Polish
|
|
400
|
+
13. Start-up banner with URL
|
|
401
|
+
14. Graceful shutdown
|
|
402
|
+
15. Error handling for missing sessions directory, malformed JSON, bz2 read failures
|
|
403
|
+
16. `fs.watch` on sessions directory for auto-refresh (optional — SSE push to client)
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## 9. Non-Goals / Future Considerations
|
|
408
|
+
|
|
409
|
+
- **Authentication**: Not needed — localhost only.
|
|
410
|
+
- **Bundling**: Not needed — vanilla JS with CDN Chart.js is sufficient.
|
|
411
|
+
- **WebSockets**: Nice-to-have for live reload when new session data appears. Initial implementation uses manual refresh. SSE could be added later.
|
|
412
|
+
- **CSS framework**: Not needed — custom styles are minimal and keep zero dependencies.
|
|
413
|
+
- **Testing**: The dashboard is a read-only visualization tool. Manual testing is sufficient for initial release. If automated tests are desired, use supertest for API + Playwright for E2E.
|
|
414
|
+
- **npm workspace**: Not needed initially. Dashboard lives in `dashboard/` and is imported lazily. If the dependency graph becomes complex, it can be promoted to a workspace.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Curl-based API tests for the deepenc-harness dashboard
|
|
3
|
+
# Usage: bash dashboard/test-api.sh [port]
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
PORT="${1:-3098}"
|
|
7
|
+
BASE="http://127.0.0.1:$PORT"
|
|
8
|
+
PASS=0
|
|
9
|
+
FAIL=0
|
|
10
|
+
|
|
11
|
+
ok() { PASS=$((PASS+1)); echo " PASS: $1"; }
|
|
12
|
+
fail() { FAIL=$((FAIL+1)); echo " FAIL: $1"; }
|
|
13
|
+
|
|
14
|
+
check_json() {
|
|
15
|
+
local desc="$1" url="$2" expected="$3"
|
|
16
|
+
local body
|
|
17
|
+
body=$(curl -sf "$url" 2>/dev/null) || { fail "$desc (curl failed)"; return; }
|
|
18
|
+
if echo "$body" | python3 -c "
|
|
19
|
+
import sys, json
|
|
20
|
+
try:
|
|
21
|
+
d = json.load(sys.stdin)
|
|
22
|
+
exec('''
|
|
23
|
+
$expected
|
|
24
|
+
''')
|
|
25
|
+
sys.exit(0)
|
|
26
|
+
except Exception as e:
|
|
27
|
+
print(' expected:', e)
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
" 2>/dev/null; then
|
|
30
|
+
ok "$desc"
|
|
31
|
+
else
|
|
32
|
+
fail "$desc"
|
|
33
|
+
fi
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
check_status() {
|
|
37
|
+
local desc="$1" url="$2" expected_status="$3"
|
|
38
|
+
local status
|
|
39
|
+
status=$(curl -s -o /dev/null -w "%{http_code}" "$url" 2>/dev/null) || { fail "$desc (curl failed)"; return; }
|
|
40
|
+
if [ "$status" = "$expected_status" ]; then
|
|
41
|
+
ok "$desc"
|
|
42
|
+
else
|
|
43
|
+
fail "$desc (expected $expected_status, got $status)"
|
|
44
|
+
fi
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
echo "Starting dashboard server on port $PORT..."
|
|
48
|
+
node lib/index.js dashboard --port "$PORT" > /tmp/dash-test.log 2>&1 &
|
|
49
|
+
SERVER_PID=$!
|
|
50
|
+
trap "kill $SERVER_PID 2>/dev/null; exit 1" INT TERM
|
|
51
|
+
|
|
52
|
+
sleep 2
|
|
53
|
+
|
|
54
|
+
echo ""
|
|
55
|
+
echo "=== API Tests ==="
|
|
56
|
+
echo ""
|
|
57
|
+
|
|
58
|
+
check_json "Health endpoint returns ok" \
|
|
59
|
+
"$BASE/api/health" \
|
|
60
|
+
"assert d['status'] == 'ok'"
|
|
61
|
+
|
|
62
|
+
check_json "Days endpoint lists days" \
|
|
63
|
+
"$BASE/api/days" \
|
|
64
|
+
"assert len(d['days']) >= 2; assert '2026-05-11' in d['days']"
|
|
65
|
+
|
|
66
|
+
check_json "Latest day returns correct date" \
|
|
67
|
+
"$BASE/api/days/latest" \
|
|
68
|
+
"assert d['date'] == '2026-05-12'"
|
|
69
|
+
|
|
70
|
+
check_json "Specific day returns correct data" \
|
|
71
|
+
"$BASE/api/days/2026-05-11" \
|
|
72
|
+
"assert d['total_sessions'] == 10; assert d['total_prompter_time_hours'] == 18.8"
|
|
73
|
+
|
|
74
|
+
check_status "Missing day returns 404" \
|
|
75
|
+
"$BASE/api/days/2099-01-01" 404
|
|
76
|
+
|
|
77
|
+
check_json "Sessions endpoint returns list" \
|
|
78
|
+
"$BASE/api/sessions" \
|
|
79
|
+
"assert d['total'] >= 18; assert len(d['sessions']) > 0"
|
|
80
|
+
|
|
81
|
+
check_json "Session detail returns summary + detail" \
|
|
82
|
+
"$BASE/api/sessions/2026-05-12-asm-optimizer-dq-implementation" \
|
|
83
|
+
"assert d['summary'] is not None; assert d['detail'] is not None"
|
|
84
|
+
|
|
85
|
+
check_status "Missing session returns 404" \
|
|
86
|
+
"$BASE/api/sessions/nonexistent-session" 404
|
|
87
|
+
|
|
88
|
+
check_json "Stats returns cross-day aggregates" \
|
|
89
|
+
"$BASE/api/stats" \
|
|
90
|
+
"assert d['total_days'] == 2; assert d['total_sessions'] == 18"
|
|
91
|
+
|
|
92
|
+
check_json "Search by summary returns results" \
|
|
93
|
+
"$BASE/api/search?q=SIMD" \
|
|
94
|
+
"assert len(d['results']) > 0"
|
|
95
|
+
|
|
96
|
+
check_json "Search with no match returns empty" \
|
|
97
|
+
"$BASE/api/search?q=zzz_nonexistent_zzz" \
|
|
98
|
+
"assert len(d['results']) == 0"
|
|
99
|
+
|
|
100
|
+
check_json "Search by transcript content returns results" \
|
|
101
|
+
"$BASE/api/search?q=AVX2" \
|
|
102
|
+
"assert len(d['results']) > 0"
|
|
103
|
+
|
|
104
|
+
check_json "Git log returns commits" \
|
|
105
|
+
"$BASE/api/git/log" \
|
|
106
|
+
"assert len(d['commits']) > 0"
|
|
107
|
+
|
|
108
|
+
check_json "Git stats returns per_date" \
|
|
109
|
+
"$BASE/api/git/stats" \
|
|
110
|
+
"assert d['total_commits'] > 0; assert 'per_date' in d"
|
|
111
|
+
|
|
112
|
+
check_json "Git commit diff returns content" \
|
|
113
|
+
"$BASE/api/git/commit/HEAD" \
|
|
114
|
+
"assert len(d['diff']) > 100"
|
|
115
|
+
|
|
116
|
+
check_status "Missing commit returns 404" \
|
|
117
|
+
"$BASE/api/git/commit/0000000000000000000000000000000000000000" 404
|
|
118
|
+
|
|
119
|
+
echo ""
|
|
120
|
+
echo "=== Results ==="
|
|
121
|
+
echo "Passed: $PASS"
|
|
122
|
+
echo "Failed: $FAIL"
|
|
123
|
+
|
|
124
|
+
kill $SERVER_PID 2>/dev/null
|
|
125
|
+
wait $SERVER_PID 2>/dev/null || true
|
|
126
|
+
|
|
127
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"isolatedModules": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts"]
|
|
16
|
+
}
|
package/lib/util/paths.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url'
|
|
2
2
|
import { dirname, resolve } from 'node:path'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
3
4
|
|
|
4
5
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
5
6
|
export const PKG_ROOT = resolve(__dirname, '../..')
|
|
6
7
|
|
|
7
8
|
export function resolveScript(path) {
|
|
8
|
-
|
|
9
|
+
const base = resolve(PKG_ROOT, 'scripts', path)
|
|
10
|
+
if (existsSync(base)) return base
|
|
11
|
+
if (existsSync(base + '.js')) return base + '.js'
|
|
12
|
+
if (existsSync(base + '.mjs')) return base + '.mjs'
|
|
13
|
+
if (existsSync(base + '.sh')) return base + '.sh'
|
|
14
|
+
const asDir = resolve(base, 'index.js')
|
|
15
|
+
if (existsSync(asDir)) return asDir
|
|
16
|
+
return base
|
|
9
17
|
}
|
|
10
18
|
|
|
11
19
|
export function resolveSkillScript(skillName, path) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opensassi/opencode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Agent skill harness for opencode — bootstrap, system-design, git workflow, profiling, and more",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"lib/",
|
|
16
16
|
"skills/",
|
|
17
17
|
"scripts/",
|
|
18
|
+
"dashboard/",
|
|
18
19
|
"AGENTS.md",
|
|
19
20
|
"skills-index.json",
|
|
20
21
|
"!scripts/FlameGraph/",
|
|
@@ -28,12 +29,19 @@
|
|
|
28
29
|
"validate-all": "node scripts/extract-artifacts.js --all && node scripts/test-artifacts.js",
|
|
29
30
|
"verify-animation": "node scripts/verify-artifact.js",
|
|
30
31
|
"check-artifacts": "node scripts/check-artifacts.js",
|
|
32
|
+
"build:dashboard": "tsc -p dashboard/tsconfig.json",
|
|
33
|
+
"test:dashboard": "npx playwright test --config dashboard/playwright.config.ts",
|
|
31
34
|
"start": "bin/opencode.js"
|
|
32
35
|
},
|
|
33
36
|
"engines": {
|
|
34
37
|
"node": ">=22"
|
|
35
38
|
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"express": "^5.2.1"
|
|
41
|
+
},
|
|
36
42
|
"devDependencies": {
|
|
43
|
+
"@playwright/test": "^1.52.0",
|
|
44
|
+
"@types/express": "^5.0.0",
|
|
37
45
|
"playwright": "^1.60.0",
|
|
38
46
|
"sharp": "^0.34.5"
|
|
39
47
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { startDashboard } from '../dashboard/dist/index.js'
|
|
3
|
+
|
|
4
|
+
const args = process.argv.slice(2)
|
|
5
|
+
const portIdx = args.indexOf('--port')
|
|
6
|
+
const hostIdx = args.indexOf('--host')
|
|
7
|
+
const sessionsIdx = args.indexOf('--sessions')
|
|
8
|
+
const repoIdx = args.indexOf('--repo')
|
|
9
|
+
const gitSinceIdx = args.indexOf('--git-since')
|
|
10
|
+
|
|
11
|
+
startDashboard({
|
|
12
|
+
port: portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : undefined,
|
|
13
|
+
host: hostIdx !== -1 ? args[hostIdx + 1] : undefined,
|
|
14
|
+
sessionsDir: sessionsIdx !== -1 ? args[sessionsIdx + 1] : undefined,
|
|
15
|
+
repoDir: repoIdx !== -1 ? args[repoIdx + 1] : undefined,
|
|
16
|
+
gitSince: gitSinceIdx !== -1 ? args[gitSinceIdx + 1] : undefined,
|
|
17
|
+
})
|