@tenonhq/dovetail-dashboard 0.0.31 → 0.0.32

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-dashboard",
3
- "version": "0.0.31",
3
+ "version": "0.0.32",
4
4
  "description": "Update Set Dashboard for Dovetail",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -173,9 +173,16 @@
173
173
  }
174
174
  .cp-list-item:hover { background: var(--bg-subtle); }
175
175
  .cp-list-item.active {
176
+ /* Selected plan: filled raised surface + brand-emerald bar + bold.
177
+ --brand flips to a bright emerald (#32b57f) in dark mode, so the bar
178
+ stays legible without using the unreadable deep green on dark grounds. */
176
179
  background: var(--surface-raised);
177
- border-left: 3px solid var(--accent);
180
+ border-left: 4px solid var(--brand);
181
+ padding-left: 12px;
182
+ font-weight: var(--weight-semibold);
183
+ box-shadow: inset 0 0 0 1px var(--border-subtle);
178
184
  }
185
+ .cp-list-item.active:hover { background: var(--surface-raised); }
179
186
  .cp-list-row { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
180
187
  .cp-list-title { font-weight: var(--weight-semibold); font-size: 13px; color: var(--fg); }
181
188
  .cp-list-meta { font-size: 11px; color: var(--fg-muted); display: flex; gap: 8px; }
@@ -278,7 +285,7 @@
278
285
  padding: 1px 6px;
279
286
  border-radius: var(--radius-xs);
280
287
  font-size: 13px;
281
- color: var(--emerald-700);
288
+ color: var(--brand);
282
289
  }
283
290
  .cp-tab-panel pre {
284
291
  background: var(--neutral-900);
@@ -292,7 +299,7 @@
292
299
  .cp-tab-panel th, .cp-tab-panel td { border: 1px solid var(--border); padding: 7px 11px; }
293
300
  .cp-tab-panel th { background: var(--bg-subtle); }
294
301
  .cp-tab-panel a { color: var(--brand); }
295
- .cp-tab-panel a:hover { color: var(--emerald-700); }
302
+ .cp-tab-panel a:hover { color: var(--brand); }
296
303
  .cp-tab-panel blockquote {
297
304
  border-left: 3px solid var(--accent);
298
305
  padding-left: 12px;
@@ -635,7 +642,7 @@
635
642
  color: var(--fg-muted);
636
643
  text-align: center;
637
644
  }
638
- .cp-c-step-done .cp-c-step-label { color: var(--emerald-700); }
645
+ .cp-c-step-done .cp-c-step-label { color: var(--brand); }
639
646
  .cp-c-step-active .cp-c-step-label { color: var(--fg); font-weight: var(--weight-bold); }
640
647
  .cp-c-step-error .cp-c-step-label { color: var(--error-700); }
641
648
  .cp-c-step-note {
@@ -1526,3 +1533,48 @@
1526
1533
  margin-top: 10px;
1527
1534
  font-size: 13px;
1528
1535
  }
1536
+
1537
+ /* ─── Version history (History tab) ─────────────────────────────────────────
1538
+ * Snapshot rows with View (inline expand) + Restore. Uses semantic tokens so
1539
+ * it tracks light/dark; --brand flips to bright emerald in dark mode. */
1540
+ .cp-versions { display: flex; flex-direction: column; gap: 8px; }
1541
+ .cp-version {
1542
+ border: 1px solid var(--border);
1543
+ border-radius: var(--radius-md);
1544
+ background: var(--surface);
1545
+ }
1546
+ .cp-version--current {
1547
+ border-left: 4px solid var(--brand);
1548
+ background: var(--surface-raised);
1549
+ }
1550
+ .cp-version-head {
1551
+ display: flex;
1552
+ align-items: center;
1553
+ gap: 10px;
1554
+ padding: 10px 14px;
1555
+ }
1556
+ .cp-version-label { font-weight: var(--weight-semibold); }
1557
+ .cp-version-status { text-transform: none; }
1558
+ .cp-version-stamp { color: var(--fg-muted); font-size: 12px; }
1559
+ .cp-version-actions { margin-left: auto; display: flex; gap: 8px; }
1560
+ .cp-version-btn {
1561
+ border: 1px solid var(--border-strong);
1562
+ background: var(--surface);
1563
+ color: var(--fg);
1564
+ border-radius: var(--radius-sm);
1565
+ padding: 4px 12px;
1566
+ font-size: 13px;
1567
+ cursor: pointer;
1568
+ transition: background var(--dur-1) var(--ease-out);
1569
+ }
1570
+ .cp-version-btn:hover { background: var(--bg-subtle); }
1571
+ .cp-version-btn:disabled { opacity: 0.6; cursor: default; }
1572
+ .cp-version-btn--primary {
1573
+ border-color: var(--brand);
1574
+ color: var(--brand);
1575
+ font-weight: var(--weight-semibold);
1576
+ }
1577
+ .cp-version-body {
1578
+ border-top: 1px solid var(--border);
1579
+ padding: 14px;
1580
+ }
@@ -15,9 +15,9 @@
15
15
  } catch (e) {}
16
16
  })();
17
17
  </script>
18
- <link rel="stylesheet" href="tokens.css" />
19
- <link rel="stylesheet" href="styles.css" />
20
- <link rel="stylesheet" href="claude-plans.css" />
18
+ <link rel="stylesheet" href="/tokens.css" />
19
+ <link rel="stylesheet" href="/styles.css" />
20
+ <link rel="stylesheet" href="/claude-plans.css" />
21
21
  </head>
22
22
  <body class="cp-body">
23
23
  <header>
@@ -127,11 +127,15 @@
127
127
  <button class="cp-tab" data-tab="questions">
128
128
  Q&amp;A <span class="cp-tab-count" id="cp-question-count">0</span>
129
129
  </button>
130
+ <button class="cp-tab" data-tab="versions">
131
+ History <span class="cp-tab-count" id="cp-version-count">0</span>
132
+ </button>
130
133
  </nav>
131
134
  <div class="cp-tab-panel" id="cp-tab-plan"></div>
132
135
  <div class="cp-tab-panel" id="cp-tab-prompts" hidden></div>
133
136
  <div class="cp-tab-panel" id="cp-tab-artifacts" hidden></div>
134
137
  <div class="cp-tab-panel" id="cp-tab-questions" hidden></div>
138
+ <div class="cp-tab-panel" id="cp-tab-versions" hidden></div>
135
139
  </article>
136
140
  </section>
137
141
  </main>
@@ -139,6 +143,6 @@
139
143
  <script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js"></script>
140
144
  <script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.5/dist/purify.min.js"></script>
141
145
  <script src="https://cdn.jsdelivr.net/npm/mermaid@10.9.6/dist/mermaid.min.js"></script>
142
- <script src="claude-plans.js"></script>
146
+ <script src="/claude-plans.js"></script>
143
147
  </body>
144
148
  </html>
@@ -64,6 +64,8 @@
64
64
  promptsPanel: document.getElementById("cp-tab-prompts"),
65
65
  questionsPanel: document.getElementById("cp-tab-questions"),
66
66
  questionCount: document.getElementById("cp-question-count"),
67
+ versionsPanel: document.getElementById("cp-tab-versions"),
68
+ versionCount: document.getElementById("cp-version-count"),
67
69
  stageMap: document.getElementById("cp-stage-map"),
68
70
  tabs: document.querySelectorAll(".cp-tab"),
69
71
  topics: document.getElementById("cp-topics"),
@@ -630,6 +632,7 @@
630
632
  // v2 surfaces — Stage Map strip + Questions tab.
631
633
  renderStageMap(plan);
632
634
  renderQuestions(plan);
635
+ renderVersions(plan);
633
636
 
634
637
  addPlanSectionCopyBtns();
635
638
  addTabsCopyAll(plan, artifacts, prompts);
@@ -746,6 +749,7 @@
746
749
  els.artifactsPanel.hidden = tab !== "artifacts";
747
750
  if (els.promptsPanel) els.promptsPanel.hidden = tab !== "prompts";
748
751
  if (els.questionsPanel) els.questionsPanel.hidden = tab !== "questions";
752
+ if (els.versionsPanel) els.versionsPanel.hidden = tab !== "versions";
749
753
  }
750
754
 
751
755
  /* ─── v2 bidirectional pipeline (Phase E) ──────────────────────────────────
@@ -1007,6 +1011,142 @@
1007
1011
  });
1008
1012
  }
1009
1013
 
1014
+ function renderVersions(plan) {
1015
+ if (!els.versionsPanel) return;
1016
+ var panel = els.versionsPanel;
1017
+ panel.innerHTML = "";
1018
+ fetch("/api/claude-plans/" + encodeURIComponent(plan.slug) + "/versions")
1019
+ .then(function (r) { return r.json(); })
1020
+ .then(function (data) {
1021
+ // Bail if the user switched plans while this fetch was in flight —
1022
+ // otherwise a stale response would paint the wrong plan's history.
1023
+ if (state.selectedSlug !== plan.slug) return;
1024
+ var versions = (data && data.versions) || [];
1025
+ if (els.versionCount) els.versionCount.textContent = String(versions.length);
1026
+ if (versions.length === 0) {
1027
+ var empty = document.createElement("div");
1028
+ empty.className = "cp-detail-empty";
1029
+ empty.style.padding = "40px 0";
1030
+ empty.textContent =
1031
+ "No prior versions yet. A snapshot is saved automatically whenever a " +
1032
+ "push changes this plan's content.";
1033
+ panel.appendChild(empty);
1034
+ return;
1035
+ }
1036
+ var list = document.createElement("div");
1037
+ list.className = "cp-versions";
1038
+ // Synthetic "current" row at the top for orientation (not restorable).
1039
+ list.appendChild(versionRow({
1040
+ version: null,
1041
+ saved_at: plan.updated_at,
1042
+ title: plan.title,
1043
+ status: plan.status
1044
+ }, plan, true));
1045
+ versions.forEach(function (v) {
1046
+ list.appendChild(versionRow(v, plan, false));
1047
+ });
1048
+ panel.appendChild(list);
1049
+ })
1050
+ .catch(function () {
1051
+ panel.innerHTML = "";
1052
+ var err = document.createElement("div");
1053
+ err.className = "cp-detail-empty";
1054
+ err.style.padding = "40px 0";
1055
+ err.textContent = "Could not load version history.";
1056
+ panel.appendChild(err);
1057
+ });
1058
+ }
1059
+
1060
+ function versionRow(v, plan, isCurrent) {
1061
+ var row = document.createElement("div");
1062
+ row.className = "cp-version" + (isCurrent ? " cp-version--current" : "");
1063
+
1064
+ var head = document.createElement("div");
1065
+ head.className = "cp-version-head";
1066
+
1067
+ var label = document.createElement("span");
1068
+ label.className = "cp-version-label";
1069
+ label.textContent = isCurrent ? "Current" : "v" + v.version;
1070
+ head.appendChild(label);
1071
+
1072
+ if (v.status) {
1073
+ var status = document.createElement("span");
1074
+ status.className = "cp-status-pill cp-version-status";
1075
+ status.textContent = v.status;
1076
+ head.appendChild(status);
1077
+ }
1078
+
1079
+ var stamp = document.createElement("span");
1080
+ stamp.className = "cp-stamp cp-version-stamp";
1081
+ stamp.textContent = v.saved_at ? fmtTime(v.saved_at) : "";
1082
+ head.appendChild(stamp);
1083
+
1084
+ var actions = document.createElement("span");
1085
+ actions.className = "cp-version-actions";
1086
+ if (!isCurrent) {
1087
+ var viewBtn = document.createElement("button");
1088
+ viewBtn.type = "button";
1089
+ viewBtn.className = "cp-version-btn";
1090
+ viewBtn.textContent = "View";
1091
+ var body = null;
1092
+ viewBtn.addEventListener("click", function () {
1093
+ if (body && body.parentNode) {
1094
+ body.parentNode.removeChild(body);
1095
+ body = null;
1096
+ viewBtn.textContent = "View";
1097
+ return;
1098
+ }
1099
+ viewBtn.textContent = "Hide";
1100
+ body = document.createElement("div");
1101
+ body.className = "cp-version-body cp-tab-panel";
1102
+ row.appendChild(body);
1103
+ fetch("/api/claude-plans/" + encodeURIComponent(plan.slug) + "/versions/" + v.version)
1104
+ .then(function (r) { return r.json(); })
1105
+ .then(function (snap) {
1106
+ var p = snap && snap.plan;
1107
+ if (!p) { body.textContent = "Snapshot unavailable."; return; }
1108
+ if (p.content_html && window.DOMPurify) {
1109
+ body.innerHTML = window.DOMPurify.sanitize(p.content_html);
1110
+ } else {
1111
+ renderMarkdown(p.content_md || "", body);
1112
+ }
1113
+ })
1114
+ .catch(function () { body.textContent = "Could not load snapshot."; });
1115
+ });
1116
+ actions.appendChild(viewBtn);
1117
+
1118
+ var restoreBtn = document.createElement("button");
1119
+ restoreBtn.type = "button";
1120
+ restoreBtn.className = "cp-version-btn cp-version-btn--primary";
1121
+ restoreBtn.textContent = "Restore";
1122
+ restoreBtn.addEventListener("click", function () {
1123
+ restoreVersion(plan.slug, v.version, restoreBtn);
1124
+ });
1125
+ actions.appendChild(restoreBtn);
1126
+ }
1127
+ head.appendChild(actions);
1128
+ row.appendChild(head);
1129
+ return row;
1130
+ }
1131
+
1132
+ function restoreVersion(slug, version, btn) {
1133
+ if (btn) { btn.disabled = true; btn.textContent = "Restoring…"; }
1134
+ fetch("/api/claude-plans/" + encodeURIComponent(slug) + "/versions/" + version + "/restore", {
1135
+ method: "POST"
1136
+ })
1137
+ .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, json: j }; }); })
1138
+ .then(function (res) {
1139
+ if (!res.ok) throw new Error((res.json && res.json.error) || "restore failed");
1140
+ // The plan-file write fires an SSE plan:upsert → renderDetail →
1141
+ // renderVersions refreshes the list automatically.
1142
+ showToast("Restored v" + version + " as the current plan.");
1143
+ })
1144
+ .catch(function (e) {
1145
+ if (btn) { btn.disabled = false; btn.textContent = "Restore"; }
1146
+ showToast("Restore failed: " + e.message);
1147
+ });
1148
+ }
1149
+
1010
1150
  function submitAnswer(slug, questionId, answer) {
1011
1151
  v2Post(slug, "answers", { question_id: questionId, answer: answer }).then(function () {
1012
1152
  showToast("Answer recorded.");
package/server.js CHANGED
@@ -1308,6 +1308,53 @@ app.post("/api/claude-plans/:slug/dispatch", claudePlansLimiter, express.json(),
1308
1308
  }
1309
1309
  });
1310
1310
 
1311
+ // GET /api/claude-plans/:slug/versions — list saved version snapshots (newest-first).
1312
+ app.get("/api/claude-plans/:slug/versions", function (req, res) {
1313
+ try {
1314
+ var slug = req.params.slug;
1315
+ if (!isValidSlug(slug)) return res.status(400).json({ error: "invalid slug" });
1316
+ res.json({ slug: slug, versions: claudePlansLib.listVersions(slug) });
1317
+ } catch (e) {
1318
+ sendTypedError(res, e);
1319
+ }
1320
+ });
1321
+
1322
+ // GET /api/claude-plans/:slug/versions/:n — read one full version snapshot.
1323
+ app.get("/api/claude-plans/:slug/versions/:n", function (req, res) {
1324
+ try {
1325
+ var slug = req.params.slug;
1326
+ if (!isValidSlug(slug)) return res.status(400).json({ error: "invalid slug" });
1327
+ var n = parseInt(req.params.n, 10);
1328
+ if (!(n > 0)) return res.status(400).json({ error: "invalid version" });
1329
+ var version = claudePlansLib.getVersion(slug, n);
1330
+ if (!version) return res.status(404).json({ error: "version not found" });
1331
+ res.json(version);
1332
+ } catch (e) {
1333
+ sendTypedError(res, e);
1334
+ }
1335
+ });
1336
+
1337
+ // POST /api/claude-plans/:slug/versions/:n/restore — re-push a prior version as
1338
+ // the new current record. Non-destructive (pre-restore current is snapshotted).
1339
+ // The plan-file write triggers the chokidar watcher → plan:upsert SSE, so
1340
+ // connected dashboards refresh without a manual broadcast.
1341
+ app.post(
1342
+ "/api/claude-plans/:slug/versions/:n/restore",
1343
+ claudePlansLimiter,
1344
+ function (req, res) {
1345
+ try {
1346
+ var slug = req.params.slug;
1347
+ if (!isValidSlug(slug)) return res.status(400).json({ error: "invalid slug" });
1348
+ var n = parseInt(req.params.n, 10);
1349
+ if (!(n > 0)) return res.status(400).json({ error: "invalid version" });
1350
+ var plan = claudePlansLib.restoreVersion(slug, n);
1351
+ res.json({ plan: plan });
1352
+ } catch (e) {
1353
+ sendTypedError(res, e);
1354
+ }
1355
+ }
1356
+ );
1357
+
1311
1358
  app.delete("/api/claude-plans/:slug", claudePlansLimiter, function (req, res) {
1312
1359
  try {
1313
1360
  var slug = req.params.slug;