Einheiten, Stufe 4: API und WebUI-Seite "Organisation" (#683)
- /api/organisation: Baum (Einheiten mit Pfad, Tiefe, eigener und wirksamer Obergrenze, Vorgaben; Agenten mit Einheit, Leitung, effektiver Leitung, Werkzeugen, Budget), Probleme der Struktur, bekannte Werkzeuge. Anlegen, Aendern, Loeschen, Leitung, Umzug von Agent und Einheit, Obergrenze, Sammelwerte mit Vorschau. Umzug und Obergrenze kennen preview=true; die Entscheidung trifft allein OrgAdministration. - Neue Agenten koennen ueber die API gleich in eine Einheit gelegt werden. - WebUI #/organisation: links der Baum (oberste Ebene, Einheiten, Leitungen mit Stern, Mitglieder), rechts die Bearbeitung. Eine Begruendung fuer das Audit gilt fuer jede Aenderung. Wo etwas verloren gehen kann, zuerst die Vorschau: laufende Agenten und Probleme halten an, wegfallende Werkzeuge muessen in einem Dialog bestaetigt werden. Sammelwerte als Tabelle mit Ausreissern markiert und abgewaehlt. Bei aktivierter Begrenzung ist vorausgewaehlt, was von oben ohnehin erlaubt ist. Sprachpakete en/de. Geprueft im Browser mit einem Pruefgestell (API durch Attrappe ersetzt): Baum, Auswahl, Obergrenze mit Bestaetigungsdialog, Sammelwert-Vorschau mit Ausreisser und das Schreiben nur der gewaehlten Agenten. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
68a432fec6
commit
e4ecf2d5ca
@@ -0,0 +1,471 @@
|
||||
// ClawdDotNet — Organisation (#683)
|
||||
//
|
||||
// Units, leads, members — and the three things that change rights: moving, the tool
|
||||
// ceiling, unit-wide values. Every rule is the instance's (OrgAdministration); this page
|
||||
// asks for a preview first wherever something could be lost, and shows it before writing.
|
||||
//
|
||||
// A list, not a graph: this is where the structure is edited. Watching it work is the team
|
||||
// overview (#711).
|
||||
|
||||
import { api } from "../modules/api.js";
|
||||
import { erstelleElement, erstelleKarte, erstellePlakette, erstelleModal, zeigeMeldung, formatiereDatumZeit } from "../modules/dom.js";
|
||||
import { t } from "../modules/i18n.js";
|
||||
|
||||
const SETTINGS = ["DailyCostUsd", "DailyTokens", "Model", "WhenExhausted"];
|
||||
const NUMERIC = new Set(["DailyCostUsd", "DailyTokens"]);
|
||||
|
||||
let data = null;
|
||||
let selection = null; // { kind: "unit" | "agent", id }
|
||||
let treeBox = null;
|
||||
let detailBox = null;
|
||||
let reasonInput = null;
|
||||
|
||||
export async function renderOrganisation() {
|
||||
const container = document.getElementById("ansicht-inhalt");
|
||||
if (!container) return;
|
||||
|
||||
treeBox = erstelleElement("div", { klasse: "org-baum" });
|
||||
detailBox = erstelleElement("div", { klasse: "org-detail" });
|
||||
|
||||
container.replaceChildren(erstelleElement(
|
||||
"div",
|
||||
{ klasse: "org-ansicht" },
|
||||
erstelleElement("h1", { text: t("org.title") }),
|
||||
erstelleElement("p", { klasse: "gedaempft", text: t("org.subtitle") }),
|
||||
erstelleElement("div", { id: "org-probleme" }),
|
||||
erstelleElement("div", { klasse: "org-raster" }, treeBox, detailBox)
|
||||
));
|
||||
|
||||
await load();
|
||||
}
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
data = await api("/organisation");
|
||||
} catch (err) {
|
||||
treeBox.replaceChildren(erstelleElement("div", { klasse: "fehler-zentriert", text: err.message }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection && !find(selection)) selection = null;
|
||||
renderProblems();
|
||||
renderTree();
|
||||
renderDetail();
|
||||
}
|
||||
|
||||
// ─── helpers ───
|
||||
|
||||
function find(sel) {
|
||||
if (!sel || !data) return null;
|
||||
return sel.kind === "unit"
|
||||
? data.units.find((u) => u.id === sel.id)
|
||||
: data.agents.find((a) => a.agentId === sel.id);
|
||||
}
|
||||
|
||||
function childrenOf(unitId) {
|
||||
return data.units.filter((u) => (u.parentId ?? null) === unitId);
|
||||
}
|
||||
|
||||
function membersOf(unitId) {
|
||||
return data.agents.filter((a) => (a.unitId ?? null) === unitId);
|
||||
}
|
||||
|
||||
function descendantIds(unitId) {
|
||||
const ids = new Set([unitId]);
|
||||
let grew = true;
|
||||
while (grew) {
|
||||
grew = false;
|
||||
for (const u of data.units) {
|
||||
if (u.parentId && ids.has(u.parentId) && !ids.has(u.id)) { ids.add(u.id); grew = true; }
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function reason() {
|
||||
return reasonInput?.value.trim() ?? "";
|
||||
}
|
||||
|
||||
async function send(path, method, body, successText) {
|
||||
try {
|
||||
const result = await api(path, { method, body });
|
||||
zeigeMeldung(result?.message ?? successText ?? t("common.saved"), "erfolg");
|
||||
await load();
|
||||
return true;
|
||||
} catch (err) {
|
||||
zeigeMeldung(err.message, "gefahr", 8000);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves and ceiling changes: ask for a preview, stop on problems or running agents, show
|
||||
* the tools that would be lost and let the human confirm — only then write.
|
||||
*/
|
||||
async function withPreview(path, method, body) {
|
||||
let preview;
|
||||
try {
|
||||
preview = await api(path, { method, body: { ...body, preview: true } });
|
||||
} catch (err) {
|
||||
zeigeMeldung(err.message, "gefahr", 8000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (preview.problems.length) { zeigeMeldung(preview.problems.join(" "), "gefahr", 8000); return; }
|
||||
if (preview.running.length) {
|
||||
zeigeMeldung(t("org.runningRefusal", { agents: preview.running.join(", ") }), "warnung", 8000);
|
||||
return;
|
||||
}
|
||||
|
||||
const lost = Object.entries(preview.toolsLost ?? {});
|
||||
if (!lost.length) {
|
||||
await send(path, method, { ...body, preview: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const list = erstelleElement("ul", { klasse: "org-liste" },
|
||||
...lost.map(([agent, tools]) => erstelleElement("li", { text: `${agent}: ${tools.join(", ")}` })));
|
||||
const content = erstelleElement("div", {},
|
||||
erstelleElement("p", { text: t("org.toolsLostIntro") }), list);
|
||||
|
||||
const cancel = erstelleElement("button", { type: "button", klasse: "flach", text: t("common.cancel") });
|
||||
const confirm = erstelleElement("button", { type: "button", klasse: "gefahr", text: t("org.toolsLostConfirm") });
|
||||
const modal = erstelleModal(t("org.toolsLostTitle"), content, [cancel, confirm]);
|
||||
cancel.addEventListener("click", () => modal.schliessen());
|
||||
confirm.addEventListener("click", async () => {
|
||||
modal.schliessen();
|
||||
await send(path, method, { ...body, preview: false, dropExcessTools: true });
|
||||
});
|
||||
modal.oeffnen();
|
||||
}
|
||||
|
||||
function unitOptions(select, current, exclude = new Set(), topLabel = t("org.topLevel")) {
|
||||
select.append(erstelleElement("option", { value: "", text: topLabel }));
|
||||
for (const unit of data.units) {
|
||||
if (exclude.has(unit.id)) continue;
|
||||
const option = erstelleElement("option", { value: unit.id, text: unit.path });
|
||||
if (unit.id === current) option.selected = true;
|
||||
select.append(option);
|
||||
}
|
||||
if (!current) select.value = "";
|
||||
return select;
|
||||
}
|
||||
|
||||
// ─── problems ───
|
||||
|
||||
function renderProblems() {
|
||||
const box = document.getElementById("org-probleme");
|
||||
if (!box) return;
|
||||
box.replaceChildren(...(data.problems.length
|
||||
? [erstelleElement("div", { klasse: "org-probleme" },
|
||||
erstelleElement("strong", { text: t("org.problems") }),
|
||||
erstelleElement("ul", {}, ...data.problems.map((p) => erstelleElement("li", { text: p }))))]
|
||||
: []));
|
||||
}
|
||||
|
||||
// ─── tree ───
|
||||
|
||||
function renderTree() {
|
||||
const newUnit = erstelleElement("button", {
|
||||
type: "button", klasse: "primaer klein", text: t("org.newUnit"), onclick: () => openCreateUnit()
|
||||
});
|
||||
|
||||
const list = erstelleElement("div", { klasse: "org-knoten-liste" });
|
||||
const root = erstelleElement("div", { klasse: "org-ebene" },
|
||||
erstelleElement("div", { klasse: "org-ebene-titel gedaempft text-klein", text: t("org.topLevel") }));
|
||||
for (const agent of membersOf(null)) root.append(agentRow(agent, 0));
|
||||
list.append(root);
|
||||
for (const unit of childrenOf(null)) list.append(unitBlock(unit, 0));
|
||||
|
||||
treeBox.replaceChildren(
|
||||
erstelleElement("div", { klasse: "gruppe-horizontal org-baum-kopf" },
|
||||
erstelleElement("h2", { text: t("org.structure") }), newUnit),
|
||||
list);
|
||||
}
|
||||
|
||||
function unitBlock(unit, depth) {
|
||||
const lead = unit.leaderAgentId ? ` · ${t("org.leadShort")}: ${unit.leaderAgentId}` : "";
|
||||
const count = membersOf(unit.id).length;
|
||||
const row = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: `org-knoten org-einheit ${selection?.kind === "unit" && selection.id === unit.id ? "aktiv" : ""}`,
|
||||
stil: `padding-left: ${12 + depth * 18}px`,
|
||||
onclick: () => select({ kind: "unit", id: unit.id })
|
||||
},
|
||||
erstelleElement("span", { klasse: "org-name", text: `▣ ${unit.name}` }),
|
||||
erstelleElement("span", { klasse: "gedaempft text-klein", text: `${count}${lead}` }));
|
||||
|
||||
const block = erstelleElement("div", {}, row);
|
||||
for (const agent of membersOf(unit.id)) block.append(agentRow(agent, depth + 1));
|
||||
for (const child of childrenOf(unit.id)) block.append(unitBlock(child, depth + 1));
|
||||
return block;
|
||||
}
|
||||
|
||||
function agentRow(agent, depth) {
|
||||
return erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: `org-knoten org-agent ${selection?.kind === "agent" && selection.id === agent.agentId ? "aktiv" : ""}`,
|
||||
stil: `padding-left: ${12 + depth * 18}px`,
|
||||
onclick: () => select({ kind: "agent", id: agent.agentId })
|
||||
},
|
||||
erstelleElement("span", { text: `${agent.isLead ? "★ " : "· "}${agent.displayName || agent.agentId}` }),
|
||||
erstelleElement("span", { klasse: "gedaempft text-klein", text: agent.agentId }));
|
||||
}
|
||||
|
||||
function select(sel) {
|
||||
selection = sel;
|
||||
renderTree();
|
||||
renderDetail();
|
||||
}
|
||||
|
||||
function openCreateUnit(parentId = null) {
|
||||
const id = erstelleElement("input", { type: "text", placeholder: "marketing", required: true });
|
||||
const name = erstelleElement("input", { type: "text", placeholder: "Marketing", required: true });
|
||||
const parent = unitOptions(erstelleElement("select"), parentId);
|
||||
const goal = erstelleElement("textarea", { rows: 2 });
|
||||
|
||||
const form = erstelleElement("form", { klasse: "formular-raster" },
|
||||
erstelleElement("label", { text: t("org.unitId") }), id,
|
||||
erstelleElement("label", { text: t("org.unitName") }), name,
|
||||
erstelleElement("label", { text: t("org.parent") }), parent,
|
||||
erstelleElement("label", { text: t("org.goal") }), goal);
|
||||
|
||||
const cancel = erstelleElement("button", { type: "button", klasse: "flach", text: t("common.cancel") });
|
||||
const create = erstelleElement("button", { type: "submit", klasse: "primaer", text: t("common.create") });
|
||||
const modal = erstelleModal(t("org.newUnit"), form, [cancel, create]);
|
||||
cancel.addEventListener("click", () => modal.schliessen());
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const ok = await send("/organisation/units", "POST", {
|
||||
id: id.value.trim(), name: name.value.trim(), parentId: parent.value || null, goal: goal.value, reason: reason()
|
||||
});
|
||||
if (ok) {
|
||||
modal.schliessen();
|
||||
select({ kind: "unit", id: id.value.trim() });
|
||||
}
|
||||
});
|
||||
modal.oeffnen();
|
||||
}
|
||||
|
||||
// ─── detail ───
|
||||
|
||||
function renderDetail() {
|
||||
const item = find(selection);
|
||||
reasonInput = erstelleElement("input", { type: "text", placeholder: t("org.reasonPlaceholder") });
|
||||
const reasonCard = erstelleKarte(null, [
|
||||
erstelleElement("label", { klasse: "text-klein gedaempft", text: t("org.reason") }), reasonInput
|
||||
], { klasse: "org-grund" });
|
||||
|
||||
if (!item) {
|
||||
detailBox.replaceChildren(erstelleElement("div", { klasse: "hinweis-zentriert", text: t("org.selectHint") }));
|
||||
return;
|
||||
}
|
||||
|
||||
detailBox.replaceChildren(reasonCard, ...(selection.kind === "unit" ? unitPanel(item) : agentPanel(item)));
|
||||
}
|
||||
|
||||
function unitPanel(unit) {
|
||||
const cards = [];
|
||||
|
||||
// Name and goal.
|
||||
const name = erstelleElement("input", { type: "text", value: unit.name });
|
||||
const goal = erstelleElement("textarea", { rows: 2 });
|
||||
goal.value = unit.goal ?? "";
|
||||
cards.push(erstelleKarte(unit.path, [
|
||||
erstelleElement("div", { klasse: "formular-raster" },
|
||||
erstelleElement("label", { text: t("org.unitName") }), name,
|
||||
erstelleElement("label", { text: t("org.goal") }), goal,
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "primaer klein", text: t("common.save"),
|
||||
onclick: () => send(`/organisation/units/${encodeURIComponent(unit.id)}`, "PATCH",
|
||||
{ name: name.value, goal: goal.value, reason: reason() })
|
||||
}))
|
||||
]));
|
||||
|
||||
// Lead.
|
||||
const lead = erstelleElement("select", {}, erstelleElement("option", { value: "", text: t("org.noLead") }));
|
||||
for (const member of membersOf(unit.id)) {
|
||||
const option = erstelleElement("option", { value: member.agentId, text: member.displayName || member.agentId });
|
||||
if (member.agentId === unit.leaderAgentId) option.selected = true;
|
||||
lead.append(option);
|
||||
}
|
||||
cards.push(erstelleKarte(t("org.lead"), [
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.leadHint") }),
|
||||
erstelleElement("div", { klasse: "gruppe-horizontal" }, lead,
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "klein", text: t("common.save"),
|
||||
onclick: () => send(`/organisation/units/${encodeURIComponent(unit.id)}/leader`, "PUT",
|
||||
{ agentId: lead.value || null, reason: reason() })
|
||||
}))
|
||||
]));
|
||||
|
||||
// Position in the tree.
|
||||
const parent = unitOptions(erstelleElement("select"), unit.parentId, descendantIds(unit.id));
|
||||
cards.push(erstelleKarte(t("org.position"), [
|
||||
erstelleElement("div", { klasse: "gruppe-horizontal" }, parent,
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "klein", text: t("org.move"),
|
||||
onclick: () => withPreview(`/organisation/units/${encodeURIComponent(unit.id)}/move`, "POST",
|
||||
{ target: parent.value || null, reason: reason() })
|
||||
})),
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.moveUnitHint") })
|
||||
]));
|
||||
|
||||
cards.push(ceilingCard(unit));
|
||||
cards.push(settingsCard(unit));
|
||||
|
||||
// Members.
|
||||
const members = membersOf(unit.id);
|
||||
cards.push(erstelleKarte(t("org.members"), [
|
||||
members.length
|
||||
? erstelleElement("ul", { klasse: "org-liste" }, ...members.map((m) => erstelleElement("li", {},
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "flach klein", text: `${m.isLead ? "★ " : ""}${m.displayName || m.agentId}`,
|
||||
onclick: () => select({ kind: "agent", id: m.agentId })
|
||||
}))))
|
||||
: erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.noMembers") }),
|
||||
erstelleElement("div", { klasse: "gruppe-horizontal" },
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "flach klein", text: t("org.newSubUnit"), onclick: () => openCreateUnit(unit.id)
|
||||
}),
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "gefahr klein", text: t("org.deleteUnit"),
|
||||
onclick: async () => {
|
||||
if (await send(`/organisation/units/${encodeURIComponent(unit.id)}?reason=${encodeURIComponent(reason())}`, "DELETE")) {
|
||||
selection = null;
|
||||
renderDetail();
|
||||
}
|
||||
}
|
||||
}))
|
||||
]));
|
||||
|
||||
return cards;
|
||||
}
|
||||
|
||||
function ceilingCard(unit) {
|
||||
const limited = erstelleElement("input", { type: "checkbox" });
|
||||
limited.checked = Array.isArray(unit.toolCeiling);
|
||||
const boxes = data.knownTools.map((tool) => {
|
||||
const box = erstelleElement("input", { type: "checkbox", value: tool });
|
||||
// Pre-selected: the unit's own ceiling — or, without one, what is allowed from above anyway.
|
||||
const allowed = unit.toolCeiling ?? unit.effectiveToolCeiling;
|
||||
box.checked = !allowed || allowed.includes(tool);
|
||||
return erstelleElement("label", { klasse: "org-werkzeug" }, box, ` ${tool}`);
|
||||
});
|
||||
const grid = erstelleElement("div", { klasse: "org-werkzeuge" }, ...boxes);
|
||||
const sync = () => { grid.hidden = !limited.checked; };
|
||||
limited.addEventListener("change", sync);
|
||||
sync();
|
||||
|
||||
const inherited = unit.effectiveToolCeiling && !unit.toolCeiling
|
||||
? erstelleElement("p", { klasse: "text-klein", text: t("org.ceilingInherited", { tools: unit.effectiveToolCeiling.join(", ") }) })
|
||||
: null;
|
||||
|
||||
return erstelleKarte(t("org.ceiling"), [
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.ceilingHint") }),
|
||||
inherited,
|
||||
erstelleElement("label", { klasse: "gruppe-horizontal" }, limited, t("org.ceilingLimit")),
|
||||
grid,
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "klein", text: t("common.save"),
|
||||
onclick: () => {
|
||||
const tools = limited.checked
|
||||
? [...grid.querySelectorAll("input:checked")].map((b) => b.value)
|
||||
: null;
|
||||
withPreview(`/organisation/units/${encodeURIComponent(unit.id)}/ceiling`, "PUT", { tools, reason: reason() });
|
||||
}
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
function settingsCard(unit) {
|
||||
const setting = erstelleElement("select", {},
|
||||
...SETTINGS.map((s) => erstelleElement("option", { value: s, text: t(`org.setting.${s}`) })));
|
||||
const value = erstelleElement("input", { type: "text", placeholder: "10" });
|
||||
const mode = erstelleElement("select", {},
|
||||
erstelleElement("option", { value: "PerAgent", text: t("org.modePerAgent") }),
|
||||
erstelleElement("option", { value: "Distribute", text: t("org.modeDistribute") }));
|
||||
const remember = erstelleElement("input", { type: "checkbox" });
|
||||
remember.checked = true;
|
||||
const result = erstelleElement("div");
|
||||
|
||||
setting.addEventListener("change", () => { mode.disabled = !NUMERIC.has(setting.value); });
|
||||
|
||||
const preview = async () => {
|
||||
let rows;
|
||||
try {
|
||||
rows = await api(`/organisation/units/${encodeURIComponent(unit.id)}/settings/preview`, {
|
||||
method: "POST", body: { setting: setting.value, value: value.value.trim(), mode: mode.disabled ? "PerAgent" : mode.value }
|
||||
});
|
||||
} catch (err) {
|
||||
zeigeMeldung(err.message, "gefahr", 8000);
|
||||
return;
|
||||
}
|
||||
if (!rows.length) { result.replaceChildren(erstelleElement("p", { klasse: "fehler", text: t("org.settingInvalid") })); return; }
|
||||
|
||||
const checks = new Map();
|
||||
const table = erstelleElement("table", { klasse: "daten-tabelle" },
|
||||
erstelleElement("thead", {}, erstelleElement("tr", {},
|
||||
...["", t("org.agent"), t("org.current"), t("org.proposed"), ""].map((h) => erstelleElement("th", { text: h })))),
|
||||
erstelleElement("tbody", {}, ...rows.map((row) => {
|
||||
const box = erstelleElement("input", { type: "checkbox" });
|
||||
box.checked = row.selectedByDefault;
|
||||
checks.set(row.agentId, { box, value: row.proposed });
|
||||
return erstelleElement("tr", { klasse: row.outlier ? "org-ausreisser" : "" },
|
||||
erstelleElement("td", {}, box),
|
||||
erstelleElement("td", { text: row.displayName || row.agentId }),
|
||||
erstelleElement("td", { text: row.current }),
|
||||
erstelleElement("td", { text: row.proposed }),
|
||||
erstelleElement("td", {}, row.outlier ? erstellePlakette(`⚠ ${t("org.outlier")}`, "warnung") : ""));
|
||||
})));
|
||||
|
||||
const apply = erstelleElement("button", {
|
||||
type: "button", klasse: "primaer klein", text: t("org.apply"),
|
||||
onclick: () => {
|
||||
const values = {};
|
||||
for (const [agentId, { box, value: v }] of checks) if (box.checked) values[agentId] = v;
|
||||
send(`/organisation/units/${encodeURIComponent(unit.id)}/settings`, "POST", {
|
||||
setting: setting.value, values, reason: reason(),
|
||||
defaultValue: remember.checked && mode.value === "PerAgent" ? value.value.trim() : null
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
result.replaceChildren(
|
||||
rows.some((r) => r.outlier) ? erstelleElement("p", { klasse: "text-klein", text: `⚠ ${t("org.outlierHint")}` }) : "",
|
||||
table,
|
||||
erstelleElement("label", { klasse: "gruppe-horizontal text-klein" }, remember, t("org.rememberDefault")),
|
||||
apply);
|
||||
};
|
||||
|
||||
return erstelleKarte(t("org.settings"), [
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.settingsHint") }),
|
||||
erstelleElement("div", { klasse: "gruppe-horizontal org-einstellung" }, setting, value, mode,
|
||||
erstelleElement("button", { type: "button", klasse: "klein", text: t("org.preview"), onclick: preview })),
|
||||
result
|
||||
]);
|
||||
}
|
||||
|
||||
function agentPanel(agent) {
|
||||
const unit = data.units.find((u) => u.id === agent.unitId);
|
||||
const target = unitOptions(erstelleElement("select"), agent.unitId);
|
||||
|
||||
return [
|
||||
erstelleKarte(agent.displayName || agent.agentId, [
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: `${agent.agentId} · ${agent.model}` }),
|
||||
erstelleElement("p", { text: `${t("org.unit")}: ${unit ? unit.path : t("org.topLevel")}${agent.isLead ? ` (★ ${t("org.lead")})` : ""}` }),
|
||||
erstelleElement("p", { text: `${t("org.answersTo")}: ${agent.effectiveLeadId ?? t("org.human")}` }),
|
||||
agent.unitSinceUtc ? erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.since", { time: formatiereDatumZeit(agent.unitSinceUtc) }) }) : null,
|
||||
erstelleElement("p", { klasse: "text-klein", text: `${t("org.tools")}: ${agent.tools.join(", ") || "—"}` })
|
||||
]),
|
||||
erstelleKarte(t("org.moveAgent"), [
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.moveAgentHint") }),
|
||||
erstelleElement("div", { klasse: "gruppe-horizontal" }, target,
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "klein", text: t("org.move"),
|
||||
onclick: () => withPreview(`/organisation/agents/${encodeURIComponent(agent.agentId)}/move`, "POST",
|
||||
{ target: target.value || null, reason: reason() })
|
||||
}))
|
||||
])
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user