- 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>
135 lines
5.6 KiB
TypeScript
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>
|
|
);
|
|
}
|