feat(orchestrator): S5.1 radio spot, compact menus, trend, ETA and Tasks 3D
🧪 Test / 🧪 Lock + lint (push) Canceled after 0s
🧪 Test / 🧪 Lock + lint (push) Canceled after 0s
Requirement 001 decisions 52 to 58 (operator 2026-11-09): - Radio spot: the Agents toolbar radio button expands inline with the one composer (every channel, post and call recipients, Record, Send), a gear with the radio settings (tones, squelch, read aloud, moved out of the general settings) and Cancel mayday, which leaves the toolbar; the Radio tab keeps speakers, stations and the log plus a jump to the spot. - Day/Night menu: the badge and Drain, Suspend and Resume only; each explanation is the button's tooltip. - Tasks toolbar: views, filter, estimation, sort and business value in one wrapping row with a Request choices dropdown; the status chips stay the second row. - Burndown and burnup: a trend selector (off, linear regression, velocity over N days) stored in the hub settings, the trend lines and the forecast date on the chart, and a tooltip per point with the work packages that started, ended or joined there. - ETA: every list row shows its ETA (end when done, else start or now plus the forecast length behind its predecessors), sharing one predecessor walk with the Gantt. - Tasks 3D: a sixth view on the 3D engine with the dependency edges in the Gantt colours, layouts Force and Time. - Steady 3D camera: the view fits once per graph build, refreshes keep the operator's camera. - e2e: Firefox runs with its profile as cwd, so its crashreporter no longer drops core dumps into the repository. - A forecast beyond the last second a Date can hold is no forecast, so a nearly flat regression no longer aborts the Tasks render. Evidence: make test Ran 717 OK, ruff clean, markdownlint 0 errors; e2e 30 of 30 OK in Chromium and Firefox at 1920, 1366 and 390 px; MEDUSA 0 findings after one skeptic-accepted lexical EXA006. 🎯 Fix: 93 % 💥 Break: 4 % 💸 Cost: 100 % ⏱️ Time: – 🛡️ Security: 100 % 🏆 Score: 89 🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e94f2800d5
commit
57ab29eb0d
25 files changed
+1390
-215
No files matched your search
@@ -150,6 +150,23 @@ class TestHubSettings(HubCase):
|
||||
self.assertEqual(status, 200, body)
|
||||
self.assertEqual(json.loads(body)["settings"]["speech"], {"on": False, "classes": ["broadcast", "mayday", "security"], "muted": ["Operator", "name:Docs", "s1"]})
|
||||
|
||||
def test_the_radio_gear_stores_tones_squelch_and_read_aloud_without_a_passkey_and_keeps_every_other_setting(self):
|
||||
current = self.snapshot()["settings"]
|
||||
gear = {
|
||||
**current, "sounds": {**current["sounds"], "radio": {"on": False, "volume": 0.4}}, "squelch": 0.35,
|
||||
"speech": {"on": True, "classes": ["post", "mayday"], "muted": ["s1"]},
|
||||
}
|
||||
self.assertIsNone(self.prepared(gear)["payload"])
|
||||
status, _, body = self.post_json("/api/settings", {**gear, "proof": None}, self.guard(self.host))
|
||||
self.assertEqual(status, 200, body)
|
||||
self.assertEqual(json.loads(body)["settings"], gear)
|
||||
self.assertEqual(settings.Settings(self.state / "settings.json").get(), gear)
|
||||
self.assertEqual(self.snapshot()["settings"], gear)
|
||||
for broken in ({**gear, "squelch": 1.5}, {**gear, "sounds": {**gear["sounds"], "radio": {"on": "no", "volume": 0.4}}}, {**gear, "speech": {**gear["speech"], "classes": ["post"]}}):
|
||||
with self.subTest(broken=broken):
|
||||
self.assertEqual(self.post_json("/api/settings", broken, self.guard(self.host))[0], 400)
|
||||
self.assertEqual(self.snapshot()["settings"], gear)
|
||||
|
||||
def test_the_estimation_mode_defaults_to_no_estimate_is_stored_and_survives_a_reload(self):
|
||||
self.assertEqual((settings.SETTINGS["estimation"], self.snapshot()["settings"]["estimation"]), ("none", "none"))
|
||||
good = self.guard(self.host)
|
||||
@@ -166,6 +183,29 @@ class TestHubSettings(HubCase):
|
||||
(self.state / "settings.json").write_text(json.dumps(older), encoding="utf-8")
|
||||
self.assertEqual(settings.Settings(self.state / "settings.json").get()["estimation"], "none")
|
||||
|
||||
def test_the_trend_defaults_to_off_over_seven_days_is_validated_stored_and_survives_a_reload(self):
|
||||
self.assertEqual((settings.SETTINGS["trend"], self.snapshot()["settings"]["trend"]), ({"mode": "off", "days": 7}, {"mode": "off", "days": 7}))
|
||||
good = self.guard(self.host)
|
||||
wrongs = [{"mode": mode, "days": 7} for mode in ("linear", "Regression", "", None, 1)]
|
||||
wrongs += [{"mode": "velocity", "days": days} for days in (0, 366, 1.5, True, "7", None, -3)]
|
||||
wrongs += [{"mode": "velocity"}, {"days": 7}, {"mode": "velocity", "days": 7, "extra": 1}, "velocity", ["velocity", 7]]
|
||||
for wrong in wrongs:
|
||||
with self.subTest(wrong=wrong):
|
||||
self.assertEqual(self.post_json("/api/settings", {**settings.SETTINGS, "trend": wrong}, good)[0], 400)
|
||||
self.assertEqual(self.post_json("/api/settings", {key: value for key, value in settings.SETTINGS.items() if key != "trend"}, good)[0], 400)
|
||||
self.assertEqual(self.snapshot()["settings"]["trend"], {"mode": "off", "days": 7})
|
||||
for chosen in ({"mode": "regression", "days": 7}, {"mode": "velocity", "days": 1}, {"mode": "velocity", "days": 365}, {"mode": "velocity", "days": 14}):
|
||||
with self.subTest(chosen=chosen):
|
||||
self.assertIsNone(self.prepared({**self.snapshot()["settings"], "trend": chosen})["payload"])
|
||||
status, _, body = self.post_json("/api/settings", {**self.snapshot()["settings"], "trend": chosen, "proof": None}, good)
|
||||
self.assertEqual((status, json.loads(body)["settings"]["trend"]), (200, chosen))
|
||||
self.assertEqual(json.loads((self.state / "settings.json").read_text(encoding="utf-8"))["trend"], {"mode": "velocity", "days": 14})
|
||||
self.assertEqual(settings.Settings(self.state / "settings.json").get()["trend"], {"mode": "velocity", "days": 14})
|
||||
self.assertEqual(self.snapshot()["settings"]["trend"], {"mode": "velocity", "days": 14})
|
||||
older = {key: value for key, value in self.settings_payload(STREAMS).items() if key != "trend"}
|
||||
(self.state / "settings.json").write_text(json.dumps(older), encoding="utf-8")
|
||||
self.assertEqual(settings.Settings(self.state / "settings.json").get()["trend"], {"mode": "off", "days": 7})
|
||||
|
||||
def test_settings_written_before_the_radio_filter_keep_their_values(self):
|
||||
older = {key: value for key, value in self.settings_payload(STREAMS).items() if key != "speech"}
|
||||
(self.state / "settings.json").write_text(json.dumps(older), encoding="utf-8")
|
||||
|
||||
Reference in new issue
Block a user