Freigabe durch die Leitung innerhalb von Grenzen (#687)
- UnitConfig.Approvals: Operationen, Hoechstzahl je Tag, Stunden je Freigabe; nur der Mensch setzt sie (OrgAdministration.SetApprovals mit Audit, Teilmengenregel nach oben, Senken oben senkt unten mit; Agentenaenderungen/Sonderbudget nie freigebbar) - LeadApprovals: Vorlage geht an die naechste Leitung, deren Einheit die Operation erlaubt; nie eigene Aufrufe, nie eine Leitung mit Fremdinhalt, Tagesdeckel, Frist. Entscheidung ueber Escalation approve_call/reject_call, im Audit als lead:<id> - Der Mensch sieht alles und kann zuerst entscheiden; danach wird die Leitungsaufgabe geschlossen. Nach der Frist nur noch der Mensch. - StagedCalls: Approver, ApproverUntil, ApproverTaskId (Nachruestung) - WebUI: Hinweis "bei Leitung" in der Freigabestelle, Zaehler ohne diese, Quote je Leitung; Karte in der Organisationsansicht; i18n en/de - Tests: LeadApprovalsTests (13), OrgAdministrationTests (+2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
dafb8241a0
commit
f51186269c
@@ -573,6 +573,16 @@ button.klein {
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
/* #687: a lead that approves everything */
|
||||
.daten-tabelle tr.zeile-warnung td {
|
||||
color: var(--warnung);
|
||||
}
|
||||
|
||||
/* #687: a call a lead may still decide — the human's too, but not yet the human's alone */
|
||||
.staging-bei-leitung {
|
||||
border-left: 3px solid var(--accent);
|
||||
}
|
||||
|
||||
/* ─── Datenlisten (dl/dt/dd) ─── */
|
||||
|
||||
.datenliste {
|
||||
|
||||
@@ -95,6 +95,11 @@
|
||||
"org.ceiling": "Rechte-Obergrenze",
|
||||
"org.ceilingHint": "Was die Einheit nicht darf, kann ein Mitglied nicht bekommen. Senken entzieht den Mitgliedern Werkzeuge — nach einer Bestätigung.",
|
||||
"org.ceilingLimit": "Werkzeuge dieser Einheit begrenzen",
|
||||
"org.approvals": "Freigaben durch die Leitung",
|
||||
"org.approvalsHint": "Was die Leitung dieser Einheit für die Agenten darunter an deiner Stelle freigeben darf. Eine Operation je Zeile: Werkzeug.operation, oder Werkzeug für alle. Änderungen an Agenten bleiben immer bei dir; eine Leitung gibt nie ihre eigenen Aufrufe frei. Eine Untereinheit darf nicht mehr freigeben als die Einheiten darüber. Leer: du entscheidest alles.",
|
||||
"org.approvalsOperations": "Operationen",
|
||||
"org.approvalsPerDay": "Höchstens je Tag",
|
||||
"org.approvalsHours": "Stunden je Freigabe",
|
||||
"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.",
|
||||
@@ -608,6 +613,16 @@
|
||||
"staging.rejectButton": "Aufruf ablehnen",
|
||||
"staging.rejected": "Aufruf wurde abgelehnt.",
|
||||
"staging.rejectFailed": "Ablehnen fehlgeschlagen: {error}",
|
||||
"staging.withLead": "Die Leitung {lead} darf dies bis {until} entscheiden, im Rahmen dessen, was ihre Einheit erlaubt. Du kannst vorher entscheiden; danach liegt es allein bei dir.",
|
||||
"staging.leadsTitle": "Freigaben durch Leitungen",
|
||||
"staging.leadsHint": "Was Leitungen in den letzten 30 Tagen an deiner Stelle freigegeben haben. Eine Leitung, die alles freigibt, verursacht nur Kosten — sieh dir ihre Entscheidungen an.",
|
||||
"staging.leadName": "Leitung",
|
||||
"staging.leadRouted": "Vorgelegt",
|
||||
"staging.leadApproved": "Freigegeben",
|
||||
"staging.leadRejected": "Abgelehnt",
|
||||
"staging.leadLapsed": "Dir überlassen",
|
||||
"staging.leadRate": "Freigabequote",
|
||||
"staging.leadAllApproved": "Alles freigegeben — prüfe, ob diese Leitung wirklich hinsieht.",
|
||||
|
||||
"agentChats.title": "Agenten-Kommunikation (A2A)",
|
||||
"agentChats.allAgents": "Alle Agenten",
|
||||
|
||||
@@ -95,6 +95,11 @@
|
||||
"org.ceiling": "Tool ceiling",
|
||||
"org.ceilingHint": "What the unit may not have, a member cannot be granted. Lowering it removes tools from the members — after a confirmation.",
|
||||
"org.ceilingLimit": "Limit the tools of this unit",
|
||||
"org.approvals": "Approvals by the lead",
|
||||
"org.approvalsHint": "What the lead of this unit may approve for the agents below it, instead of you. One operation per line: Tool.operation, or Tool for all of it. Changes to agents always stay with you; a lead never approves its own calls. A sub-unit can grant no more than the units above it. Empty: you decide everything.",
|
||||
"org.approvalsOperations": "Operations",
|
||||
"org.approvalsPerDay": "Per day at most",
|
||||
"org.approvalsHours": "Hours per approval",
|
||||
"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.",
|
||||
@@ -608,6 +613,16 @@
|
||||
"staging.rejectButton": "Reject Call",
|
||||
"staging.rejected": "Call was rejected.",
|
||||
"staging.rejectFailed": "Rejection failed: {error}",
|
||||
"staging.withLead": "Its lead {lead} may decide this until {until}, within the limits its unit grants. You can decide it first; after that it is yours alone.",
|
||||
"staging.leadsTitle": "Approvals by leads",
|
||||
"staging.leadsHint": "What leads approved instead of you in the last 30 days. A lead that approves everything does nothing but cost — look at its decisions.",
|
||||
"staging.leadName": "Lead",
|
||||
"staging.leadRouted": "Given",
|
||||
"staging.leadApproved": "Approved",
|
||||
"staging.leadRejected": "Rejected",
|
||||
"staging.leadLapsed": "Left to you",
|
||||
"staging.leadRate": "Approval rate",
|
||||
"staging.leadAllApproved": "Everything approved — check whether this lead really looks.",
|
||||
|
||||
"agentChats.title": "Agent Communication (A2A)",
|
||||
"agentChats.allAgents": "All Agents",
|
||||
|
||||
@@ -366,6 +366,7 @@ function unitPanel(unit) {
|
||||
|
||||
cards.push(unitWorkspaceCard(unit));
|
||||
cards.push(ceilingCard(unit));
|
||||
cards.push(approvalsCard(unit));
|
||||
cards.push(settingsCard(unit));
|
||||
|
||||
// Members.
|
||||
@@ -432,6 +433,35 @@ function ceilingCard(unit) {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* #687: What the unit's lead may approve instead of the human. One operation per line —
|
||||
* "Tool.operation", or "Tool" for all of it. Changes to agents are refused by the server.
|
||||
*/
|
||||
function approvalsCard(unit) {
|
||||
const current = unit.approvals;
|
||||
const ops = erstelleElement("textarea", { rows: 3, placeholder: "Database.insert\nFileRW.delete" });
|
||||
ops.value = (current?.operations ?? []).join("\n");
|
||||
const perDay = erstelleElement("input", { type: "number", min: "0", step: "1", value: String(current?.perDay ?? 20) });
|
||||
const hours = erstelleElement("input", { type: "number", min: "0.25", step: "0.25", value: String(current?.hours ?? 4) });
|
||||
|
||||
return erstelleKarte(t("org.approvals"), [
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.approvalsHint") }),
|
||||
erstelleElement("div", { klasse: "formular-raster" },
|
||||
erstelleElement("label", { text: t("org.approvalsOperations") }), ops,
|
||||
erstelleElement("label", { text: t("org.approvalsPerDay") }), perDay,
|
||||
erstelleElement("label", { text: t("org.approvalsHours") }), hours),
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "klein", text: t("common.save"),
|
||||
onclick: () => send(`/organisation/units/${encodeURIComponent(unit.id)}/approvals`, "PUT", {
|
||||
operations: ops.value.split(/[\n,]/).map((o) => o.trim()).filter(Boolean),
|
||||
perDay: Number(perDay.value),
|
||||
hours: Number(hours.value),
|
||||
reason: reason()
|
||||
})
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
function settingsCard(unit) {
|
||||
const setting = erstelleElement("select", {},
|
||||
...SETTINGS.map((s) => erstelleElement("option", { value: s, text: t(`org.setting.${s}`) })));
|
||||
|
||||
@@ -16,10 +16,15 @@ import {
|
||||
|
||||
let wartendeAufrufe = [];
|
||||
|
||||
// #687: What a lead may still decide is not yet the human's to do — it is shown, not counted.
|
||||
function menschWartet(anfrage) {
|
||||
return !anfrage.withLead;
|
||||
}
|
||||
|
||||
export async function holeWartendeAnzahl() {
|
||||
try {
|
||||
const liste = await api("/staging");
|
||||
return Array.isArray(liste) ? liste.length : 0;
|
||||
return Array.isArray(liste) ? liste.filter(menschWartet).length : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
@@ -48,7 +53,8 @@ export async function rendereStaging() {
|
||||
|
||||
const listeBereich = erstelleElement("div", { id: "staging-liste-bereich" });
|
||||
const dauerBereich = erstelleElement("div", { id: "staging-dauer-bereich" });
|
||||
layout.append(kopfZeile, listeBereich, dauerBereich);
|
||||
const leitungBereich = erstelleElement("div", { id: "staging-leitung-bereich" });
|
||||
layout.append(kopfZeile, listeBereich, dauerBereich, leitungBereich);
|
||||
container.append(layout);
|
||||
|
||||
await ladeFreigaben();
|
||||
@@ -67,8 +73,9 @@ async function ladeFreigaben() {
|
||||
container.replaceChildren();
|
||||
|
||||
// Aktualisiere Badge in Sidebar
|
||||
aktualisiereStagingBadge(wartendeAufrufe.length);
|
||||
aktualisiereStagingBadge(wartendeAufrufe.filter(menschWartet).length);
|
||||
ladeDauerfreigaben();
|
||||
ladeLeitungen();
|
||||
|
||||
if (!wartendeAufrufe || wartendeAufrufe.length === 0) {
|
||||
container.append(
|
||||
@@ -137,6 +144,14 @@ async function ladeFreigaben() {
|
||||
|
||||
const teile = [datenliste, argBox];
|
||||
|
||||
// #687: A lead decides it within its unit's limits — the human can still decide first.
|
||||
if (anfrage.withLead) {
|
||||
teile.unshift(erstelleElement("div", {
|
||||
klasse: "hinweis-box",
|
||||
text: t("staging.withLead", { lead: anfrage.withLead, until: formatiereDatumZeit(anfrage.leadUntil) })
|
||||
}));
|
||||
}
|
||||
|
||||
if (anfrage.suggestAllowSimilar) {
|
||||
teile.push(
|
||||
erstelleElement("div", {
|
||||
@@ -169,7 +184,8 @@ async function ladeFreigaben() {
|
||||
}
|
||||
|
||||
const karte = erstelleKarte(`${anfrage.tool}.${anfrage.action || "?"}`, teile, {
|
||||
aktionen: kopfAktionen
|
||||
aktionen: kopfAktionen,
|
||||
klasse: anfrage.withLead ? "staging-bei-leitung" : undefined
|
||||
});
|
||||
|
||||
container.append(karte);
|
||||
@@ -226,6 +242,44 @@ async function bestaetigeFreigabe(id, gleichartig = false) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* #687: How each lead decided what it was given. A lead that approves everything does
|
||||
* nothing but cost — the rate is the finding, so it is shown, not hidden in the audit.
|
||||
*/
|
||||
async function ladeLeitungen() {
|
||||
const bereich = document.getElementById("staging-leitung-bereich");
|
||||
if (!bereich) return;
|
||||
let liste = [];
|
||||
try { liste = await api("/staging/leads"); } catch { liste = []; }
|
||||
if (!Array.isArray(liste) || liste.length === 0) {
|
||||
bereich.replaceChildren();
|
||||
return;
|
||||
}
|
||||
|
||||
const tabelle = erstelleElement("table", { klasse: "daten-tabelle" });
|
||||
tabelle.append(erstelleElement("thead", {}, erstelleElement("tr", {},
|
||||
...["staging.leadName", "staging.leadRouted", "staging.leadApproved", "staging.leadRejected", "staging.leadLapsed", "staging.leadRate"]
|
||||
.map((k) => erstelleElement("th", { text: t(k) })))));
|
||||
const koerper = erstelleElement("tbody");
|
||||
for (const l of liste) {
|
||||
const quote = l.approvalRate == null ? "—" : `${Math.round(l.approvalRate * 100)} %`;
|
||||
// Everything approved, with enough decisions to mean something, is worth a look.
|
||||
const auffaellig = l.approvalRate === 1 && l.approved >= 5;
|
||||
koerper.append(erstelleElement("tr", { klasse: auffaellig ? "zeile-warnung" : undefined },
|
||||
erstelleElement("td", { text: l.leadId }),
|
||||
erstelleElement("td", { text: String(l.routed) }),
|
||||
erstelleElement("td", { text: String(l.approved) }),
|
||||
erstelleElement("td", { text: String(l.rejected) }),
|
||||
erstelleElement("td", { text: String(l.lapsed) }),
|
||||
erstelleElement("td", { text: quote, title: auffaellig ? t("staging.leadAllApproved") : "" })));
|
||||
}
|
||||
tabelle.append(koerper);
|
||||
bereich.replaceChildren(erstelleKarte(t("staging.leadsTitle"), [
|
||||
erstelleElement("p", { klasse: "gedaempft", text: t("staging.leadsHint") }),
|
||||
tabelle
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* #641: Die geltenden Dauerfreigaben - "Gleichartiges kuenftig erlauben" - mit Widerruf.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user