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

+8
View File
@@ -5,6 +5,7 @@ import { navigate } from "../router";
import { confirmDialog, toast } from "../dialogs";
import { Icon } from "./Icon";
import { Spinner } from "./Activity";
import { JobTemplateDialog } from "./JobTemplateDialog";
interface JobInfo {
id: string;
@@ -28,6 +29,7 @@ export function Jobs({ onMenu }: { onMenu: () => void }) {
const [path, setPath] = useState("");
const [busy, setBusy] = useState<string>();
const [hook, setHook] = useState<{ id: string; url: string; secret: string }>();
const [templates, setTemplates] = useState(false);
const reload = () =>
get<JobInfo[]>("/jobs")
@@ -64,6 +66,12 @@ export function Jobs({ onMenu }: { onMenu: () => void }) {
<div className="page">
<div className="page-inner" style={{ maxWidth: 980 }}>
<div className="notice">{t("jobs.intro")}</div>
<div className="row" style={{ margin: "12px 0" }}>
<button className="primary" onClick={() => setTemplates(true)}>
<Icon name="plus" /> {t("jobs.fromTemplate")}
</button>
</div>
{templates && <JobTemplateDialog onClose={() => setTemplates(false)} onCreated={() => void reload()} />}
{!jobs && <div className="empty"><Spinner /></div>}
{jobs?.length === 0 && <div className="empty">{t("jobs.none")}</div>}
{jobs?.map((j) => (