Verlorene Updates erkennen statt warten: optimistische Pruefung beim Ersetzen, Konflikte im Audit und im Lagebild (#720)

FileLedger merkt je Agent die gelesene Fassung einer Datei in gemeinsamen
Bereichen. write, copy und Downloads (FTP, RocketChat) ersetzen eine Datei nur,
wenn sie seit dem Lesen unveraendert ist; eine fremde Datei nicht ungelesen.
append bleibt ungeprueft, macht aeltere Lesestaende aber ungueltig.
Abgelehnte Schreibvorgaenge: eigener Audit-Status Conflict, Tabelle
FileConflicts, Hotspots (>= 3 in 7 Tagen) in der Teamuebersicht.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-28 10:45:46 +02:00
co-authored by Claude Opus 5.5
parent 8a7212c240
commit 79a3a7854f
21 changed files with 818 additions and 11 deletions
@@ -869,6 +869,32 @@ button.klein {
min-width: 0;
}
/* #720: files with repeated write conflicts, above the team canvas */
.team-konflikte {
border: 1px solid var(--divider);
border-left: 4px solid var(--gefahr, #c0392b);
background: var(--surface);
padding: 10px 14px;
margin-bottom: 12px;
}
.team-konflikte[hidden] {
display: none;
}
.team-konflikte p {
margin: 4px 0 6px;
}
.team-konflikte ul {
margin: 0;
padding-left: 18px;
}
.team-konflikte code {
word-break: break-all;
}
/* #712: the runtime card */
.laufzeit-feld {
display: grid;
@@ -982,5 +982,9 @@
"runtime.oauthToken": "Abo-Token",
"runtime.oauthTokenHint": "Erzeugt mit 'claude setup-token' auf einem Rechner, an dem du angemeldet bist.",
"runtime.keepStored": "hinterlegt – leer lassen zum Behalten",
"runtime.accountSaved": "Claude-Code-Konto gespeichert."
"runtime.accountSaved": "Claude-Code-Konto gespeichert.",
"team.conflicts.title": "Dateien, in denen sich Agenten gegenseitig überschreiben",
"team.conflicts.hint": "Hier wurden Schreibvorgänge abgelehnt, weil sie die Änderung eines anderen Agenten verloren hätten. Passiert das oft, sollte die Ablage anders werden: eine Datei je Agent oder Tag, Anhängen statt Ersetzen oder eine Tabelle.",
"team.conflicts.count": "{n} Konflikte in 7 Tagen, zwischen {agents}; zuletzt {time}"
}
@@ -982,5 +982,9 @@
"runtime.oauthToken": "Subscription token",
"runtime.oauthTokenHint": "Created with 'claude setup-token' on a machine where you are signed in.",
"runtime.keepStored": "stored — leave empty to keep",
"runtime.accountSaved": "Claude Code account saved."
"runtime.accountSaved": "Claude Code account saved.",
"team.conflicts.title": "Files agents keep overwriting each other in",
"team.conflicts.hint": "Writes were refused here because they would have lost another agent's change. If it keeps happening, change how the file is kept: one file per agent or day, appending instead of replacing, or a table.",
"team.conflicts.count": "{n} conflicts in 7 days, between {agents}; last {time}"
}
@@ -106,6 +106,7 @@ let matchInfo = null;
let zoomValue = null;
let panel = null;
let summary = null;
let hotspots = null;
let unsubscribe = null;
let refreshTimer = null;
@@ -151,8 +152,12 @@ export async function renderTeam() {
panel = erstelleElement("aside", { klasse: "team-seitenleiste" });
panel.hidden = true;
// #720: files agents keep overwriting each other in — hidden while there are none.
hotspots = erstelleElement("div", { klasse: "team-konflikte", role: "status" });
hotspots.hidden = true;
const area = erstelleElement("div", { klasse: "team-arbeitsflaeche" }, buildTree(), canvas, panel);
container.replaceChildren(erstelleElement("div", { klasse: "team-ansicht" }, head, area, buildLegend()));
container.replaceChildren(erstelleElement("div", { klasse: "team-ansicht" }, head, hotspots, area, buildLegend()));
if (!holeAktiveInstanz()) {
canvas.replaceChildren(erstelleElement("div", { klasse: "hinweis-zentriert", text: t("team.noInstance") }));
@@ -1068,6 +1073,28 @@ function renderSummary() {
summary.replaceChildren(
...(parts.length ? parts : [erstelleElement("span", { klasse: "gedaempft", text: t("team.empty") })])
);
renderHotspots();
}
/**
* #720: Files with repeated write conflicts in the last week. Detecting is code; what to do
* about it — split the file, append instead, make it a table — is a human's call.
*/
function renderHotspots() {
if (!hotspots || !snapshot) return;
const list = snapshot.conflictHotspots ?? [];
hotspots.hidden = list.length === 0;
if (list.length === 0) {
hotspots.replaceChildren();
return;
}
hotspots.replaceChildren(
erstelleElement("strong", { text: `⚠ ${t("team.conflicts.title")}` }),
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("team.conflicts.hint") }),
erstelleElement("ul", {},
...list.map((h) => erstelleElement("li", {},
erstelleElement("code", { text: h.path }),
` — ${t("team.conflicts.count", { n: h.conflicts, agents: h.agents.join(", "), time: formatiereDatumZeit(h.lastAtUtc) })}`))));
}
/** The card tones by the names the badges use. */