realitydiff 0.4.0__py3-none-any.whl
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.
- realitydiff/__init__.py +8 -0
- realitydiff/__main__.py +4 -0
- realitydiff/api.py +270 -0
- realitydiff/blame.py +85 -0
- realitydiff/claim.py +104 -0
- realitydiff/cli.py +168 -0
- realitydiff/compress.py +94 -0
- realitydiff/config.py +66 -0
- realitydiff/diffing.py +210 -0
- realitydiff/hashing.py +24 -0
- realitydiff/models.py +239 -0
- realitydiff/reasoner.py +440 -0
- realitydiff/schedule.py +217 -0
- realitydiff/service.py +173 -0
- realitydiff/static/app.js +814 -0
- realitydiff/static/favicon.svg +4 -0
- realitydiff/static/index.html +175 -0
- realitydiff/static/styles.css +477 -0
- realitydiff/store.py +296 -0
- realitydiff/watcher.py +42 -0
- realitydiff/xai.py +506 -0
- realitydiff-0.4.0.dist-info/METADATA +115 -0
- realitydiff-0.4.0.dist-info/RECORD +26 -0
- realitydiff-0.4.0.dist-info/WHEEL +4 -0
- realitydiff-0.4.0.dist-info/entry_points.txt +2 -0
- realitydiff-0.4.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
const $ = (id) => document.getElementById(id);
|
|
2
|
+
|
|
3
|
+
let currentId = null;
|
|
4
|
+
let browseSha = null;
|
|
5
|
+
let lastLog = [];
|
|
6
|
+
let claimsCache = [];
|
|
7
|
+
let overlayTimer = null;
|
|
8
|
+
let watchingClaims = new Set();
|
|
9
|
+
let listStatus = localStorage.getItem("rd-list-status") || "all";
|
|
10
|
+
|
|
11
|
+
function errorMessage(data, fallback) {
|
|
12
|
+
if (!data) return fallback;
|
|
13
|
+
const detail = data.detail;
|
|
14
|
+
if (typeof detail === "string") return detail;
|
|
15
|
+
if (Array.isArray(detail)) {
|
|
16
|
+
return detail.map((item) => item.msg || item.message || JSON.stringify(item)).join("; ");
|
|
17
|
+
}
|
|
18
|
+
if (detail && typeof detail === "object") return JSON.stringify(detail);
|
|
19
|
+
return fallback;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function api(path, opts = {}) {
|
|
23
|
+
const { timeoutMs = 240000, ...fetchOpts } = opts;
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(path, {
|
|
28
|
+
headers: { "Content-Type": "application/json" },
|
|
29
|
+
...fetchOpts,
|
|
30
|
+
signal: controller.signal,
|
|
31
|
+
});
|
|
32
|
+
const text = await res.text();
|
|
33
|
+
let data;
|
|
34
|
+
try { data = text ? JSON.parse(text) : null; } catch { data = { detail: text }; }
|
|
35
|
+
if (!res.ok) throw new Error(errorMessage(data, res.statusText));
|
|
36
|
+
return data;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
if (err.name === "AbortError") throw new Error("Request timed out. Try again.");
|
|
39
|
+
throw err;
|
|
40
|
+
} finally {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toast(msg) {
|
|
46
|
+
const el = $("toast");
|
|
47
|
+
el.textContent = String(msg || "").slice(0, 400);
|
|
48
|
+
el.classList.remove("hidden");
|
|
49
|
+
setTimeout(() => el.classList.add("hidden"), 4800);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function setBusy(btn, busy) {
|
|
53
|
+
btn.disabled = busy;
|
|
54
|
+
btn.dataset.prev = btn.dataset.prev || btn.textContent;
|
|
55
|
+
btn.textContent = busy ? "…" : btn.dataset.prev;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function showOverlay(title, sub) {
|
|
59
|
+
$("overlay-title").textContent = title;
|
|
60
|
+
$("overlay-sub").innerHTML = `${escapeHtml(sub)} <span id="elapsed">0s</span>`;
|
|
61
|
+
$("overlay").classList.remove("hidden");
|
|
62
|
+
const started = Date.now();
|
|
63
|
+
clearInterval(overlayTimer);
|
|
64
|
+
overlayTimer = setInterval(() => {
|
|
65
|
+
const el = $("elapsed");
|
|
66
|
+
if (el) el.textContent = `${Math.round((Date.now() - started) / 1000)}s`;
|
|
67
|
+
}, 250);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function hideOverlay() {
|
|
71
|
+
clearInterval(overlayTimer);
|
|
72
|
+
$("overlay").classList.add("hidden");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function setWatchStatus(text) {
|
|
76
|
+
const el = $("watch-status");
|
|
77
|
+
if (!text) {
|
|
78
|
+
el.classList.add("hidden");
|
|
79
|
+
el.textContent = "";
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
el.classList.remove("hidden");
|
|
83
|
+
el.textContent = text;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseRoute() {
|
|
87
|
+
const raw = location.hash.replace(/^#/, "");
|
|
88
|
+
if (!raw) return { id: null, sha: null };
|
|
89
|
+
const [id, sha] = raw.split("/");
|
|
90
|
+
return { id: id || null, sha: sha || null };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function setRoute(id, sha) {
|
|
94
|
+
currentId = id;
|
|
95
|
+
browseSha = sha || null;
|
|
96
|
+
location.hash = sha ? `${id}/${sha}` : id;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function weightPercent(weight) {
|
|
100
|
+
const n = Number(weight);
|
|
101
|
+
if (!Number.isFinite(n)) return 0;
|
|
102
|
+
return Math.max(0, Math.min(100, Math.round(n * 100)));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function evidenceItem(item) {
|
|
106
|
+
const li = document.createElement("li");
|
|
107
|
+
const notes = item.notes ? `<span class="notes">${escapeHtml(item.notes)}</span>` : "";
|
|
108
|
+
const pct = weightPercent(item.weight);
|
|
109
|
+
li.innerHTML = `<div>${escapeHtml(item.statement)}</div>
|
|
110
|
+
<span class="weight" aria-hidden="true"><span class="weight-fill" style="width:${pct}%"></span></span>
|
|
111
|
+
<span class="w">weight ${Number(item.weight).toFixed(2)}</span>
|
|
112
|
+
${item.source_url ? `<a class="src" href="${escapeAttr(item.source_url)}" target="_blank" rel="noreferrer">${escapeHtml(item.source_title || item.source_url)}</a>` : ""}
|
|
113
|
+
${notes}`;
|
|
114
|
+
return li;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function escapeHtml(s) {
|
|
118
|
+
return String(s || "").replace(/[&<>"']/g, (c) => ({
|
|
119
|
+
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
|
|
120
|
+
}[c]));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function escapeAttr(s) {
|
|
124
|
+
return escapeHtml(s).replace(/`/g, "");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function relativeTime(iso) {
|
|
128
|
+
if (!iso) return "";
|
|
129
|
+
const then = new Date(iso).getTime();
|
|
130
|
+
if (Number.isNaN(then)) return iso;
|
|
131
|
+
const delta = Date.now() - then;
|
|
132
|
+
const mins = Math.round(delta / 60000);
|
|
133
|
+
if (Math.abs(mins) < 1) return "just now";
|
|
134
|
+
if (Math.abs(mins) < 60) return `${mins}m ago`;
|
|
135
|
+
const hours = Math.round(mins / 60);
|
|
136
|
+
if (Math.abs(hours) < 48) return `${hours}h ago`;
|
|
137
|
+
return new Date(iso).toISOString().slice(0, 16).replace("T", " ") + "Z";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function sparkPath(values, w = 120, h = 28) {
|
|
141
|
+
if (!values.length) return "";
|
|
142
|
+
const min = Math.min(...values, 0);
|
|
143
|
+
const max = Math.max(...values, 100);
|
|
144
|
+
const span = Math.max(1, max - min);
|
|
145
|
+
return values.map((v, i) => {
|
|
146
|
+
const x = values.length === 1 ? w / 2 : (i / (values.length - 1)) * w;
|
|
147
|
+
const y = h - ((v - min) / span) * (h - 4) - 2;
|
|
148
|
+
return `${i === 0 ? "M" : "L"}${x.toFixed(1)} ${y.toFixed(1)}`;
|
|
149
|
+
}).join(" ");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function renderSpark(el, values) {
|
|
153
|
+
const d = sparkPath(values);
|
|
154
|
+
el.innerHTML = d
|
|
155
|
+
? `<path d="${d}" fill="none" stroke="#b6e388" stroke-width="1.5"/>`
|
|
156
|
+
: "";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function refreshList() {
|
|
160
|
+
claimsCache = await api("/api/claims", { timeoutMs: 15000 });
|
|
161
|
+
renderClaimList();
|
|
162
|
+
if (!currentId && !parseRoute().id && claimsCache[0]) selectClaim(claimsCache[0].id);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function matchesClaim(claim, query, status) {
|
|
166
|
+
const q = String(query || "").toLowerCase();
|
|
167
|
+
const textOk = !q || (claim.title || "").toLowerCase().includes(q) || String(claim.id || "").includes(q);
|
|
168
|
+
if (!textOk) return false;
|
|
169
|
+
if (status === "open") return !claim.closed;
|
|
170
|
+
if (status === "closed") return Boolean(claim.closed);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function visibleClaims() {
|
|
175
|
+
const q = $("claim-filter") ? $("claim-filter").value : "";
|
|
176
|
+
return claimsCache.filter((c) => matchesClaim(c, q, listStatus));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function renderClaimList() {
|
|
180
|
+
const q = ($("claim-filter").value || "").toLowerCase();
|
|
181
|
+
const ul = $("claim-list");
|
|
182
|
+
ul.innerHTML = "";
|
|
183
|
+
document.querySelectorAll("#status-filter [data-status]").forEach((btn) => {
|
|
184
|
+
const status = btn.dataset.status;
|
|
185
|
+
btn.classList.toggle("on", status === listStatus);
|
|
186
|
+
btn.setAttribute("aria-selected", status === listStatus ? "true" : "false");
|
|
187
|
+
const badge = btn.querySelector(".seg-n");
|
|
188
|
+
if (badge) badge.textContent = String(claimsCache.filter((c) => matchesClaim(c, q, status)).length);
|
|
189
|
+
});
|
|
190
|
+
const rows = claimsCache.filter((c) => matchesClaim(c, q, listStatus));
|
|
191
|
+
const heads = $("heads-count");
|
|
192
|
+
if (heads) heads.textContent = String(rows.length);
|
|
193
|
+
if (!rows.length) {
|
|
194
|
+
const li = document.createElement("li");
|
|
195
|
+
li.className = "empty-item";
|
|
196
|
+
li.textContent = claimsCache.length ? "No claims match." : "No claims yet.";
|
|
197
|
+
ul.appendChild(li);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
rows.forEach((c) => {
|
|
201
|
+
const li = document.createElement("li");
|
|
202
|
+
li.tabIndex = 0;
|
|
203
|
+
li.dataset.id = c.id;
|
|
204
|
+
if (c.id === currentId) li.classList.add("active");
|
|
205
|
+
if (c.closed) li.classList.add("closed");
|
|
206
|
+
if (c.outcome_verdict) li.classList.add(c.outcome_verdict);
|
|
207
|
+
const conf = c.confidence == null ? 0 : Number(c.confidence);
|
|
208
|
+
const label = c.closed ? "closed · " + escapeHtml(c.outcome_verdict || "") : (c.confidence == null ? "—" : conf.toFixed(1) + "%");
|
|
209
|
+
const watched = c.last_watched_at ? ` · watched ${escapeHtml(relativeTime(c.last_watched_at))}` : "";
|
|
210
|
+
li.innerHTML = `<div>${escapeHtml(c.title)}</div>
|
|
211
|
+
<div class="claim-meta"><span class="meter" aria-hidden="true"><span style="width:${Math.max(0, Math.min(100, conf))}%"></span></span>
|
|
212
|
+
<span>${label}</span>
|
|
213
|
+
<span>${c.commit_count} commits${watched}</span></div>`;
|
|
214
|
+
const spark = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
215
|
+
spark.setAttribute("class", "mini-spark");
|
|
216
|
+
spark.setAttribute("viewBox", "0 0 120 28");
|
|
217
|
+
spark.setAttribute("aria-hidden", "true");
|
|
218
|
+
renderSpark(spark, c.spark || []);
|
|
219
|
+
li.appendChild(spark);
|
|
220
|
+
const open = () => {
|
|
221
|
+
selectClaim(c.id);
|
|
222
|
+
focusStageOnNarrow();
|
|
223
|
+
};
|
|
224
|
+
li.onclick = open;
|
|
225
|
+
li.onkeydown = (ev) => {
|
|
226
|
+
if (ev.key === "Enter" || ev.key === " ") {
|
|
227
|
+
ev.preventDefault();
|
|
228
|
+
open();
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
ul.appendChild(li);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function selectClaim(id, sha) {
|
|
236
|
+
currentId = id;
|
|
237
|
+
browseSha = sha || null;
|
|
238
|
+
if (location.hash.replace(/^#/, "") !== (sha ? `${id}/${sha}` : id)) {
|
|
239
|
+
location.hash = sha ? `${id}/${sha}` : id;
|
|
240
|
+
}
|
|
241
|
+
$("empty").classList.add("hidden");
|
|
242
|
+
$("claim-view").classList.remove("hidden");
|
|
243
|
+
let data;
|
|
244
|
+
let head;
|
|
245
|
+
let state;
|
|
246
|
+
let browsing = Boolean(sha);
|
|
247
|
+
if (sha) {
|
|
248
|
+
data = await api(`/api/claims/${id}/at/${sha}`, { timeoutMs: 15000 });
|
|
249
|
+
head = data.commit;
|
|
250
|
+
state = head?.state;
|
|
251
|
+
$("browse-banner").classList.remove("hidden");
|
|
252
|
+
$("browse-sha").textContent = (head?.id || sha).slice(0, 12);
|
|
253
|
+
} else {
|
|
254
|
+
data = await api(`/api/claims/${id}`, { timeoutMs: 15000 });
|
|
255
|
+
head = data.head;
|
|
256
|
+
state = head?.state || data.working;
|
|
257
|
+
$("browse-banner").classList.add("hidden");
|
|
258
|
+
}
|
|
259
|
+
$("claim-id").textContent = data.claim.id;
|
|
260
|
+
$("claim-title").textContent = state?.statement || data.claim.title;
|
|
261
|
+
$("claim-refined").textContent = state?.refined_statement || "";
|
|
262
|
+
$("claim-reason").textContent = head?.reason || "";
|
|
263
|
+
const conf = state?.confidence ?? 0;
|
|
264
|
+
$("conf-value").textContent = `${Number(conf).toFixed(1)}%`;
|
|
265
|
+
$("conf-fill").style.width = `${conf}%`;
|
|
266
|
+
const card = $("confidence-card");
|
|
267
|
+
if (card) {
|
|
268
|
+
card.classList.remove("low", "mid", "high");
|
|
269
|
+
card.classList.add(Number(conf) >= 67 ? "high" : Number(conf) <= 33 ? "low" : "mid");
|
|
270
|
+
}
|
|
271
|
+
const closed = isClosedClaim(data.claim, state);
|
|
272
|
+
const verdict = data.claim.outcome_verdict || state?.outcome?.verdict || "";
|
|
273
|
+
renderClosedState({
|
|
274
|
+
closed,
|
|
275
|
+
verdict,
|
|
276
|
+
outcome: state?.outcome,
|
|
277
|
+
watching: Boolean(data.claim.watching),
|
|
278
|
+
browsing,
|
|
279
|
+
});
|
|
280
|
+
$("dirty-chip").classList.toggle("hidden", browsing || !data.dirty);
|
|
281
|
+
const interval = String(data.claim.watch_interval_seconds || 300);
|
|
282
|
+
if ([...$("watch-interval").options].some((opt) => opt.value === interval)) {
|
|
283
|
+
$("watch-interval").value = interval;
|
|
284
|
+
}
|
|
285
|
+
fillList($("ev-for"), state?.evidence_for || [], evidenceItem, $("count-for"));
|
|
286
|
+
fillList($("ev-against"), state?.evidence_against || [], evidenceItem, $("count-against"));
|
|
287
|
+
fillList($("unknowns"), state?.unknowns || [], (u) => {
|
|
288
|
+
const li = document.createElement("li");
|
|
289
|
+
li.innerHTML = `<div>${escapeHtml(u.question)}</div><span class="w">${escapeHtml(u.why_it_matters)}</span>`;
|
|
290
|
+
return li;
|
|
291
|
+
}, $("count-unknowns"));
|
|
292
|
+
fillList($("predictions"), state?.predictions || [], (p) => {
|
|
293
|
+
const li = document.createElement("li");
|
|
294
|
+
li.innerHTML = `<div>${escapeHtml(p.statement)} <span class="pred-status ${escapeAttr(p.status)}">${escapeHtml(p.status)}</span></div>
|
|
295
|
+
<span class="w">${p.due ? escapeHtml(p.due) : "no due date"}${p.how_to_falsify ? " · " + escapeHtml(p.how_to_falsify) : ""}</span>`;
|
|
296
|
+
return li;
|
|
297
|
+
}, $("count-predictions"));
|
|
298
|
+
$("summary").textContent = state?.summary || "";
|
|
299
|
+
lastLog = await api(`/api/claims/${id}/log`, { timeoutMs: 15000 });
|
|
300
|
+
const spark = [...lastLog].reverse().map((c) => c.new_confidence);
|
|
301
|
+
renderSpark($("spark"), spark);
|
|
302
|
+
const viewed = lastLog.find((c) => c.id === head?.id) || lastLog[0];
|
|
303
|
+
const prev = viewed?.previous_confidence;
|
|
304
|
+
const deltaEl = $("conf-delta");
|
|
305
|
+
if (prev == null) {
|
|
306
|
+
deltaEl.textContent = browsing ? "initial commit" : "initial HEAD";
|
|
307
|
+
deltaEl.className = "conf-delta";
|
|
308
|
+
} else {
|
|
309
|
+
const d = Math.round((conf - prev) * 10) / 10;
|
|
310
|
+
deltaEl.textContent = `${d >= 0 ? "+" : ""}${d} from previous commit`;
|
|
311
|
+
deltaEl.className = "conf-delta " + (d > 0 ? "up" : d < 0 ? "down" : "");
|
|
312
|
+
}
|
|
313
|
+
renderLog(lastLog, head?.id);
|
|
314
|
+
await refreshList();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function isClosedClaim(claim, state) {
|
|
318
|
+
if (claim && (claim.closed === true || claim.closed === 1 || claim.closed === "true")) return true;
|
|
319
|
+
const outcome = state && state.outcome;
|
|
320
|
+
if (outcome && typeof outcome === "object" && outcome.verdict) return true;
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function renderClosedState({ closed, verdict, outcome, watching, browsing }) {
|
|
325
|
+
const view = $("claim-view");
|
|
326
|
+
if (view) view.classList.toggle("is-closed", Boolean(closed));
|
|
327
|
+
const label = $("conf-label");
|
|
328
|
+
if (label) label.textContent = closed ? "final" : "confidence";
|
|
329
|
+
const watchingChip = $("watching-chip");
|
|
330
|
+
const closedChip = $("closed-chip");
|
|
331
|
+
const closedText = `closed · ${verdict || "settled"}`;
|
|
332
|
+
if (closed) {
|
|
333
|
+
if (closedChip) {
|
|
334
|
+
closedChip.className = `chip closed ${escapeAttr(verdict)}`;
|
|
335
|
+
closedChip.textContent = closedText;
|
|
336
|
+
if (watchingChip) watchingChip.classList.add("hidden");
|
|
337
|
+
} else if (watchingChip) {
|
|
338
|
+
watchingChip.className = `chip closed ${escapeAttr(verdict)}`;
|
|
339
|
+
watchingChip.textContent = closedText;
|
|
340
|
+
watchingChip.classList.remove("hidden");
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
if (closedChip) {
|
|
344
|
+
closedChip.className = "chip closed hidden";
|
|
345
|
+
closedChip.textContent = "closed";
|
|
346
|
+
}
|
|
347
|
+
if (watchingChip) {
|
|
348
|
+
watchingChip.classList.remove("hidden");
|
|
349
|
+
watchingChip.className = watching ? "chip" : "chip off";
|
|
350
|
+
watchingChip.textContent = watching ? "watching" : "paused";
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const watchBtn = $("watch-btn");
|
|
354
|
+
if (watchBtn) watchBtn.disabled = closed || browsing;
|
|
355
|
+
const toggle = $("watching-toggle");
|
|
356
|
+
if (toggle) {
|
|
357
|
+
toggle.checked = Boolean(watching);
|
|
358
|
+
toggle.disabled = closed || browsing;
|
|
359
|
+
}
|
|
360
|
+
const interval = $("watch-interval");
|
|
361
|
+
if (interval) interval.disabled = closed || browsing;
|
|
362
|
+
renderOutcome(outcome, closed);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function outcomeSummaryText(summary, sourceUrl) {
|
|
366
|
+
let text = String(summary || "").trim();
|
|
367
|
+
if (sourceUrl) text = text.split(sourceUrl).join(" ");
|
|
368
|
+
text = text.replace(/https?:\/\/\S+/g, " ");
|
|
369
|
+
text = text.replace(/Primary source:\s*/gi, " ");
|
|
370
|
+
text = text.replace(/\s+/g, " ").replace(/\s+([.,;:])/g, "$1").trim();
|
|
371
|
+
return text || "This claim is closed.";
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function renderOutcome(outcome, closed) {
|
|
375
|
+
const banner = $("outcome-banner");
|
|
376
|
+
const source = $("outcome-source");
|
|
377
|
+
if (!banner) return;
|
|
378
|
+
if (!closed && !outcome) {
|
|
379
|
+
banner.className = "outcome-banner hidden";
|
|
380
|
+
if (source) source.classList.add("hidden");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const verdict = (outcome && outcome.verdict) || "";
|
|
384
|
+
banner.className = `outcome-banner ${escapeAttr(verdict)}`;
|
|
385
|
+
const verdictEl = $("outcome-verdict");
|
|
386
|
+
const summaryEl = $("outcome-summary");
|
|
387
|
+
if (verdictEl) verdictEl.textContent = verdict || "settled";
|
|
388
|
+
if (summaryEl) {
|
|
389
|
+
summaryEl.textContent = outcomeSummaryText(
|
|
390
|
+
outcome && outcome.summary,
|
|
391
|
+
outcome && outcome.source_url,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
if (!source) return;
|
|
395
|
+
if (outcome && outcome.source_url) {
|
|
396
|
+
source.href = outcome.source_url;
|
|
397
|
+
source.textContent = outcome.source_title || outcome.source_url;
|
|
398
|
+
source.classList.remove("hidden");
|
|
399
|
+
} else {
|
|
400
|
+
source.removeAttribute("href");
|
|
401
|
+
source.textContent = "";
|
|
402
|
+
source.classList.add("hidden");
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function fillList(ul, items, render, countEl) {
|
|
407
|
+
ul.innerHTML = "";
|
|
408
|
+
if (countEl) {
|
|
409
|
+
countEl.textContent = String(items.length);
|
|
410
|
+
document.querySelectorAll(`[data-count-for="${countEl.id}"]`).forEach((el) => {
|
|
411
|
+
el.textContent = String(items.length);
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
if (!items.length) {
|
|
415
|
+
const li = document.createElement("li");
|
|
416
|
+
li.className = "empty-item";
|
|
417
|
+
li.textContent = "none yet";
|
|
418
|
+
ul.appendChild(li);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
items.forEach((item) => ul.appendChild(render(item)));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function renderLog(log, activeSha) {
|
|
425
|
+
$("log-meta").textContent = `${log.length} commits`;
|
|
426
|
+
const ol = $("commit-log");
|
|
427
|
+
ol.innerHTML = "";
|
|
428
|
+
const chronological = [...log].reverse();
|
|
429
|
+
chronological.forEach((c, idx) => {
|
|
430
|
+
const newestFirstIndex = log.length - 1 - idx;
|
|
431
|
+
const li = document.createElement("li");
|
|
432
|
+
if (activeSha && c.id === activeSha) li.classList.add("active");
|
|
433
|
+
const prev = c.previous_confidence;
|
|
434
|
+
let arrow = "";
|
|
435
|
+
if (prev != null) {
|
|
436
|
+
const d = c.new_confidence - prev;
|
|
437
|
+
const cls = d > 0 ? "up" : d < 0 ? "down" : "";
|
|
438
|
+
arrow = ` <span class="arrow ${cls}">${prev.toFixed(1)}% → ${c.new_confidence.toFixed(1)}%</span>`;
|
|
439
|
+
}
|
|
440
|
+
li.innerHTML = `<div>
|
|
441
|
+
<span class="sha">${c.id.slice(0, 12)}${c.id === log[0]?.id ? " (HEAD)" : ""}</span>
|
|
442
|
+
<span class="msg">${escapeHtml(c.message)}${arrow}</span>
|
|
443
|
+
<span class="why">${escapeHtml((c.reason || "").slice(0, 220))}${(c.reason || "").length > 220 ? "…" : ""}</span>
|
|
444
|
+
<span class="status">${escapeHtml(c.author)} · ${relativeTime(c.created_at)}</span>
|
|
445
|
+
<div class="commit-actions">
|
|
446
|
+
<button type="button" class="ghost browse-commit">browse from this commit</button>
|
|
447
|
+
<button type="button" class="ghost diff-commit">diff</button>
|
|
448
|
+
</div>
|
|
449
|
+
</div>`;
|
|
450
|
+
li.querySelector(".browse-commit").onclick = (ev) => {
|
|
451
|
+
ev.stopPropagation();
|
|
452
|
+
selectClaim(currentId, c.id);
|
|
453
|
+
};
|
|
454
|
+
li.querySelector(".diff-commit").onclick = async (ev) => {
|
|
455
|
+
ev.stopPropagation();
|
|
456
|
+
if (newestFirstIndex + 1 >= log.length) {
|
|
457
|
+
showDetail("root commit", formatDiffHtml(await api(`/api/claims/${currentId}/diff?b=${c.id}`)));
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const parent = log[newestFirstIndex + 1];
|
|
461
|
+
const diff = await api(`/api/claims/${currentId}/diff?a=${parent.id}&b=${c.id}`);
|
|
462
|
+
showDetail(`diff ${parent.id.slice(0, 8)}..${c.id.slice(0, 8)}`, formatDiffHtml(diff));
|
|
463
|
+
};
|
|
464
|
+
ol.appendChild(li);
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function snip(value) {
|
|
469
|
+
if (value == null) return "";
|
|
470
|
+
const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
471
|
+
return escapeHtml(text.slice(0, 500)) + (text.length > 500 ? "…" : "");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function formatDiffHtml(diff) {
|
|
475
|
+
const delta = diff.confidence_delta ?? 0;
|
|
476
|
+
const hunks = (diff.changes || []).map((ch) => {
|
|
477
|
+
const kind = ch.kind === "removed" ? "removed" : ch.kind;
|
|
478
|
+
const body = kind === "added"
|
|
479
|
+
? snip(ch.after)
|
|
480
|
+
: kind === "removed"
|
|
481
|
+
? snip(ch.before)
|
|
482
|
+
: `− ${snip(ch.before)}\n+ ${snip(ch.after)}`;
|
|
483
|
+
return `<div class="hunk ${kind}"><div class="path">${kind === "added" ? "+" : kind === "removed" ? "−" : "~"} ${escapeHtml(ch.path)}</div><div class="snip">${body}</div></div>`;
|
|
484
|
+
}).join("");
|
|
485
|
+
return `<div class="hunk changed"><div class="path">confidence ${diff.previous_confidence ?? "∅"}% → ${diff.new_confidence ?? "∅"}% (${delta >= 0 ? "+" : ""}${delta})</div><div class="snip">${escapeHtml(diff.reason || "(none)")}</div></div>${hunks || "<p class='status'>no field changes</p>"}`;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function formatBlameHtml(blame) {
|
|
489
|
+
if (!blame.entries?.length) return "<p class='status'>empty</p>";
|
|
490
|
+
return blame.entries.map((e) =>
|
|
491
|
+
`<div class="blame-row"><div class="path">${escapeHtml(e.path)}</div>
|
|
492
|
+
<div class="status">introduced ${escapeHtml(e.introduced_in.slice(0, 12))} (${escapeHtml(e.author)})</div>
|
|
493
|
+
<div class="status">last ${escapeHtml(e.last_changed_in.slice(0, 12))} (${escapeHtml(e.last_author)})</div></div>`
|
|
494
|
+
).join("");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function showDetail(title, html) {
|
|
498
|
+
$("detail").classList.remove("hidden");
|
|
499
|
+
$("inspector-backdrop").classList.remove("hidden");
|
|
500
|
+
$("detail-title").textContent = title;
|
|
501
|
+
$("detail-body").innerHTML = html;
|
|
502
|
+
const close = $("detail-close");
|
|
503
|
+
if (close) close.focus();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function hideDetail() {
|
|
507
|
+
$("detail").classList.add("hidden");
|
|
508
|
+
$("inspector-backdrop").classList.add("hidden");
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function typingTarget(el) {
|
|
512
|
+
if (!el) return false;
|
|
513
|
+
const tag = el.tagName;
|
|
514
|
+
return tag === "TEXTAREA" || tag === "INPUT" || tag === "SELECT" || el.isContentEditable;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function focusStageOnNarrow() {
|
|
518
|
+
if (window.innerWidth > 1100) return;
|
|
519
|
+
const app = $("app");
|
|
520
|
+
app.classList.remove("show-rail", "show-log");
|
|
521
|
+
document.querySelectorAll(".pane-switch button").forEach((btn) => {
|
|
522
|
+
btn.classList.toggle("on", btn.id === "show-stage");
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function moveClaim(delta) {
|
|
527
|
+
const rows = visibleClaims();
|
|
528
|
+
if (!rows.length) return;
|
|
529
|
+
let idx = rows.findIndex((c) => c.id === currentId);
|
|
530
|
+
if (idx < 0) idx = delta > 0 ? -1 : 0;
|
|
531
|
+
const next = rows[(idx + delta + rows.length) % rows.length];
|
|
532
|
+
await selectClaim(next.id);
|
|
533
|
+
document.querySelector(`#claim-list li[data-id="${CSS.escape(next.id)}"]`)?.scrollIntoView({ block: "nearest" });
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function restorePaneSizes() {
|
|
537
|
+
const rail = Number(localStorage.getItem("rd-rail-w") || 300);
|
|
538
|
+
const log = Number(localStorage.getItem("rd-log-w") || 360);
|
|
539
|
+
document.documentElement.style.setProperty("--rail-w", `${Math.max(220, rail)}px`);
|
|
540
|
+
document.documentElement.style.setProperty("--log-w", `${Math.max(260, log)}px`);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function enableResize() {
|
|
544
|
+
document.querySelectorAll(".gutter").forEach((gutter) => {
|
|
545
|
+
gutter.addEventListener("mousedown", (ev) => {
|
|
546
|
+
ev.preventDefault();
|
|
547
|
+
gutter.classList.add("dragging");
|
|
548
|
+
const side = gutter.dataset.side;
|
|
549
|
+
const startX = ev.clientX;
|
|
550
|
+
const startRail = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--rail-w")) || 300;
|
|
551
|
+
const startLog = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--log-w")) || 360;
|
|
552
|
+
const onMove = (move) => {
|
|
553
|
+
const dx = move.clientX - startX;
|
|
554
|
+
if (side === "rail") {
|
|
555
|
+
const next = Math.min(480, Math.max(220, startRail + dx));
|
|
556
|
+
document.documentElement.style.setProperty("--rail-w", `${next}px`);
|
|
557
|
+
localStorage.setItem("rd-rail-w", String(next));
|
|
558
|
+
} else {
|
|
559
|
+
const next = Math.min(560, Math.max(260, startLog - dx));
|
|
560
|
+
document.documentElement.style.setProperty("--log-w", `${next}px`);
|
|
561
|
+
localStorage.setItem("rd-log-w", String(next));
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
const onUp = () => {
|
|
565
|
+
gutter.classList.remove("dragging");
|
|
566
|
+
window.removeEventListener("mousemove", onMove);
|
|
567
|
+
window.removeEventListener("mouseup", onUp);
|
|
568
|
+
};
|
|
569
|
+
window.addEventListener("mousemove", onMove);
|
|
570
|
+
window.addEventListener("mouseup", onUp);
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
$("open-form").addEventListener("submit", async (e) => {
|
|
576
|
+
e.preventDefault();
|
|
577
|
+
const btn = $("open-btn");
|
|
578
|
+
setBusy(btn, true);
|
|
579
|
+
showOverlay("Surveying the internet", "Grok is searching the web and X…");
|
|
580
|
+
try {
|
|
581
|
+
const statement = $("claim-input").value.trim();
|
|
582
|
+
const created = await api("/api/claims", {
|
|
583
|
+
method: "POST",
|
|
584
|
+
body: JSON.stringify({ statement }),
|
|
585
|
+
});
|
|
586
|
+
$("claim-input").value = "";
|
|
587
|
+
await selectClaim(created.claim.id);
|
|
588
|
+
toast("Initial belief committed.");
|
|
589
|
+
} catch (err) {
|
|
590
|
+
toast(err.message);
|
|
591
|
+
} finally {
|
|
592
|
+
setBusy(btn, false);
|
|
593
|
+
hideOverlay();
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
$("watch-btn").onclick = async () => {
|
|
598
|
+
if (!currentId) return;
|
|
599
|
+
const current = claimsCache.find((c) => c.id === currentId);
|
|
600
|
+
if (current?.closed) {
|
|
601
|
+
toast("Claim already closed.");
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (watchingClaims.has(currentId)) {
|
|
605
|
+
toast("Already watching this claim — you can keep browsing.");
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const watchingId = currentId;
|
|
609
|
+
const browseAtStart = browseSha;
|
|
610
|
+
const btn = $("watch-btn");
|
|
611
|
+
watchingClaims.add(watchingId);
|
|
612
|
+
setBusy(btn, true);
|
|
613
|
+
setWatchStatus("claim.watch() is searching. The rest of the workspace stays usable.");
|
|
614
|
+
try {
|
|
615
|
+
const result = await api(`/api/claims/${watchingId}/watch`, { method: "POST" });
|
|
616
|
+
if (result.in_progress) {
|
|
617
|
+
toast(result.detail || "Watch already running.");
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (currentId === watchingId) {
|
|
621
|
+
await selectClaim(watchingId, browseAtStart);
|
|
622
|
+
}
|
|
623
|
+
if (result.changed && result.diff) {
|
|
624
|
+
showDetail(result.closed ? "claim closed" : "watch diff", formatDiffHtml(result.diff));
|
|
625
|
+
}
|
|
626
|
+
if (result.closed) {
|
|
627
|
+
toast(result.detail || "Claim closed with a final outcome.");
|
|
628
|
+
} else if (result.changed) {
|
|
629
|
+
toast(result.detail || "Belief updated.");
|
|
630
|
+
} else {
|
|
631
|
+
toast(result.detail || "No meaningful change — dropped.");
|
|
632
|
+
}
|
|
633
|
+
} catch (err) {
|
|
634
|
+
toast(err.message);
|
|
635
|
+
} finally {
|
|
636
|
+
watchingClaims.delete(watchingId);
|
|
637
|
+
setBusy(btn, false);
|
|
638
|
+
setWatchStatus("");
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
$("diff-btn").onclick = async () => {
|
|
643
|
+
if (!currentId) return;
|
|
644
|
+
if (lastLog.length < 2) {
|
|
645
|
+
toast("Need at least two commits.");
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const [head, parent] = lastLog;
|
|
649
|
+
const diff = await api(`/api/claims/${currentId}/diff?a=${parent.id}&b=${head.id}`);
|
|
650
|
+
showDetail("claim.diff()", formatDiffHtml(diff));
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
$("blame-btn").onclick = async () => {
|
|
654
|
+
if (!currentId) return;
|
|
655
|
+
const blame = await api(`/api/claims/${currentId}/blame`);
|
|
656
|
+
showDetail("claim.blame()", formatBlameHtml(blame));
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
$("revert-btn").onclick = async () => {
|
|
660
|
+
if (!currentId) return;
|
|
661
|
+
if (browseSha) {
|
|
662
|
+
if (!window.confirm(`Revert live HEAD toward browsed commit ${browseSha.slice(0, 12)}?`)) return;
|
|
663
|
+
const commit = await api(`/api/claims/${currentId}/revert`, {
|
|
664
|
+
method: "POST",
|
|
665
|
+
body: JSON.stringify({ sha: browseSha }),
|
|
666
|
+
});
|
|
667
|
+
await selectClaim(currentId);
|
|
668
|
+
toast("Reverted toward " + (commit.parent_id || "").slice(0, 12));
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (lastLog.length < 2) {
|
|
672
|
+
toast("Nothing to revert to.");
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const target = lastLog[1];
|
|
676
|
+
if (!window.confirm(`Revert HEAD to ${target.id.slice(0, 12)}? This writes a new commit.`)) return;
|
|
677
|
+
const commit = await api(`/api/claims/${currentId}/revert`, {
|
|
678
|
+
method: "POST",
|
|
679
|
+
body: JSON.stringify({ sha: target.id }),
|
|
680
|
+
});
|
|
681
|
+
await selectClaim(currentId);
|
|
682
|
+
toast("Reverted toward " + (commit.parent_id || "").slice(0, 12));
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
async function saveWatchConfig() {
|
|
686
|
+
if (!currentId) return;
|
|
687
|
+
await api(`/api/claims/${currentId}/watching`, {
|
|
688
|
+
method: "POST",
|
|
689
|
+
body: JSON.stringify({
|
|
690
|
+
watching: $("watching-toggle").checked,
|
|
691
|
+
interval_seconds: Number($("watch-interval").value),
|
|
692
|
+
}),
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
$("watching-toggle").onchange = async () => {
|
|
697
|
+
try {
|
|
698
|
+
await saveWatchConfig();
|
|
699
|
+
await selectClaim(currentId, browseSha);
|
|
700
|
+
} catch (err) {
|
|
701
|
+
toast(err.message);
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
$("watch-interval").onchange = async () => {
|
|
706
|
+
try {
|
|
707
|
+
await saveWatchConfig();
|
|
708
|
+
toast("Watch interval saved for this claim.");
|
|
709
|
+
} catch (err) {
|
|
710
|
+
toast(err.message);
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
$("service-btn").onclick = async () => {
|
|
715
|
+
try {
|
|
716
|
+
const status = await api("/api/service", { timeoutMs: 8000 });
|
|
717
|
+
if (status.installed === "true") {
|
|
718
|
+
if (!window.confirm("Uninstall the background watch service?")) return;
|
|
719
|
+
await api("/api/service", { method: "DELETE", timeoutMs: 15000 });
|
|
720
|
+
$("service-btn").textContent = "install timer service";
|
|
721
|
+
toast("Background timer removed.");
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
const interval = Number($("watch-interval").value || 300);
|
|
725
|
+
await api("/api/service", {
|
|
726
|
+
method: "POST",
|
|
727
|
+
timeoutMs: 15000,
|
|
728
|
+
body: JSON.stringify({ interval_seconds: interval }),
|
|
729
|
+
});
|
|
730
|
+
$("service-btn").textContent = "remove timer service";
|
|
731
|
+
toast(`Installed a system timer that runs claim.watch() every ${interval}s.`);
|
|
732
|
+
} catch (err) {
|
|
733
|
+
toast(err.message);
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
$("copy-id").onclick = async () => {
|
|
738
|
+
if (!currentId) return;
|
|
739
|
+
await navigator.clipboard.writeText(currentId);
|
|
740
|
+
const btn = $("copy-id");
|
|
741
|
+
btn.textContent = "copied";
|
|
742
|
+
toast("Copied " + currentId);
|
|
743
|
+
setTimeout(() => { btn.textContent = "copy"; }, 1200);
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
$("browse-head").onclick = () => {
|
|
747
|
+
if (currentId) selectClaim(currentId);
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
$("detail-close").onclick = () => hideDetail();
|
|
751
|
+
$("inspector-backdrop").onclick = () => hideDetail();
|
|
752
|
+
$("claim-filter").addEventListener("input", renderClaimList);
|
|
753
|
+
document.querySelectorAll("#status-filter [data-status]").forEach((btn) => {
|
|
754
|
+
btn.addEventListener("click", () => {
|
|
755
|
+
listStatus = btn.dataset.status || "all";
|
|
756
|
+
localStorage.setItem("rd-list-status", listStatus);
|
|
757
|
+
renderClaimList();
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
function showPane(which) {
|
|
762
|
+
const app = $("app");
|
|
763
|
+
app.classList.toggle("show-rail", which === "claims");
|
|
764
|
+
app.classList.toggle("show-log", which === "history");
|
|
765
|
+
document.querySelectorAll(".pane-switch button").forEach((btn) => {
|
|
766
|
+
btn.classList.toggle("on", btn.id === `show-${which === "claims" ? "claims" : which === "history" ? "log" : "stage"}`);
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
$("show-claims").onclick = () => showPane("claims");
|
|
770
|
+
$("show-stage").onclick = () => showPane("belief");
|
|
771
|
+
$("show-log").onclick = () => showPane("history");
|
|
772
|
+
|
|
773
|
+
document.addEventListener("keydown", (ev) => {
|
|
774
|
+
if (ev.key === "Escape") {
|
|
775
|
+
if (!$("detail").classList.contains("hidden")) {
|
|
776
|
+
hideDetail();
|
|
777
|
+
ev.preventDefault();
|
|
778
|
+
}
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (typingTarget(ev.target)) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (ev.key === "/") {
|
|
785
|
+
ev.preventDefault();
|
|
786
|
+
if (window.innerWidth <= 1100) showPane("claims");
|
|
787
|
+
$("claim-filter").focus();
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (ev.key === "j") { ev.preventDefault(); moveClaim(1); return; }
|
|
791
|
+
if (ev.key === "k") { ev.preventDefault(); moveClaim(-1); return; }
|
|
792
|
+
if (ev.key === "w" && currentId) { ev.preventDefault(); $("watch-btn").click(); return; }
|
|
793
|
+
if (ev.key === "d" && currentId) { ev.preventDefault(); $("diff-btn").click(); return; }
|
|
794
|
+
if (ev.key === "b" && currentId) { ev.preventDefault(); $("blame-btn").click(); return; }
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
window.addEventListener("hashchange", () => {
|
|
798
|
+
const { id, sha } = parseRoute();
|
|
799
|
+
if (id && (id !== currentId || sha !== browseSha)) selectClaim(id, sha).catch((err) => toast(err.message));
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
restorePaneSizes();
|
|
803
|
+
enableResize();
|
|
804
|
+
|
|
805
|
+
api("/api/service", { timeoutMs: 8000 }).then((status) => {
|
|
806
|
+
if (status.installed === "true") $("service-btn").textContent = "remove timer service";
|
|
807
|
+
}).catch(() => {});
|
|
808
|
+
|
|
809
|
+
refreshList()
|
|
810
|
+
.then(() => {
|
|
811
|
+
const { id, sha } = parseRoute();
|
|
812
|
+
if (id) return selectClaim(id, sha);
|
|
813
|
+
})
|
|
814
|
+
.catch((err) => toast(err.message));
|