Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- AGENTS.md +3 -0
- CLAUDE.md +3 -0
- tests/test_admin_session.js +61 -0
- web_demo/static/shared/admin-session.js +49 -0
- web_demo/templates/admin.html +96 -3
AGENTS.md
CHANGED
|
@@ -253,6 +253,9 @@ for `recommend_ring_size()`; exact midpoint ties prefer the smaller size. Raw
|
|
| 253 |
shot fields remain unchanged. The visible result cards prefer the separate
|
| 254 |
`session_recommendation` object. A single footer below the cards reads
|
| 255 |
`Based on N measurements. (Please measure at least 3 times for best reliability)`
|
|
|
|
|
|
|
|
|
|
| 256 |
|
| 257 |
Aggregation is partitioned by `handedness + finger + ring_model`; failed
|
| 258 |
fingers are excluded and exact duplicate images are identified by SHA-256.
|
|
|
|
| 253 |
shot fields remain unchanged. The visible result cards prefer the separate
|
| 254 |
`session_recommendation` object. A single footer below the cards reads
|
| 255 |
`Based on N measurements. (Please measure at least 3 times for best reliability)`
|
| 256 |
+
The admin Records table preserves its raw-shot columns and adds a separate
|
| 257 |
+
`Session Recommendation` summary column with an on-demand JSON view; legacy
|
| 258 |
+
rows display `—` in that column.
|
| 259 |
|
| 260 |
Aggregation is partitioned by `handedness + finger + ring_model`; failed
|
| 261 |
fingers are excluded and exact duplicate images are identified by SHA-256.
|
CLAUDE.md
CHANGED
|
@@ -253,6 +253,9 @@ for `recommend_ring_size()`; exact midpoint ties prefer the smaller size. Raw
|
|
| 253 |
shot fields remain unchanged. The visible result cards prefer the separate
|
| 254 |
`session_recommendation` object. A single footer below the cards reads
|
| 255 |
`Based on N measurements. (Please measure at least 3 times for best reliability)`
|
|
|
|
|
|
|
|
|
|
| 256 |
|
| 257 |
Aggregation is partitioned by `handedness + finger + ring_model`; failed
|
| 258 |
fingers are excluded and exact duplicate images are identified by SHA-256.
|
|
|
|
| 253 |
shot fields remain unchanged. The visible result cards prefer the separate
|
| 254 |
`session_recommendation` object. A single footer below the cards reads
|
| 255 |
`Based on N measurements. (Please measure at least 3 times for best reliability)`
|
| 256 |
+
The admin Records table preserves its raw-shot columns and adds a separate
|
| 257 |
+
`Session Recommendation` summary column with an on-demand JSON view; legacy
|
| 258 |
+
rows display `—` in that column.
|
| 259 |
|
| 260 |
Aggregation is partitioned by `handedness + finger + ring_model`; failed
|
| 261 |
fingers are excluded and exact duplicate images are identified by SHA-256.
|
tests/test_admin_session.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const assert = require("node:assert/strict");
|
| 2 |
+
const fs = require("node:fs");
|
| 3 |
+
const test = require("node:test");
|
| 4 |
+
const vm = require("node:vm");
|
| 5 |
+
|
| 6 |
+
const source = fs.readFileSync(
|
| 7 |
+
"web_demo/static/shared/admin-session.js",
|
| 8 |
+
"utf8",
|
| 9 |
+
);
|
| 10 |
+
|
| 11 |
+
function loadHelper() {
|
| 12 |
+
const window = {};
|
| 13 |
+
vm.runInNewContext(source, { window, Array, Object, String });
|
| 14 |
+
return window.AdminSession;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
test("returns no summary for a legacy record", () => {
|
| 18 |
+
assert.equal(loadHelper().summary({ session_recommendation: null }), null);
|
| 19 |
+
});
|
| 20 |
+
|
| 21 |
+
test("summarizes a complete session recommendation", () => {
|
| 22 |
+
const summary = loadHelper().summary({
|
| 23 |
+
session_id: "12345678-abcd-ef01-2345-6789abcdef01",
|
| 24 |
+
session_attempt_index: 4,
|
| 25 |
+
session_recommendation: {
|
| 26 |
+
overall_best_size: 7,
|
| 27 |
+
handedness: "Right",
|
| 28 |
+
successful_shots: 3,
|
| 29 |
+
per_finger: {
|
| 30 |
+
index: { status: "ok", best_match: 7 },
|
| 31 |
+
middle: { status: "ok", best_match: 8 },
|
| 32 |
+
ring: { status: "ok", best_match: 6 },
|
| 33 |
+
},
|
| 34 |
+
},
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
assert.equal(summary.overallSize, 7);
|
| 38 |
+
assert.equal(summary.handedness, "Right");
|
| 39 |
+
assert.equal(summary.successfulShots, 3);
|
| 40 |
+
assert.equal(summary.attemptIndex, 4);
|
| 41 |
+
assert.equal(summary.shortSessionId, "12345678");
|
| 42 |
+
assert.deepEqual({ ...summary.perFinger }, { index: 7, middle: 8, ring: 6 });
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
test("uses JSON fallbacks and omits unsuccessful fingers", () => {
|
| 46 |
+
const summary = loadHelper().summary({
|
| 47 |
+
session_recommendation: {
|
| 48 |
+
session_id: "fallback-session",
|
| 49 |
+
attempt_index: 2,
|
| 50 |
+
overall_best_size: 8,
|
| 51 |
+
per_finger: {
|
| 52 |
+
index: { status: "ok", best_match: 8 },
|
| 53 |
+
middle: { status: "failed", best_match: null },
|
| 54 |
+
},
|
| 55 |
+
},
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
assert.equal(summary.attemptIndex, 2);
|
| 59 |
+
assert.equal(summary.shortSessionId, "fallback");
|
| 60 |
+
assert.deepEqual({ ...summary.perFinger }, { index: 8 });
|
| 61 |
+
});
|
web_demo/static/shared/admin-session.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
(function (root) {
|
| 2 |
+
"use strict";
|
| 3 |
+
|
| 4 |
+
const FINGERS = ["index", "middle", "ring"];
|
| 5 |
+
|
| 6 |
+
function present(value) {
|
| 7 |
+
return value !== null && value !== undefined && value !== "";
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
function summary(row) {
|
| 11 |
+
const recommendation = row && row.session_recommendation;
|
| 12 |
+
if (!recommendation || typeof recommendation !== "object" || Array.isArray(recommendation)) {
|
| 13 |
+
return null;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
const perFinger = {};
|
| 17 |
+
const storedPerFinger = recommendation.per_finger;
|
| 18 |
+
if (storedPerFinger && typeof storedPerFinger === "object") {
|
| 19 |
+
FINGERS.forEach((finger) => {
|
| 20 |
+
const item = storedPerFinger[finger];
|
| 21 |
+
if (item && item.status === "ok" && present(item.best_match)) {
|
| 22 |
+
perFinger[finger] = item.best_match;
|
| 23 |
+
}
|
| 24 |
+
});
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
const sessionId = present(row.session_id)
|
| 28 |
+
? String(row.session_id)
|
| 29 |
+
: (present(recommendation.session_id) ? String(recommendation.session_id) : "");
|
| 30 |
+
|
| 31 |
+
return {
|
| 32 |
+
overallSize: present(recommendation.overall_best_size)
|
| 33 |
+
? recommendation.overall_best_size
|
| 34 |
+
: null,
|
| 35 |
+
handedness: present(recommendation.handedness) ? recommendation.handedness : "",
|
| 36 |
+
successfulShots: present(recommendation.successful_shots)
|
| 37 |
+
? recommendation.successful_shots
|
| 38 |
+
: null,
|
| 39 |
+
attemptIndex: present(row.session_attempt_index)
|
| 40 |
+
? row.session_attempt_index
|
| 41 |
+
: (present(recommendation.attempt_index) ? recommendation.attempt_index : null),
|
| 42 |
+
sessionId,
|
| 43 |
+
shortSessionId: sessionId ? sessionId.slice(0, 8) : "",
|
| 44 |
+
perFinger,
|
| 45 |
+
};
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
root.AdminSession = { summary };
|
| 49 |
+
}(window));
|
web_demo/templates/admin.html
CHANGED
|
@@ -76,6 +76,19 @@
|
|
| 76 |
.finger-cell { font-size: 12px; }
|
| 77 |
.finger-cell .size { font-weight: 600; }
|
| 78 |
.finger-cell .detail { color: var(--ink-soft); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
.rating-stars { color: #e3a73b; letter-spacing: 1px; font-size: 13px; }
|
| 80 |
.rating-stars .rating-empty { color: rgba(45, 33, 33, 0.18); }
|
| 81 |
.comment-cell {
|
|
@@ -153,6 +166,26 @@
|
|
| 153 |
.top-kol-list li:last-child { border-bottom: none; }
|
| 154 |
.top-kol-list .name { font-weight: 500; }
|
| 155 |
.top-kol-list .count { color: var(--ink-soft); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
</style>
|
| 157 |
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
| 158 |
</head>
|
|
@@ -201,6 +234,7 @@
|
|
| 201 |
<th>Index</th>
|
| 202 |
<th>Middle</th>
|
| 203 |
<th>Ring</th>
|
|
|
|
| 204 |
<th>Conf</th>
|
| 205 |
<th>Fail</th>
|
| 206 |
<th>Rating</th>
|
|
@@ -209,7 +243,7 @@
|
|
| 209 |
</tr>
|
| 210 |
</thead>
|
| 211 |
<tbody id="tableBody">
|
| 212 |
-
<tr><td colspan="
|
| 213 |
</tbody>
|
| 214 |
</table>
|
| 215 |
</div>
|
|
@@ -306,6 +340,15 @@
|
|
| 306 |
</div>
|
| 307 |
</div>
|
| 308 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
<script>
|
| 310 |
const loginGate = document.getElementById("loginGate");
|
| 311 |
const adminContent = document.getElementById("adminContent");
|
|
@@ -396,6 +439,32 @@
|
|
| 396 |
return `<span class="comment-cell" title="${esc(msg)}">${esc(trimmed)}${more}</span>`;
|
| 397 |
};
|
| 398 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
const rowHtml = (r) => {
|
| 400 |
const pf = r.per_finger || {};
|
| 401 |
const photoThumb = r.photo_url
|
|
@@ -413,6 +482,7 @@
|
|
| 413 |
<td class="finger-cell">${fmtFinger(pf, "index")}</td>
|
| 414 |
<td class="finger-cell">${fmtFinger(pf, "middle")}</td>
|
| 415 |
<td class="finger-cell">${fmtFinger(pf, "ring")}</td>
|
|
|
|
| 416 |
<td>${r.confidence != null ? (r.confidence * 100).toFixed(0) + "%" : "-"}</td>
|
| 417 |
<td>${r.fail_reason ? '<span class="fail">' + r.fail_reason + "</span>" : ""}</td>
|
| 418 |
<td>${fmtRating(r.feedback_rating)}</td>
|
|
@@ -428,7 +498,7 @@
|
|
| 428 |
if (currentPage < 1) currentPage = 1;
|
| 429 |
if (total === 0) {
|
| 430 |
countLabel.textContent = "0 records";
|
| 431 |
-
tbody.innerHTML = '<tr><td colspan="
|
| 432 |
} else {
|
| 433 |
const start = (currentPage - 1) * PAGE_SIZE;
|
| 434 |
const slice = allRows.slice(start, start + PAGE_SIZE);
|
|
@@ -455,10 +525,33 @@
|
|
| 455 |
currentPage = 1;
|
| 456 |
renderPage();
|
| 457 |
} catch (e) {
|
| 458 |
-
tbody.innerHTML = `<tr><td colspan="
|
| 459 |
}
|
| 460 |
};
|
| 461 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
window.deleteRow = async (btn) => {
|
| 463 |
if (!confirm("Delete this measurement record?")) return;
|
| 464 |
const tr = btn.closest("tr");
|
|
|
|
| 76 |
.finger-cell { font-size: 12px; }
|
| 77 |
.finger-cell .size { font-weight: 600; }
|
| 78 |
.finger-cell .detail { color: var(--ink-soft); }
|
| 79 |
+
.session-cell {
|
| 80 |
+
min-width: 190px; white-space: normal; font-size: 12px;
|
| 81 |
+
line-height: 1.35;
|
| 82 |
+
}
|
| 83 |
+
.session-overall { font-weight: 600; color: var(--ink); }
|
| 84 |
+
.session-fingers { margin-top: 2px; color: var(--ink); }
|
| 85 |
+
.session-meta { margin-top: 2px; color: var(--ink-soft); }
|
| 86 |
+
.session-json-btn {
|
| 87 |
+
margin-top: 5px; padding: 2px 7px; font-size: 11px; cursor: pointer;
|
| 88 |
+
border: 1px solid var(--border); border-radius: 4px;
|
| 89 |
+
background: #fff; color: var(--ink-soft);
|
| 90 |
+
}
|
| 91 |
+
.session-json-btn:hover { background: var(--sand); color: var(--ink); }
|
| 92 |
.rating-stars { color: #e3a73b; letter-spacing: 1px; font-size: 13px; }
|
| 93 |
.rating-stars .rating-empty { color: rgba(45, 33, 33, 0.18); }
|
| 94 |
.comment-cell {
|
|
|
|
| 166 |
.top-kol-list li:last-child { border-bottom: none; }
|
| 167 |
.top-kol-list .name { font-weight: 500; }
|
| 168 |
.top-kol-list .count { color: var(--ink-soft); }
|
| 169 |
+
.session-dialog {
|
| 170 |
+
width: min(720px, calc(100vw - 32px)); max-height: calc(100vh - 48px);
|
| 171 |
+
border: 1px solid var(--border); border-radius: 10px; padding: 0;
|
| 172 |
+
color: var(--ink); background: #fff; box-shadow: 0 12px 40px rgba(34, 26, 26, 0.25);
|
| 173 |
+
}
|
| 174 |
+
.session-dialog::backdrop { background: rgba(34, 26, 26, 0.45); }
|
| 175 |
+
.session-dialog-head {
|
| 176 |
+
display: flex; align-items: center; justify-content: space-between;
|
| 177 |
+
gap: 16px; padding: 14px 16px; border-bottom: 1px solid var(--border);
|
| 178 |
+
}
|
| 179 |
+
.session-dialog-head h2 { margin: 0; font-size: 15px; }
|
| 180 |
+
.session-dialog-head button {
|
| 181 |
+
padding: 4px 10px; cursor: pointer; border: 1px solid var(--border);
|
| 182 |
+
border-radius: 5px; background: #fff; color: var(--ink);
|
| 183 |
+
}
|
| 184 |
+
.session-dialog pre {
|
| 185 |
+
margin: 0; padding: 16px; overflow: auto; max-height: calc(100vh - 130px);
|
| 186 |
+
white-space: pre-wrap; overflow-wrap: anywhere; font-size: 12px; line-height: 1.45;
|
| 187 |
+
background: #fcfaf6;
|
| 188 |
+
}
|
| 189 |
</style>
|
| 190 |
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
| 191 |
</head>
|
|
|
|
| 234 |
<th>Index</th>
|
| 235 |
<th>Middle</th>
|
| 236 |
<th>Ring</th>
|
| 237 |
+
<th>Session Recommendation</th>
|
| 238 |
<th>Conf</th>
|
| 239 |
<th>Fail</th>
|
| 240 |
<th>Rating</th>
|
|
|
|
| 243 |
</tr>
|
| 244 |
</thead>
|
| 245 |
<tbody id="tableBody">
|
| 246 |
+
<tr><td colspan="14" class="empty">Loading...</td></tr>
|
| 247 |
</tbody>
|
| 248 |
</table>
|
| 249 |
</div>
|
|
|
|
| 340 |
</div>
|
| 341 |
</div>
|
| 342 |
|
| 343 |
+
<dialog class="session-dialog" id="sessionJsonDialog">
|
| 344 |
+
<div class="session-dialog-head">
|
| 345 |
+
<h2 id="sessionJsonTitle">Session Recommendation JSON</h2>
|
| 346 |
+
<button type="button" onclick="closeSessionJson()">Close</button>
|
| 347 |
+
</div>
|
| 348 |
+
<pre id="sessionJsonOutput"></pre>
|
| 349 |
+
</dialog>
|
| 350 |
+
|
| 351 |
+
<script src="/static/shared/admin-session.js"></script>
|
| 352 |
<script>
|
| 353 |
const loginGate = document.getElementById("loginGate");
|
| 354 |
const adminContent = document.getElementById("adminContent");
|
|
|
|
| 439 |
return `<span class="comment-cell" title="${esc(msg)}">${esc(trimmed)}${more}</span>`;
|
| 440 |
};
|
| 441 |
|
| 442 |
+
const fmtSessionRecommendation = (row) => {
|
| 443 |
+
const session = window.AdminSession.summary(row);
|
| 444 |
+
if (!session) return '<span class="detail">—</span>';
|
| 445 |
+
|
| 446 |
+
const fingerLabels = { index: "I", middle: "M", ring: "R" };
|
| 447 |
+
const fingers = Object.entries(session.perFinger)
|
| 448 |
+
.map(([finger, size]) => `${fingerLabels[finger]} ${esc(size)}`)
|
| 449 |
+
.join(" · ");
|
| 450 |
+
const evidence = [];
|
| 451 |
+
if (session.handedness) evidence.push(esc(session.handedness));
|
| 452 |
+
if (session.successfulShots != null) {
|
| 453 |
+
const noun = Number(session.successfulShots) === 1 ? "shot" : "shots";
|
| 454 |
+
evidence.push(`${esc(session.successfulShots)} successful ${noun}`);
|
| 455 |
+
}
|
| 456 |
+
const audit = [];
|
| 457 |
+
if (session.attemptIndex != null) audit.push(`Attempt ${esc(session.attemptIndex)}`);
|
| 458 |
+
if (session.shortSessionId) audit.push(esc(session.shortSessionId));
|
| 459 |
+
|
| 460 |
+
return `
|
| 461 |
+
<div class="session-overall">Overall ${session.overallSize != null ? esc(session.overallSize) : "—"}</div>
|
| 462 |
+
${fingers ? `<div class="session-fingers">${fingers}</div>` : ""}
|
| 463 |
+
${evidence.length ? `<div class="session-meta">${evidence.join(" · ")}</div>` : ""}
|
| 464 |
+
${audit.length ? `<div class="session-meta">${audit.join(" · ")}</div>` : ""}
|
| 465 |
+
<button class="session-json-btn" type="button" onclick="viewSessionJson(this)">View JSON</button>`;
|
| 466 |
+
};
|
| 467 |
+
|
| 468 |
const rowHtml = (r) => {
|
| 469 |
const pf = r.per_finger || {};
|
| 470 |
const photoThumb = r.photo_url
|
|
|
|
| 482 |
<td class="finger-cell">${fmtFinger(pf, "index")}</td>
|
| 483 |
<td class="finger-cell">${fmtFinger(pf, "middle")}</td>
|
| 484 |
<td class="finger-cell">${fmtFinger(pf, "ring")}</td>
|
| 485 |
+
<td class="session-cell">${fmtSessionRecommendation(r)}</td>
|
| 486 |
<td>${r.confidence != null ? (r.confidence * 100).toFixed(0) + "%" : "-"}</td>
|
| 487 |
<td>${r.fail_reason ? '<span class="fail">' + r.fail_reason + "</span>" : ""}</td>
|
| 488 |
<td>${fmtRating(r.feedback_rating)}</td>
|
|
|
|
| 498 |
if (currentPage < 1) currentPage = 1;
|
| 499 |
if (total === 0) {
|
| 500 |
countLabel.textContent = "0 records";
|
| 501 |
+
tbody.innerHTML = '<tr><td colspan="14" class="empty">No measurements yet</td></tr>';
|
| 502 |
} else {
|
| 503 |
const start = (currentPage - 1) * PAGE_SIZE;
|
| 504 |
const slice = allRows.slice(start, start + PAGE_SIZE);
|
|
|
|
| 525 |
currentPage = 1;
|
| 526 |
renderPage();
|
| 527 |
} catch (e) {
|
| 528 |
+
tbody.innerHTML = `<tr><td colspan="14" class="empty">Error loading data: ${e.message}</td></tr>`;
|
| 529 |
}
|
| 530 |
};
|
| 531 |
|
| 532 |
+
window.viewSessionJson = (btn) => {
|
| 533 |
+
const rowId = btn.closest("tr").dataset.id;
|
| 534 |
+
const row = allRows.find((item) => String(item.id) === String(rowId));
|
| 535 |
+
if (!row || !row.session_recommendation) return;
|
| 536 |
+
|
| 537 |
+
const dialog = document.getElementById("sessionJsonDialog");
|
| 538 |
+
const shortId = row.session_id ? ` · ${String(row.session_id).slice(0, 8)}` : "";
|
| 539 |
+
document.getElementById("sessionJsonTitle").textContent = `Session Recommendation${shortId}`;
|
| 540 |
+
document.getElementById("sessionJsonOutput").textContent = JSON.stringify(
|
| 541 |
+
row.session_recommendation,
|
| 542 |
+
null,
|
| 543 |
+
2,
|
| 544 |
+
);
|
| 545 |
+
if (typeof dialog.showModal === "function") dialog.showModal();
|
| 546 |
+
else dialog.setAttribute("open", "");
|
| 547 |
+
};
|
| 548 |
+
|
| 549 |
+
window.closeSessionJson = () => {
|
| 550 |
+
const dialog = document.getElementById("sessionJsonDialog");
|
| 551 |
+
if (typeof dialog.close === "function") dialog.close();
|
| 552 |
+
else dialog.removeAttribute("open");
|
| 553 |
+
};
|
| 554 |
+
|
| 555 |
window.deleteRow = async (btn) => {
|
| 556 |
if (!confirm("Delete this measurement record?")) return;
|
| 557 |
const tr = btn.closest("tr");
|