Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab1bd67464 | ||
|
|
f649f2fd7a |
@@ -1586,6 +1586,14 @@ Pixelgrafik: charmant, aber mit wenig Information. Die Verbindung aus aufklappba
|
|||||||
Organisation, Live-Zuständen und nach oben gereichten Problemen mit Direktzugriff hat
|
Organisation, Live-Zuständen und nach oben gereichten Problemen mit Direktzugriff hat
|
||||||
keines davon.
|
keines davon.
|
||||||
|
|
||||||
|
> **Umgesetzt (#688) als Ausbau dieser Ansicht, nicht als zweite.** Jede Einheitskarte der
|
||||||
|
> Teamübersicht trägt die Lage der Abteilung samt allem darunter: Läufe und Kosten heute
|
||||||
|
> (mit Warnzeichen, wenn #699 heute eine Kostenmeldung für sie erhoben hat), offene und
|
||||||
|
> blockierte Aufgaben, offene Vorgänge aus der Einheit (#689). Die Instanzkarte summiert
|
||||||
|
> dieselben Werte über alle. Die Seitenleiste führt von dort in die Kosten je Einheit und
|
||||||
|
> die Vorgänge. **Offen:** dieselbe Ansicht in der Avalonia-Oberfläche — dort gibt es die
|
||||||
|
> Teamübersicht noch gar nicht.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11 — Reihenfolge
|
## 11 — Reihenfolge
|
||||||
|
|||||||
@@ -146,7 +146,9 @@ public sealed class AppHost : IAsyncDisposable
|
|||||||
|
|
||||||
/// <summary>#711: The team overview. Stateless, like <see cref="TaskAdmin"/>.</summary>
|
/// <summary>#711: The team overview. Stateless, like <see cref="TaskAdmin"/>.</summary>
|
||||||
public TeamOverview Team => new(Engine, Staging, AuditRepository, Usage, () => Instance.Agents,
|
public TeamOverview Team => new(Engine, Staging, AuditRepository, Usage, () => Instance.Agents,
|
||||||
organisation: () => Core.Organisation.OrgChart.From(Instance));
|
organisation: () => Core.Organisation.OrgChart.From(Instance),
|
||||||
|
board: Board, cases: Cases,
|
||||||
|
costAlerts: CostWatch is { } watch ? ct => watch.RecentAsync(1, ct) : null);
|
||||||
|
|
||||||
public TelegramClientManager? TelegramClient { get; private init; }
|
public TelegramClientManager? TelegramClient { get; private init; }
|
||||||
public OpenRouterStatusService? Status { get; private init; }
|
public OpenRouterStatusService? Status { get; private init; }
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using ClawdDotNet.Core.Config;
|
|||||||
using ClawdDotNet.Core.Engine;
|
using ClawdDotNet.Core.Engine;
|
||||||
using ClawdDotNet.Core.Organisation;
|
using ClawdDotNet.Core.Organisation;
|
||||||
using ClawdDotNet.Core.Staging;
|
using ClawdDotNet.Core.Staging;
|
||||||
|
using ClawdDotNet.Core.Tasks;
|
||||||
|
|
||||||
namespace ClawdDotNet.App.Services;
|
namespace ClawdDotNet.App.Services;
|
||||||
|
|
||||||
@@ -43,7 +44,12 @@ public sealed record TeamMember(
|
|||||||
string? LastRunResult,
|
string? LastRunResult,
|
||||||
long TypicalRunMs,
|
long TypicalRunMs,
|
||||||
string? UnitId = null,
|
string? UnitId = null,
|
||||||
bool IsLead = false);
|
bool IsLead = false,
|
||||||
|
// #688: the agent's day — what a department's situation is added up from.
|
||||||
|
int RunsToday = 0,
|
||||||
|
decimal CostToday = 0,
|
||||||
|
int OpenTasks = 0,
|
||||||
|
int BlockedTasks = 0);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// One unit in the team overview (#711, stage 1), with what happens inside it rolled up —
|
/// One unit in the team overview (#711, stage 1), with what happens inside it rolled up —
|
||||||
@@ -60,7 +66,15 @@ public sealed record TeamUnit(
|
|||||||
int Agents,
|
int Agents,
|
||||||
IReadOnlyDictionary<TeamState, int> States,
|
IReadOnlyDictionary<TeamState, int> States,
|
||||||
int PendingApprovals,
|
int PendingApprovals,
|
||||||
TeamState Worst);
|
TeamState Worst,
|
||||||
|
// #688: the department's situation, not the agent's — runs and cost today, tasks open and
|
||||||
|
// blocked, cases raised inside it, and the cost alert of today, if any (#699).
|
||||||
|
int RunsToday = 0,
|
||||||
|
decimal CostToday = 0,
|
||||||
|
int OpenTasks = 0,
|
||||||
|
int BlockedTasks = 0,
|
||||||
|
int OpenCases = 0,
|
||||||
|
string? CostAlert = null);
|
||||||
|
|
||||||
/// <summary>The whole picture at one moment: every agent and unit, and who reached whom recently.</summary>
|
/// <summary>The whole picture at one moment: every agent and unit, and who reached whom recently.</summary>
|
||||||
public sealed record TeamSnapshot(
|
public sealed record TeamSnapshot(
|
||||||
@@ -89,7 +103,10 @@ public sealed class TeamOverview(
|
|||||||
IUsageRepository? usage,
|
IUsageRepository? usage,
|
||||||
Func<IReadOnlyList<AgentConfig>> agents,
|
Func<IReadOnlyList<AgentConfig>> agents,
|
||||||
TimeProvider? clock = null,
|
TimeProvider? clock = null,
|
||||||
Func<OrgChart>? organisation = null)
|
Func<OrgChart>? organisation = null,
|
||||||
|
TaskboardService? board = null,
|
||||||
|
Core.Cases.ICaseRepository? cases = null,
|
||||||
|
Func<CancellationToken, Task<IReadOnlyList<Core.Accounting.RaisedCostAlert>>>? costAlerts = null)
|
||||||
{
|
{
|
||||||
/// <summary>How far back the last receipt of each agent is looked for.</summary>
|
/// <summary>How far back the last receipt of each agent is looked for.</summary>
|
||||||
public static readonly TimeSpan ReceiptWindow = TimeSpan.FromDays(3);
|
public static readonly TimeSpan ReceiptWindow = TimeSpan.FromDays(3);
|
||||||
@@ -140,6 +157,14 @@ public sealed class TeamOverview(
|
|||||||
|
|
||||||
var org = organisation?.Invoke() ?? new OrgChart([], configured);
|
var org = organisation?.Invoke() ?? new OrgChart([], configured);
|
||||||
|
|
||||||
|
// #688: the day of each agent, in one query each — not one per agent.
|
||||||
|
var today = DateOnly.FromDateTime(_clock.GetLocalNow().DateTime);
|
||||||
|
var usageToday = usage is null ? [] : await usage.GetDailyByAgentAsync(today, today, ct);
|
||||||
|
var openTasks = board is null ? [] : await board.ListAsync(new TaskQuery { Status = TaskItemStatus.Todo, Limit = 5_000 }, ct);
|
||||||
|
var blockedTasks = board is null ? [] : await board.ListAsync(new TaskQuery { Status = TaskItemStatus.Blocked, Limit = 5_000 }, ct);
|
||||||
|
int Count(IReadOnlyList<TaskItem> tasks, string agentId)
|
||||||
|
=> tasks.Count(t => string.Equals(TaskAssignee.AgentId(t.Assignee), agentId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
var members = new List<TeamMember>();
|
var members = new List<TeamMember>();
|
||||||
foreach (var agent in configured)
|
foreach (var agent in configured)
|
||||||
{
|
{
|
||||||
@@ -155,7 +180,10 @@ public sealed class TeamOverview(
|
|||||||
members.Add(new TeamMember(
|
members.Add(new TeamMember(
|
||||||
agent.AgentId, agent.DisplayName, agent.Model, state, runs, approvals,
|
agent.AgentId, agent.DisplayName, agent.Model, state, runs, approvals,
|
||||||
last?.Status, last?.OccurredAt, last?.ResultRef, (long)typical.TotalMilliseconds,
|
last?.Status, last?.OccurredAt, last?.ResultRef, (long)typical.TotalMilliseconds,
|
||||||
org.UnitOf(agent.AgentId)?.Id, org.LedBy(agent.AgentId) is not null));
|
org.UnitOf(agent.AgentId)?.Id, org.LedBy(agent.AgentId) is not null,
|
||||||
|
usageToday.Where(u => string.Equals(u.AgentId, agent.AgentId, StringComparison.OrdinalIgnoreCase)).Sum(u => u.Runs),
|
||||||
|
usageToday.Where(u => string.Equals(u.AgentId, agent.AgentId, StringComparison.OrdinalIgnoreCase)).Sum(u => u.CostUsd),
|
||||||
|
Count(openTasks, agent.AgentId), Count(blockedTasks, agent.AgentId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// #720: files where agents keep getting in each other's way — for a human to decide
|
// #720: files where agents keep getting in each other's way — for a human to decide
|
||||||
@@ -165,7 +193,19 @@ public sealed class TeamOverview(
|
|||||||
.Select(h => h with { Path = Readable(h.Path, configured) }).ToList()
|
.Select(h => h with { Path = Readable(h.Path, configured) }).ToList()
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
return new TeamSnapshot(members, links, now, RollUp(org, members), hotspots);
|
var openCases = cases is null ? [] : await cases.ListOpenAsync(ct);
|
||||||
|
var alerts = costAlerts is null ? [] : (await costAlerts(ct)).Where(a => a.Period == today.ToString("yyyy-MM-dd")).ToList();
|
||||||
|
var units = RollUp(org, members)
|
||||||
|
.Select(u => u with
|
||||||
|
{
|
||||||
|
OpenCases = openCases.Count(c => c.OpenedUnitId is { } id
|
||||||
|
&& (string.Equals(id, u.Id, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| org.Ancestors(id).Any(a => string.Equals(a.Id, u.Id, StringComparison.OrdinalIgnoreCase)))),
|
||||||
|
CostAlert = alerts.FirstOrDefault(a => string.Equals(a.Scope, u.Id, StringComparison.OrdinalIgnoreCase))?.Kind.ToString()
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new TeamSnapshot(members, links, now, units, hotspots);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -222,7 +262,8 @@ public sealed class TeamOverview(
|
|||||||
|
|
||||||
result.Add(new TeamUnit(
|
result.Add(new TeamUnit(
|
||||||
unit.Id, unit.Name, unit.ParentId, org.PathOf(unit.Id), org.LeaderOf(unit.Id)?.AgentId, unit.Goal,
|
unit.Id, unit.Name, unit.ParentId, org.PathOf(unit.Id), org.LeaderOf(unit.Id)?.AgentId, unit.Goal,
|
||||||
inside.Count, states, inside.Sum(m => m.PendingApprovals), worst));
|
inside.Count, states, inside.Sum(m => m.PendingApprovals), worst,
|
||||||
|
inside.Sum(m => m.RunsToday), inside.Sum(m => m.CostToday), inside.Sum(m => m.OpenTasks), inside.Sum(m => m.BlockedTasks)));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2332,6 +2332,21 @@ td.klein {
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* #688: a department's situation — runs, cost, tasks, cases */
|
||||||
|
.team-lage {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 10px;
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--gedaempft);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-lage .ton-warnung {
|
||||||
|
color: var(--warnung);
|
||||||
|
}
|
||||||
|
|
||||||
.team-chip {
|
.team-chip {
|
||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|||||||
@@ -203,6 +203,13 @@
|
|||||||
"team.members": "Mitglieder",
|
"team.members": "Mitglieder",
|
||||||
"team.subUnits": "Untereinheiten",
|
"team.subUnits": "Untereinheiten",
|
||||||
"team.openOrganisation": "Organisation bearbeiten",
|
"team.openOrganisation": "Organisation bearbeiten",
|
||||||
|
"team.openCosts": "Kosten je Einheit",
|
||||||
|
"team.openCases": "Offene Vorgänge",
|
||||||
|
"team.situation.runsHint": "Läufe heute, in dieser Einheit und darunter",
|
||||||
|
"team.situation.costHint": "Kosten heute, in dieser Einheit und darunter",
|
||||||
|
"team.situation.tasksHint": "Offene Aufgaben",
|
||||||
|
"team.situation.blockedHint": "Blockierte Aufgaben — warten auf jemanden",
|
||||||
|
"team.situation.casesHint": "Offene Vorgänge aus dieser Einheit",
|
||||||
"team.rootSubtitle": "Instanz · oberste Ebene",
|
"team.rootSubtitle": "Instanz · oberste Ebene",
|
||||||
"team.structure": "Struktur",
|
"team.structure": "Struktur",
|
||||||
"team.searchPlaceholder": "Agenten, Einheiten, Modelle suchen…",
|
"team.searchPlaceholder": "Agenten, Einheiten, Modelle suchen…",
|
||||||
|
|||||||
@@ -203,6 +203,13 @@
|
|||||||
"team.members": "Members",
|
"team.members": "Members",
|
||||||
"team.subUnits": "Sub-units",
|
"team.subUnits": "Sub-units",
|
||||||
"team.openOrganisation": "Edit organisation",
|
"team.openOrganisation": "Edit organisation",
|
||||||
|
"team.openCosts": "Costs by unit",
|
||||||
|
"team.openCases": "Open cases",
|
||||||
|
"team.situation.runsHint": "Runs today, in this unit and below",
|
||||||
|
"team.situation.costHint": "Cost today, in this unit and below",
|
||||||
|
"team.situation.tasksHint": "Open tasks",
|
||||||
|
"team.situation.blockedHint": "Blocked tasks — waiting for someone",
|
||||||
|
"team.situation.casesHint": "Open cases raised in this unit",
|
||||||
"team.rootSubtitle": "Instance · top level",
|
"team.rootSubtitle": "Instance · top level",
|
||||||
"team.structure": "Structure",
|
"team.structure": "Structure",
|
||||||
"team.searchPlaceholder": "Search agents, units, models…",
|
"team.searchPlaceholder": "Search agents, units, models…",
|
||||||
|
|||||||
@@ -235,7 +235,8 @@ function indexSnapshot() {
|
|||||||
agents: snapshot.members.length,
|
agents: snapshot.members.length,
|
||||||
states,
|
states,
|
||||||
pendingApprovals: snapshot.members.reduce((sum, m) => sum + m.pendingApprovals, 0),
|
pendingApprovals: snapshot.members.reduce((sum, m) => sum + m.pendingApprovals, 0),
|
||||||
worst
|
worst,
|
||||||
|
situation: sumSituation(snapshot.members)
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const unit of snapshot.units ?? []) {
|
for (const unit of snapshot.units ?? []) {
|
||||||
@@ -250,7 +251,15 @@ function indexSnapshot() {
|
|||||||
agents: unit.agents,
|
agents: unit.agents,
|
||||||
states: unit.states ?? {},
|
states: unit.states ?? {},
|
||||||
pendingApprovals: unit.pendingApprovals,
|
pendingApprovals: unit.pendingApprovals,
|
||||||
worst: unit.worst
|
worst: unit.worst,
|
||||||
|
situation: {
|
||||||
|
runsToday: unit.runsToday ?? 0,
|
||||||
|
costToday: unit.costToday ?? 0,
|
||||||
|
openTasks: unit.openTasks ?? 0,
|
||||||
|
blockedTasks: unit.blockedTasks ?? 0,
|
||||||
|
openCases: unit.openCases ?? 0,
|
||||||
|
costAlert: unit.costAlert ?? null
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -727,6 +736,33 @@ function visibleMembers(card) {
|
|||||||
return { rows, hidden: all.length - rows.length };
|
return { rows, hidden: all.length - rows.length };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** #688: The instance's own situation — the units' come from the instance, this total is added up here. */
|
||||||
|
function sumSituation(members) {
|
||||||
|
const sum = (key) => members.reduce((n, m) => n + (m[key] ?? 0), 0);
|
||||||
|
return { runsToday: sum("runsToday"), costToday: sum("costToday"), openTasks: sum("openTasks"), blockedTasks: sum("blockedTasks"), openCases: 0, costAlert: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* #688: A department's situation in one line — runs and cost today, tasks open and blocked,
|
||||||
|
* cases raised in it. The way in from the top: first the unit that stands out, then the agent.
|
||||||
|
*/
|
||||||
|
function situationLine(card) {
|
||||||
|
const s = card.situation;
|
||||||
|
if (!s) return null;
|
||||||
|
const parts = [
|
||||||
|
erstelleElement("span", { title: t("team.situation.runsHint"), text: `▶ ${s.runsToday}` }),
|
||||||
|
erstelleElement("span", {
|
||||||
|
klasse: s.costAlert ? "ton-warnung" : "",
|
||||||
|
title: s.costAlert ? t(`usage.units.kind.${s.costAlert}`) : t("team.situation.costHint"),
|
||||||
|
text: `${s.costAlert ? "⚠ " : ""}${Number(s.costToday).toFixed(2)} $`
|
||||||
|
}),
|
||||||
|
erstelleElement("span", { title: t("team.situation.tasksHint"), text: `☐ ${s.openTasks}` })
|
||||||
|
];
|
||||||
|
if (s.blockedTasks) parts.push(erstelleElement("span", { klasse: "ton-warnung", title: t("team.situation.blockedHint"), text: `⛔ ${s.blockedTasks}` }));
|
||||||
|
if (s.openCases) parts.push(erstelleElement("span", { klasse: "ton-warnung", title: t("team.situation.casesHint"), text: `⚖ ${s.openCases}` }));
|
||||||
|
return erstelleElement("div", { klasse: "team-lage" }, ...parts);
|
||||||
|
}
|
||||||
|
|
||||||
function buildCard(card) {
|
function buildCard(card) {
|
||||||
const worst = STATES[card.worst] ?? STATES.Idle;
|
const worst = STATES[card.worst] ?? STATES.Idle;
|
||||||
const dimmed = hits && !hits.onPath.has(card.id);
|
const dimmed = hits && !hits.onPath.has(card.id);
|
||||||
@@ -754,7 +790,8 @@ function buildCard(card) {
|
|||||||
erstelleElement("span", { klasse: "team-marker", text: worst.symbol }),
|
erstelleElement("span", { klasse: "team-marker", text: worst.symbol }),
|
||||||
erstelleElement("span", { klasse: "team-karte-name", text: card.title })),
|
erstelleElement("span", { klasse: "team-karte-name", text: card.title })),
|
||||||
erstelleElement("div", { klasse: "team-karte-unter", text: card.subtitle }),
|
erstelleElement("div", { klasse: "team-karte-unter", text: card.subtitle }),
|
||||||
erstelleElement("div", { klasse: "team-chips" }, ...chips(card))));
|
erstelleElement("div", { klasse: "team-chips" }, ...chips(card)),
|
||||||
|
situationLine(card)));
|
||||||
|
|
||||||
const { rows, hidden } = visibleMembers(card);
|
const { rows, hidden } = visibleMembers(card);
|
||||||
if (rows.length || hidden) {
|
if (rows.length || hidden) {
|
||||||
@@ -1222,8 +1259,9 @@ function cardRows(cardId) {
|
|||||||
panelHead(card.title),
|
panelHead(card.title),
|
||||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: card.unit?.path ?? card.subtitle }),
|
erstelleElement("p", { klasse: "gedaempft text-klein", text: card.unit?.path ?? card.subtitle }),
|
||||||
erstelleElement("div", { klasse: "team-chips" }, ...chips(card)),
|
erstelleElement("div", { klasse: "team-chips" }, ...chips(card)),
|
||||||
|
situationLine(card),
|
||||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("team.subtreeHint") })
|
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("team.subtreeHint") })
|
||||||
];
|
].filter(Boolean);
|
||||||
|
|
||||||
if (card.unit?.goal) rows.push(erstelleElement("p", { klasse: "text-klein", text: card.unit.goal }));
|
if (card.unit?.goal) rows.push(erstelleElement("p", { klasse: "text-klein", text: card.unit.goal }));
|
||||||
|
|
||||||
@@ -1263,6 +1301,8 @@ function cardRows(cardId) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if (card.unit) actions.append(link("#/organisation", t("team.openOrganisation")));
|
if (card.unit) actions.append(link("#/organisation", t("team.openOrganisation")));
|
||||||
|
if (card.situation?.costAlert || card.situation?.costToday > 0) actions.append(link("#/verbrauch", t("team.openCosts")));
|
||||||
|
if (card.situation?.openCases) actions.append(link("#/cases", t("team.openCases")));
|
||||||
if (actions.children.length) rows.push(actions);
|
if (actions.children.length) rows.push(actions);
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,7 +212,14 @@ public sealed record TeamUnitDto(
|
|||||||
int Agents,
|
int Agents,
|
||||||
IReadOnlyDictionary<string, int> States,
|
IReadOnlyDictionary<string, int> States,
|
||||||
int PendingApprovals,
|
int PendingApprovals,
|
||||||
string Worst);
|
string Worst,
|
||||||
|
// #688: the department's situation.
|
||||||
|
int RunsToday = 0,
|
||||||
|
decimal CostToday = 0,
|
||||||
|
int OpenTasks = 0,
|
||||||
|
int BlockedTasks = 0,
|
||||||
|
int OpenCases = 0,
|
||||||
|
string? CostAlert = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// #711: One agent in the team overview. <see cref="State"/> is one of Idle, Working, Stuck,
|
/// #711: One agent in the team overview. <see cref="State"/> is one of Idle, Working, Stuck,
|
||||||
@@ -230,7 +237,12 @@ public sealed record TeamMemberDto(
|
|||||||
string? LastRunResult,
|
string? LastRunResult,
|
||||||
long TypicalRunMs,
|
long TypicalRunMs,
|
||||||
string? UnitId,
|
string? UnitId,
|
||||||
bool IsLead);
|
bool IsLead,
|
||||||
|
// #688: the agent's day.
|
||||||
|
int RunsToday = 0,
|
||||||
|
decimal CostToday = 0,
|
||||||
|
int OpenTasks = 0,
|
||||||
|
int BlockedTasks = 0);
|
||||||
|
|
||||||
/// <summary>#711: A run in progress.</summary>
|
/// <summary>#711: A run in progress.</summary>
|
||||||
public sealed record ActiveRunDto(string RunId, string AgentId, string Source, string? TaskId, string? CalledBy, DateTime StartedAtUtc);
|
public sealed record ActiveRunDto(string RunId, string AgentId, string Source, string? TaskId, string? CalledBy, DateTime StartedAtUtc);
|
||||||
|
|||||||
@@ -95,13 +95,14 @@ internal static class Mapping
|
|||||||
m.AgentId, m.DisplayName, m.Model, m.State.ToString(),
|
m.AgentId, m.DisplayName, m.Model, m.State.ToString(),
|
||||||
m.Runs.Select(ToDto).ToList(),
|
m.Runs.Select(ToDto).ToList(),
|
||||||
m.PendingApprovals, m.LastRunStatus, m.LastRunAtUtc, m.LastRunResult, m.TypicalRunMs,
|
m.PendingApprovals, m.LastRunStatus, m.LastRunAtUtc, m.LastRunResult, m.TypicalRunMs,
|
||||||
m.UnitId, m.IsLead)).ToList(),
|
m.UnitId, m.IsLead, m.RunsToday, m.CostToday, m.OpenTasks, m.BlockedTasks)).ToList(),
|
||||||
snapshot.RecentLinks.Select(ToDto).ToList(),
|
snapshot.RecentLinks.Select(ToDto).ToList(),
|
||||||
snapshot.AtUtc,
|
snapshot.AtUtc,
|
||||||
(snapshot.Units ?? []).Select(u => new TeamUnitDto(
|
(snapshot.Units ?? []).Select(u => new TeamUnitDto(
|
||||||
u.Id, u.Name, u.ParentId, u.Path, u.LeaderAgentId, u.Goal, u.Agents,
|
u.Id, u.Name, u.ParentId, u.Path, u.LeaderAgentId, u.Goal, u.Agents,
|
||||||
u.States.ToDictionary(s => s.Key.ToString(), s => s.Value),
|
u.States.ToDictionary(s => s.Key.ToString(), s => s.Value),
|
||||||
u.PendingApprovals, u.Worst.ToString())).ToList(),
|
u.PendingApprovals, u.Worst.ToString(),
|
||||||
|
u.RunsToday, u.CostToday, u.OpenTasks, u.BlockedTasks, u.OpenCases, u.CostAlert)).ToList(),
|
||||||
(snapshot.ConflictHotspots ?? []).Select(h => new ConflictHotspotDto(
|
(snapshot.ConflictHotspots ?? []).Select(h => new ConflictHotspotDto(
|
||||||
h.Workspace, h.Path, h.Conflicts, h.LastAtUtc, h.Agents)).ToList());
|
h.Workspace, h.Path, h.Conflicts, h.LastAtUtc, h.Agents)).ToList());
|
||||||
|
|
||||||
|
|||||||
@@ -186,4 +186,63 @@ public sealed class TeamOverviewTests
|
|||||||
TeamOverview.Readable(elsewhere, configured).ShouldBe(elsewhere);
|
TeamOverview.Readable(elsewhere, configured).ShouldBe(elsewhere);
|
||||||
TeamOverview.Readable(elsewhere, []).ShouldBe(elsewhere);
|
TeamOverview.Readable(elsewhere, []).ShouldBe(elsewhere);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════ the department's situation (#688) ═══════════════════
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void A_departments_day_is_added_up_along_the_tree()
|
||||||
|
{
|
||||||
|
var units = TeamOverview.RollUp(Shop(),
|
||||||
|
[
|
||||||
|
Member("marketing-lead", "marketing", TeamState.Idle) with { RunsToday = 2, CostToday = 0.5m, OpenTasks = 1 },
|
||||||
|
Member("ads", "amazon", TeamState.Idle) with { RunsToday = 5, CostToday = 1.25m, OpenTasks = 3, BlockedTasks = 1 }
|
||||||
|
]);
|
||||||
|
|
||||||
|
var marketing = units.Single(u => u.Id == "marketing");
|
||||||
|
marketing.RunsToday.ShouldBe(7);
|
||||||
|
marketing.CostToday.ShouldBe(1.75m);
|
||||||
|
marketing.OpenTasks.ShouldBe(4);
|
||||||
|
marketing.BlockedTasks.ShouldBe(1);
|
||||||
|
units.Single(u => u.Id == "amazon").CostToday.ShouldBe(1.25m);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task The_snapshot_carries_runs_cost_tasks_cases_and_the_cost_alert_of_today()
|
||||||
|
{
|
||||||
|
var directory = Path.Combine(Path.GetTempPath(), "clawd-tests", "team-" + Guid.NewGuid().ToString("N")[..8]);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var storage = new Core.Storage.SqliteStorage(Path.Combine(directory, "state.db"));
|
||||||
|
var usage = new Core.Accounting.SqliteUsageRepository(storage);
|
||||||
|
var board = new Core.Tasks.TaskboardService(new Core.Tasks.SqliteTaskRepository(storage), Path.Combine(directory, "tasks"));
|
||||||
|
var cases = new Core.Cases.SqliteCaseRepository(storage);
|
||||||
|
await usage.RecordAsync(new Core.Accounting.RunUsage { AgentId = "ads", Model = "m", CostUsd = 0.4m, CostIsKnown = true, Status = "Completed", OccurredAt = DateTime.Now }, default);
|
||||||
|
await board.CreateAsync(new Core.Tasks.TaskItem { Title = "Bid for spring", Assignee = "@ads" }, default);
|
||||||
|
await board.CreateAsync(new Core.Tasks.TaskItem { Title = "Waiting", Assignee = "@ads", Status = Core.Tasks.TaskItemStatus.Blocked }, default);
|
||||||
|
await cases.SaveAsync(new Core.Cases.CaseRecord { Id = "c-1", Subject = "Field", OpenedBy = "ads", OpenedUnitId = "amazon", OpenedAt = DateTime.UtcNow }, default);
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Now).ToString("yyyy-MM-dd");
|
||||||
|
|
||||||
|
var overview = new TeamOverview(null, null, null, usage, () => Shop().Agents.ToList(),
|
||||||
|
organisation: Shop, board: board, cases: cases,
|
||||||
|
costAlerts: _ => Task.FromResult<IReadOnlyList<Core.Accounting.RaisedCostAlert>>(
|
||||||
|
[new Core.Accounting.RaisedCostAlert(today, "amazon", Core.Accounting.CostAlertKind.Deviation, "x", DateTime.UtcNow)]));
|
||||||
|
|
||||||
|
var snapshot = await overview.SnapshotAsync(default);
|
||||||
|
|
||||||
|
var ads = snapshot.Members.Single(m => m.AgentId == "ads");
|
||||||
|
ads.RunsToday.ShouldBe(1);
|
||||||
|
ads.CostToday.ShouldBe(0.4m);
|
||||||
|
ads.OpenTasks.ShouldBe(1);
|
||||||
|
ads.BlockedTasks.ShouldBe(1);
|
||||||
|
var marketing = snapshot.Units!.Single(u => u.Id == "marketing");
|
||||||
|
marketing.OpenCases.ShouldBe(1, "a case raised in amazon lies inside marketing");
|
||||||
|
snapshot.Units!.Single(u => u.Id == "amazon").CostAlert.ShouldBe("Deviation");
|
||||||
|
marketing.CostAlert.ShouldBeNull("the alert was raised on amazon, not on marketing");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Core.Storage.SqliteStorage.ReleasePooledConnections(directory);
|
||||||
|
try { Directory.Delete(directory, recursive: true); } catch { /* cleanup is secondary */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user