Files
codecraft/web/src/components/JobTemplateDialog.tsx
T
Richard ZinkandClaude Opus 5.5 01c1484033 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>
2026-11-09 06:07:03 +01:00

135 lines
5.6 KiB
TypeScript

import { useEffect, useState } from "react";
import { useFetch } from "../hooks";
import { post } from "../api";
import { getLang, useT } from "../i18n";
import { JOB_TEMPLATES, SCHEDULES, type JobTemplate } from "../jobTemplates";
import { Icon } from "./Icon";
import { ModelSelect } from "./ModelSelect";
import { FolderPicker } from "./NewSession";
/** Create an automation from a template: pick it, choose the project, adjust, create. */
export function JobTemplateDialog({ onClose, onCreated }: { onClose: () => void; onCreated: () => void }) {
const t = useT();
const de = getLang() === "de";
const [tpl, setTpl] = useState<JobTemplate>();
const [name, setName] = useState("");
const [schedule, setSchedule] = useState("");
const [agent, setAgent] = useState("build");
const [mode, setMode] = useState("auto");
const [model, setModel] = useState("");
const [prompt, setPrompt] = useState("");
const [cwd, setCwd] = useState(localStorage.getItem("codecraft.lastDir") ?? "");
const [error, setError] = useState<string>();
const [busy, setBusy] = useState(false);
const config = useFetch<{ default_model?: string }>("/config").data;
useEffect(() => {
if (!model && config?.default_model) setModel(config.default_model);
}, [config, model]);
const choose = (x: JobTemplate) => {
setTpl(x);
setName(de ? x.name.de : x.name.en);
setSchedule(x.schedule);
setAgent(x.agent);
setPrompt(x.prompt);
};
const create = async () => {
setBusy(true);
setError(undefined);
try {
await post("/jobs/create", { cwd, name: name.trim(), schedule: schedule.trim(), agent, permission_mode: mode, model: model || undefined, prompt });
onCreated();
onClose();
} catch (e: any) {
setError(e.message);
} finally {
setBusy(false);
}
};
return (
<div className="modal-backdrop" onClick={onClose}>
<div className="modal job-tpl" role="dialog" aria-label={t("jobs.fromTemplate")} onClick={(e) => e.stopPropagation()}>
{!tpl ? (
<>
<h3>{t("jobs.fromTemplate")}</h3>
<div className="job-tpl-grid">
{JOB_TEMPLATES.map((x) => (
<button key={x.id} className="job-tpl-card" onClick={() => choose(x)}>
<Icon name={x.icon} />
<span>
<b>{de ? x.name.de : x.name.en}</b>
<small>{de ? x.desc.de : x.desc.en}</small>
</span>
</button>
))}
</div>
<div className="row" style={{ marginTop: 12, justifyContent: "flex-end" }}>
<button onClick={onClose}>{t("common.cancel")}</button>
</div>
</>
) : (
<>
<h3>
<Icon name={tpl.icon} /> {de ? tpl.name.de : tpl.name.en}
</h3>
<label className="field">
<span>{t("jobs.tplName")}</span>
<input value={name} onChange={(e) => setName(e.target.value)} />
</label>
<div className="field">
<span>{t("jobs.tplProject")}</span>
<FolderPicker path={cwd} onPick={setCwd} />
</div>
<div className="field">
<span>{t("jobs.tplSchedule")}</span>
<div className="row" style={{ gap: 6, flexWrap: "wrap" }}>
{SCHEDULES.map((s) => (
<button key={s.cron} type="button" className={`chip-btn ${schedule === s.cron ? "active" : ""}`} onClick={() => setSchedule(s.cron)}>
{de ? s.de : s.en}
</button>
))}
</div>
<input className="mono" value={schedule} onChange={(e) => setSchedule(e.target.value)} placeholder="0 3 * * *" />
<small className="hint">{t("jobs.tplCronHint")}</small>
</div>
<div className="row" style={{ gap: 10, flexWrap: "wrap" }}>
<label className="field">
<span>{t("jobs.tplMode")}</span>
<select value={agent} onChange={(e) => setAgent(e.target.value)}>
<option value="build">build</option>
<option value="plan">plan</option>
</select>
</label>
<label className="field">
<span>{t("composer.permissionMode")}</span>
<select value={mode} onChange={(e) => setMode(e.target.value)}>
<option value="auto">{t("permissionMode.auto")}</option>
<option value="accept_edits">{t("permissionMode.accept_edits")}</option>
<option value="bypass">{t("permissionMode.bypass")}</option>
</select>
</label>
<label className="field">
<span>{t("composer.model")}</span>
<ModelSelect value={model} onChange={setModel} />
</label>
</div>
<label className="field">
<span>{t("jobs.tplPrompt")}</span>
<textarea rows={6} value={prompt} onChange={(e) => setPrompt(e.target.value)} />
</label>
{error && <div className="notice err">{error}</div>}
<div className="row" style={{ marginTop: 12, justifyContent: "flex-end" }}>
<button onClick={() => setTpl(undefined)}>{t("common.back")}</button>
<button className="primary" disabled={busy || !cwd || !name.trim() || !schedule.trim() || !prompt.trim()} onClick={create}>
{t("jobs.tplCreate")}
</button>
</div>
</>
)}
</div>
</div>
);
}