fix(orchestrator): S1 fit, rename once, Deutschlandfunk e2e, squelch, tree cause

Requirement 001 phase 6, S1:

- Fit: Agents and Artifacts in a graph view fill the window from 861 px
  up (body.fit flex column); the e2e walk probes #agents/orbit at every
  width for a page no taller than the window.
- Rename once: a session seen under two names by one source (two claims)
  flipped its name on every state build, so the Stream logged a rename
  each poll. Names.observe keeps only the newest sighting per session
  and source, and the timeline emits each (session, before, after) pair
  once.
- Deutschlandfunk: root cause was the signed media-src, which never
  listed the redirect target f111.rndfnk.com that st01.sslstream.dlf.de
  answers with (fixed in ba97f7c). The e2e now replays that chain, a
  302 from one https origin to a second, in Chromium and Firefox, with
  self-signed throwaway certificates the drivers accept.
- Squelch: a settings slider 0..1 scales every tone; at 0 no beep plays,
  music on or off (Voice.beep skips a silent tone). Stored settings
  without the key load with 1.0.
- Tree: the missing tree file error names `pkgmgr branch tree` and the
  release and install step that makes the timer write it.

Evidence: make test 655 OK, ruff and markdownlint clean
(/tmp/s1-make-test-1.log); e2e 17 OK, none skipped (/tmp/s1-e2e-3.log);
MEDUSA 14 files, 0 findings.

🎯 Fix: 94 %
💥 Break: 4 %
💸 Cost: 100 %
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 90
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-08 13:37:02 +00:00
1 parent 86b55dbc91
commit a388c6ef77
15 files changed
+148 -26

No files matched your search

+18 -2
View File
@@ -65,9 +65,14 @@ class Directory:
def renames(self, begin: float, end: float) -> list[dict]:
found = []
shown = set()
for session, own in self.by_session.items():
for before, after in itertools.pairwise(own):
if before["name"] != after["name"] and begin <= after["first_seen"] <= end:
pair = (session, before["name"], after["name"])
if before["name"] == after["name"] or pair in shown:
continue
shown.add(pair)
if begin <= after["first_seen"] <= end:
found.append(event({
"id": f"rename:{session}:{after['first_seen']}", "ts": after["first_seen"], "kind": "rename", "agent": after["name"],
"now": self.now(session), "session": session, "title": f"{before['name']} heißt jetzt {after['name']}",
@@ -89,9 +94,20 @@ class Names:
self.observed: dict[str, str] = observed if isinstance(observed, dict) else {}
def observe(self, sightings: list[tuple[str, str, float | None, str]], now: float) -> None:
"""Args:
sightings: (session, name, seen, source); of several sightings of
one session from one source only the newest counts, so two
claims under different names cannot flip the name back and
forth on every state build.
"""
newest: dict[tuple[str, str], tuple[str, str, float | None, str]] = {}
for sighting in sightings:
key = (sighting[0], sighting[3])
if key not in newest or (sighting[2] or -math.inf) >= (newest[key][2] or -math.inf):
newest[key] = sighting
with self.lock:
changed = False
for session, name, seen, source in sightings:
for session, name, seen, source in newest.values():
key = f"{session}\x00{source}"
if not session or not name or self.observed.get(key) == name:
continue
@@ -36,6 +36,7 @@ SETTINGS = {
"direct": {"on": True, "volume": 0.5},
"radio": {"on": False, "volume": 0.25},
},
"squelch": 1.0,
"notify": True,
"streams": [],
"speech": {"on": False, "classes": ["mayday", "pan-pan", "security"], "muted": []},
@@ -195,6 +196,7 @@ def valid_settings(raw: object) -> dict:
"colors": {key: color(colors[key], f"colors.{key}") for key in SETTINGS["colors"]},
"music": {"on": flag(music["on"], "music.on"), "volume": number_in(music["volume"], "music.volume", 0.0, 1.0)},
"sounds": {kind_name: tone_setting(sounds[kind_name], kind_name) for kind_name in TONES},
"squelch": number_in(data["squelch"], "squelch", 0.0, 1.0),
"notify": flag(data["notify"], "notify"),
"streams": [stream_entry(entry, index) for index, entry in enumerate(data["streams"])],
"speech": speech_setting(data["speech"]),
+4 -1
View File
@@ -191,7 +191,10 @@ class Tree:
try:
info = self.file.stat()
except OSError as error:
return {"repos": [], "scanned": None, "error": f"no pkgmgr tree file at {self.file}: {error.strerror}"}
return {"repos": [], "scanned": None, "error": (
f"no pkgmgr tree file at {self.file}: {error.strerror}. The tree timer runs `pkgmgr branch tree`, which the installed pkgmgr "
"lacks until a release ships it: release pkgmgr and install it, then the timer writes the file within a minute."
)}
stamp = (info.st_mtime_ns, info.st_size)
if self.cached is None or stamp != self.stamp:
self.cached, self.stamp = read_tree(self.file), stamp
+8
View File
@@ -209,6 +209,14 @@ table.diff tr.more td { padding: 8px 16px; color: var(--muted); font-style: ital
.graph-body { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 14px; align-items: start; }
.stage-card { padding: 8px; }
.stage svg { display: block; width: 100%; height: auto; max-height: calc(100vh - var(--head) - 120px); }
@media (min-width: 861px) {
body.fit { display: flex; flex-direction: column; height: 100vh; }
body.fit .shell, body.fit main, body.fit .graph, body.fit .stage-card, body.fit .stage { display: flex; flex: 1; flex-direction: column; min-height: 0; }
body.fit main { padding-bottom: var(--sp-5); }
body.fit .graph-body { flex: 1; align-items: stretch; min-height: 0; }
body.fit .stage svg, body.fit .stage canvas.space, body.fit .graph3d { flex: 1; height: auto; min-height: 0; max-height: none; }
body.fit .panel { position: static; max-height: none; min-height: 0; overflow: auto; }
}
.legend { display: flex; flex-wrap: wrap; gap: 6px 16px; padding: 6px 10px 4px; color: var(--muted); font-size: var(--fs-sm); }
.legend span { display: inline-flex; align-items: center; gap: 6px; }
.swatch { display: inline-block; width: 18px; height: 4px; border-radius: 2px; background: var(--c); }
+1 -1
View File
@@ -47,7 +47,7 @@
const newRadio = state.radio.filter((row) => !known.radio.has(`${row.ts}|${row.from}|${row.text.slice(0, 40)}`));
const direct = newRadio.filter((row) => row.to.includes(HUMAN));
const present = {mayday: newAlarms.length > 0, review: fresh.some((item) => item.kind !== "operator"), direct: direct.length > 0, radio: newRadio.length > 0};
const kind = SOUND_KINDS.map(([id]) => id).find((id) => present[id] && settings.sounds[id].on);
const kind = Voice.beep(present, settings, SOUND_KINDS.map(([id]) => id));
if (kind) {
play(kind, loudness(kind));
}
+1
View File
@@ -70,6 +70,7 @@
const main = document.getElementById("main");
main.setAttribute("aria-labelledby", `tab-${ui.screen}`);
main.replaceChildren(screenOf());
document.body.classList.toggle("fit", ["agents", "artifacts"].includes(ui.screen) && !["tree", "hierarchy"].includes(ui.views[ui.screen]));
drawn.screen = ui.screen;
drawn.sig = sig;
if (g3.host && !g3.host.isConnected) {
+3 -1
View File
@@ -111,6 +111,7 @@
const loudest = () => Math.max(...tones.map((tone) => Number(tone.level.value)), Number(volume.value));
return {id, on, level, row: h("div", {class: "sound-row"}, h("strong", {}, label), h("label", {class: "check"}, on, "an"), level, btn("Test", "btn small ghost", () => play(id, id === "mayday" ? loudest() : Number(level.value))))};
});
const squelch = h("input", {type: "range", min: 0, max: 1, step: 0.05, value: settings.squelch, "aria-label": "Squelch"});
const notify = h("input", {type: "checkbox", checked: settings.notify});
const speechOn = h("input", {type: "checkbox", checked: settings.speech.on, disabled: !state.speech.tts});
const spoken = SPEECH_CLASSES.map(([id, label]) => ({id, label, box: h("input", {type: "checkbox", checked: id === "mayday" || settings.speech.classes.includes(id), disabled: id === "mayday", title: id === "mayday" ? "MAYDAY wird immer vorgelesen" : null})}));
@@ -130,7 +131,7 @@
h("section", {}, h("h3", {}, "Musik"), h("p", {class: "muted"}, "Generative Ambient-Musik, offline erzeugt: ruhig bei leerer Inbox, etwas gespannter, wenn etwas wartet, leiser unter Meldungen."),
h("div", {class: "grid"}, h("label", {class: "check"}, musicOn, "Musik an"), h("label", {}, "Gesamtlautstärke (Musik und Radio)", volume))),
h("section", {}, h("h3", {}, "Töne und Benachrichtigungen"), h("p", {class: "muted"}, "MAYDAY und PAN-PAN sind immer am lautesten. Bei mehreren neuen Ereignissen spielt nur das dringendste."),
tones.map((tone) => tone.row), h("label", {class: "check"}, notify, "Desktop-Benachrichtigungen zeigen, wenn der Browser sie erlaubt")),
tones.map((tone) => tone.row), h("label", {}, "Squelch: scales every tone, 0 plays none", squelch), h("label", {class: "check"}, notify, "Desktop-Benachrichtigungen zeigen, wenn der Browser sie erlaubt")),
h("section", {}, h("h3", {}, "Funk vorlesen"),
h("p", {class: "muted"}, state.speech.tts
? "Neuer Funk wird in Seefunk-Sprechweise (SMCP) vorgelesen, eine Meldung nach der anderen, jede Station mit eigener Stimme; Musik und Radio werden solange leiser. Die Lautstärke folgt den Tönen oben, MAYDAY wird immer und am lautesten gesprochen."
@@ -152,6 +153,7 @@
colors: Object.fromEntries(COLOR_FIELDS.map(([id]) => [id, colors[id].value])),
music: {on: musicOn.checked, volume: Number(volume.value)},
sounds: Object.fromEntries(tones.map((tone) => [tone.id, {on: tone.on.checked, volume: Number(tone.level.value)}])),
squelch: Number(squelch.value),
notify: notify.checked,
streams: rows.map((entry) => entry.read()),
speech: {
+7 -5
View File
@@ -12,10 +12,12 @@ const Voice = (() => {
}
function loudness(tone, settings) {
if (tone !== "mayday") {
return settings.sounds[tone].volume;
}
return Math.max(...Object.values(settings.sounds).map((sound) => sound.volume), settings.music.volume);
const level = tone === "mayday" ? Math.max(...Object.values(settings.sounds).map((sound) => sound.volume), settings.music.volume) : settings.sounds[tone].volume;
return level * settings.squelch;
}
function beep(present, settings, kinds) {
return kinds.find((kind) => present[kind] && settings.sounds[kind].on && loudness(kind, settings) > 0) || null;
}
function speakable(row, speech) {
@@ -64,7 +66,7 @@ const Voice = (() => {
return buffer;
}
return {toneOf, loudness, speakable, enqueue, frames, wav};
return {toneOf, loudness, beep, speakable, enqueue, frames, wav};
})();
if (typeof module === "object" && module.exports) {
module.exports = Voice;