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:
Richard
2026-09-30 20:12:18 +02:00
co-authored by Claude Opus 5.5
parent dafb8241a0
commit f51186269c
27 changed files with 948 additions and 20 deletions
@@ -27,7 +27,8 @@ internal static class OrganisationEndpoints
u.Id, u.Name, u.ParentId, u.LeaderAgentId, u.Goal, u.ToolCeiling,
chart.ToolCeiling(u.Id)?.Order(StringComparer.OrdinalIgnoreCase).ToList(),
new UnitDefaultsDto(u.Defaults.Model, u.Defaults.DailyCostUsd, u.Defaults.DailyTokens, u.Defaults.WhenExhausted),
chart.PathOf(u.Id), chart.Depth(u.Id)))
chart.PathOf(u.Id), chart.Depth(u.Id),
u.Approvals is { } ap ? new UnitApprovalsDto(ap.Operations, ap.PerDay, ap.Hours) : null))
.ToList();
var agents = host.Instance.Agents
@@ -80,6 +81,10 @@ internal static class OrganisationEndpoints
? Results.Ok(ToDto(host.Organisation.PreviewMoveAgent(agentId, input.Target)))
: host.Organisation.MoveAgent(agentId, input.Target, input.Reason, input.DropExcessTools).ToResponse());
// #687: What the lead may approve instead of the human.
org.MapPut("/units/{unitId}/approvals", (AppHost host, string unitId, ApprovalsInputDto input) =>
host.Organisation.SetApprovals(unitId, input.Operations, input.PerDay, input.Hours, input.Reason).ToResponse());
org.MapPut("/units/{unitId}/ceiling", (AppHost host, string unitId, CeilingInputDto input) =>
input.Preview
? Results.Ok(ToDto(host.Organisation.PreviewToolCeiling(unitId, input.Tools)))