Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2b8a34d9f | ||
|
|
0718a4bd0d | ||
|
|
920d3fed64 | ||
|
|
5b8656d969 |
@@ -1239,9 +1239,17 @@ der richtige Handgriff meist schon da.
|
||||
Ereignistypen mit Absender und Empfänger: Delegation, Task-Zuweisung, später Besprechung
|
||||
und Eskalation, jeweils mit eigener Linienart. Das Verblassen nach einigen Sekunden
|
||||
geschieht im Browser.
|
||||
- **Bibliothek:** Cytoscape.js mit ELK-Layout. Es kann verschachtelte Container und
|
||||
hierarchisches Layout und bleibt bei Hunderten Nodes flüssig, ohne React. Als Datei im
|
||||
`wwwroot`, nicht über ein CDN.
|
||||
- **Bibliothek:** ELK (elkjs) für das Layout, die Karten als HTML, die Linien als SVG.
|
||||
Als Datei im `wwwroot`, nicht über ein CDN. Stufe 0 und 1 zeichneten mit Cytoscape.js
|
||||
auf einer Canvas. Das wurde am 2026-09-24 ersetzt: Ein Canvas-Knoten kann keine
|
||||
Agentenliste, keine Suchhervorhebung und nicht den Hausstil tragen.
|
||||
- **Gestalt (Rückmeldung 2026-09-24):** Jede Einheit bleibt immer als Karte sichtbar,
|
||||
verbunden als Baum. Zugeklappt wird nur die Agentenliste einer großen Einheit; sie
|
||||
zeigt dann, wer Aufmerksamkeit braucht, und „+n weitere". Eine Linie trägt die Farbe
|
||||
des dringlichsten Zustands darunter, eine rote Linie führt also von oben zum Problem.
|
||||
Ohne Durchklicken durch zehn Ebenen. Links ein Strukturbaum mit Suche und Zustandsfilter,
|
||||
der die Wege zu den Treffern zeigt und den Rest abblendet. Hausstil bleibt: Radius 0,
|
||||
App-Akzent.
|
||||
- **Nur in der WebUI.** Ein zweiter Graph-Renderer in Avalonia wäre doppelte Arbeit, die
|
||||
auseinanderläuft. Der Desktop bettet die Ansicht ein oder verlinkt sie.
|
||||
- Englisch, Texte über Sprachpakete (#661).
|
||||
|
||||
@@ -114,7 +114,8 @@ public sealed class AppHost : IAsyncDisposable
|
||||
LoggerFactory.CreateLogger("ClawdDotNet.App.Tasks"));
|
||||
|
||||
/// <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));
|
||||
|
||||
public TelegramClientManager? TelegramClient { get; private init; }
|
||||
public OpenRouterStatusService? Status { get; private init; }
|
||||
|
||||
@@ -3,6 +3,7 @@ using ClawdDotNet.Core.Activity;
|
||||
using ClawdDotNet.Core.Audit;
|
||||
using ClawdDotNet.Core.Config;
|
||||
using ClawdDotNet.Core.Engine;
|
||||
using ClawdDotNet.Core.Organisation;
|
||||
using ClawdDotNet.Core.Staging;
|
||||
|
||||
namespace ClawdDotNet.App.Services;
|
||||
@@ -40,10 +41,33 @@ public sealed record TeamMember(
|
||||
string? LastRunStatus,
|
||||
DateTime? LastRunAtUtc,
|
||||
string? LastRunResult,
|
||||
long TypicalRunMs);
|
||||
long TypicalRunMs,
|
||||
string? UnitId = null,
|
||||
bool IsLead = false);
|
||||
|
||||
/// <summary>The whole picture at one moment: every agent, and who reached whom recently.</summary>
|
||||
public sealed record TeamSnapshot(IReadOnlyList<TeamMember> Members, IReadOnlyList<AgentLink> RecentLinks, DateTime AtUtc);
|
||||
/// <summary>
|
||||
/// One unit in the team overview (#711, stage 1), with what happens inside it rolled up —
|
||||
/// its own members and those of every unit below. A collapsed container shows this, so a
|
||||
/// problem three levels down is still visible from the top.
|
||||
/// </summary>
|
||||
public sealed record TeamUnit(
|
||||
string Id,
|
||||
string Name,
|
||||
string? ParentId,
|
||||
string Path,
|
||||
string? LeaderAgentId,
|
||||
string? Goal,
|
||||
int Agents,
|
||||
IReadOnlyDictionary<TeamState, int> States,
|
||||
int PendingApprovals,
|
||||
TeamState Worst);
|
||||
|
||||
/// <summary>The whole picture at one moment: every agent and unit, and who reached whom recently.</summary>
|
||||
public sealed record TeamSnapshot(
|
||||
IReadOnlyList<TeamMember> Members,
|
||||
IReadOnlyList<AgentLink> RecentLinks,
|
||||
DateTime AtUtc,
|
||||
IReadOnlyList<TeamUnit>? Units = null);
|
||||
|
||||
/// <summary>
|
||||
/// #711: The team overview — who is working, who reached whom, where something is stuck.
|
||||
@@ -63,7 +87,8 @@ public sealed class TeamOverview(
|
||||
IAuditRepository? audit,
|
||||
IUsageRepository? usage,
|
||||
Func<IReadOnlyList<AgentConfig>> agents,
|
||||
TimeProvider? clock = null)
|
||||
TimeProvider? clock = null,
|
||||
Func<OrgChart>? organisation = null)
|
||||
{
|
||||
/// <summary>How far back the last receipt of each agent is looked for.</summary>
|
||||
public static readonly TimeSpan ReceiptWindow = TimeSpan.FromDays(3);
|
||||
@@ -106,6 +131,8 @@ public sealed class TeamOverview(
|
||||
}
|
||||
}
|
||||
|
||||
var org = organisation?.Invoke() ?? new OrgChart([], configured);
|
||||
|
||||
var members = new List<TeamMember>();
|
||||
foreach (var agent in configured)
|
||||
{
|
||||
@@ -120,10 +147,54 @@ public sealed class TeamOverview(
|
||||
var state = Classify(runs, approvals, last?.Status, typical, now);
|
||||
members.Add(new TeamMember(
|
||||
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));
|
||||
}
|
||||
|
||||
return new TeamSnapshot(members, links, now);
|
||||
return new TeamSnapshot(members, links, now, RollUp(org, members));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How much a state needs someone — the higher, the sooner. A container takes the most
|
||||
/// urgent state of everything inside it, so "somewhere in here" points at the right thing.
|
||||
/// </summary>
|
||||
public static int Urgency(TeamState state) => state switch
|
||||
{
|
||||
TeamState.Stuck => 5,
|
||||
TeamState.Problem => 4,
|
||||
TeamState.WaitingForApproval => 3,
|
||||
TeamState.BudgetExhausted => 2,
|
||||
TeamState.Working => 1,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Every unit with the states of all agents in it and below. Pure, like
|
||||
/// <see cref="Classify"/>: the page draws the numbers, it does not add them up.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<TeamUnit> RollUp(OrgChart org, IReadOnlyList<TeamMember> members)
|
||||
{
|
||||
var result = new List<TeamUnit>();
|
||||
foreach (var unit in org.Units.OrderBy(u => org.PathOf(u.Id), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
// Everyone whose unit is this one or lies below it. Ancestors() stops at a
|
||||
// cycle, so a broken tree cannot hang the overview.
|
||||
var inside = members
|
||||
.Where(m => m.UnitId is { } id
|
||||
&& (string.Equals(id, unit.Id, StringComparison.OrdinalIgnoreCase)
|
||||
|| org.Ancestors(id).Any(a => string.Equals(a.Id, unit.Id, StringComparison.OrdinalIgnoreCase))))
|
||||
.ToList();
|
||||
|
||||
var states = inside
|
||||
.GroupBy(m => m.State)
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
var worst = inside.Select(m => m.State).DefaultIfEmpty(TeamState.Idle).MaxBy(Urgency);
|
||||
|
||||
result.Add(new TeamUnit(
|
||||
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));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -157,7 +157,10 @@ public sealed class BackupService
|
||||
var connectionString = new SqliteConnectionStringBuilder
|
||||
{
|
||||
DataSource = sourcePath,
|
||||
Mode = SqliteOpenMode.ReadOnly
|
||||
Mode = SqliteOpenMode.ReadOnly,
|
||||
// Eine einmalige Kopie: Aus dem Pool heraus hielte die Verbindung die Datei
|
||||
// danach offen, und unter Windows ließe sie sich nicht mehr ersetzen oder löschen.
|
||||
Pooling = false
|
||||
}.ToString();
|
||||
|
||||
using var conn = new SqliteConnection(connectionString);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace ClawdDotNet.Core.Storage;
|
||||
@@ -19,6 +20,9 @@ public sealed class SqliteStorage
|
||||
private readonly string _connectionString;
|
||||
private readonly SemaphoreSlim _writeGate = new(1, 1);
|
||||
|
||||
/// <summary>Every database opened in this process: full path → connection string.</summary>
|
||||
private static readonly ConcurrentDictionary<string, string> OpenedDatabases = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public string DatabasePath { get; }
|
||||
|
||||
public SqliteStorage(string databasePath)
|
||||
@@ -37,6 +41,7 @@ public sealed class SqliteStorage
|
||||
Pooling = true,
|
||||
DefaultTimeout = 30
|
||||
}.ToString();
|
||||
OpenedDatabases[Path.GetFullPath(databasePath)] = _connectionString;
|
||||
|
||||
Initialize();
|
||||
|
||||
@@ -51,6 +56,33 @@ public sealed class SqliteStorage
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the pooled connections of the databases at <paramref name="path"/> — a
|
||||
/// database file, or a directory and everything below it — so the files can be deleted
|
||||
/// or replaced.
|
||||
///
|
||||
/// <para>Not <see cref="SqliteConnection.ClearAllPools"/>: that is process-wide and also
|
||||
/// takes the connections of every other database in use. The tests used it to clean up
|
||||
/// their temporary databases, and a test running in parallel then lost its connection
|
||||
/// while opening it — an ObjectDisposedException in <c>ApplyPragmas</c>, one in every
|
||||
/// few full runs, always in a different test.</para>
|
||||
/// </summary>
|
||||
public static void ReleasePooledConnections(string path)
|
||||
{
|
||||
var full = Path.GetFullPath(path);
|
||||
var below = full.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
|
||||
foreach (var (database, connectionString) in OpenedDatabases)
|
||||
{
|
||||
if (!string.Equals(database, full, StringComparison.OrdinalIgnoreCase)
|
||||
&& !database.StartsWith(below, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
using var connection = new SqliteConnection(connectionString);
|
||||
SqliteConnection.ClearPool(connection);
|
||||
}
|
||||
}
|
||||
|
||||
public SqliteConnection OpenConnection()
|
||||
{
|
||||
var connection = new SqliteConnection(_connectionString);
|
||||
|
||||
@@ -1722,7 +1722,9 @@ td.klein {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ─── Team overview (#711) ─── */
|
||||
/* ─── Team overview (#711) ───
|
||||
A work surface in the blueprint style: dot grid, cards with a head, state stripes,
|
||||
square ports on the lines, floating bars. Square like everything else. */
|
||||
|
||||
.team-ansicht {
|
||||
display: flex;
|
||||
@@ -1731,45 +1733,474 @@ td.klein {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.team-kopf {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.team-kopf h1 { margin: 0; }
|
||||
.team-kopf p { margin: 4px 0 0; max-width: 72ch; }
|
||||
|
||||
.team-zusammenfassung {
|
||||
flex-wrap: wrap;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.team-flaeche {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-height: 420px;
|
||||
/* Tones: every state has a colour, shared by markers, chips, lines and cards. */
|
||||
.ton-neutral { --ton: var(--gedaempft); }
|
||||
.ton-aktiv { --ton: var(--accent); }
|
||||
.ton-warnung { --ton: var(--warnung); }
|
||||
.ton-gefahr { --ton: var(--gefahr); }
|
||||
|
||||
.team-marker {
|
||||
color: var(--ton, var(--gedaempft));
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.team-graph {
|
||||
.team-arbeitsflaeche {
|
||||
flex: none;
|
||||
/* A fixed height: the tree and the panel scroll inside it, the page does not grow. */
|
||||
height: max(520px, calc(100vh - 250px));
|
||||
display: grid;
|
||||
grid-template-columns: 264px minmax(0, 1fr) auto;
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
border: 1px solid var(--divider);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
/* The tree on the left */
|
||||
|
||||
.team-baum {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
border-right: 1px solid var(--divider);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.team-baum-kopf {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--divider);
|
||||
}
|
||||
|
||||
.team-baum-titel {
|
||||
font-family: var(--schrift-titel);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--gedaempft);
|
||||
}
|
||||
|
||||
.team-suche {
|
||||
width: 100%;
|
||||
padding: 7px 10px;
|
||||
}
|
||||
|
||||
.team-filter {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.team-filter button {
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
background: transparent;
|
||||
color: var(--gedaempft);
|
||||
}
|
||||
|
||||
.team-filter button[aria-pressed="true"] {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.team-treffer-info {
|
||||
min-height: 16px;
|
||||
font-size: 12px;
|
||||
color: var(--gedaempft);
|
||||
}
|
||||
|
||||
.team-baum-liste {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.team-baum-liste ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.team-baum-eintrag {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
padding: 4px 10px 4px calc(8px + var(--tiefe, 0) * 14px);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.team-baum-eintrag.ausgewaehlt {
|
||||
background: color-mix(in srgb, var(--accent) 18%, transparent);
|
||||
box-shadow: inset 2px 0 0 var(--accent);
|
||||
}
|
||||
|
||||
.team-baum-eintrag.treffer .team-baum-name {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.team-baum-eintrag.gedimmt { opacity: 0.45; }
|
||||
|
||||
.team-pfeil {
|
||||
flex: none;
|
||||
width: 14px;
|
||||
font-size: 10px;
|
||||
color: var(--gedaempft);
|
||||
}
|
||||
|
||||
.team-baum-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 420px;
|
||||
background: var(--surface-subtil);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.team-zahl {
|
||||
flex: none;
|
||||
font-size: 11px;
|
||||
color: var(--gedaempft);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.team-zahl.ton-aktiv,
|
||||
.team-zahl.ton-warnung,
|
||||
.team-zahl.ton-gefahr { color: var(--ton); }
|
||||
|
||||
/* The canvas */
|
||||
|
||||
.team-leinwand {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
background-color: var(--bg);
|
||||
background-image: radial-gradient(circle, var(--divider) 1px, transparent 1.3px);
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
.team-leinwand.zieht { cursor: grabbing; }
|
||||
|
||||
.team-welt {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.team-welt.gleitend { transition: transform 0.35s ease; }
|
||||
|
||||
.team-kanten,
|
||||
.team-links {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.team-links { z-index: 1; }
|
||||
|
||||
.team-kante {
|
||||
fill: none;
|
||||
stroke: color-mix(in srgb, var(--ton) 55%, var(--divider));
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.team-kante.ton-gefahr,
|
||||
.team-kante.ton-warnung {
|
||||
stroke: var(--ton);
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.team-kante.ton-gefahr {
|
||||
filter: drop-shadow(0 0 4px color-mix(in srgb, var(--gefahr) 70%, transparent));
|
||||
}
|
||||
|
||||
.team-port {
|
||||
fill: var(--bg);
|
||||
stroke: color-mix(in srgb, var(--ton) 70%, var(--divider));
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.team-port.ton-gefahr,
|
||||
.team-port.ton-warnung { fill: var(--ton); stroke: var(--ton); }
|
||||
|
||||
.team-kante.gedimmt,
|
||||
.team-port.gedimmt { opacity: 0.2; }
|
||||
|
||||
.team-link {
|
||||
fill: none;
|
||||
stroke: var(--accent);
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 6 5;
|
||||
animation: team-fluss 0.9s linear infinite;
|
||||
}
|
||||
|
||||
.team-pfeilspitze { fill: var(--accent); }
|
||||
|
||||
@keyframes team-fluss {
|
||||
to { stroke-dashoffset: -11; }
|
||||
}
|
||||
|
||||
.team-link-zahl {
|
||||
font-family: var(--schrift-text);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
fill: var(--text);
|
||||
paint-order: stroke;
|
||||
stroke: var(--bg);
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
|
||||
.team-karte {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--divider);
|
||||
border-left: 3px solid var(--ton, var(--divider));
|
||||
box-shadow: 0 12px 28px -16px rgba(0, 0, 0, 0.55);
|
||||
transition: opacity 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.team-karte.ton-neutral { border-left-color: var(--divider); }
|
||||
|
||||
.team-karte.ton-gefahr {
|
||||
border-color: color-mix(in srgb, var(--gefahr) 60%, var(--divider));
|
||||
border-left-color: var(--gefahr);
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, var(--gefahr) 35%, transparent),
|
||||
0 0 26px -6px color-mix(in srgb, var(--gefahr) 60%, transparent);
|
||||
}
|
||||
|
||||
.team-karte.ton-warnung {
|
||||
border-color: color-mix(in srgb, var(--warnung) 45%, var(--divider));
|
||||
border-left-color: var(--warnung);
|
||||
}
|
||||
|
||||
.team-karte.wurzel .team-karte-kopf {
|
||||
background: color-mix(in srgb, var(--accent) 12%, var(--surface));
|
||||
}
|
||||
|
||||
.team-karte.ausgewaehlt,
|
||||
.team-karte.treffer {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.team-karte.gedimmt { opacity: 0.3; }
|
||||
|
||||
.team-karte-kopf {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px 12px 9px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--divider);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.team-karte-titel {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
font-family: var(--schrift-titel);
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.team-karte-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.team-karte-unter {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--gedaempft);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.team-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.team-chip {
|
||||
padding: 1px 6px;
|
||||
font-size: 11px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--gedaempft);
|
||||
border: 1px solid var(--divider);
|
||||
}
|
||||
|
||||
.team-chip.ton-aktiv,
|
||||
.team-chip.ton-warnung,
|
||||
.team-chip.ton-gefahr {
|
||||
color: var(--ton);
|
||||
border-color: color-mix(in srgb, var(--ton) 55%, transparent);
|
||||
background: color-mix(in srgb, var(--ton) 12%, transparent);
|
||||
}
|
||||
|
||||
.team-karte-liste { padding: 4px 0; }
|
||||
|
||||
.team-zeile {
|
||||
display: grid;
|
||||
grid-template-columns: 16px minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 5px 12px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.team-zeile.ausgewaehlt { background: color-mix(in srgb, var(--accent) 16%, transparent); }
|
||||
.team-zeile.treffer { box-shadow: inset 2px 0 0 var(--accent); }
|
||||
.team-zeile.gedimmt { opacity: 0.4; }
|
||||
.team-zeile.blinkt { animation: team-blinkt 1.4s ease; }
|
||||
|
||||
@keyframes team-blinkt {
|
||||
0%, 55% { background: color-mix(in srgb, var(--accent) 35%, transparent); }
|
||||
100% { background: transparent; }
|
||||
}
|
||||
|
||||
.team-zeile-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.team-zeile-zustand {
|
||||
font-size: 11px;
|
||||
color: var(--gedaempft);
|
||||
}
|
||||
|
||||
.team-zeile.ton-aktiv .team-zeile-zustand,
|
||||
.team-zeile.ton-warnung .team-zeile-zustand,
|
||||
.team-zeile.ton-gefahr .team-zeile-zustand { color: var(--ton); }
|
||||
|
||||
.team-mehr {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 4px 12px 7px 36px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.team-mehr:hover:not(:disabled) {
|
||||
background: transparent;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Floating bars */
|
||||
|
||||
.team-leiste {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 4px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--divider);
|
||||
box-shadow: 0 12px 30px -14px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.team-leiste button {
|
||||
padding: 6px 12px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.team-leiste button[aria-pressed="true"] {
|
||||
background: color-mix(in srgb, var(--accent) 20%, transparent);
|
||||
box-shadow: inset 0 -2px 0 var(--accent);
|
||||
}
|
||||
|
||||
.team-leiste-mitte {
|
||||
left: 50%;
|
||||
bottom: 16px;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.team-leiste-zoom {
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
}
|
||||
|
||||
.team-zoomwert {
|
||||
align-self: center;
|
||||
min-width: 48px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
color: var(--gedaempft);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Side panel */
|
||||
|
||||
.team-seitenleiste {
|
||||
width: 320px;
|
||||
flex-shrink: 0;
|
||||
padding: 14px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--divider);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
overflow-y: auto;
|
||||
background: var(--surface);
|
||||
border-left: 1px solid var(--divider);
|
||||
}
|
||||
|
||||
.team-seitenleiste-kopf {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.team-seitenleiste[hidden] { display: none; }
|
||||
|
||||
.team-seitenleiste h3 {
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
.team-seitenleiste-kopf { justify-content: space-between; }
|
||||
.team-seitenleiste h2 { margin: 0; }
|
||||
.team-seitenleiste h3 { margin: 8px 0 0; }
|
||||
|
||||
.team-seitenleiste .team-zeile { padding: 5px 4px; }
|
||||
|
||||
.team-ergebnis {
|
||||
white-space: pre-wrap;
|
||||
@@ -1786,14 +2217,28 @@ td.klein {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.team-eintrag {
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.team-legende {
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.team-flaeche { flex-direction: column; }
|
||||
.team-seitenleiste { width: auto; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.team-link,
|
||||
.team-zeile.blinkt { animation: none; }
|
||||
.team-welt.gleitend { transition: none; }
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.team-arbeitsflaeche { height: auto; grid-template-columns: minmax(0, 1fr); grid-template-rows: auto; }
|
||||
.team-baum { max-height: 280px; border-right: 0; border-bottom: 1px solid var(--divider); }
|
||||
.team-leinwand { min-height: 60vh; }
|
||||
.team-seitenleiste { width: auto; border-left: 0; border-top: 1px solid var(--divider); }
|
||||
}
|
||||
|
||||
/* ─── Organisation (#683) ─── */
|
||||
|
||||
@@ -128,10 +128,10 @@
|
||||
"org.toolsLostConfirm": "Werkzeuge entfernen und übernehmen",
|
||||
"org.runningRefusal": "Nicht, solange {agents} läuft — ein Agent darf nicht mitten im Lauf andere Rechte bekommen.",
|
||||
"team.title": "Teamübersicht",
|
||||
"team.subtitle": "Wer arbeitet, wer hat in den letzten Sekunden wen erreicht, und wo klemmt es. Ein Klick auf einen Agenten zeigt Einzelheiten.",
|
||||
"team.subtitle": "Jede Einheit ist eine Karte, verbunden wie die Organisation. Eine Linie trägt die Farbe des dringlichsten Zustands darunter, eine rote Linie führt also zum Problem. Klick auf Karte oder Agent zeigt Einzelheiten.",
|
||||
"team.noInstance": "Keine Instanz ausgewählt. Oben eine aktive Instanz wählen.",
|
||||
"team.empty": "Diese Instanz hat keine Agenten.",
|
||||
"team.legendLink": "hat erreicht (verblasst nach 30 s)",
|
||||
"team.legendLink": "Live-Verbindung (verblasst nach 30 s)",
|
||||
"team.state.Idle": "bereit",
|
||||
"team.state.Working": "arbeitet",
|
||||
"team.state.Stuck": "hängt",
|
||||
@@ -148,6 +148,36 @@
|
||||
"team.openApprovals": "Freigaben öffnen",
|
||||
"team.openChat": "Chat öffnen",
|
||||
"team.openRuns": "Läufe anzeigen",
|
||||
"team.unitAgentsOne": "1 Agent",
|
||||
"team.unitAgents": "{n} Agenten",
|
||||
"team.legendLead": "Leitung der Einheit",
|
||||
"team.leads": "Leitet",
|
||||
"team.memberOf": "Mitglied von",
|
||||
"team.members": "Mitglieder",
|
||||
"team.subUnits": "Untereinheiten",
|
||||
"team.openOrganisation": "Organisation bearbeiten",
|
||||
"team.rootSubtitle": "Instanz · oberste Ebene",
|
||||
"team.structure": "Struktur",
|
||||
"team.searchPlaceholder": "Agenten, Einheiten, Modelle suchen…",
|
||||
"team.filterLabel": "Nach Zustand filtern",
|
||||
"team.filter.all": "Alle",
|
||||
"team.filter.problems": "Probleme",
|
||||
"team.filter.waiting": "Freigaben",
|
||||
"team.filter.working": "Arbeiten",
|
||||
"team.filter.budget": "Budget",
|
||||
"team.needsAttention": "{n} brauchen Aufmerksamkeit",
|
||||
"team.matches": "{n} Treffer",
|
||||
"team.matchesOne": "1 Treffer",
|
||||
"team.noMatches": "Keine Treffer",
|
||||
"team.moreMembers": "+{n} weitere",
|
||||
"team.showFewer": "Weniger zeigen",
|
||||
"team.showAllMembers": "Alle Mitglieder zeigen",
|
||||
"team.zoomIn": "Vergrößern",
|
||||
"team.zoomOut": "Verkleinern",
|
||||
"team.fit": "Einpassen",
|
||||
"team.liveLinks": "Live-Verbindungen",
|
||||
"team.subtreeHint": "Die Zahlen umfassen alle Einheiten darunter.",
|
||||
"team.legendLine": "Linie = dringlichster Zustand darunter",
|
||||
"nav.services": "Dienste",
|
||||
"nav.group.analytics": "Analyse",
|
||||
"nav.usage": "Tokennutzung",
|
||||
|
||||
@@ -128,10 +128,10 @@
|
||||
"org.toolsLostConfirm": "Remove the tools and apply",
|
||||
"org.runningRefusal": "Not while {agents} is running — an agent must not get different rights in the middle of a run.",
|
||||
"team.title": "Team overview",
|
||||
"team.subtitle": "Who is working, who reached whom in the last seconds, and where something is stuck. Click an agent for details.",
|
||||
"team.subtitle": "Every unit is a card, connected as the organisation. A line takes the colour of the most urgent state below it, so a red line leads to the problem. Click a card or an agent for details.",
|
||||
"team.noInstance": "No instance selected. Choose an active instance in the header.",
|
||||
"team.empty": "No agents in this instance.",
|
||||
"team.legendLink": "reached (fades after 30 s)",
|
||||
"team.legendLink": "live link (fades after 30 s)",
|
||||
"team.state.Idle": "idle",
|
||||
"team.state.Working": "working",
|
||||
"team.state.Stuck": "stuck",
|
||||
@@ -148,6 +148,36 @@
|
||||
"team.openApprovals": "Open approvals",
|
||||
"team.openChat": "Open chat",
|
||||
"team.openRuns": "Show runs",
|
||||
"team.unitAgentsOne": "1 agent",
|
||||
"team.unitAgents": "{n} agents",
|
||||
"team.legendLead": "lead of the unit",
|
||||
"team.leads": "Leads",
|
||||
"team.memberOf": "Member of",
|
||||
"team.members": "Members",
|
||||
"team.subUnits": "Sub-units",
|
||||
"team.openOrganisation": "Edit organisation",
|
||||
"team.rootSubtitle": "Instance · top level",
|
||||
"team.structure": "Structure",
|
||||
"team.searchPlaceholder": "Search agents, units, models…",
|
||||
"team.filterLabel": "Filter by state",
|
||||
"team.filter.all": "All",
|
||||
"team.filter.problems": "Problems",
|
||||
"team.filter.waiting": "Approvals",
|
||||
"team.filter.working": "Working",
|
||||
"team.filter.budget": "Budget",
|
||||
"team.needsAttention": "{n} need attention",
|
||||
"team.matches": "{n} matches",
|
||||
"team.matchesOne": "1 match",
|
||||
"team.noMatches": "No matches",
|
||||
"team.moreMembers": "+{n} more",
|
||||
"team.showFewer": "Show fewer",
|
||||
"team.showAllMembers": "Show all members",
|
||||
"team.zoomIn": "Zoom in",
|
||||
"team.zoomOut": "Zoom out",
|
||||
"team.fit": "Fit",
|
||||
"team.liveLinks": "Live links",
|
||||
"team.subtreeHint": "Counts include all units below.",
|
||||
"team.legendLine": "line = most urgent state below",
|
||||
"nav.services": "Services",
|
||||
"nav.group.analytics": "Analytics",
|
||||
"nav.usage": "Token Usage",
|
||||
|
||||
+11
-7
@@ -9,15 +9,19 @@ Downloaded 2026-09-24 from the npm registry. Each tarball was checked against th
|
||||
|
||||
| Package | Version | File | License | SHA-256 of the file |
|
||||
|---|---|---|---|---|
|
||||
| [cytoscape](https://www.npmjs.com/package/cytoscape) | 3.34.3 | `cytoscape/cytoscape.min.js` | MIT | `5f3b5b529546d5af1fc5628590af033b74511a5b6f789f5f4682845863228b91` |
|
||||
| [elkjs](https://www.npmjs.com/package/elkjs) | 0.9.3 | `elkjs/elk.bundled.js` | EPL-2.0 | `b0745abd7f23cd91690a1587e377edbe19fd7233c783300290936720546216d4` |
|
||||
| [cytoscape-elk](https://www.npmjs.com/package/cytoscape-elk) | 2.3.0 | `cytoscape-elk/cytoscape-elk.js` | MIT | `25acbb9dea9b0dc6fef0e79b697547ee56e579f0e1c22ae4353131e72c7e140a` |
|
||||
|
||||
**Why elkjs 0.9.3 and not the newest.** cytoscape-elk 2.3.0 declares `elkjs ^0.9.3`. A newer
|
||||
elkjs would be untested with the adapter.
|
||||
**Why elkjs 0.9.3 and not the newest.** It came in together with cytoscape-elk 2.3.0,
|
||||
which declared `elkjs ^0.9.3`. Cytoscape and the adapter were removed again on 2026-09-24
|
||||
(see below); elkjs stays at the version it was checked at. Raising it is a separate change.
|
||||
|
||||
**How they are loaded.** All three are UMD bundles and, loaded as classic scripts in this
|
||||
order, register the globals `cytoscape`, `ELK` and `cytoscapeElk`. The team overview (#711)
|
||||
loads them on demand, so pages that do not draw a graph do not pay for ~2 MB of script.
|
||||
**Why Cytoscape is gone.** The team overview (#711) first drew its graph with Cytoscape.js
|
||||
on a canvas. A canvas node cannot hold a list of agents, a search highlight or the house
|
||||
style, so the cards became plain HTML placed by ELK, with the lines in SVG. Nothing else
|
||||
used Cytoscape.
|
||||
|
||||
**How it is loaded.** `elk.bundled.js` is a UMD bundle and, loaded as a classic script,
|
||||
registers the global `ELK`. The team overview loads it on demand, so pages that do not draw
|
||||
the organisation do not pay for ~1.5 MB of script.
|
||||
|
||||
The license texts sit next to each file.
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
|
||||
|
||||
Copyright (c) 2016-2018, 2020, The Cytoscape Consortium.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the “Software”), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,356 +0,0 @@
|
||||
(function webpackUniversalModuleDefinition(root, factory) {
|
||||
if(typeof exports === 'object' && typeof module === 'object')
|
||||
module.exports = factory(require("elkjs/lib/elk.bundled.js"));
|
||||
else if(typeof define === 'function' && define.amd)
|
||||
define(["elkjs"], factory);
|
||||
else if(typeof exports === 'object')
|
||||
exports["cytoscapeElk"] = factory(require("elkjs/lib/elk.bundled.js"));
|
||||
else
|
||||
root["cytoscapeElk"] = factory(root["ELK"]);
|
||||
})(this, function(__WEBPACK_EXTERNAL_MODULE__632__) {
|
||||
return /******/ (function() { // webpackBootstrap
|
||||
/******/ "use strict";
|
||||
/******/ var __webpack_modules__ = ({
|
||||
|
||||
/***/ 632:
|
||||
/***/ (function(module) {
|
||||
|
||||
module.exports = __WEBPACK_EXTERNAL_MODULE__632__;
|
||||
|
||||
/***/ })
|
||||
|
||||
/******/ });
|
||||
/************************************************************************/
|
||||
/******/ // The module cache
|
||||
/******/ var __webpack_module_cache__ = {};
|
||||
/******/
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
/******/ // Check if module is in cache
|
||||
/******/ var cachedModule = __webpack_module_cache__[moduleId];
|
||||
/******/ if (cachedModule !== undefined) {
|
||||
/******/ return cachedModule.exports;
|
||||
/******/ }
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = __webpack_module_cache__[moduleId] = {
|
||||
/******/ // no module.id needed
|
||||
/******/ // no module.loaded needed
|
||||
/******/ exports: {}
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Execute the module function
|
||||
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
||||
/******/
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports;
|
||||
/******/ }
|
||||
/******/
|
||||
/************************************************************************/
|
||||
/******/ /* webpack/runtime/compat get default export */
|
||||
/******/ !function() {
|
||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||
/******/ __webpack_require__.n = function(module) {
|
||||
/******/ var getter = module && module.__esModule ?
|
||||
/******/ function() { return module['default']; } :
|
||||
/******/ function() { return module; };
|
||||
/******/ __webpack_require__.d(getter, { a: getter });
|
||||
/******/ return getter;
|
||||
/******/ };
|
||||
/******/ }();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/define property getters */
|
||||
/******/ !function() {
|
||||
/******/ // define getter functions for harmony exports
|
||||
/******/ __webpack_require__.d = function(exports, definition) {
|
||||
/******/ for(var key in definition) {
|
||||
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
|
||||
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
|
||||
/******/ }
|
||||
/******/ }
|
||||
/******/ };
|
||||
/******/ }();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
||||
/******/ !function() {
|
||||
/******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
|
||||
/******/ }();
|
||||
/******/
|
||||
/************************************************************************/
|
||||
var __webpack_exports__ = {};
|
||||
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
|
||||
!function() {
|
||||
|
||||
// EXPORTS
|
||||
__webpack_require__.d(__webpack_exports__, {
|
||||
"default": function() { return /* binding */ src; }
|
||||
});
|
||||
|
||||
// EXTERNAL MODULE: external {"commonjs":"elkjs/lib/elk.bundled.js","commonjs2":"elkjs/lib/elk.bundled.js","amd":"elkjs","root":"ELK"}
|
||||
var elk_bundled_js_amd_elkjs_root_ELK_ = __webpack_require__(632);
|
||||
var elk_bundled_js_amd_elkjs_root_ELK_default = /*#__PURE__*/__webpack_require__.n(elk_bundled_js_amd_elkjs_root_ELK_);
|
||||
;// CONCATENATED MODULE: ./src/assign.js
|
||||
// Simple, internal Object.assign() polyfill for options objects etc.
|
||||
function assign_assign(tgt) {
|
||||
for (var _len = arguments.length, srcs = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
||||
srcs[_key - 1] = arguments[_key];
|
||||
}
|
||||
|
||||
srcs.forEach(function (src) {
|
||||
Object.keys(src).forEach(function (k) {
|
||||
return tgt[k] = src[k];
|
||||
});
|
||||
});
|
||||
return tgt;
|
||||
}
|
||||
|
||||
/* harmony default export */ var src_assign = (Object.assign != null ? Object.assign.bind(Object) : assign_assign);
|
||||
;// CONCATENATED MODULE: ./src/defaults.js
|
||||
var defaults = {
|
||||
nodeDimensionsIncludeLabels: false,
|
||||
// Boolean which changes whether label dimensions are included when calculating node dimensions
|
||||
fit: true,
|
||||
// Whether to fit
|
||||
padding: 20,
|
||||
// Padding on fit
|
||||
animate: false,
|
||||
// Whether to transition the node positions
|
||||
animateFilter: function animateFilter() {
|
||||
return true;
|
||||
},
|
||||
// Whether to animate specific nodes when animation is on; non-animated nodes immediately go to their final positions
|
||||
animationDuration: 500,
|
||||
// Duration of animation in ms if enabled
|
||||
animationEasing: undefined,
|
||||
// Easing of animation if enabled
|
||||
transform: function transform(node, pos) {
|
||||
return pos;
|
||||
},
|
||||
// A function that applies a transform to the final node position
|
||||
ready: undefined,
|
||||
// Callback on layoutready
|
||||
stop: undefined,
|
||||
// Callback on layoutstop
|
||||
nodeLayoutOptions: undefined,
|
||||
// Special options for only the nodes
|
||||
elk: {
|
||||
// Options to pass directly to ELK `layoutOptions`. The subsequent identifier has to be used as property key in quotes.
|
||||
// E.g. for 'org.eclipse.elk.direction' use:
|
||||
// 'elk.direction'
|
||||
// Primary/mandatory, the elk algorithm to use
|
||||
// one of 'box', 'disco', 'force', 'layered', 'mrtree', 'radial', 'random', 'stress'
|
||||
// (see https://www.eclipse.org/elk/reference/algorithms.html)
|
||||
algorithm: undefined
|
||||
},
|
||||
priority: function priority() {
|
||||
return null;
|
||||
} // Edges with a non-nil value are skipped when geedy edge cycle breaking is enabled
|
||||
|
||||
};
|
||||
/* harmony default export */ var src_defaults = (defaults);
|
||||
;// CONCATENATED MODULE: ./src/layout.js
|
||||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
||||
|
||||
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
|
||||
|
||||
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
|
||||
|
||||
|
||||
|
||||
|
||||
var elkOverrides = {};
|
||||
|
||||
var getPos = function getPos(ele, options) {
|
||||
var dims = ele.layoutDimensions(options);
|
||||
var parent = ele.parent();
|
||||
var k = ele.scratch('elk');
|
||||
var p = {
|
||||
x: k.x,
|
||||
y: k.y
|
||||
};
|
||||
|
||||
while (parent.nonempty()) {
|
||||
var kp = parent.scratch('elk');
|
||||
p.x += kp.x;
|
||||
p.y += kp.y;
|
||||
parent = parent.parent();
|
||||
} // elk considers a node position to be its top-left corner, while cy is the centre
|
||||
|
||||
|
||||
p.x += dims.w / 2;
|
||||
p.y += dims.h / 2;
|
||||
return p;
|
||||
};
|
||||
|
||||
var makeNode = function makeNode(node, options) {
|
||||
var k = {
|
||||
_cyEle: node,
|
||||
id: node.id()
|
||||
}; // Apply nodeLayoutOptions per user-specified function
|
||||
// e.g. nodeLayoutOptions => n.scratch('layoutOptions')
|
||||
|
||||
if (options.nodeLayoutOptions) {
|
||||
k.layoutOptions = options.nodeLayoutOptions(node);
|
||||
}
|
||||
|
||||
if (!node.isParent()) {
|
||||
var dims = node.layoutDimensions(options);
|
||||
var p = node.position(); // the elk position is the top-left corner, cy is the centre
|
||||
|
||||
k.x = p.x - dims.w / 2;
|
||||
k.y = p.y - dims.h / 2;
|
||||
k.width = dims.w;
|
||||
k.height = dims.h;
|
||||
}
|
||||
|
||||
node.scratch('elk', k);
|
||||
return k;
|
||||
};
|
||||
|
||||
var makeEdge = function makeEdge(edge
|
||||
/*, options*/
|
||||
) {
|
||||
var k = {
|
||||
_cyEle: edge,
|
||||
id: edge.id(),
|
||||
source: edge.data('source'),
|
||||
target: edge.data('target')
|
||||
};
|
||||
edge.scratch('elk', k);
|
||||
return k;
|
||||
};
|
||||
|
||||
var makeGraph = function makeGraph(nodes, edges, options) {
|
||||
var elkNodes = [];
|
||||
var elkEdges = [];
|
||||
var elkEleLookup = {};
|
||||
var graph = {
|
||||
id: 'root',
|
||||
children: [],
|
||||
edges: []
|
||||
}; // map all nodes
|
||||
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var n = nodes[i];
|
||||
var k = makeNode(n, options);
|
||||
elkNodes.push(k);
|
||||
elkEleLookup[n.id()] = k;
|
||||
} // map all edges
|
||||
|
||||
|
||||
for (var _i = 0; _i < edges.length; _i++) {
|
||||
var e = edges[_i];
|
||||
|
||||
var _k = makeEdge(e, options);
|
||||
|
||||
elkEdges.push(_k);
|
||||
elkEleLookup[e.id()] = _k;
|
||||
} // make hierarchy
|
||||
|
||||
|
||||
for (var _i2 = 0; _i2 < elkNodes.length; _i2++) {
|
||||
var _k2 = elkNodes[_i2];
|
||||
var _n = _k2._cyEle;
|
||||
|
||||
if (!_n.isChild()) {
|
||||
graph.children.push(_k2);
|
||||
} else {
|
||||
var parent = _n.parent();
|
||||
|
||||
var parentK = elkEleLookup[parent.id()];
|
||||
var children = parentK.children = parentK.children || [];
|
||||
children.push(_k2);
|
||||
}
|
||||
}
|
||||
|
||||
for (var _i3 = 0; _i3 < elkEdges.length; _i3++) {
|
||||
var _k3 = elkEdges[_i3]; // put all edges in the top level for now
|
||||
// TODO does this cause issues in certain edgecases?
|
||||
|
||||
/*let e = k._cyEle;
|
||||
let parentSrc = e.source().parent();
|
||||
let parentTgt = e.target().parent();
|
||||
if ( false && parentSrc.nonempty() && parentTgt.nonempty() && parentSrc.same( parentTgt ) ){
|
||||
let kp = elkEleLookup[ parentSrc.id() ];
|
||||
kp.edges = kp.edges || [];
|
||||
kp.edges.push( k );
|
||||
} else {*/
|
||||
|
||||
graph.edges.push(_k3); //}
|
||||
}
|
||||
|
||||
return graph;
|
||||
};
|
||||
|
||||
var Layout = /*#__PURE__*/function () {
|
||||
function Layout(options) {
|
||||
_classCallCheck(this, Layout);
|
||||
|
||||
var elkOptions = options.elk;
|
||||
var cy = options.cy;
|
||||
this.options = src_assign({}, src_defaults, options);
|
||||
this.options.elk = src_assign({
|
||||
aspectRatio: cy.width() / cy.height()
|
||||
}, src_defaults.elk, elkOptions, elkOverrides);
|
||||
}
|
||||
|
||||
_createClass(Layout, [{
|
||||
key: "run",
|
||||
value: function run() {
|
||||
var layout = this;
|
||||
var options = this.options;
|
||||
var eles = options.eles;
|
||||
var nodes = eles.nodes();
|
||||
var edges = eles.edges();
|
||||
var elk = new (elk_bundled_js_amd_elkjs_root_ELK_default())();
|
||||
var graph = makeGraph(nodes, edges, options);
|
||||
graph['layoutOptions'] = options.elk;
|
||||
elk.layout(graph).then(function () {
|
||||
nodes.filter(function (n) {
|
||||
return !n.isParent();
|
||||
}).layoutPositions(layout, options, function (n) {
|
||||
return getPos(n, options);
|
||||
});
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}, {
|
||||
key: "stop",
|
||||
value: function stop() {
|
||||
return this; // chaining
|
||||
}
|
||||
}, {
|
||||
key: "destroy",
|
||||
value: function destroy() {
|
||||
return this; // chaining
|
||||
}
|
||||
}]);
|
||||
|
||||
return Layout;
|
||||
}();
|
||||
|
||||
/* harmony default export */ var layout = (Layout);
|
||||
;// CONCATENATED MODULE: ./src/index.js
|
||||
// registers the extension on a cytoscape lib ref
|
||||
|
||||
var register = function register(cytoscape) {
|
||||
if (!cytoscape) {
|
||||
return;
|
||||
} // can't register if cytoscape unspecified
|
||||
|
||||
|
||||
cytoscape('layout', 'elk', layout); // register with cytoscape.js
|
||||
};
|
||||
|
||||
if (typeof cytoscape !== 'undefined') {
|
||||
// expose to global cytoscape (i.e. window.cytoscape)
|
||||
// eslint-disable-next-line no-undef
|
||||
register(cytoscape);
|
||||
}
|
||||
|
||||
/* harmony default export */ var src = (register);
|
||||
}();
|
||||
__webpack_exports__ = __webpack_exports__["default"];
|
||||
/******/ return __webpack_exports__;
|
||||
/******/ })()
|
||||
;
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
Copyright (c) 2016-2026, The Cytoscape Consortium.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the “Software”), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -125,8 +125,29 @@ public sealed record TaskDto(
|
||||
/// </summary>
|
||||
public sealed record TaskBudgetMissDto(int Count, DateTime? LastAt, DateTime? DeferredUntil, bool CatchUpPending, bool CarriedOver);
|
||||
|
||||
/// <summary>#711: The team overview — every agent and who reached whom recently.</summary>
|
||||
public sealed record TeamSnapshotDto(IReadOnlyList<TeamMemberDto> Members, IReadOnlyList<AgentLinkDto> Links, DateTime AtUtc);
|
||||
/// <summary>#711: The team overview — every agent and unit, and who reached whom recently.</summary>
|
||||
public sealed record TeamSnapshotDto(
|
||||
IReadOnlyList<TeamMemberDto> Members,
|
||||
IReadOnlyList<AgentLinkDto> Links,
|
||||
DateTime AtUtc,
|
||||
IReadOnlyList<TeamUnitDto> Units);
|
||||
|
||||
/// <summary>
|
||||
/// #711, stage 1: A unit with the states of everyone in it and below, counted by the
|
||||
/// instance. <see cref="States"/> maps a state name to a count; <see cref="Worst"/> is the
|
||||
/// most urgent state inside.
|
||||
/// </summary>
|
||||
public sealed record TeamUnitDto(
|
||||
string Id,
|
||||
string Name,
|
||||
string? ParentId,
|
||||
string Path,
|
||||
string? LeaderAgentId,
|
||||
string? Goal,
|
||||
int Agents,
|
||||
IReadOnlyDictionary<string, int> States,
|
||||
int PendingApprovals,
|
||||
string Worst);
|
||||
|
||||
/// <summary>
|
||||
/// #711: One agent in the team overview. <see cref="State"/> is one of Idle, Working, Stuck,
|
||||
@@ -142,7 +163,9 @@ public sealed record TeamMemberDto(
|
||||
string? LastRunStatus,
|
||||
DateTime? LastRunAtUtc,
|
||||
string? LastRunResult,
|
||||
long TypicalRunMs);
|
||||
long TypicalRunMs,
|
||||
string? UnitId,
|
||||
bool IsLead);
|
||||
|
||||
/// <summary>#711: A run in progress.</summary>
|
||||
public sealed record ActiveRunDto(string RunId, string AgentId, string Source, string? TaskId, string? CalledBy, DateTime StartedAtUtc);
|
||||
|
||||
@@ -74,9 +74,14 @@ internal static class Mapping
|
||||
snapshot.Members.Select(m => new TeamMemberDto(
|
||||
m.AgentId, m.DisplayName, m.Model, m.State.ToString(),
|
||||
m.Runs.Select(ToDto).ToList(),
|
||||
m.PendingApprovals, m.LastRunStatus, m.LastRunAtUtc, m.LastRunResult, m.TypicalRunMs)).ToList(),
|
||||
m.PendingApprovals, m.LastRunStatus, m.LastRunAtUtc, m.LastRunResult, m.TypicalRunMs,
|
||||
m.UnitId, m.IsLead)).ToList(),
|
||||
snapshot.RecentLinks.Select(ToDto).ToList(),
|
||||
snapshot.AtUtc);
|
||||
snapshot.AtUtc,
|
||||
(snapshot.Units ?? []).Select(u => new TeamUnitDto(
|
||||
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.PendingApprovals, u.Worst.ToString())).ToList());
|
||||
|
||||
public static ActiveRunDto ToDto(this Core.Activity.ActiveRun run)
|
||||
=> new(run.RunId, run.AgentId, run.Source, run.TaskId, run.CalledBy, run.StartedAtUtc);
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class TaskAdministrationTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_dir);
|
||||
try { Directory.Delete(_dir, recursive: true); }
|
||||
catch { /* Aufraeumen ist Nebensache */ }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using ClawdDotNet.App.Services;
|
||||
using ClawdDotNet.Core.Activity;
|
||||
using ClawdDotNet.Core.Config;
|
||||
using ClawdDotNet.Core.Organisation;
|
||||
using Shouldly;
|
||||
|
||||
namespace ClawdDotNet.App.Tests.Services;
|
||||
@@ -79,4 +81,85 @@ public sealed class TeamOverviewTests
|
||||
public void A_cancelled_run_is_not_a_problem()
|
||||
// Cancelled means a human stopped it — that was a decision, not a fault.
|
||||
=> TeamOverview.Classify([], 0, "Cancelled", TimeSpan.Zero, Now).ShouldBe(TeamState.Idle);
|
||||
|
||||
// ═══════════════════ stage 1: units roll up ═══════════════════
|
||||
|
||||
/// <summary>marketing (lead) › amazon (lead, ads) ; accounting (bookkeeper) ; ceo at the top.</summary>
|
||||
private static OrgChart Shop() => new(
|
||||
[
|
||||
new UnitConfig { Id = "marketing", Name = "Marketing", LeaderAgentId = "marketing-lead" },
|
||||
new UnitConfig { Id = "amazon", Name = "Amazon", ParentId = "marketing", LeaderAgentId = "amazon-lead" },
|
||||
new UnitConfig { Id = "accounting", Name = "Accounting" }
|
||||
],
|
||||
[
|
||||
new AgentConfig { AgentId = "ceo" },
|
||||
new AgentConfig { AgentId = "marketing-lead", UnitId = "marketing" },
|
||||
new AgentConfig { AgentId = "amazon-lead", UnitId = "amazon" },
|
||||
new AgentConfig { AgentId = "ads", UnitId = "amazon" },
|
||||
new AgentConfig { AgentId = "bookkeeper", UnitId = "accounting" }
|
||||
]);
|
||||
|
||||
private static TeamMember Member(string id, string? unit, TeamState state, int approvals = 0)
|
||||
=> new(id, id, "m", state, [], approvals, null, null, null, 0, unit);
|
||||
|
||||
[Fact]
|
||||
public void A_problem_deep_down_shows_on_every_container_above_it()
|
||||
{
|
||||
var units = TeamOverview.RollUp(Shop(),
|
||||
[
|
||||
Member("ceo", null, TeamState.Working),
|
||||
Member("marketing-lead", "marketing", TeamState.Working),
|
||||
Member("amazon-lead", "amazon", TeamState.Idle),
|
||||
Member("ads", "amazon", TeamState.Problem),
|
||||
Member("bookkeeper", "accounting", TeamState.Idle)
|
||||
]);
|
||||
|
||||
var marketing = units.Single(u => u.Id == "marketing");
|
||||
marketing.Agents.ShouldBe(3, "its own lead and the two in amazon below");
|
||||
marketing.Worst.ShouldBe(TeamState.Problem);
|
||||
marketing.States[TeamState.Working].ShouldBe(1);
|
||||
marketing.States[TeamState.Problem].ShouldBe(1);
|
||||
|
||||
units.Single(u => u.Id == "amazon").Worst.ShouldBe(TeamState.Problem);
|
||||
units.Single(u => u.Id == "accounting").Worst.ShouldBe(TeamState.Idle);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Open_approvals_are_added_up_along_the_tree()
|
||||
{
|
||||
var units = TeamOverview.RollUp(Shop(),
|
||||
[
|
||||
Member("marketing-lead", "marketing", TeamState.WaitingForApproval, approvals: 1),
|
||||
Member("ads", "amazon", TeamState.WaitingForApproval, approvals: 2)
|
||||
]);
|
||||
|
||||
units.Single(u => u.Id == "marketing").PendingApprovals.ShouldBe(3);
|
||||
units.Single(u => u.Id == "amazon").PendingApprovals.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_unit_knows_its_path_and_its_lead()
|
||||
{
|
||||
var amazon = TeamOverview.RollUp(Shop(), []).Single(u => u.Id == "amazon");
|
||||
|
||||
amazon.Path.ShouldBe("Marketing / Amazon");
|
||||
amazon.LeaderAgentId.ShouldBe("amazon-lead");
|
||||
amazon.Agents.ShouldBe(0);
|
||||
amazon.Worst.ShouldBe(TeamState.Idle);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stuck_is_the_most_urgent_and_idle_the_least()
|
||||
{
|
||||
var byUrgency = Enum.GetValues<TeamState>().OrderByDescending(TeamOverview.Urgency).ToList();
|
||||
|
||||
byUrgency.First().ShouldBe(TeamState.Stuck);
|
||||
byUrgency.Last().ShouldBe(TeamState.Idle);
|
||||
TeamOverview.Urgency(TeamState.Problem).ShouldBeGreaterThan(TeamOverview.Urgency(TeamState.WaitingForApproval));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Without_units_there_is_nothing_to_roll_up()
|
||||
=> TeamOverview.RollUp(new OrgChart([], [new AgentConfig { AgentId = "a" }]), [Member("a", null, TeamState.Problem)])
|
||||
.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ public sealed class AuditRepositoryTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||
try { Directory.Delete(_directory, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ public sealed class BackupServiceTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_root);
|
||||
try { Directory.Delete(_root, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
}
|
||||
@@ -95,7 +95,7 @@ public sealed class BackupServiceTests : IDisposable
|
||||
}, default);
|
||||
|
||||
// Verbindungen schließen, damit VACUUM INTO auf eine ruhige Datei trifft.
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_instanceDir);
|
||||
}
|
||||
|
||||
private string ZipPath => Path.Combine(_root, "sicherung.zip");
|
||||
|
||||
@@ -40,7 +40,7 @@ public sealed class LicenseBackupTests : IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
SecretKeyStore.UseDirectory(null);
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_root);
|
||||
try { Directory.Delete(_root, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public sealed class BudgetGuardTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||
try { Directory.Delete(_directory, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
}
|
||||
@@ -220,7 +220,7 @@ public sealed class UsageRepositoryTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||
try { Directory.Delete(_directory, recursive: true); }
|
||||
catch { }
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ public sealed class MemoryRepositoryTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||
try { Directory.Delete(_directory, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ public sealed class StagingRepositoryTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||
try { Directory.Delete(_directory, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ public sealed class StagingServiceTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||
try { Directory.Delete(_directory, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ public sealed class WerkzeugrechteTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||
try { Directory.Delete(_directory, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
using ClawdDotNet.Core.Storage;
|
||||
using Shouldly;
|
||||
|
||||
namespace ClawdDotNet.Core.Tests.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// Releasing the pool of one database must leave every other database alone. The tests used
|
||||
/// the process-wide ClearAllPools to clean up, and a test running in parallel lost its
|
||||
/// connection in the middle of opening it — one red run in every few, always another test.
|
||||
/// </summary>
|
||||
public sealed class PoolReleaseTests : IDisposable
|
||||
{
|
||||
private readonly string _root = Path.Combine(Path.GetTempPath(), "clawd-tests", "pool-" + Guid.NewGuid().ToString("N")[..8]);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SqliteStorage.ReleasePooledConnections(_root);
|
||||
try { Directory.Delete(_root, recursive: true); }
|
||||
catch { /* cleaning up is secondary */ }
|
||||
}
|
||||
|
||||
private static async Task TouchAsync(SqliteStorage storage)
|
||||
=> await storage.WriteAsync(async connection =>
|
||||
{
|
||||
await using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "INSERT OR REPLACE INTO ToolState (Key, Value) VALUES ('k', 'v')";
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}, default);
|
||||
|
||||
[Fact]
|
||||
public async Task Releasing_one_directory_frees_its_files_and_nothing_else()
|
||||
{
|
||||
var first = new SqliteStorage(Path.Combine(_root, "a", "state.db"));
|
||||
var second = new SqliteStorage(Path.Combine(_root, "b", "state.db"));
|
||||
await TouchAsync(first);
|
||||
await TouchAsync(second);
|
||||
|
||||
SqliteStorage.ReleasePooledConnections(Path.Combine(_root, "a"));
|
||||
|
||||
// The released database can go — on Windows an open handle would prevent that.
|
||||
Should.NotThrow(() => Directory.Delete(Path.Combine(_root, "a"), recursive: true));
|
||||
|
||||
// The other one keeps working, and so does its pool.
|
||||
await TouchAsync(second);
|
||||
await using var connection = await second.OpenConnectionAsync(default);
|
||||
await using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "SELECT Value FROM ToolState WHERE Key = 'k'";
|
||||
(await cmd.ExecuteScalarAsync()).ShouldBe("v");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_database_file_can_be_named_directly()
|
||||
{
|
||||
var path = Path.Combine(_root, "c", "state.db");
|
||||
await TouchAsync(new SqliteStorage(path));
|
||||
|
||||
SqliteStorage.ReleasePooledConnections(path);
|
||||
|
||||
Should.NotThrow(() => File.Delete(path));
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public sealed class BudgetMissTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||
try { Directory.Delete(_directory, recursive: true); }
|
||||
catch { /* cleanup is secondary */ }
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ public sealed class CoordinationMigrationTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||
try { Directory.Delete(_directory, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public sealed class ScannerCapacityTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||
try { Directory.Delete(_directory, recursive: true); }
|
||||
catch { /* cleanup is secondary */ }
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public sealed class TaskRepositoryTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||
try { Directory.Delete(_directory, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public sealed class TaskScannerTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||
try { Directory.Delete(_directory, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class TaskboardServiceTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||
try { Directory.Delete(_directory, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public sealed class PromptHistoryTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||
try { Directory.Delete(_directory, recursive: true); }
|
||||
catch { /* cleanup is best effort */ }
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public sealed class ManagerBackupsTests : IAsyncDisposable
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_root);
|
||||
try { Directory.Delete(_root, recursive: true); }
|
||||
catch { /* Ein liegengebliebener Testordner faerbt keinen Lauf rot. */ }
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class MemoryToolTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_dir);
|
||||
try { Directory.Delete(_dir, recursive: true); }
|
||||
catch { /* Aufraeumen ist Nebensache */ }
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public sealed class AssignmentReachTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||
try { Directory.Delete(_directory, recursive: true); }
|
||||
catch { /* cleanup is secondary */ }
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ public sealed class DublettenTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_verzeichnis);
|
||||
try { Directory.Delete(_verzeichnis, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user