Add backups with S3, daily cost push, automation templates and a browser tool

- Daily backups (database + config) into <data_dir>/backups, keep N,
  optional upload to any S3-compatible bucket (SigV4, bucket pruning);
  settings panel with run-now, list and download
- Daily cost summary push at 20:00 (opt-in per user, preview button)
- Automations from templates (dependency check, security review, test
  health, daily summary, TODO report, docs freshness) via POST /jobs/create
- browser tool: headless Chromium over CDP – click, type, keys, waits,
  JavaScript, navigation, screenshots, console errors; requests to private
  network addresses are blocked (sub-resources and redirects included)
- Secrets masked in the config view: S3 secret key, metrics token
- Replace the remaining text arrows/refresh glyphs with SVG icons

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-09 06:07:03 +01:00
1 parent 873447ed17
commit 01c1484033
25 files changed
+1720 -47

No files matched your search

+39
View File
@@ -141,6 +141,44 @@ function VersionInfo() {
return <div className="hint mono">{info ? `CodeCraft ${info.version}${info.commit ? ` (${info.commit})` : ""}` : "…"}</div>;
}
/** Daily cost summary at 20:00 (per user, all devices). */
function DigestToggle() {
const t = useT();
const [on, setOn] = useState<boolean>();
useEffect(() => {
get<{ enabled: boolean }>("/push/digest").then((r) => setOn(r.enabled)).catch(() => {});
}, []);
if (on === undefined) return null;
return (
<div className="row" style={{ gap: 8, flexBasis: "100%", alignItems: "center", flexWrap: "wrap" }}>
<label className="row" style={{ gap: 6, alignItems: "center" }}>
<input
type="checkbox"
checked={on}
onChange={(e) => {
const v = e.target.checked;
setOn(v);
put("/push/digest", { enabled: v }).catch((err) => (setOn(!v), toast(err.message)));
}}
/>
{t("settings.digest")}
</label>
{on && (
<button
className="small ghost"
onClick={() =>
post<{ sent: number; errors: string[] }>("/push/digest")
.then((r) => toast(r.sent ? t("settings.pushTestSent") : r.errors.join("; ") || t("settings.pushTestNone"), r.sent ? "success" : "error"))
.catch((e) => toast(e.message))
}
>
{t("settings.digestNow")}
</button>
)}
</div>
);
}
/** Web Push on this device: enable/disable and a test notification. */
function PushControl() {
const t = useT();
@@ -189,6 +227,7 @@ function PushControl() {
<button className="small ghost" disabled={busy} onClick={run(async () => (await disablePush(), setState("off")))}>
{t("settings.pushDisable")}
</button>
<DigestToggle />
</>
) : (
<button className="small" disabled={busy} onClick={run(async () => (await enablePush(), setState("on")))}>