Quotas: reset one address, or give some back
POST /rules/{ref}/reset-quota takes an optional address (one entry of a
per-address quota starts again) and give (bytes taken off what was used,
one address's or everyone's, until the next reset). The firewall keeps
the adjustment for its next apply, which writes the new usage into the
ruleset; housecarld applies at once and saves the usage. hc rule
reset-quota --address/--give; Reset and Give back in the web UI, per
address in the usage dialog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ba2de59263
commit
4c13297f27
14 files changed
+339
-23
No files matched your search
@@ -175,24 +175,57 @@ function quotaLine(r, q) {
|
||||
return h("div", { class: "muted small" }, (qt.over ? "quota over " : "quota ") + fmtBytes(qt.bytes) + each + ", resets " + resets,
|
||||
used ? [" · ", used] : null,
|
||||
q && q.next_reset ? " · next reset " + new Date(q.next_reset).toLocaleString() : null,
|
||||
can("config", true) && q && r.pending !== "new" ? [" ", h("button", { class: "small", onclick: () => resetQuota(r) }, "Reset")] : null);
|
||||
can("config", true) && q && r.pending !== "new" ? [" ", h("button", { class: "small", onclick: () => resetQuota(r) }, "Reset"),
|
||||
" ", h("button", { class: "small", onclick: () => giveBack(r) }, "Give back…")] : null);
|
||||
}
|
||||
|
||||
async function usageDialog(r, q) {
|
||||
const names = await lookupNames(q.addresses.map((a) => a.address));
|
||||
// A row's action closes this dialog first: it opens one of its own.
|
||||
const then = (f) => (e) => { e.target.closest(".modal").querySelector(".buttons button").click(); f(); };
|
||||
const write = can("config", true);
|
||||
await modal("Quota usage of " + (r.comment || "rule #" + r.id), h("div", null,
|
||||
h("p", { class: "muted" }, "Each " + (r.quota.per_source ? "source" : "destination") + " has " + fmtBytes(r.quota.bytes) + "."),
|
||||
table([
|
||||
{ label: "Address", render: (a) => h("span", { class: "mono" }, named(names, a.address)) },
|
||||
{ label: "Used", render: (a) => fmtBytes(a.used) },
|
||||
{ label: "", render: (a) => usageBar(a.used, r.quota.bytes) },
|
||||
], q.addresses)), [{ label: "Close", value: null }], { wide: true });
|
||||
write ? { label: "", render: (a) => h("span", { class: "nowrap" },
|
||||
h("button", { class: "small", onclick: then(() => resetQuota(r, a.address)) }, "Reset"), " ",
|
||||
h("button", { class: "small", onclick: then(() => giveBack(r, a.address)) }, "Give back…")) } : null,
|
||||
].filter(Boolean), q.addresses)), [{ label: "Close", value: null }], { wide: true });
|
||||
}
|
||||
|
||||
async function resetQuota(r) {
|
||||
// giveBack takes an amount off what a quota used (one address's, or
|
||||
// everyone's): extra allowance until its next reset.
|
||||
async function giveBack(r, address) {
|
||||
const amount = input({ type: "number", min: 1, value: "1", class: "short" });
|
||||
const unit = select([["mb", "MB"], ["gb", "GB"]], "gb", { class: "inline" });
|
||||
const perAddress = r.quota.per_source || r.quota.per_destination;
|
||||
const who = address ? " for " + address : perAddress ? " for every address" : "";
|
||||
await modal("Give back quota", h("div", null,
|
||||
h("p", null, "Take this much off what " + (r.comment || "rule #" + r.id) + "'s quota used" + who + ": extra allowance until its next reset, without changing the rule. This is done at once, not staged."),
|
||||
field("Amount", h("div", null, amount, " ", unit))), [
|
||||
{ label: "Cancel", value: null },
|
||||
{ label: "Give back", kind: "primary", onclick: async () => {
|
||||
const n = parseInt(amount.value, 10);
|
||||
if (!(n > 0)) { toast("Give an amount of 1 or more", "err"); return false; }
|
||||
const give = n * (unit.value === "gb" ? 1 << 30 : 1 << 20);
|
||||
try {
|
||||
await api("POST", "/rules/" + r.id + "/reset-quota", { body: address ? { give, address } : { give } });
|
||||
toast("Gave back " + fmtBytes(give)); rerender();
|
||||
} catch (e) { modal("Not given back", errorBox(e)); return false; }
|
||||
} },
|
||||
]);
|
||||
}
|
||||
|
||||
async function resetQuota(r, address) {
|
||||
if (!await confirmDialog("Reset the quota", "Start the quota of " + (r.comment || "rule #" + r.id) + " again now" +
|
||||
(r.quota.per_source || r.quota.per_destination ? ", for every address" : "") + "? This is done at once, not staged.", "Reset")) return;
|
||||
try { await api("POST", "/rules/" + r.id + "/reset-quota"); toast("Quota reset"); rerender(); } catch (e) { modal("Not reset", errorBox(e)); }
|
||||
(address ? " for " + address : r.quota.per_source || r.quota.per_destination ? ", for every address" : "") + "? This is done at once, not staged.", "Reset")) return;
|
||||
try {
|
||||
await api("POST", "/rules/" + r.id + "/reset-quota", address ? { body: { address } } : {});
|
||||
toast("Quota reset"); rerender();
|
||||
} catch (e) { modal("Not reset", errorBox(e)); }
|
||||
}
|
||||
|
||||
async function move(r, where) {
|
||||
|
||||
Reference in new issue
Block a user