@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 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.12",
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
201
+ .replace(/"/g, "&quot;");
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,