Organisationsvorlagen: ein ganzes Haus als importierbare Datei (#691)
- Core/Organisation/OrgTemplate: format, export, parse, structural checks. Export empties every credential (nested, name variants) and names it; a template that carries one is refused. - OrgAdministration: ExportTemplate, PreviewTemplate (problems, warnings, every text in full), ImportTemplateAsync - only with confirmation, units before agents, own workspace per unit, doctrines, model and budget factor overridable, templateOrigin on every unit and agent. - Endpoints GET /organisation/template, POST .../preview, POST .../import; templates card in the organisation view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
11465f3d74
commit
706b5cfd9e
@@ -2637,6 +2637,19 @@ button.org-einheit .org-name {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* #691: a template before it comes in — every agent with its full texts. */
|
||||
.org-vorlage-agent {
|
||||
border-left: 3px solid var(--accent);
|
||||
padding: 6px 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.org-vorlage-agent pre {
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* #702: a vote beside the call — every ballot with its reasoning and evidence. */
|
||||
.staging-stimmen {
|
||||
list-style: none;
|
||||
|
||||
@@ -152,6 +152,23 @@
|
||||
"org.voteTallyText": "{yes} ja · {no} nein · {missing} fehlen",
|
||||
"org.voteOutcome": "Ergebnis",
|
||||
"org.voteCost": "Kosten der Abstimmenden",
|
||||
"org.templates": "Organisationsvorlagen",
|
||||
"org.templatesHint": "Ein ganzes Haus als Datei: Einheiten, Regeln, Doktrinen und Agenten mit ihren Texten, Werkzeugen, Modellen und Budgets. Exportiert ohne Zugangsdaten — sie werden benannt, nie mitgegeben. Importiert nur in IDs und Namen, die diese Instanz noch nicht hat, und erst, nachdem du gelesen hast, was hereinkommt.",
|
||||
"org.templateNamePlaceholder": "Name der Vorlage",
|
||||
"org.templateExport": "Exportieren",
|
||||
"org.templateExportedCredentials": "Exportiert. {n} Zugangsdaten wurden weggelassen und sind in der Datei benannt.",
|
||||
"org.templateFile": "Vorlage importieren",
|
||||
"org.templateModel": "Modell für alle Agenten (optional)",
|
||||
"org.templateModelPlaceholder": "Modelle der Vorlage behalten",
|
||||
"org.templateBudgetFactor": "Budgetfaktor",
|
||||
"org.templateProblems": "Die Vorlage lässt sich nicht importieren:",
|
||||
"org.templateUnits": "{n} Einheit(en)",
|
||||
"org.templateAgents": "{n} Agent(en)",
|
||||
"org.templateDay": "Tag",
|
||||
"org.templateCredentials": "Danach einzutragende Zugangsdaten",
|
||||
"org.templateConfirm": "Ich habe die Identity- und Soul-Texte gelesen. Sie werden zu den Prompts meiner Agenten.",
|
||||
"org.templateImport": "Importieren",
|
||||
"org.templateUnreadable": "Die Datei ist keine Vorlage: {error}",
|
||||
"org.ceilingInherited": "Von oben begrenzt auf: {tools}",
|
||||
"org.settings": "Sammelwerte",
|
||||
"org.settingsHint": "Werden nach einer Vorschau in die Agenten geschrieben. Die Einheit merkt sich den Wert nur als Vorgabe für neue Mitglieder — zur Laufzeit übertrumpft nichts einen Agenten.",
|
||||
|
||||
@@ -152,6 +152,23 @@
|
||||
"org.voteTallyText": "{yes} yes · {no} no · {missing} missing",
|
||||
"org.voteOutcome": "Outcome",
|
||||
"org.voteCost": "Cost of the voters",
|
||||
"org.templates": "Organisation templates",
|
||||
"org.templatesHint": "A whole house as a file: units, rules, doctrines and agents with their texts, tools, models and budgets. Exported without credentials — they are named, never carried. Imported only into ids and names this instance does not have yet, after you have read what comes in.",
|
||||
"org.templateNamePlaceholder": "Template name",
|
||||
"org.templateExport": "Export",
|
||||
"org.templateExportedCredentials": "Exported. {n} credential(s) were left out and are named in the file.",
|
||||
"org.templateFile": "Import a template",
|
||||
"org.templateModel": "Model for all agents (optional)",
|
||||
"org.templateModelPlaceholder": "keep the template's models",
|
||||
"org.templateBudgetFactor": "Budget factor",
|
||||
"org.templateProblems": "The template cannot be imported:",
|
||||
"org.templateUnits": "{n} unit(s)",
|
||||
"org.templateAgents": "{n} agent(s)",
|
||||
"org.templateDay": "day",
|
||||
"org.templateCredentials": "Credentials to fill in afterwards",
|
||||
"org.templateConfirm": "I have read the Identity and Soul texts. They become my agents' prompts.",
|
||||
"org.templateImport": "Import",
|
||||
"org.templateUnreadable": "The file is no template: {error}",
|
||||
"org.ceilingInherited": "Limited from above to: {tools}",
|
||||
"org.settings": "Unit-wide values",
|
||||
"org.settingsHint": "Written into the agents after a preview. The unit keeps the value only as a default for new members — nothing overrides an agent at run time.",
|
||||
|
||||
@@ -307,7 +307,10 @@ function renderDetail() {
|
||||
], { klasse: "org-grund" });
|
||||
|
||||
if (!item) {
|
||||
detailBox.replaceChildren(erstelleElement("div", { klasse: "hinweis-zentriert", text: t("org.selectHint") }));
|
||||
detailBox.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: t("org.selectHint") }),
|
||||
reasonCard,
|
||||
templatesCard());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -317,6 +320,119 @@ function renderDetail() {
|
||||
detailBox.replaceChildren(reasonCard, ...panels);
|
||||
}
|
||||
|
||||
/**
|
||||
* #691: The organisation as a template file. Export never carries credentials; import shows
|
||||
* everything first — units, agents, models, budgets and the full Identity and Soul texts, which
|
||||
* become prompts — and only runs after an explicit confirmation.
|
||||
*/
|
||||
function templatesCard() {
|
||||
const name = erstelleElement("input", { type: "text", placeholder: t("org.templateNamePlaceholder") });
|
||||
const file = erstelleElement("input", { type: "file", accept: ".json,application/json" });
|
||||
const model = erstelleElement("input", { type: "text", placeholder: t("org.templateModelPlaceholder") });
|
||||
const factor = erstelleElement("input", { type: "number", min: "0.01", step: "0.1", value: "1" });
|
||||
const result = erstelleElement("div", { klasse: "org-vorlage" });
|
||||
let template = null;
|
||||
let source = "";
|
||||
|
||||
const body = () => ({
|
||||
template,
|
||||
model: model.value.trim() || null,
|
||||
budgetFactor: Number(factor.value) > 0 && Number(factor.value) !== 1 ? Number(factor.value) : null,
|
||||
source,
|
||||
reason: reason()
|
||||
});
|
||||
|
||||
const exportTemplate = async () => {
|
||||
try {
|
||||
const tpl = await api(`/organisation/template?name=${encodeURIComponent(name.value.trim())}`);
|
||||
const blob = new Blob([JSON.stringify(tpl, null, 2)], { type: "application/json" });
|
||||
const link = erstelleElement("a", { href: URL.createObjectURL(blob) });
|
||||
link.download = `${(name.value.trim() || "organisation").replace(/[^\w-]+/g, "_")}.org-template.json`;
|
||||
link.click();
|
||||
setTimeout(() => URL.revokeObjectURL(link.href), 1000);
|
||||
if (tpl.credentials?.length) zeigeMeldung(t("org.templateExportedCredentials", { n: tpl.credentials.length }), "erfolg", 8000);
|
||||
} catch (err) {
|
||||
zeigeMeldung(err.message, "gefahr", 8000);
|
||||
}
|
||||
};
|
||||
|
||||
const preview = async () => {
|
||||
if (!template) return;
|
||||
let p;
|
||||
try {
|
||||
p = await api("/organisation/template/preview", { method: "POST", body: body() });
|
||||
} catch (err) {
|
||||
result.replaceChildren(erstelleElement("p", { klasse: "fehler", text: err.message }));
|
||||
return;
|
||||
}
|
||||
|
||||
const confirm = erstelleElement("input", { type: "checkbox" });
|
||||
const run = erstelleElement("button", {
|
||||
type: "button", klasse: "primaer klein", text: t("org.templateImport"), disabled: true,
|
||||
onclick: async () => {
|
||||
const ok = await send("/organisation/template/import", "POST", { ...body(), confirm: confirm.checked });
|
||||
if (ok) { template = null; result.replaceChildren(); file.value = ""; }
|
||||
}
|
||||
});
|
||||
confirm.addEventListener("change", () => { run.disabled = !confirm.checked || !p.canImport; });
|
||||
|
||||
result.replaceChildren(
|
||||
erstelleElement("h4", { text: `${p.name} · v${p.version} · ${p.fingerprint}` }),
|
||||
p.problems.length
|
||||
? erstelleElement("div", { klasse: "org-probleme" },
|
||||
erstelleElement("strong", { text: t("org.templateProblems") }),
|
||||
erstelleElement("ul", {}, ...p.problems.map((x) => erstelleElement("li", { text: x }))))
|
||||
: "",
|
||||
...p.warnings.map((w) => erstelleElement("p", { klasse: "hinweis-box warnung text-klein", text: w })),
|
||||
erstelleElement("h4", { text: t("org.templateUnits", { n: p.units.length }) }),
|
||||
erstelleElement("ul", { klasse: "org-liste" }, ...p.units.map((u) => erstelleElement("li", { klasse: "text-klein",
|
||||
text: `${u.path}${u.leaderAgentId ? ` · ${t("org.leadShort")}: ${u.leaderAgentId}` : ""}` }))),
|
||||
erstelleElement("h4", { text: t("org.templateAgents", { n: p.agents.length }) }),
|
||||
...p.agents.map((a) => erstelleElement("div", { klasse: "org-vorlage-agent" },
|
||||
erstelleElement("strong", { text: `${a.displayName} (${a.agentId})` }),
|
||||
erstelleElement("div", { klasse: "gedaempft text-klein",
|
||||
text: `${a.unitId ?? t("org.topLevel")} · ${a.model} · $${a.dailyCostUsd}/${t("org.templateDay")} · ${a.tools.join(", ") || "—"}` }),
|
||||
erstelleElement("div", { klasse: "text-klein", text: "Identity" }),
|
||||
erstelleElement("pre", { klasse: "code-block", text: a.identity || "—" }),
|
||||
erstelleElement("div", { klasse: "text-klein", text: "Soul" }),
|
||||
erstelleElement("pre", { klasse: "code-block", text: a.soul || "—" }))),
|
||||
p.credentials.length
|
||||
? erstelleElement("div", {},
|
||||
erstelleElement("h4", { text: t("org.templateCredentials") }),
|
||||
erstelleElement("ul", { klasse: "org-liste" }, ...p.credentials.map((c) => erstelleElement("li", { klasse: "text-klein",
|
||||
text: `${c.agentId} · ${c.tool} · ${c.field}` }))))
|
||||
: "",
|
||||
erstelleElement("label", { klasse: "gruppe-horizontal text-klein" }, confirm, erstelleElement("span", { text: t("org.templateConfirm") })),
|
||||
run);
|
||||
};
|
||||
|
||||
file.addEventListener("change", async () => {
|
||||
const f = file.files?.[0];
|
||||
if (!f) return;
|
||||
try {
|
||||
template = JSON.parse(await f.text());
|
||||
source = f.name;
|
||||
await preview();
|
||||
} catch (err) {
|
||||
template = null;
|
||||
result.replaceChildren(erstelleElement("p", { klasse: "fehler", text: t("org.templateUnreadable", { error: err.message }) }));
|
||||
}
|
||||
});
|
||||
model.addEventListener("change", preview);
|
||||
factor.addEventListener("change", preview);
|
||||
|
||||
return erstelleKarte(t("org.templates"), [
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.templatesHint") }),
|
||||
erstelleElement("div", { klasse: "gruppe-horizontal" }, name,
|
||||
erstelleElement("button", { type: "button", klasse: "klein", text: t("org.templateExport"), onclick: exportTemplate })),
|
||||
erstelleElement("div", { klasse: "formular-raster" },
|
||||
erstelleElement("label", { text: t("org.templateFile") }), file,
|
||||
erstelleElement("label", { text: t("org.templateModel") }), model,
|
||||
erstelleElement("label", { text: t("org.templateBudgetFactor") }), factor),
|
||||
result
|
||||
]);
|
||||
}
|
||||
|
||||
function unitPanel(unit) {
|
||||
const cards = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user