The Agents views gain a Hierarchie view: an org chart of the chair and the line offices read from the council claims. The operator appoints, hands over and recalls an office there; the page signs each change with the passkey and a host unit (client/offices.py, offices path unit) verifies the signature, removes a live holder itself and runs a plain council.py claim for the successor. council.py loses the --operator flag, so no agent can override a live holder. The agent order is now separate from task priority: rasci.py agents order stores it in agent-order.json beside the register through the nofollow helpers and never writes a --prio pin. It only breaks ties of equal task rank, also among the queue grants. The Hierarchie view edits it as a drag list. Host units run the installed skill scripts, so orchestrator.sh keeps the offices unit off while the installed council.py, nofollow.py or rasci.py differ from the tree. Job files, council files and hub records are read with O_NOFOLLOW and O_NONBLOCK and size caps; nofollow.rewrite replaces a claim without creating directories, so a beat never revives a released office. 🎯 Fix: 92 % 💥 Break: 4 % 💸 Cost: 100 % ⏱️ Time: 79 % 🛡️ Security: 100 % 🏆 Score: 100 🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
197 lines
11 KiB
JavaScript
197 lines
11 KiB
JavaScript
function orbit() {
|
||
const nodes = state.graph.nodes;
|
||
positions = layout(nodes);
|
||
const svg = chartFrame(400, "Orbit: Operator in der Mitte; wer zusammen arbeitet oder funkt, rückt zusammen, Stille driften nach außen");
|
||
svg.append(...RINGS.map((r) => s("circle", {class: "ring", r})));
|
||
const edgeLayer = s("g", {});
|
||
const halos = new Map();
|
||
for (const link of aggregate()) {
|
||
const from = positions.get(link.source);
|
||
const shade = tone(link.cls);
|
||
const words = `${link.count}× ${CLASSES[link.cls] || link.cls}`;
|
||
if (!from) {
|
||
continue;
|
||
}
|
||
if (link.target === "*") {
|
||
const index = halos.get(link.source) || 0;
|
||
halos.set(link.source, index + 1);
|
||
edgeLayer.append(s("circle", {class: `halo t-${shade}`, cx: fixed(from.x), cy: fixed(from.y), r: radiusOf(link.source) + 7 + index * 7, "stroke-width": width(link.count)},
|
||
s("title", {}, `${link.source} an alle: ${words}`)));
|
||
continue;
|
||
}
|
||
const to = positions.get(link.target);
|
||
if (!to || to === from) {
|
||
continue;
|
||
}
|
||
const control = Space.bend(from, to);
|
||
const start = toward(from, control, radiusOf(link.source) + 2);
|
||
const end = toward(to, control, radiusOf(link.target) + 4);
|
||
edgeLayer.append(s("path", {class: `edge t-${shade}`, d: `M${fixed(start.x)},${fixed(start.y)} Q${fixed(control.x)},${fixed(control.y)} ${fixed(end.x)},${fixed(end.y)}`, "stroke-width": fixed(width(link.count)), "marker-end": `url(#tip-${shade})`},
|
||
s("title", {}, `${link.source} → ${link.target}: ${words}`)));
|
||
}
|
||
const nodeLayer = s("g", {}, nodes.map((node) => nodeMark(node, positions.get(node.id), node.kind === "operator" || node.kind === "subagent" ? null : RADIUS[node.kind] + 9)));
|
||
const pulseLayer = s("g", {});
|
||
svg.append(edgeLayer, nodeLayer, pulseLayer);
|
||
graphDom.pulses = pulseLayer;
|
||
return svg;
|
||
}
|
||
|
||
function chord() {
|
||
const nodes = state.graph.nodes.filter((node) => node.kind !== "subagent");
|
||
const fresh = targets();
|
||
const radius = 250;
|
||
const step = (2 * Math.PI) / Math.max(nodes.length, 1);
|
||
const spots = new Map(nodes.map((node) => {
|
||
const angle = spotOf(node.id, fresh).chord;
|
||
return [node.id, {x: radius * Math.cos(angle), y: radius * Math.sin(angle), angle}];
|
||
}));
|
||
const svg = chartFrame(450, "Chord: Nachrichten zwischen Agenten, Reihenfolge nach Nähe");
|
||
const arcs = s("g", {});
|
||
const point = (angle, r) => `${fixed(r * Math.cos(angle))},${fixed(r * Math.sin(angle))}`;
|
||
for (const node of nodes) {
|
||
const at = spots.get(node.id);
|
||
const half = Math.min(step * 0.36, 0.5);
|
||
arcs.append(s("path", {class: `group-arc k-${node.kind}`, d: `M${point(at.angle - half, radius + 14)} A${radius + 14},${radius + 14} 0 0 1 ${point(at.angle + half, radius + 14)}`}));
|
||
}
|
||
const pairs = new Map();
|
||
const fans = new Map();
|
||
for (const link of aggregate()) {
|
||
const shade = tone(link.cls);
|
||
if (!spots.has(link.source)) {
|
||
continue;
|
||
}
|
||
if (link.target === "*") {
|
||
const key = `${link.source}\u0000${shade}`;
|
||
fans.set(key, {source: link.source, shade, count: (fans.has(key) ? fans.get(key).count : 0) + link.count});
|
||
continue;
|
||
}
|
||
if (!spots.has(link.target) || link.target === link.source) {
|
||
continue;
|
||
}
|
||
const [a, b] = [link.source, link.target].sort();
|
||
const key = `${a}\u0000${b}\u0000${shade}`;
|
||
pairs.set(key, {a, b, shade, count: (pairs.has(key) ? pairs.get(key).count : 0) + link.count});
|
||
}
|
||
const lanes = LEGEND.map(([name]) => name);
|
||
for (const pair of pairs.values()) {
|
||
const from = spots.get(pair.a);
|
||
const to = spots.get(pair.b);
|
||
const pull = 0.06 + 0.12 * lanes.indexOf(pair.shade);
|
||
const control = {x: ((from.x + to.x) / 2) * pull, y: ((from.y + to.y) / 2) * pull};
|
||
const start = toward(from, control, RADIUS.session + 2);
|
||
const end = toward(to, control, RADIUS.session + 2);
|
||
arcs.append(s("path", {class: `edge t-${pair.shade}`, d: `M${fixed(start.x)},${fixed(start.y)} Q${fixed(control.x)},${fixed(control.y)} ${fixed(end.x)},${fixed(end.y)}`, "stroke-width": fixed(width(pair.count))},
|
||
s("title", {}, `${pair.a} ↔ ${pair.b}: ${pair.count}× ${LEGEND.find(([name]) => name === pair.shade)[1]}`)));
|
||
}
|
||
const offsets = new Map();
|
||
for (const fan of fans.values()) {
|
||
const at = spots.get(fan.source);
|
||
const index = offsets.get(fan.source) || 0;
|
||
offsets.set(fan.source, index + 1);
|
||
const angle = at.angle + (index - 1) * 0.05;
|
||
const length = Math.min(8 + 6 * Math.log2(1 + fan.count), 40);
|
||
arcs.append(s("line", {class: `tick t-${fan.shade}`, x1: fixed((radius + 22) * Math.cos(angle)), y1: fixed((radius + 22) * Math.sin(angle)), x2: fixed((radius + 22 + length) * Math.cos(angle)), y2: fixed((radius + 22 + length) * Math.sin(angle)), "stroke-width": 4},
|
||
s("title", {}, `${fan.source} an alle: ${fan.count}×`)));
|
||
}
|
||
svg.append(arcs, s("g", {}, nodes.map((node) => nodeMark(node, spots.get(node.id), 74))));
|
||
return svg;
|
||
}
|
||
|
||
function swimlane() {
|
||
const rank = new Map([HUMAN, ...state.graph.layout.order].map((id, index) => [id, index]));
|
||
const lanes = state.graph.nodes.filter((node) => node.kind !== "subagent").sort((a, b) => (rank.has(a.id) ? rank.get(a.id) : 1e6) - (rank.has(b.id) ? rank.get(b.id) : 1e6));
|
||
const begin = state.since;
|
||
const end = Math.max(state.until, state.since + 1);
|
||
const left = 160;
|
||
const right = 24;
|
||
const top = 40;
|
||
const lane = 36;
|
||
const full = 1120;
|
||
const height = top + lanes.length * lane + 12;
|
||
const x = (ts) => left + ((ts - begin) / (end - begin)) * (full - left - right);
|
||
const y = new Map(lanes.map((node, index) => [node.id, top + index * lane + lane / 2]));
|
||
const svg = s("svg", {viewBox: `0 0 ${full} ${height}`, class: "chart", role: "img", "aria-label": "Swimlane: Nachrichten über die Zeit"}, defs());
|
||
lanes.forEach((node, index) => {
|
||
svg.append(s("rect", {class: index % 2 ? "lane odd" : "lane", x: 0, y: top + index * lane, width: full, height: lane}));
|
||
svg.append(s("text", {class: "lane-label", x: 14, y: top + index * lane + lane / 2, dy: "0.35em", "text-anchor": "start", on: {click: () => select(node.id)}}, node.label));
|
||
});
|
||
for (let index = 0; index <= 6; index += 1) {
|
||
const ts = begin + ((end - begin) * index) / 6;
|
||
svg.append(s("line", {class: "grid", x1: fixed(x(ts)), x2: fixed(x(ts)), y1: top - 6, y2: height - 8}));
|
||
svg.append(s("text", {class: "axis", x: fixed(x(ts)), y: top - 14, "text-anchor": index === 0 ? "start" : index === 6 ? "end" : "middle"}, (end - begin > DAY ? absolute : clock).format(stampOf(ts))));
|
||
}
|
||
if (live()) {
|
||
svg.append(s("line", {class: "now", x1: fixed(x(end)), x2: fixed(x(end)), y1: top - 6, y2: height - 8}));
|
||
}
|
||
const marks = s("g", {});
|
||
const arrows = (cls, ts, from, to, words) => {
|
||
const x0 = fixed(x(ts));
|
||
const y0 = y.get(from);
|
||
if (y0 === undefined) {
|
||
return;
|
||
}
|
||
const shade = tone(cls);
|
||
const hint = s("title", {}, `${clock.format(stampOf(ts))} · ${dash(labelOf(from))} → ${to.length ? to.map(labelOf).join(", ") : "alle"}\n${words}`);
|
||
if (!to.length) {
|
||
const rays = [[-6, -13], [0, -15], [6, -13], [-6, 13], [0, 15], [6, 13]].map(([dx, dy]) => `M${x0},${y0} l${dx},${dy}`).join(" ");
|
||
marks.append(s("g", {class: `mark t-${shade}`}, hint, s("path", {d: rays}), s("circle", {cx: x0, cy: y0, r: 3.5})));
|
||
return;
|
||
}
|
||
const bundle = s("g", {class: `mark t-${shade}`}, hint, s("circle", {cx: x0, cy: y0, r: 3}));
|
||
for (const target of to) {
|
||
const y1 = y.get(target);
|
||
if (y1 !== undefined && y1 !== y0) {
|
||
bundle.append(s("line", {x1: x0, y1: y0, x2: x0, y2: y1 > y0 ? y1 - 6 : y1 + 6, "marker-end": `url(#tip-${shade})`}));
|
||
}
|
||
}
|
||
marks.append(bundle);
|
||
};
|
||
for (const row of state.radio) {
|
||
arrows(row.class, row.ts, row.source, row.targets, row.text.length > 120 ? `${row.text.slice(0, 120)}…` : row.text);
|
||
}
|
||
for (const grant of state.grants) {
|
||
arrows("grant", grant.ts, chairKey(), [grant.target], `Queue-Freigabe ${grant.kind} (${grant.id})`);
|
||
}
|
||
svg.append(marks);
|
||
return svg;
|
||
}
|
||
|
||
function legend() {
|
||
return h("div", {class: "legend"}, LEGEND.map(([name, label]) => h("span", {}, h("i", {class: `swatch t-${name}`}), label)),
|
||
h("span", {class: "muted"}, "Linienstärke = Anzahl · Nähe = Zusammenarbeit der letzten Zeit"));
|
||
}
|
||
|
||
function side() {
|
||
const node = state.graph.nodes.find((entry) => entry.id === ui.node);
|
||
if (!node) {
|
||
return [h("h2", {class: "panel-title"}, "Details"), h("p", {class: "muted"}, "Klicke auf einen Knoten: sein Panel zeigt Aufgaben, Claims, Subagenten und Verlauf.")];
|
||
}
|
||
const messages = state.radio.filter((row) => row.source === node.id || row.targets.includes(node.id)).slice(-10).reverse();
|
||
return [
|
||
h("div", {class: "panel-head"},
|
||
h("div", {}, h("h2", {class: "panel-title"}, node.label), h("span", {class: "muted"}, `${NODE_KINDS[node.kind]}${node.parent ? ` von ${labelOf(node.parent)}` : ""}`)),
|
||
iconBtn("bi-x-lg", "Schließen", "btn small ghost", () => select(node.id))),
|
||
h("h3", {}, "Letzte Nachrichten", counter(messages.length)),
|
||
messages.length ? h("ul", {class: "mini"}, messages.map((row) => h("li", {},
|
||
h("div", {class: "row"}, klass(row.class), when(row.ts)),
|
||
h("span", {class: "muted"}, `${dash(row.from)} → ${targetsOf(row)}`),
|
||
h("span", {}, row.text)))) : h("p", {class: "muted"}, "Keine Nachrichten im gewählten Zeitraum."),
|
||
];
|
||
}
|
||
|
||
function viewSwitch(screen) {
|
||
return h("div", {class: "segmented", role: "group", "aria-label": "Ansicht"}, Nav.viewsOf(screen).map(([id, name]) => h("button", {
|
||
type: "button", class: "icon-only", ...named(name), "aria-pressed": String(ui.views[screen] === id), on: {click: () => go(screen, id)},
|
||
}, icon(Nav.ICONS[id]))));
|
||
}
|
||
|
||
function agentsScreen() {
|
||
const view = ui.views.agents;
|
||
const stageBox = h("div", {class: "stage"});
|
||
const aside = h("aside", {class: "card panel", "aria-live": "polite"});
|
||
const toolbar = agentsToolbar();
|
||
graphDom = {stage: stageBox, aside, pulses: null, toolbar};
|
||
const body = view === "tree" ? spawnScreen() : view === "hierarchy" ? hierarchyScreen() : h("div", {class: "graph-body"}, h("div", {class: "card stage-card"}, stageBox, view === "3d" ? null : legend()), aside);
|
||
return h("div", {class: "graph"}, toolbar.bar, h("div", {class: "card controls"}, viewSwitch("agents"), view === "3d" ? layoutTools("agents") : null), body);
|
||
}
|