Files
devplacepy/nodes/extension/ui/popup.js
T
retoorandClaude Sonnet 5.5 02ab747cd0 Code Farm: per-plot auto-replant pause and crop-lock check; community sync and node work
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>
2026-10-17 02:23:43 +02:00

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();
}