@tekyzinc/gsd-t 3.18.12 → 3.18.13
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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/scripts/gsd-t-dashboard-server.js +88 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [3.18.13] - 2026-04-23
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Dashboard `/transcripts` returned raw JSON to browsers** — after the v3.18.12 always-enabled Live Stream button fix, opening the dashboard with no spawn data and clicking Live Stream landed the user on `{"spawns":[]}` because `/transcripts` always served JSON. The route now does Accept-header content negotiation: browsers (`Accept: text/html`) get a proper dark-themed HTML index page with a sortable table of spawns (or a friendly empty state with a `/gsd-t-quick` CTA when no transcripts exist); programmatic clients (`fetch()` default `*/*`, or explicit `application/json`) keep getting the JSON shape the dashboard polling code already consumes — full back-compat.
|
|
10
|
+
|
|
5
11
|
## [3.18.12] - 2026-04-23
|
|
6
12
|
|
|
7
13
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "3.18.
|
|
3
|
+
"version": "3.18.13",
|
|
4
4
|
"description": "GSD-T: Contract-Driven Development for Claude Code — 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
|
|
5
5
|
"author": "Tekyz, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -181,10 +181,97 @@ function handleTranscriptsList(req, res, projectDir) {
|
|
|
181
181
|
const sorted = idx.spawns
|
|
182
182
|
.slice()
|
|
183
183
|
.sort((a, b) => (Date.parse(b.startedAt) || 0) - (Date.parse(a.startedAt) || 0));
|
|
184
|
+
|
|
185
|
+
// Content negotiation: browser navigations send Accept: text/html, fetch()
|
|
186
|
+
// defaults to */*. We serve HTML only when the client explicitly asks for it,
|
|
187
|
+
// so the existing dashboard fetch (which expects JSON) stays unaffected.
|
|
188
|
+
const accept = String(req.headers["accept"] || "");
|
|
189
|
+
if (accept.includes("text/html")) {
|
|
190
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
191
|
+
res.end(renderTranscriptsHtml(sorted));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
184
194
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
185
195
|
res.end(JSON.stringify({ spawns: sorted }));
|
|
186
196
|
}
|
|
187
197
|
|
|
198
|
+
function renderTranscriptsHtml(spawns) {
|
|
199
|
+
const escape = (s) => String(s == null ? "" : s)
|
|
200
|
+
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
201
|
+
.replace(/"/g, """);
|
|
202
|
+
const fmtDuration = (a, b) => {
|
|
203
|
+
const start = Date.parse(a); const end = Date.parse(b || "") || Date.now();
|
|
204
|
+
if (!Number.isFinite(start)) return "—";
|
|
205
|
+
const ms = Math.max(0, end - start);
|
|
206
|
+
const s = Math.floor(ms / 1000); const m = Math.floor(s / 60); const r = s % 60;
|
|
207
|
+
return m > 0 ? `${m}m ${r}s` : `${r}s`;
|
|
208
|
+
};
|
|
209
|
+
const fmtTime = (s) => { const d = new Date(s); return Number.isFinite(d.getTime()) ? d.toLocaleString() : "—"; };
|
|
210
|
+
const liveStatuses = new Set(["initializing", "running"]);
|
|
211
|
+
const isLive = (s) => liveStatuses.has(s);
|
|
212
|
+
|
|
213
|
+
const rows = spawns.map((s) => {
|
|
214
|
+
const live = isLive(s.status);
|
|
215
|
+
const statusBadge = `<span class="status status-${escape(s.status || 'unknown')}">${escape(s.status || 'unknown')}</span>`;
|
|
216
|
+
return `<tr class="${live ? 'row-live' : ''}">
|
|
217
|
+
<td><a href="/transcript/${encodeURIComponent(s.spawnId)}">${escape(s.spawnId)}</a></td>
|
|
218
|
+
<td>${escape(s.command || '—')}</td>
|
|
219
|
+
<td>${escape(s.description || '—')}</td>
|
|
220
|
+
<td>${statusBadge}</td>
|
|
221
|
+
<td>${escape(fmtTime(s.startedAt))}</td>
|
|
222
|
+
<td>${escape(fmtDuration(s.startedAt, s.endedAt))}</td>
|
|
223
|
+
</tr>`;
|
|
224
|
+
}).join("");
|
|
225
|
+
|
|
226
|
+
const empty = `<div class="empty">
|
|
227
|
+
<h2>No spawn transcripts yet</h2>
|
|
228
|
+
<p>Transcripts appear here as soon as the first agent spawns. Run any GSD-T command (for example <code>/gsd-t-quick</code>) to generate one.</p>
|
|
229
|
+
<p><a href="/">← Back to dashboard</a></p>
|
|
230
|
+
</div>`;
|
|
231
|
+
|
|
232
|
+
const table = spawns.length ? `<table>
|
|
233
|
+
<thead><tr><th>Spawn ID</th><th>Command</th><th>Description</th><th>Status</th><th>Started</th><th>Duration</th></tr></thead>
|
|
234
|
+
<tbody>${rows}</tbody>
|
|
235
|
+
</table>` : empty;
|
|
236
|
+
|
|
237
|
+
return `<!DOCTYPE html>
|
|
238
|
+
<html lang="en"><head><meta charset="UTF-8"><title>GSD-T Transcripts</title>
|
|
239
|
+
<style>
|
|
240
|
+
:root{--bg:#0d1117;--surface:#161b22;--border:#30363d;--text:#e6edf3;--muted:#7d8590;
|
|
241
|
+
--green:#3fb950;--green-bg:#1a3a1e;--blue:#388bfd;--blue-bg:#1f3a5f;--yellow:#d29922;--red:#f85149;
|
|
242
|
+
--font:'SF Mono','Fira Code',Menlo,monospace;}
|
|
243
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
244
|
+
body{background:var(--bg);color:var(--text);font-family:var(--font);font-size:13px;padding:20px;line-height:1.5}
|
|
245
|
+
.hdr{display:flex;align-items:center;gap:12px;margin-bottom:18px;padding-bottom:12px;border-bottom:1px solid var(--border)}
|
|
246
|
+
.logo{color:var(--blue);font-weight:bold;font-size:14px}
|
|
247
|
+
.hright{margin-left:auto;color:var(--muted);font-size:11px}
|
|
248
|
+
a{color:var(--blue);text-decoration:none}a:hover{text-decoration:underline}
|
|
249
|
+
table{width:100%;border-collapse:collapse;background:var(--surface);border:1px solid var(--border);border-radius:6px;overflow:hidden}
|
|
250
|
+
th,td{padding:8px 12px;text-align:left;border-bottom:1px solid var(--border);font-size:12px}
|
|
251
|
+
th{background:#1c2128;color:var(--muted);text-transform:uppercase;font-size:10px;letter-spacing:0.5px}
|
|
252
|
+
tbody tr:last-child td{border-bottom:none}
|
|
253
|
+
tbody tr:hover{background:#1c2128}
|
|
254
|
+
tr.row-live{background:var(--green-bg)}
|
|
255
|
+
.status{display:inline-block;padding:2px 8px;border-radius:10px;font-size:10px;text-transform:uppercase;letter-spacing:0.5px;border:1px solid}
|
|
256
|
+
.status-running,.status-initializing{background:var(--green-bg);color:var(--green);border-color:var(--green)}
|
|
257
|
+
.status-done{background:var(--blue-bg);color:var(--blue);border-color:var(--blue)}
|
|
258
|
+
.status-stopped{background:#2a2a2a;color:var(--muted);border-color:var(--border)}
|
|
259
|
+
.status-failed,.status-crashed{background:#3a1a1a;color:var(--red);border-color:var(--red)}
|
|
260
|
+
.status-unknown{color:var(--muted);border-color:var(--border)}
|
|
261
|
+
.empty{text-align:center;padding:60px 20px;color:var(--muted);background:var(--surface);border:1px solid var(--border);border-radius:6px}
|
|
262
|
+
.empty h2{color:var(--text);margin-bottom:12px;font-size:16px}
|
|
263
|
+
.empty p{margin-bottom:8px}
|
|
264
|
+
.empty code{background:#1c2128;padding:2px 6px;border-radius:3px;color:var(--green)}
|
|
265
|
+
</style></head><body>
|
|
266
|
+
<div class="hdr">
|
|
267
|
+
<span class="logo">GSD-T Transcripts</span>
|
|
268
|
+
<a href="/" style="font-size:11px">← Dashboard</a>
|
|
269
|
+
<span class="hright">${spawns.length} spawn${spawns.length === 1 ? '' : 's'}</span>
|
|
270
|
+
</div>
|
|
271
|
+
${table}
|
|
272
|
+
</body></html>`;
|
|
273
|
+
}
|
|
274
|
+
|
|
188
275
|
function handleTranscriptPage(req, res, spawnId, transcriptHtmlPath) {
|
|
189
276
|
if (!isValidSpawnId(spawnId)) { res.writeHead(400); res.end("Invalid spawn id"); return; }
|
|
190
277
|
fs.readFile(transcriptHtmlPath, (err, data) => {
|
|
@@ -587,6 +674,7 @@ module.exports = {
|
|
|
587
674
|
readIndexEntry,
|
|
588
675
|
isValidSpawnId,
|
|
589
676
|
handleTranscriptsList,
|
|
677
|
+
renderTranscriptsHtml,
|
|
590
678
|
handleTranscriptStream,
|
|
591
679
|
handleTranscriptPage,
|
|
592
680
|
handleTranscriptKill,
|