@tenonhq/dovetail-dashboard 0.0.31 → 0.0.33
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 +1 -1
- package/public/claude-plans.css +56 -4
- package/public/claude-plans.html +8 -4
- package/public/claude-plans.js +140 -0
- package/public/tokens.css +2 -0
- package/server.js +47 -0
package/package.json
CHANGED
package/public/claude-plans.css
CHANGED
|
@@ -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:
|
|
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(--
|
|
288
|
+
color: var(--code-inline-fg);
|
|
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(--
|
|
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(--
|
|
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
|
+
}
|
package/public/claude-plans.html
CHANGED
|
@@ -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&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>
|
package/public/claude-plans.js
CHANGED
|
@@ -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/public/tokens.css
CHANGED
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
--fg-subtle: var(--neutral-300);
|
|
80
80
|
--fg-on-dark: var(--neutral-25);
|
|
81
81
|
--fg-on-neon: var(--emerald-500);
|
|
82
|
+
--code-inline-fg: var(--brand);
|
|
82
83
|
|
|
83
84
|
--accent: var(--neon-300);
|
|
84
85
|
--accent-hover: var(--neon-500);
|
|
@@ -187,6 +188,7 @@
|
|
|
187
188
|
--fg-subtle: #6f8378;
|
|
188
189
|
--fg-on-dark: #ecf3ef;
|
|
189
190
|
--fg-on-neon: var(--emerald-900);
|
|
191
|
+
--code-inline-fg: var(--neon-300);
|
|
190
192
|
|
|
191
193
|
--accent: var(--neon-300);
|
|
192
194
|
--accent-hover: var(--neon-500);
|
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;
|