Diffs name their chain; history shows the ruleset's diff

The ruleset's diff names the chain each hunk is in on its @@ line, as
git names the function. A history entry shows what it did to the
ruleset (web page, hc history diff, GET /history/{id}/diff), rebuilt
from the current configuration by stepping back through the later
entries. A changed setting is listed by its own fields
(builtins.drop_invalid) instead of the whole object.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-22 23:39:03 +01:00
1 parent 4e35236dba
commit 92147c8681
12 files changed
+289 -11

No files matched your search

+21 -5
View File
@@ -1,5 +1,5 @@
// History: every configuration change, with undo.
import { h, mount, table, time, confirmDialog, modal, errorBox, badge } from "../ui.js";
import { h, mount, table, time, confirmDialog, modal, errorBox, badge, diffView } from "../ui.js";
import { get, change, can, state } from "../api.js";
import { rerender } from "../app.js";
@@ -29,7 +29,7 @@ async function undo(e) {
}
async function entry(el, id) {
const e = await get("/history/" + id);
const [e, d] = await Promise.all([get("/history/" + id), get("/history/" + id + "/diff").catch(() => null)]);
mount(el,
h("nav", { class: "back-link" }, h("a", { href: "#/history" }, "← History")),
h("div", { class: "page-head" },
@@ -40,7 +40,11 @@ async function entry(el, id) {
can("config", true) && !e.reverted_by ? h("div", { class: "actions" }, h("button", { onclick: () => undo(e) }, "Undo")) : null),
e.changes.map((c) => h("div", { class: "panel" },
h("h3", null, action(c) + " " + c.kind + " ", h("span", { class: "mono" }, c.label)),
fieldsTable(c))));
fieldsTable(c))),
d ? h("div", { class: "panel" },
h("h3", null, "Ruleset"),
h("p", { class: "muted small" }, "What this change did to the ruleset, rendered by this version of Housecarl."),
diffView(d.diff)) : null);
}
function action(c) {
@@ -50,9 +54,10 @@ function action(c) {
}
// fieldsTable lists the fields that differ (all of them for a create or
// delete).
// delete). An object within (settings' builtins) is listed by its own
// fields, "builtins.drop_invalid", so only what changed shows.
function fieldsTable(c) {
const a = c.before || {}, b = c.after || {};
const a = flatten(c.before || {}), b = flatten(c.after || {});
const keys = [...new Set([...Object.keys(a), ...Object.keys(b)])].sort();
const show = (v, k) => {
if (v === undefined || v === "") return "–";
@@ -68,3 +73,14 @@ function fieldsTable(c) {
c.after ? { label: c.before ? "After" : "Value", render: (k) => h("span", { class: "mono small" }, show(b[k], k)) } : null,
].filter(Boolean), rows);
}
// flatten lifts the fields of objects within an object to dotted keys
// ({builtins: {loopback: true}} → {"builtins.loopback": true}); lists
// stay whole.
function flatten(o, prefix = "", out = {}) {
for (const [k, v] of Object.entries(o)) {
if (v && typeof v === "object" && !Array.isArray(v) && Object.keys(v).length) flatten(v, prefix + k + ".", out);
else out[prefix + k] = v;
}
return out;
}