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>
183 lines
5.4 KiB
JavaScript
183 lines
5.4 KiB
JavaScript
const POLL_MS = 2000;
|
|
const UNITS = ["B", "KB", "MB", "GB", "TB"];
|
|
const STATE_LABELS = {
|
|
off: "Off",
|
|
connecting: "Connecting",
|
|
online: "Online",
|
|
backoff: "Reconnecting",
|
|
halted: "Stopped",
|
|
suspended: "Suspended",
|
|
};
|
|
|
|
function formatBytes(value) {
|
|
let size = Math.max(0, Number(value) || 0);
|
|
let unit = 0;
|
|
while (size >= 1024 && unit < UNITS.length - 1) {
|
|
size /= 1024;
|
|
unit += 1;
|
|
}
|
|
return `${unit === 0 ? size : size.toFixed(1)} ${UNITS[unit]}`;
|
|
}
|
|
|
|
function pad(value) {
|
|
return String(value).padStart(2, "0");
|
|
}
|
|
|
|
function formatInstant(iso) {
|
|
const moment = new Date(iso);
|
|
if (Number.isNaN(moment.getTime())) {
|
|
return "";
|
|
}
|
|
return `${pad(moment.getDate())}/${pad(moment.getMonth() + 1)}/${moment.getFullYear()} ${pad(moment.getHours())}:${pad(moment.getMinutes())}`;
|
|
}
|
|
|
|
export function describeState(state) {
|
|
if (state.state === "suspended") {
|
|
const ends = formatInstant(state.suspendedUntil);
|
|
return ends ? `Suspended until ${ends}, retrying automatically` : "Suspended, retrying automatically";
|
|
}
|
|
return state.detail || state.lastError || "";
|
|
}
|
|
|
|
export function displayLabel(state, config) {
|
|
return state.deviceLabel || config.label;
|
|
}
|
|
|
|
class Popup {
|
|
constructor(root) {
|
|
this.root = root;
|
|
this.timer = null;
|
|
this.view = null;
|
|
this.$ = (id) => root.getElementById(id);
|
|
}
|
|
|
|
async send(message) {
|
|
const reply = await chrome.runtime.sendMessage(message);
|
|
if (!reply) {
|
|
throw new Error("The background worker did not answer");
|
|
}
|
|
if (reply.error) {
|
|
throw new Error(reply.error);
|
|
}
|
|
return reply;
|
|
}
|
|
|
|
showError(error) {
|
|
this.$("error").textContent = error ? error.message || String(error) : "";
|
|
}
|
|
|
|
async run(message) {
|
|
try {
|
|
this.render(await this.send(message));
|
|
this.showError(null);
|
|
} catch (error) {
|
|
this.showError(error);
|
|
}
|
|
}
|
|
|
|
render(view) {
|
|
this.view = view;
|
|
const { config, state, usage } = view;
|
|
this.$("dot").className = `dot ${state.state}`;
|
|
this.$("state").textContent = STATE_LABELS[state.state] || state.state;
|
|
this.$("detail").textContent = describeState(state);
|
|
this.$("label").textContent = displayLabel(state, config);
|
|
this.$("disclosure-card").hidden = config.disclosed;
|
|
const toggle = this.$("toggle");
|
|
toggle.checked = config.enabled;
|
|
toggle.disabled = !config.disclosed || !config.hasSecret;
|
|
this.$("toggle-label").textContent = config.enabled ? "Sharing on" : "Sharing off";
|
|
this.$("requests").textContent = usage.requests;
|
|
this.$("failed").textContent = usage.failed;
|
|
this.$("down").textContent = formatBytes(usage.bytesDown);
|
|
this.$("up").textContent = formatBytes(usage.bytesUp);
|
|
this.$("lifetime").textContent =
|
|
`All time: ${usage.lifetimeRequests} requests, ${formatBytes(usage.lifetimeBytes)}`;
|
|
this.renderSettings(config);
|
|
this.renderConnection(config);
|
|
}
|
|
|
|
renderSettings(config) {
|
|
if (this.root.activeElement && this.$("settings").contains(this.root.activeElement)) {
|
|
return;
|
|
}
|
|
this.$("bandwidth").value = config.bandwidthMb;
|
|
this.$("concurrency").value = config.maxConcurrent;
|
|
this.$("from").value = config.activeFrom;
|
|
this.$("to").value = config.activeTo;
|
|
}
|
|
|
|
renderConnection(config) {
|
|
this.$("forget").hidden = !config.hasSecret;
|
|
this.$("connect-summary").textContent = config.hasSecret ? "Connection" : "Connect to a server";
|
|
this.$("saved").textContent = config.hasSecret
|
|
? `Connected to ${config.serverUrl} with secret ${config.secretPrefix}`
|
|
: "";
|
|
if (!config.hasSecret) {
|
|
this.$("connect-box").open = true;
|
|
}
|
|
}
|
|
|
|
async requestOrigin(serverUrl) {
|
|
const origin = new URL(serverUrl).origin;
|
|
const granted = await chrome.permissions.request({ origins: [origin + "/*"] });
|
|
if (!granted) {
|
|
throw new Error("Permission to talk to that server was not granted");
|
|
}
|
|
}
|
|
|
|
async onConnect(event) {
|
|
event.preventDefault();
|
|
const secretField = this.$("secret");
|
|
try {
|
|
await this.requestOrigin(this.$("server").value);
|
|
this.render(
|
|
await this.send({
|
|
cmd: "connect",
|
|
serverUrl: this.$("server").value,
|
|
secret: secretField.value,
|
|
label: this.$("device-label").value,
|
|
}),
|
|
);
|
|
secretField.value = "";
|
|
this.showError(null);
|
|
} catch (error) {
|
|
this.showError(error);
|
|
}
|
|
}
|
|
|
|
onSettings(event) {
|
|
event.preventDefault();
|
|
this.run({
|
|
cmd: "settings",
|
|
values: {
|
|
bandwidthMb: Number(this.$("bandwidth").value),
|
|
maxConcurrent: Number(this.$("concurrency").value),
|
|
activeFrom: this.$("from").value,
|
|
activeTo: this.$("to").value,
|
|
},
|
|
});
|
|
}
|
|
|
|
onForget() {
|
|
if (this.root.defaultView.confirm("Forget this device secret on this browser?")) {
|
|
this.run({ cmd: "forget" });
|
|
}
|
|
}
|
|
|
|
start() {
|
|
this.$("connect").addEventListener("submit", (event) => this.onConnect(event));
|
|
this.$("settings").addEventListener("submit", (event) => this.onSettings(event));
|
|
this.$("forget").addEventListener("click", () => this.onForget());
|
|
this.$("toggle").addEventListener("change", (event) =>
|
|
this.run({ cmd: "enable", enabled: event.target.checked }),
|
|
);
|
|
this.run({ cmd: "status" });
|
|
this.timer = setInterval(() => this.run({ cmd: "status" }), POLL_MS);
|
|
}
|
|
}
|
|
|
|
if (typeof document !== "undefined") {
|
|
new Popup(document).start();
|
|
}
|