@@ -1006,6 +1008,34 @@
// queued -> running -> done (rendered) / error / cancelled.
const pending = new Map();
+ // --- "notify when queue empty" checkbox ---
+ // The checkbox only appears while solves are queued/running. Permission is
+ // requested the first time it's checked (not on page load). When the queue
+ // drains to empty it fires a notification (if still checked), then unchecks
+ // itself and hides until there's work again.
+ const notifyWrap = document.getElementById("notifyWrap");
+ const notifyDone = document.getElementById("notifyDone");
+ notifyDone.onchange = () => {
+ if (notifyDone.checked && "Notification" in window
+ && Notification.permission === "default") {
+ Notification.requestPermission();
+ }
+ };
+ // Show/hide the checkbox to match the current queue state, and uncheck it
+ // once the queue is empty. Call after any change to `pending`.
+ function updateNotifyUi() {
+ notifyWrap.style.display = pending.size ? "" : "none";
+ if (!pending.size) notifyDone.checked = false;
+ }
+ // Fire the "queue empty" notification if the user opted in and granted it.
+ function notifyQueueEmpty() {
+ if (notifyDone.checked && "Notification" in window
+ && Notification.permission === "granted") {
+ new Notification("DWS Solver",
+ {body: "All queued solves have finished."});
+ }
+ }
+
function uuid() {
return crypto.randomUUID ? crypto.randomUUID()
: String(Date.now()) + Math.random();
@@ -1168,7 +1198,7 @@
card.append(
el("div", {class: "pending-head"},
[el("span", {class: "spinner"}), nameSpan,
- document.createTextNode(" — "), status]),
+ document.createTextNode(" — "), status]),
actions);
return card;
}
@@ -1190,7 +1220,11 @@
if (!entry) return;
pending.delete(token);
action(entry);
- if (pending.size === 0 && stream) {stream.close(); stream = null;}
+ if (pending.size === 0) {
+ if (stream) {stream.close(); stream = null;}
+ notifyQueueEmpty();
+ }
+ updateNotifyUi();
}
// Put a pending card into a terminal state: show the message, drop the
@@ -1283,6 +1317,7 @@
// Stash the inputs alongside the pending solve so, once it finishes,
// its card can export a self-contained {problem, solution} bundle.
pending.set(token, {token, card, n, confirmed: false, problem, maxTime: time});
+ updateNotifyUi();
fetch("/solve", {
method: "POST",
headers: {"Content-Type": "application/json"},
@@ -1330,8 +1365,10 @@
}
actions.append(el("button", {
class: "mini", type: "button",
- onclick: () => exportSolve({token, n, solution: s, problem, maxTime,
- name: solveLabel(token, n)}),
+ onclick: () => exportSolve({
+ token, n, solution: s, problem, maxTime,
+ name: solveLabel(token, n)
+ }),
}, "Export"));
// The problem may be on hand (a solve queued from this tab) or, for a
// solve opened via a share link, fetched from the server by token on
@@ -1742,6 +1779,7 @@
// Load in reverse so the first id ends up on top (each load prepends).
for (const token of sharedTokens.slice().reverse()) loadSharedSolve(token);
if (sharedTokens.length) syncStream();
+ updateNotifyUi();