DevPlace CI / test (push) Failing after 44m45s
Continuous Delivery could never be turned off once a crop was planted. Add a one-shot per-plot "stop after harvest" flag (POST /game/plot/stop, Devii tool game_plot_stop, game_plots.stop_after_harvest), cleared by the next plant, autoreplant claim or Refactor. Autoreplant now also respects crop locks through the shared _crop_lock predicate. Plan and measurements in happyplanting.md. Also includes the pending community sync, node network and related work in the tree; the full suite passed (4999) with all of it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
123 lines
4.9 KiB
JavaScript
123 lines
4.9 KiB
JavaScript
// retoor <retoor@molodetz.nl>
|
|
|
|
import { Http } from "./Http.js";
|
|
import { Poller } from "./Poller.js";
|
|
|
|
const REFRESH_MS = 10000;
|
|
const PAGE_URL = "/admin/nodes";
|
|
|
|
class AdminNodes {
|
|
constructor() {
|
|
this.root = document.querySelector("[data-nodes-root]");
|
|
if (!this.root) return;
|
|
this.modal = document.getElementById("node-suspend-modal");
|
|
this.suspendForm = document.getElementById("node-suspend-form");
|
|
this.suspendTitle = document.getElementById("node-suspend-title");
|
|
this.killswitch = document.querySelector("[data-nodes-killswitch]");
|
|
this.settings = document.querySelector("[data-nodes-settings]");
|
|
this.bindActions();
|
|
this.bindSuspendForm();
|
|
this.bindKillswitch();
|
|
this.bindSettings();
|
|
this.poller = new Poller(() => this.refresh(), REFRESH_MS, { pauseHidden: true, immediate: false });
|
|
}
|
|
|
|
async refresh() {
|
|
const response = await fetch(PAGE_URL, { headers: { "Accept": "text/html" } });
|
|
if (!response.ok) return;
|
|
const fresh = new DOMParser().parseFromString(await response.text(), "text/html");
|
|
this.root.querySelectorAll("[data-live]").forEach((section) => {
|
|
const next = fresh.querySelector(`[data-live="${section.dataset.live}"]`);
|
|
if (next) section.innerHTML = next.innerHTML;
|
|
});
|
|
const state = fresh.querySelector("[data-nodes-killswitch]");
|
|
if (state && this.killswitch) this.killswitch.innerHTML = state.innerHTML;
|
|
}
|
|
|
|
bindActions() {
|
|
this.root.addEventListener("click", async (event) => {
|
|
const button = event.target.closest("[data-node-action]");
|
|
if (!button) return;
|
|
const uid = button.dataset.uid;
|
|
const action = button.dataset.nodeAction;
|
|
if (action === "suspend") {
|
|
this.openSuspend(uid, button.dataset.label);
|
|
return;
|
|
}
|
|
if (action === "override") {
|
|
await this.override(uid, button.dataset.value, button.dataset.label);
|
|
return;
|
|
}
|
|
const urls = {
|
|
"unsuspend": `/admin/nodes/devices/${uid}/unsuspend`,
|
|
"resolve-flag": `/admin/nodes/flags/${uid}/resolve`,
|
|
};
|
|
await this.post(urls[action], {}, action === "unsuspend" ? "Suspension lifted" : "Flag resolved");
|
|
});
|
|
}
|
|
|
|
async override(uid, value, label) {
|
|
const allowing = value === "allow";
|
|
const message = allowing
|
|
? `Let ${label} serve jobs although its address is not residential or mobile?`
|
|
: `Remove the address class override of ${label}?`;
|
|
const ok = await window.app.dialog.confirm({ message, danger: allowing });
|
|
if (!ok) return;
|
|
await this.post(`/admin/nodes/devices/${uid}/class`, { value }, allowing ? "Override set" : "Override cleared");
|
|
}
|
|
|
|
openSuspend(uid, label) {
|
|
this.suspendForm.action = `/admin/nodes/devices/${uid}/suspend`;
|
|
this.suspendTitle.textContent = `Suspend device: ${label}`;
|
|
this.modal.classList.add("visible");
|
|
}
|
|
|
|
bindSuspendForm() {
|
|
this.suspendForm.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
const params = Object.fromEntries(new FormData(this.suspendForm).entries());
|
|
if (await this.post(this.suspendForm.action, params, "Device suspended")) {
|
|
this.modal.classList.remove("visible");
|
|
this.suspendForm.reset();
|
|
}
|
|
});
|
|
}
|
|
|
|
bindKillswitch() {
|
|
if (!this.killswitch) return;
|
|
this.killswitch.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
const turningOn = this.killswitch.querySelector("[name='enabled']").value === "true";
|
|
const message = turningOn
|
|
? "Switch community donation on? Consenting donors can connect again."
|
|
: "Switch community donation off? Every connected node is disconnected within seconds.";
|
|
const ok = await window.app.dialog.confirm({ message, danger: !turningOn });
|
|
if (!ok) return;
|
|
await this.post(this.killswitch.action, { enabled: String(turningOn) }, "Kill switch updated");
|
|
});
|
|
}
|
|
|
|
bindSettings() {
|
|
if (!this.settings) return;
|
|
this.settings.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
const params = Object.fromEntries(new FormData(this.settings).entries());
|
|
await this.post(this.settings.action, params, "Settings saved");
|
|
});
|
|
}
|
|
|
|
async post(url, params, message) {
|
|
try {
|
|
await Http.send(url, params);
|
|
window.app.toast.show(message, { type: "success" });
|
|
await this.refresh();
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
window.AdminNodes = AdminNodes;
|
|
export { AdminNodes };
|