From 4d5c441e9dbc2258e151424c1db8bd210db248dc Mon Sep 17 00:00:00 2001 From: ElGregor Date: Tue, 29 Sep 2026 23:07:45 +0200 Subject: [PATCH 01/24] docs(mla): add isolated GTK scaffold and coding-agent master plan --- docs/mla-next/AGENT_PLAN.md | 40 ++++++++++++++++ prototype/gtk/mla_app.py | 92 +++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 docs/mla-next/AGENT_PLAN.md create mode 100644 prototype/gtk/mla_app.py diff --git a/docs/mla-next/AGENT_PLAN.md b/docs/mla-next/AGENT_PLAN.md new file mode 100644 index 0000000..3496da9 --- /dev/null +++ b/docs/mla-next/AGENT_PLAN.md @@ -0,0 +1,40 @@ +# MLA Next: GTK4-Prototyp und Coding-Agenten-Masterplan + +Stand 2026-09-25. Isolierter Prototyp-Track für Master Linux Assistant auf Zorin OS 18.1 GNOME. Ausgangspunkt main 92bef600; vor jeder Aufgabe neu prüfen. Kein Release, kein Flutter-Ersatzbeschluss. Paket/Binär/Policy/Datenpfade bleiben vorerst linux-assistant. Bestehende v0.8.x–v1.0-Roadmap bleibt gültig; MLA-Next 0.0.1–0.4.x ist ein getrenntes Experiment. 0.5 bleibt offen. + +## Grenzen + +- GTK-UI, Flutter-UI und Dart-Host nie als Root. Keine generische Root-IPC, Shell-Strings, Agenten-Ausführung oder Secrets in Logs/Screenshots/Commits. Vorhandene polkit-Grenze nicht umgehen. +- Dieser Commit enthält ausschließlich neue Dateien unter prototype/gtk und docs/mla-next. Demo-Daten, keine Systemänderung, keine Behauptung ausgeführter Tests. Keine Integration mit install.sh, deb oder main. +- Issue #59 ist der Headless-Spike für packages/la_core; #60 betrifft die Registry; #63 das Backup-Cockpit aus Units, Timern und Journal. Agenten müssen aktuelle Issue-Bodies lesen, bevor sie implementieren. Der rsync-Ordnerkopier-Prototyp ist ausdrücklich nicht Teil von #63. + +## Architektur + +GTK4/libadwaita ist ein unprivilegierter UI-Adapter; bestehendes Flutter bleibt Vergleichsbasis. Reiner Dart-Kern packages/la_core enthält Probe-/Parsermodelle, Registry, DI und Event-Vertrag ohne Flutter-/GTK-Typen. Deskriptor: id, titleKey, viewId, kind, capabilities, requires, probeIds, actionIds und subscribedTopics. View-Factories gehören in UI-Adapter. Registry prüft doppelte IDs/Zyklen, aktiviert Abhängigkeiten Single-Flight und stoppt rückwärts. Kein dynamisches Fremdcode-Plugin. + +Fenster: AdwApplicationWindow → AdwNavigationSplitView → Sidebar und AdwToolbarView mit HeaderBar; Hauptbereich GtkStack und kontextuelle rechte Leiste. Golden/Cream/Navy über semantische Tokens; keine Polls in unsichtbaren Ansichten. Diagramme zeigen Einheiten, Messzeit und Quelle. + +IPC erst nach isoliertem Spike: JSON-RPC 2.0 über privaten Unix-Socket im XDG_RUNTIME_DIR, hello/Capabilities, 64-KiB-Framelimit vor Parsing, Peer-UID, begrenzte Queues, Timeout, Reconnect, Snapshot nach Event-Lücke; Same-UID ist keine Sandbox. Schreibende Aktionen benötigen serverseitige Allowlist und bestätigte Nutzerauswahl. Keine Backups über Repo-Tokens oder Restic-Passwörter. + +Backup nach #63: User-/System-Units, letzter Lauf, Result/Journal-Evidenz, nächster Timer, Heartbeat. Zustände unknown/stale/failed/running/ok unterscheiden. `systemctl --user start` nur für erneut validierte User-Unit nach Bestätigung. System-Unit bleibt lesend. Erfolgreicher Unit-Exit ist kein Restore-Nachweis; Prozent nur bei echter Datenquelle. + +## Versionen und Gates + +- 0.0.1: isolierte GTK-Shell mit Fixture-Dashboard, Backup und Security; Syntaxcheck, Wayland/X11, Hell/Dunkel, Resize, Tastatur; keine schreibende Funktion. +- 0.0.2: #59 Dart-Headless-Probe und #60 Registry-Vertrag; `dart test`, `dart compile exe`, Lauf ohne DISPLAY/WAYLAND_DISPLAY und bestehende Flutter-Tests. +- 0.0.3: gemeinsame Fixtures, Fehler-/Stale-Modelle, CPU-/RAM-/Startzeit-Baseline gegen Flutter-Release-Build auf demselben Zorin-Rechner. +- 0.1.0–0.1.x: lesender Monitor, Drill-down, Ringpuffer, Pausieren im Hintergrund, Langlauf und Accessibility; Werte gegen /proc plausibilisieren. +- 0.2.0–0.2.x: Notes, Dateiansicht, sicherer Dokument-Reviewer, Suchpalette, kontextuelle Browser-/Terminal-Aktionen; keine stillen Fremdclients. +- 0.3.0–0.3.x: Security-/Operations-Lagebild, Dienste/Timer und #63 Backup; Agent-Gateway nur für Diagnose und einzeln genehmigte Aktionen mit Audit. +- 0.4.0–0.4.x: Computer-Use-Evaluation, Wayland-Freigaben, Stop-Schalter, Sicherheits-/Performance-Regressionsmatrix, Packaging-/Upgrade-Probe und dokumentierte Flutter-vs-GTK-Entscheidung samt Rückfallplan. +- 0.5: offen; erst anhand 0.4.x-Evidenz definieren. + +## Agenten-Arbeitspakete + +A0 Baseline: aktuellen SHA, Issues/PRs, Flutter-Gates, Zorin-Matrix und Evidence-Map erfassen. A1 Tokens/UI-Design: Semantik, Accessibility, Fokus, Screenshots. A2 GTK-Fixture-Shell mit Test-Fakes und sauberem Lifecycle. A3 la_core: Parser/Probe, DI, Registry, Flutter-Adapter. A4 IPC: Schema, Peer- und Framing-Tests, kein Root-Execute. A5 Monitoring/Diagramme mit Zeit-/Einheiten-Validierung. A6 Backup gemäß #63 mit Unit-/Timer-/Journal-Fixtures. A7 optionale Tools mit Dateisicherheitsgrenzen. A8 Agenten-Gateway mit Audit und Nutzerfreigaben. A9 Benchmarks, Sicherheitsreview, Zorin-E2E, Merge-Entscheidung. + +Je Task: Scope/Dateien/Fixture/Failing-Test zuerst, minimaler Implementierungsschritt, Reviewer 1 für Funktion/UX/Races, Reviewer 2 für Privilegien/Secrets/argv/IPC. Handoff enthält Commit-SHA, tatsächlich ausgeführte Gates, rote/übersprungene Gates und Rückfallplan. Keine Behauptung 'fertig' ohne Test. Bei wiederholten Fehlschlägen Root Cause statt blindem Patchen. + +## Start und Nicht-Ziele + +`python3 -m py_compile prototype/gtk/mla_app.py`; danach auf Zorin mit PyGObject, GTK4 und libadwaita `python3 prototype/gtk/mla_app.py`. Diese Befehle sind Vorschläge, nicht hier ausgeführt. Keine Änderungen an bestehenden Flutter-, Python-, Policy-, Packaging- oder Release-Dateien. Kein Push auf Upstream; kein Merge in main ohne separate Freigabe. diff --git a/prototype/gtk/mla_app.py b/prototype/gtk/mla_app.py new file mode 100644 index 0000000..e44de25 --- /dev/null +++ b/prototype/gtk/mla_app.py @@ -0,0 +1,92 @@ +"""GTK4/libadwaita-Scaffold: Demo-Daten, keine Systemaktionen.""" +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Adw', '1') +from gi.repository import Adw, Gdk, Gtk + +MODULES = ( + ('dashboard', 'Dashboard', 'Demo-Lagebild, keine Live-Daten'), + ('monitor', 'Systemmonitor', 'Datenadapter folgt'), + ('backup', 'Backup-Cockpit', 'Status unbekannt; Unit-Adapter folgt'), + ('security', 'Security-Hub', 'Nur lesende Ansicht geplant'), +) + +class Window(Adw.ApplicationWindow): + def __init__(self, app): + super().__init__(application=app, title='Master Linux Assistant · Prototyp') + self.set_default_size(1200, 780) + self.stack = Gtk.Stack(hexpand=True, vexpand=True) + self.details = Gtk.Label(xalign=0, yalign=0, wrap=True) + nav = Adw.NavigationSplitView() + sidebar_view = Adw.ToolbarView() + sidebar_view.add_top_bar(Adw.HeaderBar()) + sidebar = Gtk.ListBox() + for ident, title, _ in MODULES: + row = Gtk.ListBoxRow() + row.module_id = ident + label = Gtk.Label(label=title, xalign=0) + label.set_margin_start(16) + label.set_margin_top(12) + label.set_margin_bottom(12) + row.set_child(label) + sidebar.append(row) + sidebar.connect('row-selected', self.select_module) + sidebar_view.set_content(sidebar) + nav.set_sidebar(Adw.NavigationPage.new(sidebar_view, 'Navigation')) + content_view = Adw.ToolbarView() + self.title = Gtk.Label(label='Dashboard') + self.title.add_css_class('mla-gold') + header = Adw.HeaderBar() + header.set_title_widget(self.title) + content_view.add_top_bar(header) + for ident, title, description in MODULES: + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + box.set_margin_top(24) + box.set_margin_start(24) + heading = Gtk.Label(label=title, xalign=0) + heading.add_css_class('title-1') + box.append(heading) + box.append(Gtk.Label(label=description, xalign=0, wrap=True)) + self.stack.add_named(box, ident) + detail_panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + detail_panel.set_size_request(280, -1) + detail_panel.set_margin_top(24) + detail_panel.set_margin_start(16) + detail_panel.append(Gtk.Label(label='KONTEXT', xalign=0)) + detail_panel.append(self.details) + pane = Gtk.Paned.new(Gtk.Orientation.HORIZONTAL) + pane.set_start_child(self.stack) + pane.set_end_child(detail_panel) + pane.set_position(850) + content_view.set_content(pane) + nav.set_content(Adw.NavigationPage.new(content_view, 'Arbeitsbereich')) + self.set_content(nav) + sidebar.select_row(sidebar.get_row_at_index(0)) + + def select_module(self, _sidebar, row): + if row is None: + return + ident = row.module_id + self.stack.set_visible_child_name(ident) + self.title.set_text(next(title for key, title, _ in MODULES if key == ident)) + self.details.set_text('Keine Live-Daten oder Admin-Aktionen. ' + + ('Ein Backup-Erfolg ist nicht belegt.' if ident == 'backup' + else 'Details und Datenquelle folgen.')) + +class App(Adw.Application): + def __init__(self): + super().__init__(application_id='dev.toqsick.MlaPrototype') + + def do_activate(self): + window = self.props.active_window + if window is None: + css = Gtk.CssProvider() + css.load_from_data(b'.mla-gold { color: #b8860b; }') + Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), css, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + window = Window(self) + window.present() + +if __name__ == '__main__': + App().run() From ba048d3abd741e76d0930afbcc61971ec14f0421 Mon Sep 17 00:00:00 2001 From: ElGregor Date: Tue, 29 Sep 2026 23:11:06 +0200 Subject: [PATCH 02/24] docs(mla): add scaffold setup, IPC contract and verification gates --- docs/mla-next/IPC_CONTRACT.md | 22 ++++++++++++++++++++++ docs/mla-next/VERIFY.md | 28 ++++++++++++++++++++++++++++ prototype/gtk/README.md | 20 ++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 docs/mla-next/IPC_CONTRACT.md create mode 100644 docs/mla-next/VERIFY.md create mode 100644 prototype/gtk/README.md diff --git a/docs/mla-next/IPC_CONTRACT.md b/docs/mla-next/IPC_CONTRACT.md new file mode 100644 index 0000000..a0582e3 --- /dev/null +++ b/docs/mla-next/IPC_CONTRACT.md @@ -0,0 +1,22 @@ +# MLA-Next IPC v1 — Entwurf, noch nicht implementiert + +Ziel: unprivilegierter Dart-Host (`packages/la_core` nach Issue #59), GTK-Client. Unix-Domain-Socket in einem privaten Verzeichnis unter XDG_RUNTIME_DIR. Ein Host bindet; GTK und ein optionaler Flutter-Adapter sind Clients. Kein zweiter C-Server am selben Pfad. + +## Wire + +JSON-RPC 2.0 über UTF-8 JSONL: ein Objekt plus LF je Frame, maximal 65536 Bytes **vor** Parsing. Keine Batch-Requests in v1. `mla.hello` mit Protokollversion/Capabilities zuerst; danach `backup.list`, `backup.watch` und nur später `backup.startUserUnit`. Responses spiegeln die Request-ID; Notifications (`backup.statusChanged`) haben keine ID und keine Antwort. + +Beispiel (nur Vertragsanregung, kein vorhandener Backend-Code): + +```json +{"jsonrpc":"2.0","id":"r1","method":"mla.hello","params":{"client":"mla-gtk","protocol":1}} +{"jsonrpc":"2.0","id":"r1","result":{"server":"mla-core","protocol":1,"capabilities":["backup.list"]}} +``` + +`backup.list` liefert `{revision,observedAt,jobs}`; Jobs tragen Unit-ID, Scope (`user|system`), Zustand (`ok|warn|crit|unknown`), Phase und redigierte Evidenz. `stale` ist UI-/Transportstatus, nicht stillschweigend `ok`. Event-Seq/Revisionslücken, Disconnect und Reconnect lösen vollständigen Snapshot-Neuabruf aus. Limitierte Schreibqueue, Timeouts, geordnete Writes und Schutz gegen doppelte Start-Requests gehören zum Implementierungsgate. + +## Sicherheitsgrenze + +Socket-Verzeichnis 0700, Socket 0600, Eigentümer und Peer-UID prüfen. SO_PEERCRED schützt nicht vor kompromittierten Prozessen derselben UID. Kein Shell-Execute- oder Root-Methodenname im Wire. Schreibende Aktionen erfordern serverseitig festgelegte Capabilities, erneut validierte erlaubte User-Unit, Nutzerbestätigung und Audit; System-Units bleiben lesend. Keine Tokens, Restic-Passwörter oder rohen Journalausgaben übertragen. Die vorhandene polkit-Grenze bleibt getrennt. + +Standard-JSON-RPC-Fehler für Parsing/Request/Method/Parameter; definierte anwendungsspezifische Fehler für nicht verfügbar, verboten, beschäftigt und Timeout. Fehlerdaten nur Code, messageKey und retryable, keine Secrets. Ein Schema und positive/negative Contract-Fixtures sind ein eigenes späteres Task-Paket, nicht durch diesen Text als implementiert behauptet. diff --git a/docs/mla-next/VERIFY.md b/docs/mla-next/VERIFY.md new file mode 100644 index 0000000..b3102c9 --- /dev/null +++ b/docs/mla-next/VERIFY.md @@ -0,0 +1,28 @@ +# MLA-Next: Verifikation und Handoff + +Stand dieses Branchs: keine hier dokumentierten GTK-Laufzeit-, Zorin-, IPC- oder Performance-Tests ausgeführt. Der Scaffold-Commit ist **kein** Release-Gate. + +## Gate 0: Scaffold + +- [ ] `python3 -m py_compile prototype/gtk/mla_app.py` grün, Ausgabe und Python-Version erfassen. +- [ ] Auf Zorin OS 18.1 als normaler Nutzer starten; GTK4/libadwaita-Version, Wayland/X11 festhalten. +- [ ] Dashboard, Monitor, Backup und Security anwählen; Titel, rechte Details und unbekannten Backup-Status prüfen. +- [ ] Fenster verkleinern/vergrößern; Tastatur/Fokus, Hell/Dunkel, 100/125/150%-Skalierung prüfen. +- [ ] App ohne Root, ohne Systemschreibfunktion, ohne Secrets in Screenshots/Logs. + +## Gate 1: Kern/Registry + +- [ ] Issue #59: `dart test` und `dart compile exe`; Probe ohne DISPLAY/WAYLAND_DISPLAY; Binärgröße/Startzeit messen. +- [ ] Issue #60: IDs eindeutig, fehlende/zyklische Abhängigkeiten abgewiesen, Start/Stop/Lazy-Loading getestet; Flutter-Navigation unverändert. +- [ ] Bestehende Repo-Gates nach Scope tatsächlich ausführen: `tool/check-versions.sh`, `dart format`, `flutter analyze`, `flutter test`, Python-Tests. + +## Gate 2: IPC und Backup + +- [ ] Frame-/UTF-8-/JSON-/Schema-/Timeout-/Reconnect-/Backpressure-Tests; falsche Peer-UID und Event-Lücke. +- [ ] Issue #63: User-/System-Units, Timer, Journal-Fehler und Unknown/Stale mit Fixtures; keine Repo-/Passwortzugriffe. +- [ ] `systemctl --user start` nur nach Bestätigung und serverseitiger Allowlist; Abbruch, Race und Doppel-Request prüfen. +- [ ] Kein grünes Backup allein wegen eines erfolgreichen Unit-Starts; Restore separat beurteilen. + +## Agenten-Handoff + +Je Aufgabe: Basis-SHA, Pfade, Scope, Failing-Test/Fixture, Umsetzung, Ergebnis von Reviewer 1 (Funktion/UX) und Reviewer 2 (Sicherheit), **wirklich ausgeführte** Gates mit Ausgaben, rote/übersprungene Gates, manuelle Zorin-Prüfung, Rückfallplan. Kein Merge/Release/Policy-Update ohne gesonderte Freigabe. diff --git a/prototype/gtk/README.md b/prototype/gtk/README.md new file mode 100644 index 0000000..36d9a85 --- /dev/null +++ b/prototype/gtk/README.md @@ -0,0 +1,20 @@ +# MLA GTK4-Prototyp + +Isolierter, unprivilegierter UI-Spike auf `feature/mla-gtk-scaffold`. `mla_app.py` zeigt ausschließlich Demo-Daten. Kein IPC, keine echten Backup-/Security-Checks, keine Admin-Aktionen und keine Paketintegration. Der bestehende Flutter-Hub bleibt unverändert. + +## Lokaler Start auf Zorin + +Prüfe zuerst GTK4/libadwaita/PyGObject auf dem Zielsystem. Ubuntu-basierte Paketnamen sind typischerweise `python3-gi`, `gir1.2-gtk-4.0`, `gir1.2-adw-1`; nichts davon wird durch diesen Branch installiert. Danach im Repo: + +```sh +python3 -m py_compile prototype/gtk/mla_app.py +python3 prototype/gtk/mla_app.py +``` + +**Nicht mit sudo starten.** `py_compile` prüft nur Syntax. Die GUI braucht eine laufende Desktop-Session; Wayland, X11, Skalierung und Theme wurden von diesem Commit nicht verifiziert. + +## Geplanter Aufbau + +`mla_app.py` ist die minimale Shell mit NavigationSplitView, HeaderBar, vier Fixture-Screens und rechter Kontextfläche. Module, Services, Tests und IPC werden erst in getrennten, überprüften Arbeitspaketen ergänzt. `docs/mla-next/AGENT_PLAN.md` gibt Reihenfolge und Sicherheitsgrenzen vor; `docs/mla-next/IPC_CONTRACT.md` ist ein Wire-Entwurf; `docs/mla-next/VERIFY.md` beschreibt Gates. + +Der frühere rsync-Democode aus der Unterhaltung gehört **nicht** zum produktiven Backup-Cockpit aus Issue #63 und wird hier nicht eingecheckt. From 052ef9a89d9b441ec16331e43d1b4a1bc3e7567c Mon Sep 17 00:00:00 2001 From: ElGregor Date: Tue, 29 Sep 2026 23:29:58 +0200 Subject: [PATCH 03/24] docs(mla): add SDD execution plan (gate 0, baseline, la_core spike + registry) --- ...9-29-mla-next-gate0-a0-la-core-registry.md | 390 ++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-29-mla-next-gate0-a0-la-core-registry.md diff --git a/docs/superpowers/plans/2026-09-29-mla-next-gate0-a0-la-core-registry.md b/docs/superpowers/plans/2026-09-29-mla-next-gate0-a0-la-core-registry.md new file mode 100644 index 0000000..71d0c94 --- /dev/null +++ b/docs/superpowers/plans/2026-09-29-mla-next-gate0-a0-la-core-registry.md @@ -0,0 +1,390 @@ +# MLA-Next-Start: Gate 0 + A0-Baseline + la_core-Spike (#59) + Registry-Vertrag (#60) + +> **For agentic workers:** REQUIRED SUB-SKILL: `superpowers:subagent-driven-development` (pro Task: `scripts/task-brief`, Implementer-Dispatch, `scripts/review-package`, Two-Reviewer laut `superpowers-zcode`, Fix-Loop). Schritte sind Checkboxen. Approved 2026-09-29. + +**Ziel:** Handoff §8 Nr. 1–3 vollständig: Gate-0-Verifikation des GTK-Scaffolds auf Zorin, A0-Baseline mit Evidence-Map, #59-Headless-Spike `packages/la_core` und #60-Kern-Registry — alles auf `feature/mla-gtk-scaffold`, PR #89 bleibt Draft. + +**Architektur:** Reiner Dart-Kern `packages/la_core` (keine flutter:/GTK-Importe) mit Probe-Vertrag + statischer Modul-Registry (Deskriptor ohne View-Builder; Aktivierung Single-Flight, Stopp rückwärts). GTK-Prototyp bleibt Fixture-Demo; Flutter-App wird nicht angefasst. + +**Stack:** Dart ≥3.4 (aus dem Flutter-SDK), `package:test`/`package:lints`; Python3/PyGObject (GTK4 + libadwaita 1) für Gate 0. + +## Global Constraints + +1. Nie `sudo`/root für GTK-, Dart- oder Agent-Prozesse; polkit-Grenze nicht umgehen. +2. Keine Shell-Strings; Prozesse nur mit validiertem Executable + argv (heute: nur `la_probe`, `mla_app.py`, Gates). +3. Keine Secrets/Tokens/unredigierte Dumps in Logs, Screenshots (nur `/tmp`), Commits. +4. Kein Push, kein Merge, kein Draft-Status-Change an PR #89; kein Push auf Upstream. +5. Nur wirklich gelaufene Gates abhaken — VERIFY.md-Checkboxen nur mit Datum + Beleg (BASELINE.md). +6. Nur neue Dateien (`packages/la_core/**`, `docs/mla-next/BASELINE.md`, Plan-Datei); unverändert bleiben `lib/`, `additional/`, `deb/`, `linux/`, Policies, Packaging. Einzig bestehende Datei mit Änderung: `docs/mla-next/VERIFY.md` (Checkboxen + Datumsnotizen). +7. `la_core`: reines Dart; Fire-and-Forget-Futures in `unawaited(...)`; kein `print` (stdout nur CLI-Ausgabe in `bin/la_probe.dart`). +8. `gh` immer mit `-R Toqsick/master-linux-assistant`. +9. `pkill -f` mit App-Pfad ist verboten (Ledger-Lektion v0.8.0) — Prozesse per gespeicherter PID beenden. +10. Commit-Stil wie Repo (`feat(la_core): …`, `docs(mla): …`), ein Commit pro Task-Abschluss, direkt auf `feature/mla-gtk-scaffold`. + +**Ausdrücklich out of scope (Folge-Sessions):** Flutter-Hub-Migration (`hub_shell.dart` → HubModule-Liste, #60-App-Seite), IPC-Schema/Fixtures, Backup #63, CommandHelper/Logger/Parser-Umzug (#59 „Danach"-Teil), build-deb-Integration. + +--- + +### Task 1: Branch auschecken, Ledger, Plandatei + +**Files:** Create `docs/superpowers/plans/2026-09-29-mla-next-gate0-a0-la-core-registry.md` (dieser Plan); Modify `.superpowers/sdd/progress.md` (neuer Abschnitt oben, uncommittet — Scratch). +**Interfaces:** Branch-Tip muss `ba048d3` sein (auf `92bef60` = main). + +- [x] `git fetch origin feature/mla-gtk-scaffold:feature/mla-gtk-scaffold && git checkout feature/mla-gtk-scaffold` +- [x] `git log --oneline -3` → erwartet `ba048d3`, `4d5c441`, `92bef60`; `git status --short` → leer +- [x] Ledger: neuen Abschnitt über den alten v0.8.0-Inhalt setzen; darunter je Task eine Zeile nach Review +- [x] Plandatei schreiben; Commit `docs(mla): add SDD execution plan (gate 0, baseline, la_core spike + registry)` + +### Task 2: Gate 0 — Scaffold-Verifikation, automatisierter Teil + +**Files:** Create `docs/mla-next/BASELINE.md`; Modify `docs/mla-next/VERIFY.md`. + +- [ ] Umgebung erfassen (Ausgaben 1:1 nach BASELINE.md §1 „Zorin-Matrix"): `$XDG_SESSION_TYPE`, `$XDG_CURRENT_DESKTOP`; `python3 --version`; `python3 -c "import gi; gi.require_version('Gtk','4.0'); gi.require_version('Adw','1'); from gi.repository import Gtk, Adw; print('GTK', Gtk.get_major_version(), Gtk.get_minor_version(), Gtk.get_micro_version(), '/ Adw', Adw.get_major_version(), Adw.get_minor_version(), Adw.get_micro_version())"`; `pkg-config --modversion gtk4 libadwaita-1`; `dart --version`; `flutter --version | head -2`; `. /etc/os-release && echo "$PRETTY_NAME"` +- [ ] `cd prototype/gtk && python3 -m py_compile mla_app.py` → Exit 0, keine Ausgabe; Python-Version notieren +- [ ] Wayland-Start: `python3 mla_app.py & APP=$!; sleep 3; kill -0 $APP && echo "lebt (Wayland, PID $APP)"`; Screenshot `gnome-screenshot -f /tmp/mla-gate0-wayland.png` (Fallback: D-Bus `org.gnome.Shell.Screenshot.Screenshot`); `kill $APP; wait $APP 2>/dev/null` — Kriterium: 3 s am Leben, kein Traceback auf stderr +- [ ] X11-Start: identisch mit `GDK_BACKEND=x11 python3 mla_app.py …`, Screenshot `/tmp/mla-gate0-x11.png` +- [ ] BASELINE.md §2 „Gate 0": Ergebnisse + Rohausgaben; §3 „Manuelle Checkliste (Basti)": Screens (Dashboard/Monitor/Backup/Security) anklicken, Titel + rechte Details + Backup-Status `unknown`, Resize, Tastatur/Fokus, Hell/Dunkel, 100/125/150 % Skalierung +- [ ] VERIFY.md Gate 0: Box 1 (`py_compile`) und Box 2 (Start, Versionen, Wayland/X11) → `[x]` + Notiz `2026-09-29: automatisiert verifiziert, siehe docs/mla-next/BASELINE.md; manuelle Checks offen`; Box 3/4 bleiben `[ ]`; Box 5 → `[x]` mit Notiz (unprivilegierter Start, Fixture-Only, Screenshots nur /tmp). Commit `docs(mla): gate 0 automated verification on zorin (wayland + x11)` + +### Task 3: A0 — Baseline & Evidence-Map + +**Files:** Modify `docs/mla-next/BASELINE.md`. + +- [ ] Repo-Fakten per `gh` (immer `-R Toqsick/master-linux-assistant`): `gh pr view 89 --json state,isDraft,mergeable` (erwartet OPEN/draft/MERGEABLE); `gh issue view 59|60|63 --json number,title,state` (alle offen, Milestone V0.9); `gh run list --limit 3` (Stand notieren) +- [ ] Lokale Gates im Repo-Root, Ausgaben (Zahlen!) nach BASELINE.md §4: `bash tool/check-versions.sh` (Exit 0) → `dart format --output=none --set-exit-if-changed lib test` (Exit 0) → `flutter analyze` („No issues found!") → `flutter test` („All tests passed!", Anzahl notieren; Handoff-Referenz: 184) → `cd additional/python && python3 -m unittest discover -s tests -t .` (OK) +- [ ] §5 „Evidence-Map": Tabelle Aussage → Beleg (Kommando+Output bzw. Datei:Zeile); §6 offene Punkte (GTK-Laufzeit nur bis 3 s Lebenstest verifiziert, keine Interaktion; Messbasis 0.0.3 offen). Commit `docs(mla): add A0 baseline and evidence map` + +### Task 4: #59-Spike — `packages/la_core` minimal (TDD) + +**Files:** Create `packages/la_core/pubspec.yaml`, `analysis_options.yaml`, `.gitignore`, `lib/la_core.dart`, `lib/src/probe.dart`, `bin/la_probe.dart`, `test/probe_test.dart`. + +- [ ] `pubspec.yaml`: +```yaml +name: la_core +description: Master Linux Assistant - reiner Dart-Kern (Spike #59, Registry #60). Keine Flutter-/GTK-Typen. +version: 0.0.1 +publish_to: none + +environment: + sdk: ^3.4.0 + +dev_dependencies: + lints: ^5.0.0 + test: ^1.25.0 +``` +`analysis_options.yaml`: `include: package:lints/recommended.yaml`; `.gitignore`: `.dart_tool/` + `build/` +- [ ] **RED:** `test/probe_test.dart` schreiben: +```dart +import 'package:la_core/la_core.dart'; +import 'package:test/test.dart'; + +void main() { + test('ProbeResult.describe enthaelt Level und Key', () { + const r = ProbeResult(level: ProbeLevel.warn, key: 'mem.free'); + expect(r.describe(), startsWith('warn:mem.free')); + }); + + test('ProbeLevel deckt ok/warn/crit/unknown ab', () { + expect(ProbeLevel.values.map((l) => l.name), + containsAll(['ok', 'warn', 'crit', 'unknown'])); + }); + + test('SelfProbe liefert ok mit key probe.self', () async { + final r = await SelfProbe().run(); + expect(r.level, ProbeLevel.ok); + expect(r.key, 'probe.self'); + }); +} +``` +`cd packages/la_core && dart pub get && dart test` → FAIL (lib fehlt) +- [ ] **GREEN:** `lib/src/probe.dart`: +```dart +enum ProbeLevel { ok, warn, crit, unknown } + +class ProbeResult { + const ProbeResult({ + required this.level, + required this.key, + this.params = const {}, + this.at, + }); + + final ProbeLevel level; + final String key; + final Map params; + final DateTime? at; + + String describe() => + '${level.name}:$key${at == null ? '' : ' @ ${at!.toIso8601String()}'}'; +} + +abstract interface class Probe { + String get id; + Future run(); +} + +class SelfProbe implements Probe { + @override + String get id => 'probe.self'; + + @override + Future run() async => + ProbeResult(level: ProbeLevel.ok, key: id, at: DateTime.now()); +} +``` +`lib/la_core.dart`: `export 'src/probe.dart';`; `bin/la_probe.dart`: +```dart +import 'dart:io'; + +import 'package:la_core/la_core.dart'; + +const String laProbeVersion = '0.0.1-spike.1'; + +Future main(List args) async { + if (args.contains('--version')) { + stdout.writeln('la_probe $laProbeVersion (dart ${Platform.version})'); + return; + } + stdout.writeln((await SelfProbe().run()).describe()); +} +``` +`dart test` → 3 passed; `dart analyze` → clean; `dart format --output=none --set-exit-if-changed .` → Exit 0 +- [ ] Kompilieren + Headless (Issue #59, „Dart aus dem Flutter-SDK" — `dart --version` mit `flutter --version`-Angabe abgleichen): `dart compile exe bin/la_probe.dart -o /tmp/la_probe` → „Generated: /tmp/la_probe"; `env -u DISPLAY -u WAYLAND_DISPLAY /tmp/la_probe --version` → gibt `la_probe 0.0.1-spike.1 (dart …)` aus, Exit 0 +- [ ] Messen nach BASELINE.md §7: `stat -c '%s' /tmp/la_probe`; Startzeit 5×: `s=$(date +%s%N); env -u DISPLAY -u WAYLAND_DISPLAY /tmp/la_probe --version >/dev/null; e=$(date +%s%N); echo $(( (e-s)/1000000 )) ms` +- [ ] Sanity: `flutter analyze` im Root unverändert grün (Nested-Paket darf Root-Gate nicht stören; falls doch → `analysis_options.yaml` angleichen). Commit `feat(la_core): minimal headless spike per issue #59 (probe contract, compile exe)` + +### Task 5: #60 Registry A — Deskriptor, Registrierung, Validierung (TDD) + +**Files:** Create `lib/src/module_descriptor.dart`, `lib/src/module_registry.dart`, `test/module_registry_validate_test.dart`; Modify `lib/la_core.dart` (beide Exports). + +- [ ] `module_descriptor.dart`: +```dart +enum ModuleKind { section, tool, launch } + +/// Kern-Deskriptor ohne View-Builder (Issue #60): View-Factories +/// leben in den UI-Adaptern und binden ueber [viewId]. +class ModuleDescriptor { + const ModuleDescriptor({ + required this.id, + required this.titleKey, + required this.viewId, + required this.kind, + this.capabilities = const {}, + this.requires = const {}, + this.probeIds = const {}, + this.actionIds = const {}, + this.subscribedTopics = const {}, + }); + + final String id; + final String titleKey; + final String viewId; + final ModuleKind kind; + final Set capabilities; + final Set requires; + final Set probeIds; + final Set actionIds; + final Set subscribedTopics; +} +``` +- [ ] **RED:** `test/module_registry_validate_test.dart` — Tests: (1) doppelte ID → `throwsA(isA())`; (2) unbekannte Abhängigkeit: `.having((e) => e.message, 'message', contains('ghost'))`; (3) Zyklus a→b→a wirft; (4) nach `validate()` neue `register(...)` möglich, erneute `validate()` wirft nicht. Helper: `ModuleDescriptor mod(String id, {Set requires})` und `_NoopActivator` (start/stop no-op). +- [ ] **GREEN:** `module_registry.dart` (Teil 1): +```dart +enum ModuleState { registered, starting, started, stopping, stopped } + +abstract interface class ModuleActivator { + Future start(ModuleDescriptor module); + Future stop(ModuleDescriptor module); +} + +class ModuleRegistryError implements Exception { + ModuleRegistryError(this.message); + final String message; + @override + String toString() => 'ModuleRegistryError: $message'; +} + +/// Statische Registrierung; kein Laden fremder Plugins (Issue #60). +class ModuleRegistry { + ModuleRegistry({required ModuleActivator activator}) + : _activator = activator; + + final ModuleActivator _activator; + final Map _modules = {}; + final Map _states = {}; + final Map _visible = {}; + final Map> _inflight = {}; + final List _activationOrder = []; + bool _validated = false; + + void register(ModuleDescriptor descriptor) { + if (_modules.containsKey(descriptor.id)) { + throw ModuleRegistryError('duplicate module id: ${descriptor.id}'); + } + _modules[descriptor.id] = descriptor; + _states[descriptor.id] = ModuleState.registered; + _validated = false; + } + + List get descriptors => List.unmodifiable(_modules.values); + bool isRegistered(String id) => _modules.containsKey(id); + ModuleDescriptor? operator [](String id) => _modules[id]; + ModuleState stateOf(String id) => _states[id] ?? + (throw ModuleRegistryError('unknown module id: $id')); + bool isVisible(String id) => _visible[id] ?? false; + + void validate() { + for (final d in _modules.values) { + for (final dep in d.requires) { + if (!_modules.containsKey(dep)) { + throw ModuleRegistryError( + '${d.id} requires unknown module: $dep'); + } + } + } + _checkCycles(); + _validated = true; + } + + void _checkCycles() { + final mark = {}; // fehlend=neu, 1=in Arbeit, 2=fertig + void visit(String id, List path) { + final m = mark[id] ?? 0; + if (m == 2) return; + if (m == 1) { + throw ModuleRegistryError( + 'dependency cycle: ${[...path, id].join(' -> ')}'); + } + mark[id] = 1; + for (final dep in _modules[id]!.requires) { + visit(dep, [...path, id]); + } + mark[id] = 2; + } + for (final id in _modules.keys) { + visit(id, []); + } + } +} +``` +- [ ] `dart test` → alle grün (3+4); `dart analyze`/`dart format` Gate; Export ergänzen. Commit `feat(la_core): module descriptor + registry validation per issue #60` + +### Task 6: #60 Registry B — Aktivierung & Lifecycle (TDD) + +**Files:** Modify `lib/src/module_registry.dart`; Create `test/module_registry_lifecycle_test.dart`. + +- [ ] **RED:** Tests mit `RecordingActivator` (`log` Liste; optional `Completer gate`, `start` awaited gate): (1) Topo-Reihenfolge: a→b→c, `activate('a')` → Log `[start c, start b, start a]`; (2) geteilte Abhängigkeit startet genau einmal (Lazy): c von a und b; (3) Single-Flight: zwei `activate('a')` vor `gate.complete()` → `start a` einmal im Log; (4) `deactivate('y')` mit x→y, z→y (beide aktiv) → `[stop z, stop x, stop y]`; (5) `setVisible` wirft `ModuleRegistryError` vor Start, funktioniert danach; (6) `stateOf`: `registered` → `starting` (während gate, via `await Future.delayed(Duration.zero)`) → `started` → nach `deactivate` `stopped`; (7) `deactivateAll()` stoppt in umgekehrter Aktivierungsreihenfolge. +- [ ] **GREEN:** Registry ergänzen: +```dart + Future activate(String id) { + if (!_modules.containsKey(id)) { + throw ModuleRegistryError('unknown module id: $id'); + } + if (!_validated) validate(); + return _inflight[id] ??= _activateSubtree(id); + } + + Future _activateSubtree(String id) async { + for (final depId in _dependenciesOf(id)) { + if (_states[depId] != ModuleState.started) { + await (_inflight[depId] ??= _activateSubtree(depId)); + } + } + if (_states[id] == ModuleState.started) return; + _states[id] = ModuleState.starting; + try { + await _activator.start(_modules[id]!); + _states[id] = ModuleState.started; + _activationOrder.add(id); + } finally { + _inflight.remove(id); + } + } + + List _dependenciesOf(String id) { + final out = []; + void collect(String m, Set seen) { + for (final dep in _modules[m]!.requires) { + if (seen.add(dep)) { + collect(dep, seen); + out.add(dep); + } + } + } + collect(id, {id}); + return out; + } + + Future deactivate(String id) { + if (!_modules.containsKey(id)) { + throw ModuleRegistryError('unknown module id: $id'); + } + return _inflight[id] ??= _deactivateWithDependents(id); + } + + Future _deactivateWithDependents(String id) async { + for (final active in _activationOrder.reversed) { + if (active == id) break; + if (_states[active] == ModuleState.started && + _transitivelyDependsOn(active, id)) { + await (_inflight[active] ??= _stopModule(active)); + } + } + if (_states[id] == ModuleState.started) { + await (_inflight[id] ??= _stopModule(id)); + } else { + _inflight.remove(id); + } + } + + Future _stopModule(String id) async { + _states[id] = ModuleState.stopping; + try { + await _activator.stop(_modules[id]!); + _states[id] = ModuleState.stopped; + _activationOrder.remove(id); + } finally { + _inflight.remove(id); + } + } + + bool _transitivelyDependsOn(String m, String target, [Set? seen]) { + seen ??= {}; + for (final dep in _modules[m]!.requires) { + if (dep == target || + (seen.add(dep) && _transitivelyDependsOn(dep, target, seen))) { + return true; + } + } + return false; + } + + Future deactivateAll() async { + for (final id in List.of(_activationOrder).reversed) { + if (_states[id] == ModuleState.started) { + await (_inflight[id] ??= _stopModule(id)); + } + } + } + + void setVisible(String id, bool visible) { + if (_states[id] != ModuleState.started) { + throw ModuleRegistryError( + 'setVisible($id, $visible): module not started'); + } + _visible[id] = visible; + } +``` +- [ ] `dart test` → alle grün (14 gesamt erwartet: 3 Probe + 4 Validierung + 7 Lifecycle); analyze/format Gate; Commit `feat(la_core): registry lifecycle - single-flight activation, reverse stop per issue #60` + +### Task 7: Abschluss — VERIFY abhaken, Root-Gates, Final-Review, Bericht + +- [ ] VERIFY.md Gate 1: Box 1 (#59) `[x]` mit Messwerten (Binärgröße/Startzeit → BASELINE §7); Box 2 (#60) `[x]` mit Beleg „14 dart-Tests; Flutter-Navigation unberührt (`git diff --stat 92bef60..HEAD` zeigt kein `lib/`)" + flutter-test-Zahl; Box 3 `[x]` nach frischen Root-Gates. Commit `docs(mla): check off gate 1 spike + registry portions with evidence` +- [ ] Frische Root-Gates (verification-before-completion, Outputs zitieren): `tool/check-versions.sh`, `dart format --output=none --set-exit-if-changed lib test`, `flutter analyze`, `flutter test`, Python-Tests +- [ ] Final whole-branch review: `review-package 92bef60 HEAD` → `code-reviewer`-Dispatch (kritischster Tier); Critical/Important → EIN Fix-Subagent mit kompletter Findings-Liste → Re-Review +- [ ] Ledger-Abschluss; Bericht an Basti inkl. manueller Gate-0-Checkliste (BASELINE §3) und Hinweis: PR #89 bleibt Draft, Push nur auf Anweisung + +## Ausführungsrahmen + +- SDD: pro Task Implementer-Dispatch (general-purpose) mit Brief via `scripts/task-brief`, danach `scripts/review-package` + Two-Reviewer parallel (Correctness + Completeness) laut `superpowers-zcode`; Fix-Loop bis clean; Ledger-Zeile je Task. Kein Worktree (Single-User-Repo, sauberer Tree, Branch-Checkout reicht — dokumentierte Abweichung von Phase ISOLATE). +- Fehlende Toolchain (GTK/Adw/Dart) oder rot laufende Baseline-Gates → BLOCKED melden, nicht improvisieren (Handoff-Regel 5). From 3f9ab3fd41d6138024206c87f03ac539b44e9967 Mon Sep 17 00:00:00 2001 From: ElGregor Date: Tue, 29 Sep 2026 23:38:55 +0200 Subject: [PATCH 04/24] docs(mla): gate 0 automated verification on zorin (wayland + x11) --- docs/mla-next/BASELINE.md | 109 ++++++++++++++++++++++++++++++++++++++ docs/mla-next/VERIFY.md | 6 +-- 2 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 docs/mla-next/BASELINE.md diff --git a/docs/mla-next/BASELINE.md b/docs/mla-next/BASELINE.md new file mode 100644 index 0000000..aeadcdc --- /dev/null +++ b/docs/mla-next/BASELINE.md @@ -0,0 +1,109 @@ +# MLA-Next: Baseline + +Erstellt am 2026-09-29 durch den automatisierten Gate-0-Lauf (Task 2, Scaffold-Verifikation) +auf dem Zielrechner. Evidence für PR #89. Screenshots liegen ausschließlich in `/tmp` +und sind nicht Teil des Repos. + +## §1 Zorin-Matrix (Umgebung, 2026-09-29) + +Rohausgaben 1:1 (Eingabe → Ausgabe): + +```text +$ printf '%s' "$XDG_SESSION_TYPE" +wayland +$ printf '%s' "$XDG_CURRENT_DESKTOP" +zorin:GNOME +$ . /etc/os-release && echo "$PRETTY_NAME" +Zorin OS 18.1 +$ python3 --version +Python 3.12.3 +$ python3 -c "import gi; gi.require_version('Gtk','4.0'); gi.require_version('Adw','1'); from gi.repository import Gtk, Adw; print('GTK', Gtk.get_major_version(), Gtk.get_minor_version(), Gtk.get_micro_version(), '/ Adw', Adw.get_major_version(), Adw.get_minor_version(), Adw.get_micro_version())" +GTK 4 14 5 / Adw 1 5 0 +$ pkg-config --modversion gtk4 libadwaita-1 +Package gtk4 was not found in the pkg-config search path. +Perhaps you should add the directory containing `gtk4.pc' +to the PKG_CONFIG_PATH environment variable +Package 'gtk4', required by 'virtual:world', not found +Package 'libadwaita-1', required by 'virtual:world', not found +$ dart --version +Dart SDK version: 3.13.4 (stable) (Tue Sep 15 01:01:15 2026 -0700) on "linux_x64" +$ flutter --version | head -2 +Flutter 3.47.5 • channel stable • https://github.com/flutter/flutter.git +Framework • revision 6a19cca564 (vor 12 Tagen) • 2026-09-17 14:13:22 -0400 +``` + +Ergänzend: `WAYLAND_DISPLAY=wayland-0`, `DISPLAY=:1` (XWayland verfügbar). + +Bewertung: Die GI-Introspection (Runtime) liefert GTK 4.14.5 und libadwaita 1.5.0. +Die `-dev`-Pakete (`.pc`-Dateien für pkg-config) sind auf dem Desktop nicht +installiert — für den PyGObject-Lauf irrelevant, für künftige C-Builds relevant. + +## §2 Gate 0 — automatisierte Ergebnisse (2026-09-29) + +### py_compile + +```text +$ cd prototype/gtk && python3 -m py_compile mla_app.py; echo $? +0 +``` + +Exit 0, keine Ausgabe. Python-Version: 3.12.3. + +### Wayland-Start + +Kommando: `python3 mla_app.py >/tmp/mla-gate0-wayland.stdout 2>/tmp/mla-gate0-wayland.stderr &`, +danach `sleep 3` und `kill -0`. + +- Kriterium „3 s am Leben": **erfüllt** — Meldung `lebt (Wayland, PID 154921)`. + Anmerkung zur Messung: `$!` erfasste in diesem ersten Lauf die Wrapper-Shell + statt direkt python3 (App-Prozess war python3, PID 154923); der App-Prozess lief + nachweislich über die 3 s hinaus und wurde anschließend per gespeicherter PID + beendet. Der X11-Lauf unten misst die python3-PID direkt. +- stderr: **0 Byte (leer) — kein Traceback.** stdout: 0 Byte. +- Screenshot: **nicht möglich.** `gnome-screenshot` ist nicht installiert; der + D-Bus-Fallback `org.gnome.Shell.Screenshot.Screenshot` wird von GNOME (≥ 41) + abgelehnt: + `GDBus.Error:org.freedesktop.DBus.Error.AccessDenied: Screenshot is not allowed`. + Nachholen der Aufnahme bleibt der manuellen Prüfung vorbehalten. + +### X11-Start (`GDK_BACKEND=x11`) + +Kommando: `GDK_BACKEND=x11 python3 mla_app.py >/tmp/mla-gate0-x11.stdout 2>/tmp/mla-gate0-x11.stderr &`, +danach `sleep 3` und `kill -0`. + +- Kriterium „3 s am Leben": **erfüllt** — Meldung `lebt (X11, PID 157124)`; + `ps` bestätigt `157124 python3 mla_app.py`. +- stderr: **0 Byte (leer) — kein Traceback.** stdout: 0 Byte. +- Screenshot: `/tmp/mla-gate0-x11.png` (PNG, 1368×1473, 26 275 Bytes). Aufnahme + gezielt des App-Fensters (`xdotool search --name Prototyp` → Fenster-ID, dann + `import -window `); ein Root-Grab per `scrot` lieferte nur ein schwarzes + XWayland-Root-Fenster und wurde verworfen. Inhalt: NavigationSplitView mit + Dashboard / Systemmonitor / Backup-Cockpit / Security-Hub, goldfarbener + „Dashboard"-Titel, KONTEXT-Panel rechts, dunkles Theme. + +### Zusammenfassung + +| Prüfung | Ergebnis | +| --- | --- | +| py_compile | PASS (Exit 0, keine Ausgabe) | +| Wayland-Start | PASS (≥ 3 s am Leben, stderr leer); Screenshot-Lücke dokumentiert | +| X11-Start | PASS (≥ 3 s am Leben, stderr leer, Screenshot vorhanden) | + +## §3 Manuelle Checkliste (Basti) — offen + +- [ ] Dashboard anklicken: Titel „Dashboard", rechte Details „Keine Live-Daten …" +- [ ] Systemmonitor anklicken: Titel „Systemmonitor", rechte Details („Datenadapter folgt") +- [ ] Backup-Cockpit anklicken: Titel „Backup-Cockpit", rechte Details, Backup-Status `unknown` („Ein Backup-Erfolg ist nicht belegt") +- [ ] Security-Hub anklicken: Titel „Security-Hub", rechte Details +- [ ] Fenster verkleinern/vergrößern (NavigationSplitView / Paned-Verhalten) +- [ ] Tastatur/Fokus (Sidebar-Navigation, Tab-Reihenfolge) +- [ ] Hell/Dunkel-Umschaltung +- [ ] Skalierung 100 % / 125 % / 150 % + +## §4 Repo-Gates (folgt — Task 3/A0) + +## §5 Evidence-Map (folgt — Task 3/A0) + +## §6 Offene Punkte (folgt — Task 3/A0) + +## §7 la_probe-Messung (folgt — Gate 1) diff --git a/docs/mla-next/VERIFY.md b/docs/mla-next/VERIFY.md index b3102c9..2f4d8b7 100644 --- a/docs/mla-next/VERIFY.md +++ b/docs/mla-next/VERIFY.md @@ -4,11 +4,11 @@ Stand dieses Branchs: keine hier dokumentierten GTK-Laufzeit-, Zorin-, IPC- oder ## Gate 0: Scaffold -- [ ] `python3 -m py_compile prototype/gtk/mla_app.py` grün, Ausgabe und Python-Version erfassen. -- [ ] Auf Zorin OS 18.1 als normaler Nutzer starten; GTK4/libadwaita-Version, Wayland/X11 festhalten. +- [x] `python3 -m py_compile prototype/gtk/mla_app.py` grün, Ausgabe und Python-Version erfassen. — 2026-09-29: automatisiert verifiziert, siehe docs/mla-next/BASELINE.md; manuelle Checks offen +- [x] Auf Zorin OS 18.1 als normaler Nutzer starten; GTK4/libadwaita-Version, Wayland/X11 festhalten. — 2026-09-29: automatisiert verifiziert, siehe docs/mla-next/BASELINE.md; manuelle Checks offen - [ ] Dashboard, Monitor, Backup und Security anwählen; Titel, rechte Details und unbekannten Backup-Status prüfen. - [ ] Fenster verkleinern/vergrößern; Tastatur/Fokus, Hell/Dunkel, 100/125/150%-Skalierung prüfen. -- [ ] App ohne Root, ohne Systemschreibfunktion, ohne Secrets in Screenshots/Logs. +- [x] App ohne Root, ohne Systemschreibfunktion, ohne Secrets in Screenshots/Logs. — 2026-09-29: unprivilegierter Start als normaler Nutzer, Fixture-Only (Demo-Daten, keine Systemaktionen), Screenshots nur /tmp — siehe docs/mla-next/BASELINE.md ## Gate 1: Kern/Registry From d5265f00096fe90c0560858a2f98d02c6f48be2f Mon Sep 17 00:00:00 2001 From: ElGregor Date: Tue, 29 Sep 2026 23:47:36 +0200 Subject: [PATCH 05/24] docs(mla): complete gate 0 wayland evidence chain (review fix) --- docs/mla-next/BASELINE.md | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/mla-next/BASELINE.md b/docs/mla-next/BASELINE.md index aeadcdc..147bb6f 100644 --- a/docs/mla-next/BASELINE.md +++ b/docs/mla-next/BASELINE.md @@ -55,10 +55,19 @@ Kommando: `python3 mla_app.py >/tmp/mla-gate0-wayland.stdout 2>/tmp/mla-gate0-wa danach `sleep 3` und `kill -0`. - Kriterium „3 s am Leben": **erfüllt** — Meldung `lebt (Wayland, PID 154921)`. - Anmerkung zur Messung: `$!` erfasste in diesem ersten Lauf die Wrapper-Shell - statt direkt python3 (App-Prozess war python3, PID 154923); der App-Prozess lief - nachweislich über die 3 s hinaus und wurde anschließend per gespeicherter PID - beendet. Der X11-Lauf unten misst die python3-PID direkt. + Anmerkung zu Messung und Beleglage: `$!` erfasste in diesem ersten Lauf die + Wrapper-Shell statt direkt python3; `kill -0` belegte zum Prüfpunkt also das + Leben der Wrapper-Shell (PID 154921), für das Child nur den indirekten Schluss. + Die python3-PID (154923) ist im Implementer-Report + (`.superpowers/sdd/task-2-report.md`) geführt, jedoch ohne `ps`-Zitat — ein + direkter Prozessbeleg für den Wayland-Lauf liegt nicht vor. Nachträgliche + Kontrollprüfung per `pgrep -af mla_app.py` (2026-09-29): kein + `mla_app.py`-Prozess mehr aktiv — einziger Treffer war der Self-Match der + prüfenden Shell; kein Restprozess. Die Aussage „der App-Prozess lief die + vollen 3 s als python3" beruht damit auf dem X11-Direktbeleg unten (gleiche + App; python3-PID 157124 per `ps` bestätigt) plus leerem stderr, nicht auf + direktem `ps` während des Wayland-Laufs. Der X11-Lauf misst die python3-PID + direkt. - stderr: **0 Byte (leer) — kein Traceback.** stdout: 0 Byte. - Screenshot: **nicht möglich.** `gnome-screenshot` ist nicht installiert; der D-Bus-Fallback `org.gnome.Shell.Screenshot.Screenshot` wird von GNOME (≥ 41) @@ -89,6 +98,15 @@ danach `sleep 3` und `kill -0`. | Wayland-Start | PASS (≥ 3 s am Leben, stderr leer); Screenshot-Lücke dokumentiert | | X11-Start | PASS (≥ 3 s am Leben, stderr leer, Screenshot vorhanden) | +Beleg zu VERIFY-Box 5 („unprivilegierter Start als normaler Nutzer, +Fixture-Only, Screenshots nur /tmp"): `prototype/gtk/mla_app.py` ist reine +Demo-Ware — ausschließlich Demo-Daten und GTK-/Adw-Widget-Aufbau, keine +Systemaufrufe im Code (Docstring dort: „Demo-Daten, keine Systemaktionen"). +Beide Läufe starteten ohne sudo als normaler Nutzer; die Laufprotokolle +(Kommandos, PIDs, leere stdout/stderr) stehen oben bzw. in +`.superpowers/sdd/task-2-report.md`. Screenshots liegen ausschließlich in +`/tmp` (siehe Kopf dieser Datei). + ## §3 Manuelle Checkliste (Basti) — offen - [ ] Dashboard anklicken: Titel „Dashboard", rechte Details „Keine Live-Daten …" From b6a75ab87cc1b5255e3645ff8ea11d0745939b4e Mon Sep 17 00:00:00 2001 From: ElGregor Date: Tue, 29 Sep 2026 23:55:49 +0200 Subject: [PATCH 06/24] docs(mla): add A0 baseline and evidence map --- docs/mla-next/BASELINE.md | 95 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/docs/mla-next/BASELINE.md b/docs/mla-next/BASELINE.md index 147bb6f..a2f4364 100644 --- a/docs/mla-next/BASELINE.md +++ b/docs/mla-next/BASELINE.md @@ -118,10 +118,99 @@ Beide Läufe starteten ohne sudo als normaler Nutzer; die Laufprotokolle - [ ] Hell/Dunkel-Umschaltung - [ ] Skalierung 100 % / 125 % / 150 % -## §4 Repo-Gates (folgt — Task 3/A0) +## §4 Repo-Gates (Task 3/A0, 2026-09-29) -## §5 Evidence-Map (folgt — Task 3/A0) +Lokal im Repo-Root auf `feature/mla-gtk-scaffold` (Working Tree sauber vor dem +Lauf), Reihenfolge wie CI. Umgebung siehe §1 (Dart 3.13.4, Flutter 3.47.5, +Python 3.12.3). Alle fünf Gates grün. -## §6 Offene Punkte (folgt — Task 3/A0) +| Gate | Kommando | Exit | Kernausgabe | +| --- | --- | --- | --- | +| Version-Konsistenz | `bash tool/check-versions.sh` | 0 | `version 0.8.0 is consistent` | +| Dart-Format | `dart format --output=none --set-exit-if-changed lib test` | 0 | `Formatted 118 files (0 changed) in 0.30 seconds.` | +| Analyzer | `flutter analyze` | 0 | `No issues found! (ran in 9.5s)` | +| Dart-Tests | `flutter test` | 0 | `00:02 +184: All tests passed!` | +| Python-Tests | `cd additional/python && python3 -m unittest discover -s tests -t .` | 0 | `Ran 49 tests in 0.052s` / `OK` | + +Rohausgaben 1:1 (Exit-Zeile jeweils ergänzt): + +```text +$ bash tool/check-versions.sh +version 0.8.0 is consistent +(EXIT=0) + +$ dart format --output=none --set-exit-if-changed lib test +Formatted 118 files (0 changed) in 0.30 seconds. +(EXIT=0) + +$ flutter analyze +Upgrading analysis_options.yaml to exclude build and platform directories. +Analyzing linux-assistant... +No issues found! (ran in 9.5s) +(EXIT=0) + +$ flutter test +… +00:02 +183: …/test/quick_notes_widget_test.dart: a failing load shows an error state, not an endless spinner +00:02 +184: All tests passed! +(EXIT=0) + +$ cd additional/python && python3 -m unittest discover -s tests -t . +Ran 49 tests in 0.052s + +OK +(EXIT=0) +``` + +Anmerkungen: + +- Gemessene Dart-Testanzahl: **184** (Zähler `+184: All tests passed!`) — + deckt sich mit der Handoff-Referenz von 184. +- `flutter analyze` modifizierte bei dem Lauf eigenmächtig + `analysis_options.yaml` (fügte `analyzer.exclude: build/**, linux/**` + ein; Meldung „Upgrading analysis_options.yaml …"). Die Änderung wurde + unmittelbar nach dem Lauf per `git checkout -- analysis_options.yaml` + zurückgenommen; der Commit dieses Tasks enthält nur diese Datei. Ob das + Exclude fest übernommen werden sollte, ist ein offener Punkt (§6). + +## §5 Evidence-Map (Task 3/A0, 2026-09-29) + +Jede Zeile: Aussage → konkreter Beleg (Kommando + Kernausgabe bzw. Abschnitt +dieser Datei). + +| Aussage | Beleg | +| --- | --- | +| PR #89 ist OPEN, Draft und MERGEABLE | `gh pr view 89 -R Toqsick/master-linux-assistant --json state,isDraft,mergeable` → `{"isDraft":true,"mergeable":"MERGEABLE","state":"OPEN"}` (2026-09-29) | +| Issues #59, #60, #63 sind offen und auf Milestone „V0.9 – Fundament & Lagebild" | `gh issue view 59/60/63 -R Toqsick/master-linux-assistant --json number,title,state,milestone` → jeweils `"state":"OPEN"`, Milestone Nr. 7 „V0.9 – Fundament & Lagebild"; Titel: #59 „FU2 Reiner Dart-Kern packages/la_core", #60 „FU1 Modul-Registry (#27)", #63 „LB2 Backup-Cockpit" | +| CI ist auf `feature/mla-gtk-scaffold` grün (PR-Run) | `gh run list -R Toqsick/master-linux-assistant --limit 3` → Run 36631724173, `completed success`, `pull_request`, Branch `feature/mla-gtk-scaffold`, 2026-09-29T21:12:38Z; die beiden älteren Runs (2026-09-24, `main`/`hardening/0.8.x-browser-xdg`) ebenfalls `success` | +| `version` ↔ `pubspec.yaml` ↔ `deb/DEBIAN/control` konsistent (0.8.0) | §4, Zeile Version-Konsistenz: `bash tool/check-versions.sh` → Exit 0, `version 0.8.0 is consistent` | +| Formatierungs-Gate erfüllt (118 Dateien, 0 geändert) | §4, Zeile Dart-Format: `dart format --output=none --set-exit-if-changed lib test` → Exit 0, `Formatted 118 files (0 changed)` | +| Analyzer ohne Befunde | §4, Zeile Analyzer: `flutter analyze` → Exit 0, `No issues found! (ran in 9.5s)` | +| Dart-Testsuite grün mit 184 Tests | §4, Zeile Dart-Tests: `flutter test` → Exit 0, `00:02 +184: All tests passed!` | +| Python-Testsuite des Root-Runners grün mit 49 Tests | §4, Zeile Python-Tests: `python3 -m unittest discover -s tests -t .` → Exit 0, `Ran 49 tests`, `OK` | +| Umgebung: Zorin OS 18.1, Wayland, GTK 4.14.5, Adw 1.5.0, Dart 3.13.4, Flutter 3.47.5 | §1 (Rohausgaben 1:1, 2026-09-29) | +| GTK-Scaffold kompiliert und startet unter Wayland und X11 (je ≥ 3 s am Leben, stderr leer) | §2 „Gate 0" inkl. Zusammenfassungstabelle; X11 mit direktem `ps`-Beleg (PID 157124), Wayland-Life-Beleg indirekt (siehe §2-Anmerkung) | +| Scaffold ist Fixture-Only (keine Systemaktionen) | §2 Belegabsatz zu VERIFY-Box 5 + `prototype/gtk/mla_app.py` (Docstring: „Demo-Daten, keine Systemaktionen") | + +## §6 Offene Punkte (Stand 2026-09-29) + +1. **GTK-Laufzeit nur Smoke-getestet:** verifiziert ist ausschließlich der + 3 s-Lebenstest (§2) — Wayland ohne direkten `ps`-Beleg während des Laufs, + X11 mit. Keine Interaktion (Klicks, Fokus, Themewechsel, Skalierung) — + die manuelle Checkliste §3 bleibt Basti vorbehalten und ist offen. +2. **Wayland-Screenshot-Lücke:** kein Bild des Wayland-Laufs + (`gnome-screenshot` fehlt, D-Bus-API verweigert, §2); belegt ist nur der + X11-Screenshot. Nachholen nur manuell. +3. **`-dev`-Pakete fehlen:** `gtk4.pc`/`libadwaita-1.pc` nicht installiert + (§1) — für den PyGObject-Lauf irrelevant, für künftige C-Builds + (GSettings-Schemas, Compile) relevant. +4. **`analysis_options.yaml`-Auto-Änderung:** `flutter analyze` fügt bei + jedem Lauf `analyzer.exclude: build/**, linux/**` ein (§4); im A0-Lauf + zurückgenommen. Entscheidung offen: Exclude dauerhaft übernehmen oder + Analyzer-Verhalten ignorieren — vor dem nächsten Gate-Lauf klären, sonst + bleibt der Working Tree nicht sauber. +5. **Messbasis 0.0.3 offen:** der Flutter-vs-GTK-Vergleich (Roadmap + 0.0.3/0.4.x) hat noch keine `la_probe`-Messwerte; §7 bleibt bis Gate 1 + Platzhalter. Diese Baseline (§1, §2, §4) ist die Vergleichsgrundlage. ## §7 la_probe-Messung (folgt — Gate 1) From 0513b28630bd00096b9f455b1a020e818362f722 Mon Sep 17 00:00:00 2001 From: ElGregor Date: Wed, 30 Sep 2026 00:13:36 +0200 Subject: [PATCH 07/24] feat(la_core): minimal headless spike per issue #59 (probe contract, compile exe) --- docs/mla-next/BASELINE.md | 99 ++++++- packages/la_core/.gitignore | 2 + packages/la_core/analysis_options.yaml | 1 + packages/la_core/bin/la_probe.dart | 13 + packages/la_core/lib/la_core.dart | 1 + packages/la_core/lib/src/probe.dart | 32 ++ packages/la_core/pubspec.lock | 389 +++++++++++++++++++++++++ packages/la_core/pubspec.yaml | 11 + packages/la_core/test/probe_test.dart | 20 ++ 9 files changed, 567 insertions(+), 1 deletion(-) create mode 100644 packages/la_core/.gitignore create mode 100644 packages/la_core/analysis_options.yaml create mode 100644 packages/la_core/bin/la_probe.dart create mode 100644 packages/la_core/lib/la_core.dart create mode 100644 packages/la_core/lib/src/probe.dart create mode 100644 packages/la_core/pubspec.lock create mode 100644 packages/la_core/pubspec.yaml create mode 100644 packages/la_core/test/probe_test.dart diff --git a/docs/mla-next/BASELINE.md b/docs/mla-next/BASELINE.md index a2f4364..53c1625 100644 --- a/docs/mla-next/BASELINE.md +++ b/docs/mla-next/BASELINE.md @@ -213,4 +213,101 @@ dieser Datei). 0.0.3/0.4.x) hat noch keine `la_probe`-Messwerte; §7 bleibt bis Gate 1 Platzhalter. Diese Baseline (§1, §2, §4) ist die Vergleichsgrundlage. -## §7 la_probe-Messung (folgt — Gate 1) +## §7 la_probe-Messung (Task 4/#59-Spike, 2026-09-30) + +Erste Messbasis für `la_probe` (Spike `packages/la_core`, Issue #59). Aufgenommen +am **2026-09-30 00:11 CEST** auf dem Zielrechner (§1-Umgebung). Toolchain: +`dart compile exe` mit dem Dart aus dem Flutter-SDK — +`dart --version` → `Dart SDK version: 3.13.4 (stable) (Tue Sep 15 01:01:15 2026 -0700) on "linux_x64"`, +`flutter --version | head -1` → `Flutter 3.47.5 • channel stable` (beide via +snap; Dart 3.13.4 ist die dem Flutter-3.47.5-Stand beiliegende SDK-Version). + +### Paket-Gates (`cd packages/la_core`) + +| Gate | Kommando | Exit | Kernausgabe | +| --- | --- | --- | --- | +| Abhängigkeiten | `dart pub get` | 0 | `Got dependencies!` | +| Tests (RED-Mutation) | `mv lib/src/probe.dart lib/src/probe.dart.bak && dart test` | 1 | `Error when reading 'lib/src/probe.dart': No such file or directory` … `Some tests failed.` | +| Tests (GREEN) | `dart test` (nach Restore) | 0 | `00:00 +3: All tests passed!` | +| Analyzer | `dart analyze` | 0 | `No issues found!` | +| Format | `dart format --output=none --set-exit-if-changed .` | 0 | `Formatted 4 files (0 changed) in 0.02 seconds.` | + +RED-Nachweis (Mutation statt klassischem RED, da die Implementation aus dem +unterbrochenen Vorlauf bereits existierte) — Auszug 1:1: + +```text +$ mv lib/src/probe.dart lib/src/probe.dart.bak && dart test +00:00 +0: loading test/probe_test.dart +00:00 +0 -1: loading test/probe_test.dart [E] + Failed to load "test/probe_test.dart": + lib/la_core.dart:1:1: Error: Error when reading 'lib/src/probe.dart': No such file or directory + export 'src/probe.dart'; + ^ + test/probe_test.dart:6:34: Error: Undefined name 'ProbeLevel'. + … +00:00 +0 -1: Some tests failed. +(EXIT=1) +``` + +GREEN 1:1: + +```text +$ dart test +00:00 +0: loading test/probe_test.dart +00:00 +0: test/probe_test.dart: ProbeResult.describe enthaelt Level und Key +00:00 +1: test/probe_test.dart: ProbeLevel deckt ok/warn/crit/unknown ab +00:00 +2: test/probe_test.dart: SelfProbe liefert ok mit key probe.self +00:00 +3: All tests passed! +(EXIT=0) +``` + +### Kompilieren + Headless-Lauf + +```text +$ cd packages/la_core && dart compile exe bin/la_probe.dart -o /tmp/la_probe +Generated: /tmp/la_probe +(EXIT=0) + +$ env -u DISPLAY -u WAYLAND_DISPLAY /tmp/la_probe --version +la_probe 0.0.1-spike.1 (dart 3.13.4 (stable) (Tue Sep 15 01:01:15 2026 -0700) on "linux_x64") +(EXIT=0) + +$ env -u DISPLAY -u WAYLAND_DISPLAY /tmp/la_probe +ok:probe.self @ 2026-09-30T00:11:41.458200 +(EXIT=0) +``` + +Kein Display nötig (DISPLAY und WAYLAND_DISPLAY entfernt) — der AOT-Binary +läuft headless; reiner Dart-Kern, keine GTK-/Flutter-Typen. + +### Messwerte + +| Messgröße | Wert | +| --- | --- | +| Binärgröße (`stat -c '%s' /tmp/la_probe`) | **6 547 240 Bytes** (≈ 6,24 MiB) | +| Startzeit (`--version`, 5 Läufe, je frischer Prozess) | 3 / 3 / 3 / 3 / 3 ms | +| **Median-Startzeit** | **3 ms** | +| Datum/Uhrzeit der Messung | 2026-09-30 00:11:50 CEST | +| Dart-Version (compile + Lauf) | 3.13.4 (stable), linux_x64 | + +Rohausgaben 1:1: + +```text +$ stat -c '%s' /tmp/la_probe +6547240 + +$ s=$(date +%s%N); env -u DISPLAY -u WAYLAND_DISPLAY /tmp/la_probe --version >/dev/null; e=$(date +%s%N); echo $(( (e-s)/1000000 )) ms +3 ms # run1 +3 ms # run2 +3 ms # run3 +3 ms # run4 +3 ms # run5 +``` + +Einordnung: Die 6,2-MiB-Binärgröße ist der AOT-Runtime-Overhead eines +minimalen Dart-Executables (Fixturm-Basis für die 0.0.3/0.4.x-Vergleiche); +die 3-ms-Startzeit bestätigt die Headless-Tauglichkeit des reinen Dart-Kerns. +Die Messwerte sind die Vergleichsbasis für Gate 1 (Registry #60) und die +Flutter-vs-GTK-Messbasis (löst den offenen Punkt 5 aus §6 teilweise ein — die +`la_probe`-Messwerte existieren nun; der Flutter/GTK-Vergleich selbst bleibt +offen). diff --git a/packages/la_core/.gitignore b/packages/la_core/.gitignore new file mode 100644 index 0000000..a8c7d1d --- /dev/null +++ b/packages/la_core/.gitignore @@ -0,0 +1,2 @@ +.dart_tool/ +build/ diff --git a/packages/la_core/analysis_options.yaml b/packages/la_core/analysis_options.yaml new file mode 100644 index 0000000..572dd23 --- /dev/null +++ b/packages/la_core/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lints/recommended.yaml diff --git a/packages/la_core/bin/la_probe.dart b/packages/la_core/bin/la_probe.dart new file mode 100644 index 0000000..5055683 --- /dev/null +++ b/packages/la_core/bin/la_probe.dart @@ -0,0 +1,13 @@ +import 'dart:io'; + +import 'package:la_core/la_core.dart'; + +const String laProbeVersion = '0.0.1-spike.1'; + +Future main(List args) async { + if (args.contains('--version')) { + stdout.writeln('la_probe $laProbeVersion (dart ${Platform.version})'); + return; + } + stdout.writeln((await SelfProbe().run()).describe()); +} diff --git a/packages/la_core/lib/la_core.dart b/packages/la_core/lib/la_core.dart new file mode 100644 index 0000000..f98755f --- /dev/null +++ b/packages/la_core/lib/la_core.dart @@ -0,0 +1 @@ +export 'src/probe.dart'; diff --git a/packages/la_core/lib/src/probe.dart b/packages/la_core/lib/src/probe.dart new file mode 100644 index 0000000..83d846d --- /dev/null +++ b/packages/la_core/lib/src/probe.dart @@ -0,0 +1,32 @@ +enum ProbeLevel { ok, warn, crit, unknown } + +class ProbeResult { + const ProbeResult({ + required this.level, + required this.key, + this.params = const {}, + this.at, + }); + + final ProbeLevel level; + final String key; + final Map params; + final DateTime? at; + + String describe() => + '${level.name}:$key${at == null ? '' : ' @ ${at!.toIso8601String()}'}'; +} + +abstract interface class Probe { + String get id; + Future run(); +} + +class SelfProbe implements Probe { + @override + String get id => 'probe.self'; + + @override + Future run() async => + ProbeResult(level: ProbeLevel.ok, key: id, at: DateTime.now()); +} diff --git a/packages/la_core/pubspec.lock b/packages/la_core/pubspec.lock new file mode 100644 index 0000000..31ef82c --- /dev/null +++ b/packages/la_core/pubspec.lock @@ -0,0 +1,389 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: fdcd9f70f9eb80df3bc5ed0fa67280df2595fd481948df8a9ab082e6a40ad04b + url: "https://pub.dev" + source: hosted + version: "108.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: a51c769bff3b6dfbe9d60199b8606d808290702a296bef0c26a4ca391d414e46 + url: "https://pub.dev" + source: hosted + version: "14.4.0" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d" + url: "https://pub.dev" + source: hosted + version: "1.15.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "218aeb56050c714f62a3182775320dfa04602b55074873e24e31bbd39bda96fb" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: "2635216ca6a737e60de577ffa1a48a0bec76ca8a62917cfc1bb88c14c570646f" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + lints: + dependency: "direct dev" + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" + url: "https://pub.dev" + source: hosted + version: "0.12.20" + meta: + dependency: transitive + description: + name: meta + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" + url: "https://pub.dev" + source: hosted + version: "1.19.0" + mime: + dependency: transitive + description: + name: mime + sha256: bd47de35f07e27267e69c8c8b22edf9473bfee170a60d60fcc93730c5144b7f6 + url: "https://pub.dev" + source: hosted + version: "2.1.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d + url: "https://pub.dev" + source: hosted + version: "3.0.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "4177f68c237ea2128d1bee66ac17b2ce05ba3dbaafcbdd54c5d40a39d0b6b11c" + url: "https://pub.dev" + source: hosted + version: "1.5.3" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "261236774e8b1d69cfc6b9eabbc96c40f25e7a2d6b171f3385d4f65d5734fb24" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "14c2945847669b44089bb1222f66873d7ff7103c58911917f2a63c5a62327898" + url: "https://pub.dev" + source: hosted + version: "0.10.14" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "277654b3034d17ac6f9f1cb5595db011b1d5d41e8806866db28e0abaa101c490" + url: "https://pub.dev" + source: hosted + version: "1.12.2" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: de5d145b0afff7921e5e788a880f52d7e5f3ae24068a202f6fd3b58e4ba26323 + url: "https://pub.dev" + source: hosted + version: "1.32.0" + test_api: + dependency: transitive + description: + name: test_api + sha256: "0a10344e901e5b2e63819567951cb6a06673ed6b84f40462188ff5a0c41f371f" + url: "https://pub.dev" + source: hosted + version: "0.7.14" + test_core: + dependency: transitive + description: + name: test_core + sha256: "80f3fb49087454e07e7e07c67578cfdd156c8c3a5227d8b3f47c7b2d019c2e93" + url: "https://pub.dev" + source: hosted + version: "0.6.20" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0" + url: "https://pub.dev" + source: hosted + version: "15.3.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea + url: "https://pub.dev" + source: hosted + version: "3.1.4" +sdks: + dart: ">=3.11.0 <4.0.0" diff --git a/packages/la_core/pubspec.yaml b/packages/la_core/pubspec.yaml new file mode 100644 index 0000000..cd53900 --- /dev/null +++ b/packages/la_core/pubspec.yaml @@ -0,0 +1,11 @@ +name: la_core +description: Master Linux Assistant - reiner Dart-Kern (Spike #59, Registry #60). Keine Flutter-/GTK-Typen. +version: 0.0.1 +publish_to: none + +environment: + sdk: ^3.4.0 + +dev_dependencies: + lints: ^5.0.0 + test: ^1.25.0 diff --git a/packages/la_core/test/probe_test.dart b/packages/la_core/test/probe_test.dart new file mode 100644 index 0000000..cc2472b --- /dev/null +++ b/packages/la_core/test/probe_test.dart @@ -0,0 +1,20 @@ +import 'package:la_core/la_core.dart'; +import 'package:test/test.dart'; + +void main() { + test('ProbeResult.describe enthaelt Level und Key', () { + const r = ProbeResult(level: ProbeLevel.warn, key: 'mem.free'); + expect(r.describe(), startsWith('warn:mem.free')); + }); + + test('ProbeLevel deckt ok/warn/crit/unknown ab', () { + expect(ProbeLevel.values.map((l) => l.name), + containsAll(['ok', 'warn', 'crit', 'unknown'])); + }); + + test('SelfProbe liefert ok mit key probe.self', () async { + final r = await SelfProbe().run(); + expect(r.level, ProbeLevel.ok); + expect(r.key, 'probe.self'); + }); +} From 64ff62a181eb2f4d8e7d44e26b49240cd910445f Mon Sep 17 00:00:00 2001 From: ElGregor Date: Wed, 30 Sep 2026 00:21:53 +0200 Subject: [PATCH 08/24] feat(la_core): module descriptor + registry validation per issue #60 --- packages/la_core/lib/la_core.dart | 2 + .../la_core/lib/src/module_descriptor.dart | 27 +++++++ packages/la_core/lib/src/module_registry.dart | 77 +++++++++++++++++++ .../test/module_registry_validate_test.dart | 66 ++++++++++++++++ 4 files changed, 172 insertions(+) create mode 100644 packages/la_core/lib/src/module_descriptor.dart create mode 100644 packages/la_core/lib/src/module_registry.dart create mode 100644 packages/la_core/test/module_registry_validate_test.dart diff --git a/packages/la_core/lib/la_core.dart b/packages/la_core/lib/la_core.dart index f98755f..746fbf5 100644 --- a/packages/la_core/lib/la_core.dart +++ b/packages/la_core/lib/la_core.dart @@ -1 +1,3 @@ +export 'src/module_descriptor.dart'; +export 'src/module_registry.dart'; export 'src/probe.dart'; diff --git a/packages/la_core/lib/src/module_descriptor.dart b/packages/la_core/lib/src/module_descriptor.dart new file mode 100644 index 0000000..88ebad7 --- /dev/null +++ b/packages/la_core/lib/src/module_descriptor.dart @@ -0,0 +1,27 @@ +enum ModuleKind { section, tool, launch } + +/// Kern-Deskriptor ohne View-Builder (Issue #60): View-Factories +/// leben in den UI-Adaptern und binden ueber [viewId]. +class ModuleDescriptor { + const ModuleDescriptor({ + required this.id, + required this.titleKey, + required this.viewId, + required this.kind, + this.capabilities = const {}, + this.requires = const {}, + this.probeIds = const {}, + this.actionIds = const {}, + this.subscribedTopics = const {}, + }); + + final String id; + final String titleKey; + final String viewId; + final ModuleKind kind; + final Set capabilities; + final Set requires; + final Set probeIds; + final Set actionIds; + final Set subscribedTopics; +} diff --git a/packages/la_core/lib/src/module_registry.dart b/packages/la_core/lib/src/module_registry.dart new file mode 100644 index 0000000..76fd787 --- /dev/null +++ b/packages/la_core/lib/src/module_registry.dart @@ -0,0 +1,77 @@ +import 'module_descriptor.dart'; + +enum ModuleState { registered, starting, started, stopping, stopped } + +abstract interface class ModuleActivator { + Future start(ModuleDescriptor module); + Future stop(ModuleDescriptor module); +} + +class ModuleRegistryError implements Exception { + ModuleRegistryError(this.message); + final String message; + @override + String toString() => 'ModuleRegistryError: $message'; +} + +/// Statische Registrierung; kein Laden fremder Plugins (Issue #60). +class ModuleRegistry { + ModuleRegistry({required ModuleActivator activator}) : _activator = activator; + + final ModuleActivator _activator; + final Map _modules = {}; + final Map _states = {}; + final Map _visible = {}; + final Map> _inflight = {}; + final List _activationOrder = []; + bool _validated = false; + + void register(ModuleDescriptor descriptor) { + if (_modules.containsKey(descriptor.id)) { + throw ModuleRegistryError('duplicate module id: ${descriptor.id}'); + } + _modules[descriptor.id] = descriptor; + _states[descriptor.id] = ModuleState.registered; + _validated = false; + } + + List get descriptors => List.unmodifiable(_modules.values); + bool isRegistered(String id) => _modules.containsKey(id); + ModuleDescriptor? operator [](String id) => _modules[id]; + ModuleState stateOf(String id) => + _states[id] ?? (throw ModuleRegistryError('unknown module id: $id')); + bool isVisible(String id) => _visible[id] ?? false; + + void validate() { + for (final d in _modules.values) { + for (final dep in d.requires) { + if (!_modules.containsKey(dep)) { + throw ModuleRegistryError('${d.id} requires unknown module: $dep'); + } + } + } + _checkCycles(); + _validated = true; + } + + void _checkCycles() { + final mark = {}; // fehlend=neu, 1=in Arbeit, 2=fertig + void visit(String id, List path) { + final m = mark[id] ?? 0; + if (m == 2) return; + if (m == 1) { + throw ModuleRegistryError( + 'dependency cycle: ${[...path, id].join(' -> ')}'); + } + mark[id] = 1; + for (final dep in _modules[id]!.requires) { + visit(dep, [...path, id]); + } + mark[id] = 2; + } + + for (final id in _modules.keys) { + visit(id, []); + } + } +} diff --git a/packages/la_core/test/module_registry_validate_test.dart b/packages/la_core/test/module_registry_validate_test.dart new file mode 100644 index 0000000..80c133f --- /dev/null +++ b/packages/la_core/test/module_registry_validate_test.dart @@ -0,0 +1,66 @@ +import 'package:la_core/la_core.dart'; +import 'package:test/test.dart'; + +ModuleDescriptor mod(String id, {Set? requires}) { + return ModuleDescriptor( + id: id, + titleKey: 'title.$id', + viewId: 'view.$id', + kind: ModuleKind.tool, + requires: requires ?? const {}, + ); +} + +class _NoopActivator implements ModuleActivator { + @override + Future start(ModuleDescriptor module) async {} + + @override + Future stop(ModuleDescriptor module) async {} +} + +void main() { + test('register wirft bei doppelter ID', () { + final r = ModuleRegistry(activator: _NoopActivator()); + r.register(mod('a')); + expect( + () => r.register(mod('a')), + throwsA(isA()), + ); + }); + + test('validate wirft bei unbekannter Abhaengigkeit ghost', () { + final r = ModuleRegistry(activator: _NoopActivator()); + r.register(mod('a', requires: {'ghost'})); + expect( + () => r.validate(), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('ghost'), + ), + ), + ); + }); + + test('validate erkennt Zyklus a -> b -> a', () { + final r = ModuleRegistry(activator: _NoopActivator()); + r.register(mod('a', requires: {'b'})); + r.register(mod('b', requires: {'a'})); + expect( + () => r.validate(), + throwsA(isA()), + ); + }); + + test( + 'nach validate ist neue Registrierung moeglich und erneutes validate ' + 'wirft nicht', () { + final r = ModuleRegistry(activator: _NoopActivator()); + r.register(mod('a')); + r.validate(); + r.register(mod('b')); + expect(r.validate, returnsNormally); + }); +} From 7ada911c884d8c0fe08b4b815d4c8a6f080c7a2f Mon Sep 17 00:00:00 2001 From: ElGregor Date: Wed, 30 Sep 2026 00:33:28 +0200 Subject: [PATCH 09/24] feat(la_core): registry lifecycle - single-flight activation, reverse stop per issue #60 --- packages/la_core/lib/src/module_registry.dart | 106 ++++++++++++ .../test/module_registry_lifecycle_test.dart | 156 ++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 packages/la_core/test/module_registry_lifecycle_test.dart diff --git a/packages/la_core/lib/src/module_registry.dart b/packages/la_core/lib/src/module_registry.dart index 76fd787..f4976c0 100644 --- a/packages/la_core/lib/src/module_registry.dart +++ b/packages/la_core/lib/src/module_registry.dart @@ -74,4 +74,110 @@ class ModuleRegistry { visit(id, []); } } + + Future activate(String id) { + if (!_modules.containsKey(id)) { + throw ModuleRegistryError('unknown module id: $id'); + } + if (!_validated) validate(); + return _inflight[id] ??= _activateSubtree(id); + } + + Future _activateSubtree(String id) async { + for (final depId in _dependenciesOf(id)) { + if (_states[depId] != ModuleState.started) { + await (_inflight[depId] ??= _activateSubtree(depId)); + } + } + if (_states[id] == ModuleState.started) return; + _states[id] = ModuleState.starting; + try { + await _activator.start(_modules[id]!); + _states[id] = ModuleState.started; + _activationOrder.add(id); + } finally { + _inflight.remove(id); + } + } + + List _dependenciesOf(String id) { + final out = []; + void collect(String m, Set seen) { + for (final dep in _modules[m]!.requires) { + if (seen.add(dep)) { + collect(dep, seen); + out.add(dep); + } + } + } + + collect(id, {id}); + return out; + } + + Future deactivate(String id) { + if (!_modules.containsKey(id)) { + throw ModuleRegistryError('unknown module id: $id'); + } + return _inflight[id] ??= _deactivateWithDependents(id); + } + + Future _deactivateWithDependents(String id) async { + // Snapshot: _stopModule entfernt aus _activationOrder (lazy .reversed + // wuerde sonst Concurrent Modification werfen). + for (final active in List.of(_activationOrder).reversed) { + if (active == id) break; + if (_states[active] == ModuleState.started && + _transitivelyDependsOn(active, id)) { + await (_inflight[active] ??= _stopModule(active)); + } + } + // Direkt stoppen: _inflight[id] enthaelt bereits die Future DIESES + // Deactivate-Laufs (Eintrag in deactivate()); `??=` wuerde sonst auf + // uns selbst warten -> Deadlock. Single-Flight bleibt am EntryPoint + // von deactivate() gewaehrleistet. + if (_states[id] == ModuleState.started) { + await _stopModule(id); + } else { + _inflight.remove(id); + } + } + + Future _stopModule(String id) async { + _states[id] = ModuleState.stopping; + try { + await _activator.stop(_modules[id]!); + _states[id] = ModuleState.stopped; + _activationOrder.remove(id); + } finally { + _inflight.remove(id); + } + } + + bool _transitivelyDependsOn(String m, String target, [Set? seen]) { + seen ??= {}; + for (final dep in _modules[m]!.requires) { + if (dep == target || + (seen.add(dep) && _transitivelyDependsOn(dep, target, seen))) { + return true; + } + } + return false; + } + + Future deactivateAll() async { + for (final id in List.of(_activationOrder).reversed) { + if (_states[id] == ModuleState.started) { + await (_inflight[id] ??= _stopModule(id)); + } + } + } + + void setVisible(String id, bool visible) { + if (_states[id] != ModuleState.started) { + throw ModuleRegistryError( + 'setVisible($id, $visible): module not started'); + } + _visible[id] = visible; + } } diff --git a/packages/la_core/test/module_registry_lifecycle_test.dart b/packages/la_core/test/module_registry_lifecycle_test.dart new file mode 100644 index 0000000..5530e51 --- /dev/null +++ b/packages/la_core/test/module_registry_lifecycle_test.dart @@ -0,0 +1,156 @@ +import 'dart:async'; + +import 'package:la_core/la_core.dart'; +import 'package:test/test.dart'; + +ModuleDescriptor mod(String id, {Set? requires}) { + return ModuleDescriptor( + id: id, + titleKey: 'title.$id', + viewId: 'view.$id', + kind: ModuleKind.tool, + requires: requires ?? const {}, + ); +} + +class RecordingActivator implements ModuleActivator { + RecordingActivator({this.gate}); + + final List log = []; + + /// Wenn gesetzt, wartet [start] auf dieses Gate (Single-Flight-Tests). + final Completer? gate; + + @override + Future start(ModuleDescriptor module) async { + log.add('start ${module.id}'); + final g = gate; + if (g != null) { + await g.future; + } + } + + @override + Future stop(ModuleDescriptor module) async { + log.add('stop ${module.id}'); + } +} + +void main() { + test('activate startet Abhaengigkeiten in Topo-Reihenfolge a -> b -> c', + () async { + final activator = RecordingActivator(); + final r = ModuleRegistry(activator: activator); + r.register(mod('a', requires: {'b'})); + r.register(mod('b', requires: {'c'})); + r.register(mod('c')); + + await r.activate('a'); + + expect(activator.log, ['start c', 'start b', 'start a']); + }); + + test('geteilte Abhaengigkeit startet genau einmal (lazy)', () async { + final activator = RecordingActivator(); + final r = ModuleRegistry(activator: activator); + r.register(mod('a', requires: {'c'})); + r.register(mod('b', requires: {'c'})); + r.register(mod('c')); + + await r.activate('a'); + await r.activate('b'); + + expect( + activator.log.where((entry) => entry == 'start c'), + hasLength(1), + ); + expect(activator.log, ['start c', 'start a', 'start b']); + }); + + test('Single-Flight: zwei activate vor gate.complete() starten nur einmal', + () async { + final gate = Completer(); + final activator = RecordingActivator(gate: gate); + final r = ModuleRegistry(activator: activator); + r.register(mod('a')); + + final first = r.activate('a'); + final second = r.activate('a'); + expect(activator.log, ['start a']); + + gate.complete(); + await first; + await second; + expect(activator.log, ['start a']); + }); + + test('deactivate stoppt aktive Abhaengige rueckwaerts vor dem Ziel', + () async { + final activator = RecordingActivator(); + final r = ModuleRegistry(activator: activator); + r.register(mod('x', requires: {'y'})); + r.register(mod('z', requires: {'y'})); + r.register(mod('y')); + + await r.activate('x'); + await r.activate('z'); + expect(activator.log, ['start y', 'start x', 'start z']); + activator.log.clear(); + + await r.deactivate('y'); + + expect(activator.log, ['stop z', 'stop x', 'stop y']); + }); + + test('setVisible wirft vor Start und wirkt danach', () async { + final r = ModuleRegistry(activator: RecordingActivator()); + r.register(mod('a')); + + expect( + () => r.setVisible('a', true), + throwsA(isA()), + ); + expect(r.isVisible('a'), isFalse); + + await r.activate('a'); + r.setVisible('a', true); + expect(r.isVisible('a'), isTrue); + r.setVisible('a', false); + expect(r.isVisible('a'), isFalse); + }); + + test('stateOf: registered -> starting -> started -> stopped', () async { + final gate = Completer(); + final activator = RecordingActivator(gate: gate); + final r = ModuleRegistry(activator: activator); + r.register(mod('a')); + + expect(r.stateOf('a'), ModuleState.registered); + + unawaited(r.activate('a')); + await Future.delayed(Duration.zero); + expect(r.stateOf('a'), ModuleState.starting); + + gate.complete(); + await Future.delayed(Duration.zero); + expect(r.stateOf('a'), ModuleState.started); + + await r.deactivate('a'); + expect(r.stateOf('a'), ModuleState.stopped); + }); + + test('deactivateAll stoppt in umgekehrter Aktivierungsreihenfolge', () async { + final activator = RecordingActivator(); + final r = ModuleRegistry(activator: activator); + r.register(mod('a', requires: {'b'})); + r.register(mod('b', requires: {'c'})); + r.register(mod('c')); + + await r.activate('a'); + activator.log.clear(); + + await r.deactivateAll(); + + expect(activator.log, ['stop a', 'stop b', 'stop c']); + }); +} From e41304c6a8b3f6a71839e7376b324ac7904595b3 Mon Sep 17 00:00:00 2001 From: ElGregor Date: Wed, 30 Sep 2026 00:44:10 +0200 Subject: [PATCH 10/24] docs(mla): check off gate 1 spike + registry portions with evidence --- docs/mla-next/VERIFY.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/mla-next/VERIFY.md b/docs/mla-next/VERIFY.md index 2f4d8b7..4130b54 100644 --- a/docs/mla-next/VERIFY.md +++ b/docs/mla-next/VERIFY.md @@ -12,9 +12,9 @@ Stand dieses Branchs: keine hier dokumentierten GTK-Laufzeit-, Zorin-, IPC- oder ## Gate 1: Kern/Registry -- [ ] Issue #59: `dart test` und `dart compile exe`; Probe ohne DISPLAY/WAYLAND_DISPLAY; Binärgröße/Startzeit messen. -- [ ] Issue #60: IDs eindeutig, fehlende/zyklische Abhängigkeiten abgewiesen, Start/Stop/Lazy-Loading getestet; Flutter-Navigation unverändert. -- [ ] Bestehende Repo-Gates nach Scope tatsächlich ausführen: `tool/check-versions.sh`, `dart format`, `flutter analyze`, `flutter test`, Python-Tests. +- [x] Issue #59: `dart test` und `dart compile exe`; Probe ohne DISPLAY/WAYLAND_DISPLAY; Binärgröße/Startzeit messen. — 2026-09-30: `dart test` 14/14 in packages/la_core (frisch); `dart compile exe` + Headless-Lauf ohne DISPLAY/WAYLAND_DISPLAY; Binär 6 547 240 Bytes, Median-Startzeit 3 ms — siehe docs/mla-next/BASELINE.md §7 +- [x] Issue #60: IDs eindeutig, fehlende/zyklische Abhängigkeiten abgewiesen, Start/Stop/Lazy-Loading getestet; Flutter-Navigation unverändert. — 2026-09-30: `dart test` 14/14 in packages/la_core (doppelte IDs, fehlende/zyklische Abhängigkeiten, Topo-Start/Rückwärts-Stop, Single-Flight); `git diff --stat 92bef60..HEAD -- lib/ additional/ deb/ linux/` leer (Exit 0); Root-`flutter test` +184 bestanden +- [x] Bestehende Repo-Gates nach Scope tatsächlich ausführen: `tool/check-versions.sh`, `dart format`, `flutter analyze`, `flutter test`, Python-Tests. — 2026-09-30 frisch ausgeführt, alle Exit 0: `version 0.8.0 is consistent`; `Formatted 118 files (0 changed)`; `No issues found!`; `00:02 +184: All tests passed!`; `Ran 49 tests` / `OK` — vgl. docs/mla-next/BASELINE.md §4 ## Gate 2: IPC und Backup From ac84be8bb204a17de1b3223f64c6ac22e2d6cf95 Mon Sep 17 00:00:00 2001 From: ElGregor Date: Wed, 30 Sep 2026 00:51:46 +0200 Subject: [PATCH 11/24] docs(mla): true up gate evidence (final review fix) --- docs/mla-next/BASELINE.md | 12 +++++++++--- docs/mla-next/VERIFY.md | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/mla-next/BASELINE.md b/docs/mla-next/BASELINE.md index 53c1625..6abec8b 100644 --- a/docs/mla-next/BASELINE.md +++ b/docs/mla-next/BASELINE.md @@ -132,6 +132,11 @@ Python 3.12.3). Alle fünf Gates grün. | Dart-Tests | `flutter test` | 0 | `00:02 +184: All tests passed!` | | Python-Tests | `cd additional/python && python3 -m unittest discover -s tests -t .` | 0 | `Ran 49 tests in 0.052s` / `OK` | +Frischlauf 2026-09-30: alle 5 Gates erneut Exit 0 (`flutter test` +184, +49 Python-Tests, Format/Analyze/check-versions ohne Befunde) — Details im +Task-7a-Report (Scratch, `.superpowers/sdd/task-7a-report.md`, nicht Teil +des Repos). + Rohausgaben 1:1 (Exit-Zeile jeweils ergänzt): ```text @@ -209,9 +214,10 @@ dieser Datei). zurückgenommen. Entscheidung offen: Exclude dauerhaft übernehmen oder Analyzer-Verhalten ignorieren — vor dem nächsten Gate-Lauf klären, sonst bleibt der Working Tree nicht sauber. -5. **Messbasis 0.0.3 offen:** der Flutter-vs-GTK-Vergleich (Roadmap - 0.0.3/0.4.x) hat noch keine `la_probe`-Messwerte; §7 bleibt bis Gate 1 - Platzhalter. Diese Baseline (§1, §2, §4) ist die Vergleichsgrundlage. +5. **Messbasis 0.0.3 (aufgelöst 2026-09-30, teils):** die `la_probe`-Messwerte + liegen vor (§7, Task-4/#59-Spike — §7 ist kein Platzhalter mehr); offen + bleibt der Flutter-vs-GTK-Vergleich selbst (Roadmap 0.0.3/0.4.x). Diese + Baseline (§1, §2, §4) ist die Vergleichsgrundlage. ## §7 la_probe-Messung (Task 4/#59-Spike, 2026-09-30) diff --git a/docs/mla-next/VERIFY.md b/docs/mla-next/VERIFY.md index 4130b54..12f450a 100644 --- a/docs/mla-next/VERIFY.md +++ b/docs/mla-next/VERIFY.md @@ -1,6 +1,6 @@ # MLA-Next: Verifikation und Handoff -Stand dieses Branchs: keine hier dokumentierten GTK-Laufzeit-, Zorin-, IPC- oder Performance-Tests ausgeführt. Der Scaffold-Commit ist **kein** Release-Gate. +Aktualisierung 2026-09-30: Die GTK-Scaffold-Laufzeit ist auf Zorin verifiziert (Wayland- und X11-Start — Gate 0, BASELINE §2; manuelle Checks offen), la_core-Spike und Registry sind getestet inkl. Performance-Messwerten (Gate 1, BASELINE §7). Weiterhin nicht ausgeführt: IPC-/Gate-2-Tests, Flutter-vs-GTK-Vergleichsmessung (Roadmap 0.0.3), manuelle Gate-0-Checks (BASELINE §3). Der Scaffold-Commit ist **kein** Release-Gate. ## Gate 0: Scaffold From 1367d3c9e6231695d882a58a930ac2d8ad3f599b Mon Sep 17 00:00:00 2001 From: ElGregor Date: Wed, 30 Sep 2026 01:02:58 +0200 Subject: [PATCH 12/24] chore(analyze): adopt analyzer excludes auto-written by flutter tool --- analysis_options.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/analysis_options.yaml b/analysis_options.yaml index 6364062..192d12a 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,6 +7,14 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. +# +# The excludes below are what `flutter analyze`/`flutter test` auto-write on +# every run since Flutter 3.47 ("Upgrading analysis_options.yaml"); adopted +# 2026-09-30 so automated gate runs leave a clean tree. +analyzer: + exclude: + - build/** + - linux/** include: package:flutter_lints/flutter.yaml linter: From bf4942b3ae30f9969627cfc33d8a29925b48528e Mon Sep 17 00:00:00 2001 From: ElGregor Date: Wed, 30 Sep 2026 01:02:58 +0200 Subject: [PATCH 13/24] fix(la_core): align sdk bound with committed lockfile (^3.11.0) --- packages/la_core/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/la_core/pubspec.yaml b/packages/la_core/pubspec.yaml index cd53900..0a1d579 100644 --- a/packages/la_core/pubspec.yaml +++ b/packages/la_core/pubspec.yaml @@ -4,7 +4,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ^3.4.0 + sdk: ^3.11.0 dev_dependencies: lints: ^5.0.0 From ddcafa401a4dd013789f5d3be9cb75d9fc139e42 Mon Sep 17 00:00:00 2001 From: ElGregor Date: Wed, 30 Sep 2026 01:02:58 +0200 Subject: [PATCH 14/24] style(la_core): apply dart format to registry and tests --- packages/la_core/lib/src/module_registry.dart | 6 +- .../test/module_registry_lifecycle_test.dart | 100 +++++++++--------- .../test/module_registry_validate_test.dart | 13 +-- packages/la_core/test/probe_test.dart | 6 +- 4 files changed, 61 insertions(+), 64 deletions(-) diff --git a/packages/la_core/lib/src/module_registry.dart b/packages/la_core/lib/src/module_registry.dart index f4976c0..f7202eb 100644 --- a/packages/la_core/lib/src/module_registry.dart +++ b/packages/la_core/lib/src/module_registry.dart @@ -61,7 +61,8 @@ class ModuleRegistry { if (m == 2) return; if (m == 1) { throw ModuleRegistryError( - 'dependency cycle: ${[...path, id].join(' -> ')}'); + 'dependency cycle: ${[...path, id].join(' -> ')}', + ); } mark[id] = 1; for (final dep in _modules[id]!.requires) { @@ -176,7 +177,8 @@ class ModuleRegistry { void setVisible(String id, bool visible) { if (_states[id] != ModuleState.started) { throw ModuleRegistryError( - 'setVisible($id, $visible): module not started'); + 'setVisible($id, $visible): module not started', + ); } _visible[id] = visible; } diff --git a/packages/la_core/test/module_registry_lifecycle_test.dart b/packages/la_core/test/module_registry_lifecycle_test.dart index 5530e51..f423a65 100644 --- a/packages/la_core/test/module_registry_lifecycle_test.dart +++ b/packages/la_core/test/module_registry_lifecycle_test.dart @@ -37,18 +37,20 @@ class RecordingActivator implements ModuleActivator { } void main() { - test('activate startet Abhaengigkeiten in Topo-Reihenfolge a -> b -> c', - () async { - final activator = RecordingActivator(); - final r = ModuleRegistry(activator: activator); - r.register(mod('a', requires: {'b'})); - r.register(mod('b', requires: {'c'})); - r.register(mod('c')); - - await r.activate('a'); - - expect(activator.log, ['start c', 'start b', 'start a']); - }); + test( + 'activate startet Abhaengigkeiten in Topo-Reihenfolge a -> b -> c', + () async { + final activator = RecordingActivator(); + final r = ModuleRegistry(activator: activator); + r.register(mod('a', requires: {'b'})); + r.register(mod('b', requires: {'c'})); + r.register(mod('c')); + + await r.activate('a'); + + expect(activator.log, ['start c', 'start b', 'start a']); + }, + ); test('geteilte Abhaengigkeit startet genau einmal (lazy)', () async { final activator = RecordingActivator(); @@ -60,56 +62,54 @@ void main() { await r.activate('a'); await r.activate('b'); - expect( - activator.log.where((entry) => entry == 'start c'), - hasLength(1), - ); + expect(activator.log.where((entry) => entry == 'start c'), hasLength(1)); expect(activator.log, ['start c', 'start a', 'start b']); }); - test('Single-Flight: zwei activate vor gate.complete() starten nur einmal', - () async { - final gate = Completer(); - final activator = RecordingActivator(gate: gate); - final r = ModuleRegistry(activator: activator); - r.register(mod('a')); - - final first = r.activate('a'); - final second = r.activate('a'); - expect(activator.log, ['start a']); - - gate.complete(); - await first; - await second; - expect(activator.log, ['start a']); - }); + test( + 'Single-Flight: zwei activate vor gate.complete() starten nur einmal', + () async { + final gate = Completer(); + final activator = RecordingActivator(gate: gate); + final r = ModuleRegistry(activator: activator); + r.register(mod('a')); + + final first = r.activate('a'); + final second = r.activate('a'); + expect(activator.log, ['start a']); + + gate.complete(); + await first; + await second; + expect(activator.log, ['start a']); + }, + ); - test('deactivate stoppt aktive Abhaengige rueckwaerts vor dem Ziel', - () async { - final activator = RecordingActivator(); - final r = ModuleRegistry(activator: activator); - r.register(mod('x', requires: {'y'})); - r.register(mod('z', requires: {'y'})); - r.register(mod('y')); + test( + 'deactivate stoppt aktive Abhaengige rueckwaerts vor dem Ziel', + () async { + final activator = RecordingActivator(); + final r = ModuleRegistry(activator: activator); + r.register(mod('x', requires: {'y'})); + r.register(mod('z', requires: {'y'})); + r.register(mod('y')); - await r.activate('x'); - await r.activate('z'); - expect(activator.log, ['start y', 'start x', 'start z']); - activator.log.clear(); + await r.activate('x'); + await r.activate('z'); + expect(activator.log, ['start y', 'start x', 'start z']); + activator.log.clear(); - await r.deactivate('y'); + await r.deactivate('y'); - expect(activator.log, ['stop z', 'stop x', 'stop y']); - }); + expect(activator.log, ['stop z', 'stop x', 'stop y']); + }, + ); test('setVisible wirft vor Start und wirkt danach', () async { final r = ModuleRegistry(activator: RecordingActivator()); r.register(mod('a')); - expect( - () => r.setVisible('a', true), - throwsA(isA()), - ); + expect(() => r.setVisible('a', true), throwsA(isA())); expect(r.isVisible('a'), isFalse); await r.activate('a'); diff --git a/packages/la_core/test/module_registry_validate_test.dart b/packages/la_core/test/module_registry_validate_test.dart index 80c133f..2535655 100644 --- a/packages/la_core/test/module_registry_validate_test.dart +++ b/packages/la_core/test/module_registry_validate_test.dart @@ -23,10 +23,7 @@ void main() { test('register wirft bei doppelter ID', () { final r = ModuleRegistry(activator: _NoopActivator()); r.register(mod('a')); - expect( - () => r.register(mod('a')), - throwsA(isA()), - ); + expect(() => r.register(mod('a')), throwsA(isA())); }); test('validate wirft bei unbekannter Abhaengigkeit ghost', () { @@ -48,14 +45,10 @@ void main() { final r = ModuleRegistry(activator: _NoopActivator()); r.register(mod('a', requires: {'b'})); r.register(mod('b', requires: {'a'})); - expect( - () => r.validate(), - throwsA(isA()), - ); + expect(() => r.validate(), throwsA(isA())); }); - test( - 'nach validate ist neue Registrierung moeglich und erneutes validate ' + test('nach validate ist neue Registrierung moeglich und erneutes validate ' 'wirft nicht', () { final r = ModuleRegistry(activator: _NoopActivator()); r.register(mod('a')); diff --git a/packages/la_core/test/probe_test.dart b/packages/la_core/test/probe_test.dart index cc2472b..4781e43 100644 --- a/packages/la_core/test/probe_test.dart +++ b/packages/la_core/test/probe_test.dart @@ -8,8 +8,10 @@ void main() { }); test('ProbeLevel deckt ok/warn/crit/unknown ab', () { - expect(ProbeLevel.values.map((l) => l.name), - containsAll(['ok', 'warn', 'crit', 'unknown'])); + expect( + ProbeLevel.values.map((l) => l.name), + containsAll(['ok', 'warn', 'crit', 'unknown']), + ); }); test('SelfProbe liefert ok mit key probe.self', () async { From ec6f5915c43858ac12098479310df66f051906ed Mon Sep 17 00:00:00 2001 From: ElGregor Date: Wed, 30 Sep 2026 01:02:58 +0200 Subject: [PATCH 15/24] ci(mla): run la_core gates and headless compile in Dart workflow --- .github/workflows/build.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d23ecd1..874da26 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,6 +42,26 @@ jobs: - name: Root runner tests run: python3 -m unittest discover -s tests -t . working-directory: additional/python + # MLA-Next: the pure-Dart core package (#59/#60) runs its own gates with + # plain `dart` from the same SDK — no Flutter, no GTK toolchain, no + # display (issue #59 requires the compiled probe to run headless in CI). + - name: la_core pub get + run: dart pub get + working-directory: packages/la_core + - name: la_core static analysis + run: dart analyze + working-directory: packages/la_core + - name: la_core formatting + run: dart format --output=none --set-exit-if-changed . + working-directory: packages/la_core + - name: la_core tests + run: dart test + working-directory: packages/la_core + - name: la_core headless binary + run: | + dart compile exe bin/la_probe.dart -o /tmp/la_probe + /tmp/la_probe --version + working-directory: packages/la_core - run: sudo apt-get update -y - run: sudo apt-get install -y ninja-build libgtk-3-dev libkeybinder-3.0-0 libkeybinder-3.0-dev mesa-utils - run: flutter config --enable-linux-desktop From 4aaf60821032b60bd0f007c5cf3cc4d19fccaab2 Mon Sep 17 00:00:00 2001 From: ElGregor Date: Wed, 30 Sep 2026 01:05:23 +0200 Subject: [PATCH 16/24] fix(analyze): exclude nested packages from root analyzer (monorepo) --- analysis_options.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 192d12a..38cbe5f 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -10,11 +10,15 @@ # # The excludes below are what `flutter analyze`/`flutter test` auto-write on # every run since Flutter 3.47 ("Upgrading analysis_options.yaml"); adopted -# 2026-09-30 so automated gate runs leave a clean tree. +# 2026-09-30 so automated gate runs leave a clean tree. `packages/**` is a +# deliberate monorepo addition: nested packages (la_core) own their +# analysis_options and run their own `dart analyze` gate — from the root +# context their imports (package:test, package:la_core) do not resolve. analyzer: exclude: - build/** - linux/** + - packages/** include: package:flutter_lints/flutter.yaml linter: From 513def150dbb37ff3883b2061396f7f326e4527b Mon Sep 17 00:00:00 2001 From: ElGregor Date: Wed, 30 Sep 2026 01:40:58 +0200 Subject: [PATCH 17/24] docs(mla): mirror MLA-Next milestones/issues and elaborated roadmap bodies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spiegel der GitHub-Stände vom 2026-09-30: sieben MLA-Next-Milestones (#12-#18 auf GitHub), 17 Issues #90-#106 (Label track:mla-next) und die ausgearbeiteten Bodies der Roadmap-Issues #58-#87. Nur neue Dateien unter docs/mla-next/; kein Code, keine Policy, kein Packaging. Co-Authored-By: Claude Sonnet 5.5 --- docs/mla-next/ISSUES-ROADMAP-ELABORATED.md | 1364 ++++++++++++++++++++ docs/mla-next/ISSUES.md | 597 +++++++++ docs/mla-next/MILESTONES.md | 46 + 3 files changed, 2007 insertions(+) create mode 100644 docs/mla-next/ISSUES-ROADMAP-ELABORATED.md create mode 100644 docs/mla-next/ISSUES.md create mode 100644 docs/mla-next/MILESTONES.md diff --git a/docs/mla-next/ISSUES-ROADMAP-ELABORATED.md b/docs/mla-next/ISSUES-ROADMAP-ELABORATED.md new file mode 100644 index 0000000..9a7dd47 --- /dev/null +++ b/docs/mla-next/ISSUES-ROADMAP-ELABORATED.md @@ -0,0 +1,1364 @@ +# Roadmap V0.8.6–V1.0 — ausgearbeitete Issue-Bodies (#58–#87) + +> Spiegel der GitHub-Bodies nach der Ausarbeitung vom 2026-09-30. Der Originaltext jedes Issues bleibt enthalten; ergänzt sind Ziel, Andockpunkte, Teststrategie, Risiko, Abhängigkeiten und Gates. Der Ursprung `issues-v0.8.6-v1.0.md` im Repo-Wurzelverzeichnis bleibt unverändert (die dortigen Bodies sind kürzer als der Stand auf GitHub). + +--- + +## #58 — [V0.8.6] Welle 1 „Sofort-Nutzen": Q1–Q7 + +```markdown +## Ziel +Sieben kleine, einzeln abnehmbare Verbesserungen ohne neue Architektur — der schnellste sichtbare Nutzen vor dem Fundament (V0.9). + +## Scope +Welle 1 als ein gemeinsamer Plan (7 Tasks, je S). Keine neue Architektur — alles dockt an Bestandescode an. Q7 entscheidet Basti vorab (Empfehlung: entfernen). + +## Abnahme +- [ ] **Q1 Security-Scan auf Knopfdruck:** `_checkerOutput` (`security_check/overview.dart:54`) startet nicht mehr beim ersten Build, sondern über „Prüfen"; kein pkexec-Dialog beim Betreten der Sektion; letztes Ergebnis (Befund-Tokens + Zeitstempel) in `$XDG_CACHE_HOME/linux-assistant/security-last.json`, nach App-Neustart mit „vor X" +- [ ] **Q2 Neustart-Badge:** `/var/run/reboot-required(.pkgs)` → Badge im Dashboard + Zeile in der Health-Sektion mit auslösenden Paketen; nach dem Neustart weg +- [ ] **Q3 Kachel „Fehlgeschlagene Dienste":** `systemctl [--user] list-units --failed -o json`, beide Scopes → Kachel mit Zahl und Liste (Unit, Beschreibung, Scope); zeigt auf dem Referenzsystem aktuell zwei fehlschlagende Backup-Units +- [ ] **Q4 Hotkey über gsettings in beiden Sessions:** `hotkey_manager` + `libkeybinder-3.0-0` raus (pubspec, `deb/DEBIAN/control`, README, CI-apt-Zeile); X11-Keybinding über `additional/python/setup_keybinding.py` + `keybinding_files.py`; Diagnosezeile: aktuelles Binding + verwaiste `custom*`-Einträge; Gate „Hotkey holt das Fenster, Fokus im Suchfeld" in Wayland UND X11 +- [ ] **Q5 Browser-Status:** read-only „Standard ist X" aus `AppLauncher.defaultBrowserDesktopId()` (aus V0.8.2); Warnung, wenn `gio mime x-scheme-handler/https` abweicht; neue Zeile in `features.csv` +- [ ] **Q6 Speicher-Kachel ehrlich:** Warnton nach PSI (`/proc/pressure/memory`, avg60) statt nur RAM-Quote; Swap-Zeile trennt zram (`/sys/block/zram*/mm_stat`) von Disk-Swap; bei heutiger Last kein Alarm, zram zeigt „7,2 → 2,1 GB" +- [ ] **Q7 Feedback an Upstream abklemmen:** Empfehlung entfernen (Menüeintrag, `feedback_service.dart`, `lib/layouts/feedback/*`, l10n-Keys) — Alternative: Link auf die Fork-Issues +- [ ] Fünf CI-Gates grün; l10n-Ratchet (alle vier ARB); features.csv-Check (21 Felder) bei neuen Zeilen + +## Andockpunkte (geprüft 2026-09-30) +- Q4: `hotkey_manager` in `pubspec.yaml:53`, `libkeybinder-3.0-0` in `deb/DEBIAN/control:2` und `README.md`, CI-Zeile `.github/workflows/build.yml:66` (der Ausbauplan nannte `:46`, die Zeile ist verschoben); Python-Seite `additional/python/setup_keybinding.py`, `additional/python/keybinding_files.py` +- Q5: `AppLauncher.defaultBrowserDesktopId()` existiert im Code noch **nicht** (Stand 2026-09-30, entsteht in V0.8.2 #34); `kKnownBrowsers` steht in `lib/services/app_launcher.dart:20` +- Q1: `lib/layouts/security_check/overview.dart:54` (`_checkerOutput = _runChecker()`, startet beim ersten Build); Q7: `lib/services/feedback_service.dart`, `lib/layouts/feedback/*` +- Q6: `lib/services/system_stats_service.dart` (`parseFreeOutput`, ab :32), `_memoryTile` in `lib/layouts/hub/dashboard_section.dart:226` + +## Risiko & Fallback +Q4 ist eher M als S (zwei Sitzungstypen, Diagnosezeile, Paket-Abhängigkeit entfällt). Q7 entscheidet Basti vorab (Empfehlung: entfernen); ohne Entscheidung bleibt Q7 offen und blockiert die übrigen Punkte nicht. + +## Abhängigkeiten +Blockiert durch: #41 (V0.8.5 abgeschlossen (Release-Gates, E2E)), #34 (nur Q5: XDG-Stufe und `defaultBrowserDesktopId()` aus V0.8.2) +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue Funktion → Zeile in `features.csv` (21 Felder; `yes` nur bei Zorin OS + GNOME, sonst `?`) +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Abhängigkeit: V0.8.5 abgeschlossen; Q5 braucht V0.8.2._ +_Quelle: Ausbauplan V0.8.6–V1.0, Welle 1 (Q1–Q7)._ +``` + +--- + +## #59 — [V0.9] FU2 Reiner Dart-Kern packages/la_core + +```markdown +## Ziel +Probes, Parser und Doctor laufen ohne GUI; die Reinheit erzwingt der Build. Voraussetzung für den Wächter (#66) und den Doctor (#78). + +## Scope +Logik, die ohne GUI laufen muss (Probes, Parser, Doctor), liegt in einem Paket **ohne Flutter-Abhängigkeit** — die Reinheit erzwingt der Build, nicht eine Konvention. Voraussetzung für LB3 (Wächter) und ST3 (Doctor). + +**Spike (eine Session, zuerst):** +- `dart compile exe packages/la_core/bin/la_probe.dart` mit dem Dart aus dem Flutter-SDK, lokal und in CI +- Messen: Binärgröße, Startzeit, Lauf ohne `DISPLAY`/`WAYLAND_DISPLAY` + +**Danach (Spec + Plan):** +- `CommandHelper` umziehen (heute `lib/helpers/command_helper.dart`, nur `dart:io`) +- Logger-Shim ohne `flutter/foundation` (heute hängt `lib/services/logger.dart:1` daran) +- Probe-Vertrag: `Probe`, `ProbeResult{level: ok|warn|crit|unknown, key, params, at}` +- Parser für free, df, uptime und ps aus `system_stats_service.dart:32`, `linux_filesystem.dart`, `linux_system.dart:36ff`, `linux_process.dart:19ff` +- App bindet das Paket als path-Dependency ein; `test` als dev_dependency (begründete Ausnahme vom pub-Budget) + +Nicht: `linux.dart` komplett zerlegen — umgezogen wird nur, was Probes brauchen (M5 geht schrittweise weiter). + +## Abnahme +- [ ] `flutter test` und `dart test` in `packages/la_core` grün +- [x] `la_probe --version` läuft auf Zorin ohne Session-Variablen +- [ ] `build-deb.sh` legt das Binary nach `/usr/lib/linux-assistant/` +- [ ] Risiko geprüft (Verflechtung größer als gedacht): Fallback = Probes vorerst im App-Prozess, Wächter später + +## Stand 2026-09-30 (Branch `feature/mla-gtk-scaffold`, Draft-PR #89) +Spike und Probe-Vertrag sind im Branch umgesetzt und belegt (`docs/mla-next/BASELINE.md` §7, `docs/mla-next/VERIFY.md` Gate 1): +- `dart test` in `packages/la_core`: 14/14 grün (Probe + Registry) +- `dart compile exe bin/la_probe.dart`; Lauf mit `env -u DISPLAY -u WAYLAND_DISPLAY`: `la_probe --version` Exit 0 +- Messwerte (Zorin OS 18.1, Dart 3.13.4): Binärgröße 6 547 240 Bytes, Median-Startzeit 3 ms (5 Läufe) +- Root-Gates unverändert grün: `flutter test` +184, `flutter analyze` 0 Findings, Python-Tests 49 OK +- CI-Schritte für `la_core` sind im Branch eingerichtet + +**Offen:** `build-deb.sh` legt das Binary ab · Parser-Umzug · Logger-Shim/`CommandHelper`-Umzug · App-Einbindung als path-Dependency · Risiko-Fallback prüfen. +Erste Abnahmezeile: `dart test` ist erfüllt; `flutter test` im reinen Dart-Paket ist nicht sinnvoll (bewusst ohne Flutter-Abhängigkeit) → Formulierung im Spec klären, das Kästchen bleibt daher offen. + +## Andockpunkte (geprüft 2026-09-30) +- `lib/helpers/command_helper.dart` (nur `dart:io`) und `lib/services/logger.dart:1` (importiert `flutter/foundation`, braucht einen Shim) +- Parser: `parseFreeOutput` in `lib/services/system_stats_service.dart:32`, `lib/linux/linux_filesystem.dart`, `parseUptime` in `lib/linux/linux_system.dart`, ps-Parser in `lib/linux/linux_process.dart` +- Im Branch `feature/mla-gtk-scaffold` bereits vorhanden: `packages/la_core/bin/la_probe.dart`, `lib/src/probe.dart`, `lib/src/module_descriptor.dart`, `lib/src/module_registry.dart` + +## Teststrategie +- `dart test` in `packages/la_core` (Stand 2026-09-30: 14/14); Root-`flutter test` bleibt grün (+184) +- Fixtures aus echten, geschwärzten Zorin-Ausgaben; die bestehenden Parser-Tests (`test/system_parsers_test.dart`) laufen unverändert gegen die umgezogenen Parser +- `dart compile exe` und Lauf ohne `DISPLAY`/`WAYLAND_DISPLAY` in CI + +## Risiko & Fallback +Die Verflechtung von `linux.dart` mit der UI ist größer als gedacht → Spike zuerst, nur Probe-relevantes umziehen; Fallback: Probes vorerst im App-Prozess, Wächter später. Paketierung des CLI (`dart compile exe` im deb-Build: CI-Zeit, Binärgröße) — Spike-Messung: 6 547 240 Bytes, Median-Start 3 ms (`docs/mla-next/BASELINE.md` §7). + +## Abhängigkeiten +Blockiert durch: #41 (V0.8.5 abgeschlossen) +Blockiert: #60, #66, #78 + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Tier: Core · Größe: L · Spec + Plan · zuerst im Epic._ +_Quelle: Ausbauplan V0.8.6–V1.0, Epic FU (Teilplan FU2)._ +_MLA-Next-Bezug: MLA-Next A3 #93 (Rest des Dart-Kerns)._ +``` + +--- + +## #60 — [V0.9] FU1 Modul-Registry (#27) + +```markdown +## Ziel +Ein neues Modul ist ein Listeneintrag plus Screen: Sektionen, Werkzeuge, Probes, Suchanbieter und Aktionen hängen an einer Registry statt an switch-Blöcken. + +## Scope +Weiterführung von #27, erweitert um Probes, Suchanbieter und Aktionen — ohne Panel/Dock (Grill-Entscheidung). + +- `HubModule`-Deskriptor: id, Titel-Key, Icon, Tier, Art (section|tool|launch), `screenBuilder`, `probes`, `searchProviders`, `actions`, `isAvailable(Environment)` +- Die 5 `HubSection`s und 4 `HubTool`s (`hub_shell.dart:21,30`, sechs switch-Blöcke) wandern in eine Liste + +## Abnahme +- [ ] Verhalten unverändert (Widget-Tests der Navigation) +- [ ] Ein neues Modul ist ein Listeneintrag plus Screen +- [ ] Test auf Vollständigkeit der Registry +- [ ] Flutter-Adapter: `HubModule` (Icon, Tier, `screenBuilder`, `isAvailable(Environment)`) ergänzt den reinen `la_core`-Deskriptor um UI-Typen; `la_core` bleibt frei von Flutter + +## Stand 2026-09-30 (Branch `feature/mla-gtk-scaffold`, Draft-PR #89) +Der Registry-**Kern** liegt im Paket `packages/la_core` (belegt in `docs/mla-next/VERIFY.md` Gate 1): Deskriptor (id, titleKey, viewId, kind, capabilities, requires, probeIds, actionIds, subscribedTopics), ID-Eindeutigkeit, Abweisung fehlender/zyklischer Abhängigkeiten, topologischer Start, Rückwärts-Stopp, Single-Flight-Aktivierung — `dart test` 14/14. Die Flutter-Navigation ist unverändert (`git diff --stat 92bef60..HEAD -- lib/ additional/ deb/ linux/` leer). + +**Abgrenzung:** Der Deskriptor in `la_core` ist bewusst **rein** (keine Flutter-/GTK-Typen; View-Factories gehören in die UI-Adapter). Der unten beschriebene `HubModule` mit Icon, Tier, `screenBuilder` und `isAvailable(Environment)` ist der **Flutter-Adapter** darüber und noch nicht umgesetzt — die Kästchen unten bleiben deshalb offen. + +## Andockpunkte (geprüft 2026-09-30) +- `lib/layouts/hub/hub_shell.dart:21` (`enum HubSection`) und `:30` (`enum HubTool`) sowie die zugehörigen switch-Blöcke ebenda (Plan: sechs; per `grep -n "switch" lib/layouts/hub/hub_shell.dart` neu zählen) +- Im Branch: `packages/la_core/lib/src/module_descriptor.dart`, `module_registry.dart`; Tests `module_registry_validate_test.dart`, `module_registry_lifecycle_test.dart` + +## Teststrategie +- Registry im Paket: eindeutige IDs, fehlende/zyklische Abhängigkeiten, Start/Stop, Single-Flight (bereits 14 Tests) +- Widget-Tests der Navigation belegen, dass das Verhalten unverändert bleibt +- Vollständigkeitstest: jede `HubSection`/`HubTool` hat genau einen Registry-Eintrag + +## Risiko & Fallback +`hub_shell.dart` wird auch von DS1 (#65) angefasst → FU1 zuerst, sonst Merge-Konflikte. + +## Abhängigkeiten +Blockiert durch: #59 (Spike + Probe-Vertrag) +Blockiert: #62, #63, #65, #67, #68, #69, #73, #76, #77, #78, #80, #84, #85 + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Abhängigkeit: nach FU2-Spike + Probe-Vertrag._ +_Quelle: Ausbauplan V0.8.6–V1.0, Epic FU (Teilplan FU1)._ +_MLA-Next-Bezug: MLA-Next A3 #93._ +``` + +--- + +## #61 — [V0.9] FU3 Fehlerrahmen + +```markdown +## Ziel +Fehler zeigen sich als lesbarer Fehlerbildschirm oder als Banner, nicht mehr als leeres Fenster. + +## Scope +- `FlutterError.onError` und `PlatformDispatcher.instance.onError` → Logger + nicht blockierendes Banner +- Fehler vor `runApp` (`main.dart:31-72`) zeigen einen Fehlerbildschirm mit Ursache und Doctor-Hinweis statt eines leeren Fensters (Beispiel-Gotcha: fehlendes `additional/` → `RangeError` in `Linux.getCurrentEnvironment`) + +## Abnahme +- [ ] Ein Bundle ohne `additional/` startet in den Fehlerbildschirm +- [ ] `FlutterError.onError` und `PlatformDispatcher.instance.onError` landen im Logger und zeigen ein nicht blockierendes Banner +- [ ] Der Fehlerbildschirm nennt Ursache und Doctor-Hinweis (ST3 #78) + +## Andockpunkte (geprüft 2026-09-30) +- `lib/main.dart:31-72` (Start bis `runApp`; `SingleInstance.claim` steht bei :31) +- `Linux.getCurrentEnvironment` (`lib/services/linux.dart`) — wirft `RangeError`, wenn `additional/` fehlt (Gotcha aus dem Handoff) +- `lib/services/logger.dart` (geloggt wird nie über `print`) + +## Teststrategie +- Widget-Test des Fehlerbildschirms +- Fehlerinjektion: `getCurrentEnvironment` wirft → Fehlerbildschirm mit Ursache statt leerem Fenster + +## Abhängigkeiten +Blockiert durch: — +Reihenfolge (weich): #78 (Doctor-Hinweis im Fehlerbildschirm verlinkt lose) +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Tier: Core · Größe: S · Kurzdesign._ +_Quelle: Ausbauplan V0.8.6–V1.0, Epic FU (Teilplan FU3)._ +``` + +--- + +## #62 — [V0.9] LB1 Lagebild-Leiste + +```markdown +## Ziel +Ein Blick zeigt, ob etwas Aufmerksamkeit braucht: fünf Ampeln, jede mit einer Ein-Satz-Erklärung. + +## Scope +Fünf Ampeln oben im Dashboard (`dashboard_section.dart`): Backups · Updates & Neustart · Speicher · Dienste · Sicherheit. + +- Jede Ampel ist eine Registry-Probe; Klick führt ins Detail +- Startzustand sofort aus `status.json` (falls LB3 aktiv), danach live +- Widgets: `HermesHaloDot`, `HermesBadge`, `HermesStatTile` (kein `MintY.currentColor`/`MintY.dark`) + +## Abnahme +- [ ] Heute stehen Backups auf ROT und Updates auf GELB (Neustart), Speicher je nach Schwelle +- [ ] Jede Ampel erklärt sich in einem Satz +- [ ] Klick auf eine Ampel führt in den passenden Detail-Screen +- [ ] Unbekannt oder veraltet wird als `unknown` angezeigt, nie als grün + +## Andockpunkte (geprüft 2026-09-30) +- `lib/layouts/hub/dashboard_section.dart` (`_memoryTile` :226 als Vorbild für Kacheln) +- Widgets: `lib/widgets/hermes/hermes_halo_dot.dart`, `hermes_badge.dart`, `hermes_stat_tile.dart`; Tokens `lib/layouts/hermes_tokens.dart` + +## Teststrategie +- Probe-Fakes (`ProbeResult` ok|warn|crit|unknown) → Widget-Test je Ampel +- Fixture für Backups ROT und Updates GELB (Neustart) + +## Abhängigkeiten +Blockiert durch: #60 (Registry-Probes) +Blockiert: #66 + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue Funktion → Zeile in `features.csv` (21 Felder; `yes` nur bei Zorin OS + GNOME, sonst `?`) +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Abhängigkeit: FU1 (Registry-Probes)._ +_Quelle: Ausbauplan V0.8.6–V1.0, Epic LB (Teilplan LB1)._ +_MLA-Next-Bezug: MLA-Next Lagebild #102._ +``` + +--- + +## #63 — [V0.9] LB2 Backup-Cockpit + +```markdown +## Ziel +Ein stiller Backup-Ausfall wird sichtbar (heute: rot, ohne dass es jemand merkt). + +## Scope +Backup-Status aus Units und Journal (nie aus Repos oder Tokens), in beiden Scopes, Units per Muster (Default `*restic*`, `*backup*`, `*borg*`; konfigurierbar): + +- letzter Lauf und Ergebnis (`systemctl show`) +- nächster Termin (`list-timers -o json`) +- letzter Erfolg aus dem Journal („Deactivated successfully" vs. „Failed with result") +- Heartbeat-Units werden als Totmannschalter gedeutet +- „Jetzt sichern" = `systemctl --user start `, ohne polkit + +Could: Timeshift-Plan aus `/etc/timeshift/timeshift.json`; Snapshot-Liste nur über den privilegierten Report. + +## Abnahme +- [ ] Heute ROT mit Unit-Beschreibung und letzter Journal-Zeile +- [ ] Nach erfolgreichem Lauf GRÜN +- [ ] Unit-Muster konfigurierbar (Default `*restic*`, `*backup*`, `*borg*`), beide Scopes; keine Unit-Namen oder Pfade fest im Code (öffentliches Repo) +- [ ] Kein Zugriff auf Repos, Passwörter oder Tokens; `restic snapshots` wird nie aufgerufen + +## MLA-Next / Gate 2 +Der GTK/Dart-Host-Track setzt dieselben Vorgaben um (`docs/mla-next/VERIFY.md`, Gate 2); Fixtures werden geteilt. Zusätzlich gilt dort: +- [ ] `systemctl --user start` nur nach Bestätigung und serverseitiger Allowlist; Abbruch, Race und Doppel-Request getestet +- [ ] Ein erfolgreicher Unit-Start macht das Backup nicht grün; ein erfolgreicher Unit-Exit ist kein Restore-Nachweis +- [ ] Der rsync-Ordnerkopier-Prototyp ist nicht Teil dieses Issues + +## Andockpunkte (geprüft 2026-09-30) +- CLI-Quellen: `systemctl [--user] show`, `systemctl [--user] list-timers -o json`, Journal (`journalctl`); Could: `/etc/timeshift/timeshift.json` +- Injizierbare Runner nach dem `debugOverride`-Muster (`lib/services/app_launcher.dart`, `lib/services/action_handler.dart`) + +## Teststrategie +- Fixtures: `systemctl show`-Ausgaben (ok und failed), `list-timers -o json`, Journal-Zeilen „Deactivated successfully“ vs. „Failed with result“ — geschwärzt +- Heartbeat-Unit (Totmannschalter) mit Fixture: ausbleibender Lauf → rot + +## Abhängigkeiten +Blockiert durch: #60 (Registry) +Blockiert: #66 + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue Funktion → Zeile in `features.csv` (21 Felder; `yes` nur bei Zorin OS + GNOME, sonst `?`) +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Nicht: restic-Repo, Passwörter, `restic snapshots`._ +_Quelle: Ausbauplan V0.8.6–V1.0, Epic LB (Teilplan LB2)._ +_MLA-Next-Bezug: MLA-Next A6 #101 (Dart-Host-Seite, gemeinsame Fixtures)._ +``` + +--- + +## #64 — [V0.9] QA1 Gate-Automatisierung, Spike + +```markdown +## Ziel +Klären, ob Wayland-Hotkey und Clipboard in CI automatisierbar sind (go/no-go), statt sie weiter nur manuell zu prüfen. + +## Scope +Spike (beschlossen): Headless `gnome-shell --wayland --virtual-monitor` auf dem `ubuntu-24.04`-Runner. + +- Nur Wayland-Hotkey (Test-Hook) und Clipboard +- Ergebnis: go/no-go; kein Self-hosted-Runner + +## Abnahme +- [ ] Ergebnis dokumentiert (go/no-go mit Begründung und Messwerten) +- [ ] Ergebnis nennt Messwerte (Laufzeit, Flakiness) und einen klaren Schnitt: go oder no-go +- [ ] Bei no-go bleiben die manuellen Gates; die Begründung steht in `docs/wiki/Release-Process.md` + +## Andockpunkte (geprüft 2026-09-30) +- `.github/workflows/build.yml` (Runner `ubuntu-24.04`) +- `docs/wiki/Release-Process.md` (Gate-Tabelle Wayland/X11) + +## Teststrategie +- Spike-Messwerte: Laufzeit im Runner, Wiederholbarkeit über mehrere Läufe, Fehlerbild + +## Risiko & Fallback +Headless-`gnome-shell` auf dem Runner kann instabil oder zu langsam sein — dann no-go; ein Self-hosted-Runner kommt nicht in Frage. + +## Abhängigkeiten +Blockiert durch: — +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Unabhängig — so früh wie möglich in V0.9._ +_Quelle: Ausbauplan V0.8.6–V1.0, Epic QA (Teilplan QA1)._ +``` + +--- + +## #65 — [V0.9] DS1 Token-Einheit (#29/#10) + +```markdown +## Ziel +Ein Token-System statt harter Farben: kein direkter MintY-Farbzugriff außerhalb der Token-Definition. + +## Scope +- MintY-Statik in 32 Dateien → ThemeExtension (`HermesTokens`, `MintYColors`) +- Harte Farben ersetzen; Geometrie- und Typo-Skala einführen +- Deckt die UI-Befunde I2, I4, M2, M4 + +## Abnahme +- [ ] Keine direkten MintY-Farbzugriffe mehr außerhalb der Token-Definition +- [ ] `flutter analyze` 0 Findings; Widget-Tests grün; Goldens aktualisiert (Vorbereitung für DS3) +- [ ] Geometrie- und Typografie-Skala eingeführt; harte Farbwerte ersetzt +- [ ] UI-Befunde I2, I4, M2, M4 adressiert + +## Andockpunkte (geprüft 2026-09-30) +- Die Extensions existieren bereits: `lib/layouts/hermes_tokens.dart` (`HermesTokens`), `lib/layouts/mint_y_tokens.dart` (`MintYColors`); Test `test/hermes_tokens_test.dart` +- Migrationsfläche: aktuell 37 Dateien mit `MintY.`-Zugriff (`grep -rl 'MintY\.' lib`, Stand 2026-09-30); der Plan nannte 32 — Zahl im Spec neu messen + +## Teststrategie +- `flutter analyze` 0 Findings; Widget-Tests grün; Goldens aktualisiert (Vorbereitung für DS3) +- Prüfskript oder Test, der direkte MintY-Zugriffe außerhalb der Token-Datei findet + +## Risiko & Fallback +`hub_shell.dart` wird auch von FU1 (#60) angefasst → FU1 zuerst. + +## Abhängigkeiten +Blockiert durch: #60 (beide fassen `hub_shell.dart` an) +Blockiert: #79 + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Abhängigkeit: erst nach FU1 — beide fassen `hub_shell.dart` an._ +_Quelle: Ausbauplan V0.8.6–V1.0, Epic DS (Teilplan DS1)._ +_MLA-Next-Bezug: GTK-Track: Tokens in A1 #91 (Namensangleichung offen)._ +``` + +--- + +## #66 — [V0.10] LB3 Wächter + +```markdown +## Ziel +Meldungen ohne Dauerprozess: ein Timer prüft, schreibt `status.json` und meldet nur den Übergang nach Rot. + +## Scope +Hintergrund-Meldungen ohne Dauerprozess, Tray oder Handy-Push: + +- `la_probe` (aus FU2) + `linux-assistant-watch.service/.timer` als User-Units im .deb (`/usr/lib/systemd/user/`); `build-deb.sh` und Pfad in `install.sh` prüfen; `Recommends: libnotify-bin` +- Aktivierung per Schalter in den Einstellungen (`systemctl --user enable --now`, kein root) +- Takt: alle 30 min und 5 min nach Login +- Schreibt `$XDG_STATE_HOME/linux-assistant/status.json` atomar und versioniert (`schema`) +- Meldet per `notify-send -A open=Öffnen` nur beim Übergang nach Rot (Erinnerung optional) +- „Öffnen" → Ziel-Sektion: Single-Instance-Protokoll (`single_instance.dart:87`) um `raise:
` erweitern; wartende Benachrichtigung darf den Timer-Lauf nicht blockieren (z. B. `systemd-run --user`) — klärt der Spec + +## Abnahme +- [ ] Eine Ampel wird rot → genau eine Benachrichtigung +- [ ] Zweiter Lauf ohne Änderung → keine +- [ ] Die App zeigt denselben Stand wie `status.json` +- [ ] Aktivierung nur über den Schalter in den Einstellungen (`systemctl --user enable --now`), kein root; Takt 30 min plus 5 min nach Login +- [ ] „Öffnen“ bringt die App in die Ziel-Sektion und blockiert den Timer-Lauf nicht + +## Andockpunkte (geprüft 2026-09-30) +- `lib/services/single_instance.dart:87` (`socket.write("raise")`) → Protokoll um `raise:
` erweitern +- `build-deb.sh` und `install.sh` (Pfad `/usr/lib/systemd/user/` prüfen), `deb/DEBIAN/control` (`Recommends: libnotify-bin`) +- Probe-CLI: `packages/la_core/bin/la_probe.dart` (Spike im Branch) + +## Teststrategie +- Zustandsübergänge ok→warn→crit→ok mit Fixtures: genau eine Benachrichtigung je Übergang nach Rot, keine bei unverändertem Zustand (Entprellung) +- `status.json`: atomar geschrieben (temporäre Datei + Rename), `schema`-Feld versioniert +- Unit-Dateien mit `systemd-analyze --user verify` geprüft + +## Risiko & Fallback +Benachrichtigungs-Müdigkeit → nur Übergänge nach Rot, Entprellung, Erinnerung optional. Die wartende Benachrichtigung darf den Lauf nicht blockieren (z. B. `systemd-run --user`) — klärt der Spec. Paketierung des CLI: siehe FU2 (#59). + +## Abhängigkeiten +Blockiert durch: #59 (`la_probe`-CLI), #62 (Ampel-Probes), #63 (Backup-Probe) +Blockiert: #83 + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue Funktion → Zeile in `features.csv` (21 Felder; `yes` nur bei Zorin OS + GNOME, sonst `?`) +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Abhängigkeit: FU2 (la_probe-CLI)._ +_Quelle: Ausbauplan V0.8.6–V1.0, Epic LB (Teilplan LB3)._ +``` + +--- + +## #67 — [V0.10] DI1 Dienste & Timer + +```markdown +## Ziel +Ein Screen für Dienste und Timer beider Scopes: Fehlgeschlagenes fällt auf, `--user`-Units lassen sich steuern. + +## Scope +- Watchlist (config), alle fehlgeschlagenen Units und alle Timer (`-o json`), in beiden Scopes +- Start, Stop, Restart nur für `--user`-Units; System-Units read-only (Restart über die Queue = Could) +- Journal-Tail je Unit +- n2h: Mini-Probe für Netzwerk/VPN (aktive Interfaces wie `tailscale0`/`proton0` über `ip -j link`) +- Kein D-Bus nötig (#26 zurückgestuft) + +## Abnahme +- [ ] Watchlist + fehlgeschlagene Units + Timer beider Scopes in einem Screen +- [ ] Start/Stop/Restart nur für User-Units möglich +- [ ] Journal-Tail je Unit +- [ ] Watchlist konfigurierbar; keine Unit-Namen fest im Code (öffentliches Repo) + +## Andockpunkte (geprüft 2026-09-30) +- CLI statt D-Bus (#26 zurückgestuft): `systemctl [--user] list-units --failed -o json`, `list-timers -o json`, `journalctl -u … -n`; VPN-Mini-Probe über `ip -j link` +- Watchlist in `config.json` über `lib/services/config_handler.dart` +- Überschneidung mit Q3 (#58, Kachel „Fehlgeschlagene Dienste“): gleiche Datenquelle, nicht doppelt implementieren + +## Teststrategie +- Fixtures der JSON-Ausgaben (beide Scopes); Runner injizierbar +- Scope-Prüfung im Code (nicht nur im UI): Start/Stop/Restart für System-Units wird abgewiesen + +## Abhängigkeiten +Blockiert durch: #60 (Registry) +Blockiert: #74 + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue Funktion → Zeile in `features.csv` (21 Felder; `yes` nur bei Zorin OS + GNOME, sonst `?`) +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Abhängigkeit: FU1 (Registry)._ +_Quelle: Ausbauplan V0.8.6–V1.0, Epic DI (Teilplan DI1)._ +_MLA-Next-Bezug: GTK-Track: Lagebild + Dienste #102._ +``` + +--- + +## #68 — [V0.10] DI2 Docker & Compose + +```markdown +## Ziel +Docker-Projekte im Überblick, mit ehrlichen Expositions-Flags („umgeht UFW“). + +## Scope +- Daten aus `docker ps -a --format '{{json .}}'` und `docker inspect` (NetworkMode, PortBindings, Health); gruppiert nach `com.docker.compose.project` +- Start, Stop, Restart, Logs-Tail über die Docker-CLI (keine neue Rechte-Naht, Nutzer ist in `docker`); Hinweis im UI: `docker`-Gruppe ≈ root +- Expositions-Flags: veröffentlicht auf allen Interfaces oder im LAN („umgeht UFW"), Host-Netz +- Ohne Docker: Hinweis statt Fehler + +## Abnahme +- [ ] Compose-Projekte gruppiert mit Health- und Expositions-Flags +- [ ] Degradierung ohne Docker getestet +- [ ] Start, Stop, Restart und Logs-Tail über die Docker-CLI; UI-Hinweis „`docker`-Gruppe ≈ root“ + +## Andockpunkte (geprüft 2026-09-30) +- CLI: `docker ps -a --format '{{json .}}'`, `docker inspect` (NetworkMode, PortBindings, Health); Gruppierung nach Label `com.docker.compose.project` + +## Teststrategie +- Fixtures: Compose-Projekt, Host-Netz, Bindung auf allen Interfaces, Health-Zustände +- Test ohne Docker (Binary fehlt) → Hinweis statt Fehler + +## Abhängigkeiten +Blockiert durch: #60 (Registry) +Blockiert: #74, #80 + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue Funktion → Zeile in `features.csv` (21 Felder; `yes` nur bei Zorin OS + GNOME, sonst `?`) +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Quelle: Ausbauplan V0.8.6–V1.0, Epic DI (Teilplan DI2)._ +``` + +--- + +## #69 — [V0.10] DI3 Agenten-Tile + +```markdown +## Ziel +Ein Tile zeigt, ob der Agenten-Betrieb Aufmerksamkeit braucht — ohne ein Fremd-Backend zum Core zu machen. + +## Scope +Wie in der Roadmap beschlossen (Grill Q13–Q15, Q22, Q35): + +- kritischstes TokenTelemetry-Budget (`/budgets`) +- Blocker-Zahl aus `hermes kanban boards list --json` +- Gateway-Status und Start-Knöpfe +- offline → Hinweis und „Dienst starten" (`systemctl --user`) + +Fremdes Backend: read-only-Status + Starten, nie Core. Das Hermes-Verzeichnis wird nur über CLI und systemctl gelesen. + +## Abnahme +- [ ] Tile zeigt Budget, Blocker, Gateway-Status; Start-Knöpfe funktionieren im User-Scope +- [ ] Das Hermes-Verzeichnis wird nur über CLI und `systemctl` gelesen, nie beschrieben; keine Secrets im Log +- [ ] Endpunkte und Dienstnamen kommen aus `config.json` oder Defaults, nicht fest im Code (öffentliches Repo) + +## Andockpunkte (geprüft 2026-09-30) +- TokenTelemetry `/budgets` (read-only), `hermes kanban boards list --json`, Gateway-Status über `systemctl --user` +- Cockpit-Regel: fremde Backends bekommen nur Read-only-Status und Starten + +## Teststrategie +- Fixtures: JSON von `boards list` und Budget-Antwort (geschwärzt); Offline-Fall + +## Abhängigkeiten +Blockiert durch: #60 (Registry) +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue Funktion → Zeile in `features.csv` (21 Felder; `yes` nur bei Zorin OS + GNOME, sonst `?`) +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Quelle: Ausbauplan V0.8.6–V1.0, Epic DI (Teilplan DI3)._ +``` + +--- + +## #70 — [V0.10] TE1 Such-Index-Cache + +```markdown +## Ziel +Die Suche ist beim zweiten Start sofort da; der Hänger bei „Bereite Suche vor…“ verschwindet. + +## Scope +- Ergebnis von `prepare()` (`main_search_loader.dart:63`) nach `$XDG_CACHE_HOME/linux-assistant/search-index.json` schreiben +- Beim Start: Cache sofort laden, im Hintergrund auffrischen, dezent anzeigen +- Behebt den Hänger bei „Bereite Suche vor…" (Upstream #231, #239) + +## Abnahme +- [ ] Zweiter Start zeigt Treffer, bevor der Refresh fertig ist +- [ ] Nach dem Zusammenführen keine doppelten Einträge +- [ ] Eine defekte Cache-Datei führt nie zu einem Fehler beim Start +- [ ] Die Anzeige „lädt“ ist dezent und blockiert die Suche nicht + +## Andockpunkte (geprüft 2026-09-30) +- `lib/services/main_search_loader.dart:63` (`prepare()`) +- Cache-Datei `$XDG_CACHE_HOME/linux-assistant/search-index.json` + +## Teststrategie +- Cache-Roundtrip; Zusammenführen ohne Duplikate +- Korrupte oder veraltete Cache-Datei → Neuaufbau (Schema-Version im Cache) + +## Abhängigkeiten +Blockiert durch: — +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue Funktion → Zeile in `features.csv` (21 Felder; `yes` nur bei Zorin OS + GNOME, sonst `?`) +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Quelle: Ausbauplan V0.8.6–V1.0, Epic TE (Teilplan TE1)._ +``` + +--- + +## #71 — [V0.10] QA2 Release-Workflow + +```markdown +## Ziel +Ein Release entsteht reproduzierbar aus einem Tag: CI baut das deb, GitHub-Release mit Asset und Notes. + +## Scope +- Tag `v*` → CI baut das deb → GitHub-Release mit Asset und Notes aus Conventional Commits +- Der Updater prüft den Asset-Digest (`updater.dart:124-147`) +- Den Tag setzt nur Basti, nach der Gate-Tabelle + +## Abnahme +- [ ] Release-Lauf end-to-end geprüft (CI-Build, Asset, Notes, Digest-Prüfung durch den Updater) + +## Andockpunkte (geprüft 2026-09-30) +- `lib/services/updater.dart:124-147` (Digest-Prüfung `sha256:`; :129 lehnt Assets ohne Digest ab) +- `.github/workflows/build.yml`, `build-deb.sh` (Alias-Artefakt `linux-assistant.deb`), `tool/check-versions.sh`, `docs/wiki/Release-Process.md` + +## Teststrategie +- Trockenlauf des Workflows ohne echten Release-Tag (z. B. per `workflow_dispatch` oder Pre-Release-Tag), nur nach Bastis Go +- Updater-Test: Asset ohne `sha256:`-Digest wird abgelehnt (Bestandstest bleibt grün) + +## Risiko & Fallback +Den Tag setzt nur Basti, nach der Gate-Tabelle; kein Push, Tag oder Release durch einen Agenten. + +## Abhängigkeiten +Blockiert durch: — +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Quelle: Ausbauplan V0.8.6–V1.0, Epic QA (Teilplan QA2)._ +``` + +--- + +## #72 — [V0.10] DS2 l10n (#25) + +```markdown +## Ziel +Alle sichtbaren Texte kommen aus ARB-Dateien; die Übersetzungs-Budgets sinken. + +## Scope +- `_tr(` (8×) → ARB; harte deutsche Strings in `lib/layouts/tools/*` übersetzen +- „Nachbor" korrigieren; Du/Sie einheitlich, auch in den polkit-Texten +- Budgets für it und fi senken (Ratchet in `test/l10n_test.dart`: de 0 / it 20 / fi 77, doppelseitig) +- Deckt die UI-Befunde I3, M1 + +## Abnahme +- [ ] Kein `_tr(` mehr in `lib/` +- [ ] l10n-Test mit gesenkten Budgets grün +- [ ] Du/Sie einheitlich in App und polkit-Texten +- [ ] „Nachbor“ korrigiert + +## Andockpunkte (geprüft 2026-09-30) +- `_tr(` — aktuell 8 Treffer in `lib/` (`grep -rn "_tr(" lib`); harte deutsche Strings in `lib/layouts/tools/*` +- Ratchet: `test/l10n_test.dart` (Budgets: `lib/l10n/app_it.arb` 20, `lib/l10n/linuxassistant_fi.arb` 77, doppelseitig) +- polkit-Texte in `org.linux-assistant.operations.policy` (Du/Sie einheitlich) + +## Teststrategie +- `flutter test test/l10n_test.dart` mit den gesenkten Budgets (doppelseitig: Unterschreiten ohne Nachziehen des Budgets ist ebenfalls rot) + +## Abhängigkeiten +Blockiert durch: — +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Quelle: Ausbauplan V0.8.6–V1.0, Epic DS (Teilplan DS2)._ +``` + +--- + +## #73 — [V0.11] TE2 Befehlspalette + eigene Aktionen + +```markdown +## Ziel +Jede Aktion ist per Tastatur erreichbar; eigene Aktionen laufen ohne Shell. + +## Scope +**Befehlspalette:** `>` im Suchfeld bzw. Strg+K im Hub listet alle Registry-Aktionen (Sektionen, Werkzeuge, Probe-Aktionen, Einstellungen). + +**Eigene Aktionen** aus `config.json` (`custom_actions`: name, argv, cwd, mode `output|terminal|detached`, confirm, keywords): +- argv ohne Shell, nie pkexec +- Modus `terminal` nutzt das Muster von `runExecutableInTerminal`; sudo fragt dort selbst — die Nie-Listen-konforme „Terminal+"-Mini-Variante +- Bestätigung nach dem WP-S1-Muster (`action_handler.dart`, `_confirmExecution`) + +## Abnahme +- [ ] Palette listet alle Registry-Aktionen; Strg+K und `>` funktionieren +- [ ] Custom-Action im Modus `terminal` startet im externen Terminal mit Bestätigungsdialog +- [ ] Ungültige `custom_actions`-Einträge werden übersprungen und geloggt, nie ausgeführt +- [ ] Nie pkexec, nie ein Shell-String + +## Andockpunkte (geprüft 2026-09-30) +- `lib/services/action_handler.dart:70` (`_confirmExecution`), `:222` (Bestätigung vor Ausführung), `:32`/`:57` (injizierbarer `_terminalRunner`) +- `Linux.runExecutableInTerminal` in `lib/services/linux.dart`; `config.json` (`custom_actions`) + +## Teststrategie +- Parser für `custom_actions` (gültig, ungültig, fehlendes argv, `cwd`) +- argv ohne Shell: Sonderzeichen bleiben Argumente; Bestätigungsdialog als Widget-Test +- Palette: Suche über alle Registry-Aktionen + +## Abhängigkeiten +Blockiert durch: #60 (Registry-Aktionen) +Reihenfolge (weich): #54 (WP-S1: Bestätigungsmuster für `openfile:`-Exec) +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue Funktion → Zeile in `features.csv` (21 Felder; `yes` nur bei Zorin OS + GNOME, sonst `?`) +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Abhängigkeit: FU1 (Registry-Aktionen)._ +_Quelle: Ausbauplan V0.8.6–V1.0, Epic TE (Teilplan TE2)._ +``` + +--- + +## #74 — [V0.11] TE3 Suchanbieter & Kleinigkeiten + +```markdown +## Ziel +Die Suche findet, was im Cockpit gebraucht wird: Dienste, Container, Einstellungsseiten, Notizen. + +## Scope +Neue Suchanbieter: +- Dienste und Container (führen in die DI-Screens) +- GNOME-Einstellungsseiten (`gnome-control-center --list`) +- Notizen nach Titel und Inhalt (Substring-Suche, kein FTS5) +- tldr, falls installiert (Could) + +Dazu: +- Rechner und Einheiten (n2h; eigener kleiner Parser, offline) +- Privatsphäre-Modus (M6): Recent-Dateien und Favoriten im Leerzustand ausblenden +- Executable-Badge an `openfile:`-Treffern (#49, Empfehlung 3) + +## Abnahme +- [ ] Jeder neue Anbieter liefert Treffer mit Sprungziel +- [ ] Privatsphäre-Modus blendet Recent/Favoriten im Leerzustand aus +- [ ] Rechner und Einheiten ohne neue pub-Abhängigkeit + +## Andockpunkte (geprüft 2026-09-30) +- `lib/services/main_search_loader.dart` (Anbieter-Anmeldung), `gnome-control-center --list`, `lib/services/notes_service.dart` +- Executable-Badge an `openfile:`-Treffern (#49, Empfehlung 3) + +## Teststrategie +- Je Anbieter Fixture + Sprungziel +- Privatsphäre-Modus als Widget-Test +- Rechner/Einheiten: Unit-Tests des kleinen Parsers (offline) + +## Abhängigkeiten +Blockiert durch: #67 (Dienste-Screen), #68 (Docker-Screen) +Reihenfolge (weich): #75 (Notizen-Anbieter nutzt die Quick-Notes-Struktur) +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue Funktion → Zeile in `features.csv` (21 Felder; `yes` nur bei Zorin OS + GNOME, sonst `?`) +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Abhängigkeit: DI1/DI2-Screens für den Dienste/Container-Anbieter._ +_Quelle: Ausbauplan V0.8.6–V1.0, Epic TE (Teilplan TE3)._ +``` + +--- + +## #75 — [V0.11] WZ2 Quick Notes + +```markdown +## Ziel +Schnell notieren, im richtigen Werkzeug weiterarbeiten: Capture aus der Suche, Öffnen in Obsidian. + +## Scope +- Notizordner wählbar (z. B. die Inbox des Obsidian-Vaults; Inbox-first bleibt gewahrt) +- Quick-Capture aus der Suche: `note: Text` legt eine neue Notiz an +- „In Obsidian öffnen" (`obsidian://`-URI über `xdg-open`) statt den Rich-Editor nachzubauen + +## Abnahme +- [ ] Capture legt Notiz im gewählten Ordner an; Öffnen-Übergabe an Obsidian funktioniert +- [ ] Schreiben nur in den gewählten Notizordner (Pfadgrenzen wie beim Dateimanager) +- [ ] Die `obsidian://`-URI ist korrekt kodiert und wird per argv übergeben + +## Andockpunkte (geprüft 2026-09-30) +- `lib/layouts/tools/quick_notes.dart`, `lib/services/notes_service.dart` +- Übergabe: `xdg-open obsidian://…` über argv, kein Shell-String + +## Teststrategie +- `test/notes_service_test.dart` und `test/quick_notes_widget_test.dart` erweitern: Ordnerwahl, `note:`-Capture, Kodierung der `obsidian://`-URI + +## Risiko & Fallback +Nicht: einen Rich-Editor nachbauen. Die Inbox-first-Regel des Vaults bleibt gewahrt. + +## Abhängigkeiten +Blockiert durch: — +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue Funktion → Zeile in `features.csv` (21 Felder; `yes` nur bei Zorin OS + GNOME, sonst `?`) +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Quelle: Ausbauplan V0.8.6–V1.0, Epic WZ (Teilplan WZ2)._ +_MLA-Next-Bezug: GTK-Track: Notes in A7 #98._ +``` + +--- + +## #76 — [V0.11] ST1 Boot- & Stabilitätsbericht + +```markdown +## Ziel +Nach einem Freeze zeigt die App, welche Boots unsauber endeten und was der Kernel dazu sagt. + +## Scope +Je Boot aus `journalctl --list-boots -o json`: +- Dauer und sauber/unsauber (Shutdown-Marker vorhanden?) +- Fehlerzahl (prio err) +- Treffer für GPU, nvkms, Xid und OOM (`journalctl -k -b --grep`) + +Dazu: +- Boot-Zeit aus `systemd-analyze` (heute 19,9 s Loader) → Hinweis + Sprung in den vorhandenen GRUB-Screen (`lib/layouts/grub_config/grub_config.dart`) +- SysRq-Status, persistentes Journal, konfigurierbarer Link „Notfall-Runbook" +- GPU-Kachel aus `nvidia-smi --query-gpu`; degradiert ohne NVIDIA +- SMART nur als Spike (UDisks2 liefert kein `SmartPercentUsed`), sonst weglassen + +## Abnahme +- [ ] Die unsauberen Boots vom 13.–16.09. werden als solche erkannt (Fixture aus dem echten Journal, geschwärzt) +- [ ] Geschwärzte Fixture liegt im Repo; Leak-Check leer +- [ ] GPU-Kachel degradiert ohne `nvidia-smi` + +## Andockpunkte (geprüft 2026-09-30) +- CLI: `journalctl --list-boots -o json`, `journalctl -k -b --grep`, `systemd-analyze`, `nvidia-smi --query-gpu` (degradiert ohne NVIDIA) +- `lib/layouts/grub_config/grub_config.dart` (Sprungziel für den Loader-Hinweis) + +## Teststrategie +- Fixture aus echtem Journal, geschwärzt (keine Hostnamen, IPs, Pfade): die unsauberen Boots vom 13.–16.09. +- Test ohne NVIDIA und ohne persistentes Journal + +## Risiko & Fallback +SMART nur als Spike (UDisks2 liefert kein `SmartPercentUsed`), sonst weglassen. + +## Abhängigkeiten +Blockiert durch: #60 (Screen in der Registry) +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue Funktion → Zeile in `features.csv` (21 Felder; `yes` nur bei Zorin OS + GNOME, sonst `?`) +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Quelle: Ausbauplan V0.8.6–V1.0, Epic ST (Teilplan ST1)._ +``` + +--- + +## #77 — [V0.11] ST2 Änderungs-Zeitleiste + +```markdown +## Ziel +„Was änderte sich vor dem Problem?“ — Updates, Kernel und Treiber mit Datum in einer Zeitleiste. + +## Scope +- Gefilterte Zeitleiste aus `/var/log/apt/history.log(.N.gz)`, `flatpak history`, `snap changes` (falls vorhanden), später dem Aktions-Journal (SI2) +- Kernel und NVIDIA hervorgehoben; aus ST1 ein Sprung „Was änderte sich vor Boot X?" + +## Abnahme +- [ ] Der Treiberwechsel 595.84 → 595.91 und die Kernel-Updates erscheinen mit Datum +- [ ] Rotierte `.gz`-Logs werden gelesen +- [ ] Fehlt `snap` oder `flatpak`, degradiert die Zeitleiste ohne Fehler + +## Andockpunkte (geprüft 2026-09-30) +- `/var/log/apt/history.log` (rotiert als `.N.gz`), `flatpak history`, `snap changes`; später das Aktions-Journal (#81) + +## Teststrategie +- Fixtures: apt-History (auch `.gz`), Treiberwechsel 595.84 → 595.91 und Kernel-Updates + +## Abhängigkeiten +Blockiert durch: #60 (Screen in der Registry) +Reihenfolge (weich): #81 (Aktions-Journal als Zusatzquelle), #76 (Sprung „Was änderte sich vor Boot X?“) +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue Funktion → Zeile in `features.csv` (21 Felder; `yes` nur bei Zorin OS + GNOME, sonst `?`) +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Quelle: Ausbauplan V0.8.6–V1.0, Epic ST (Teilplan ST2)._ +``` + +--- + +## #78 — [V0.11] ST3 App-Doctor + Support-Bericht + +```markdown +## Ziel +Ein kaputtes Bundle oder eine kaputte Installation wird mit einem Befehl erklärt, auch ohne GUI. + +## Scope +`la_probe --doctor` läuft ohne GUI; dazu ein Screen. Geprüft wird: +- Version (`version` vs. `dpkg`) +- Pflicht-Tools und die `additional/`-Skripte +- polkit-Policy und ihre Exec-Pfade +- Hotkey-Binding und Single-Instance-Socket +- ob `config.json` lesbar ist +- ob der Wächter-Timer läuft + +Support-Bericht als Markdown mit Schwärzung (IPs, Hostnamen, `/home`-Pfade) → Zwischenablage oder Datei. + +## Abnahme +- [ ] Bei einem kaputten Bundle (ohne `additional/`) nennt der Doctor genau das +- [ ] `la_probe --doctor` läuft ohne `DISPLAY`/`WAYLAND_DISPLAY` +- [ ] Der Support-Bericht enthält keine IPs, Hostnamen oder `/home`-Pfade + +## Andockpunkte (geprüft 2026-09-30) +- `version` vs. `dpkg`; `additional/`-Skripte; `org.linux-assistant.operations.policy` ↔ `_privilegedEntryPoints` (`lib/services/linux.dart`) ↔ chmod in `build-deb.sh` (Invariante aus `AGENTS.md`) +- `lib/services/single_instance.dart` (Socket), `additional/python/setup_keybinding.py` (Hotkey), `config.json` + +## Teststrategie +- Doctor-Checks je Fixture-Umgebung, u. a. Verzeichnis ohne `additional/` +- Schwärzung: Unit-Tests mit Beispielen für IPs, Hostnamen und `/home`-Pfade + +## Abhängigkeiten +Blockiert durch: #59 (`la_probe --doctor` ohne GUI), #60 (Screen in der Registry) +Reihenfolge (weich): #66 (Prüfung des Wächter-Timers) +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue Funktion → Zeile in `features.csv` (21 Felder; `yes` nur bei Zorin OS + GNOME, sonst `?`) +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Abhängigkeit: FU2 (la_probe-CLI)._ +_Quelle: Ausbauplan V0.8.6–V1.0, Epic ST (Teilplan ST3)._ +``` + +--- + +## #79 — [V0.11] DS3 Goldens + Tastatur (#30) + +```markdown +## Ziel +Token-Regressionen fallen in CI auf; die Bedienung per Tastatur ist definiert und getestet. + +## Scope +- Golden-Tests für die wichtigsten Screens (nach DS1, damit die Token-Fläche steht) +- Tastenkürzel Strg+1…9, Strg+K, Esc; Fokusreihenfolge prüfen (UI-Befund N4) + +## Abnahme +- [ ] Goldens laufen in CI und sind gegen Token-Regressionen empfindlich +- [ ] Tastenkürzel dokumentiert und getestet +- [ ] Tastenkürzel Strg+1…9, Strg+K und Esc dokumentiert und getestet; Fokusreihenfolge (UI-Befund N4) geprüft + +## Andockpunkte (geprüft 2026-09-30) +- `test/hermes_widgets_test.dart`, `test/hermes_tokens_test.dart`; Tastenkürzel im Hub (`lib/layouts/hub/hub_shell.dart`) + +## Teststrategie +- Golden-Update-Prozess dokumentiert (`flutter test --update-goldens`); CI vergleicht gegen die eingecheckten Bilder + +## Risiko & Fallback +#30 nennt `golden_toolkit` — das wäre eine neue Dev-Abhängigkeit; laut Leitplanke 5 nur mit begründeter Ausnahme, sonst ohne Paket lösen. Schrift- und Rendering-Unterschiede zwischen CI-Runner und Referenzsystem im Spec klären. + +## Abhängigkeiten +Blockiert durch: #65 (Token-Fläche zuerst) +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Abhängigkeit: DS1._ +_Quelle: Ausbauplan V0.8.6–V1.0, Epic DS (Teilplan DS3)._ +``` + +--- + +## #80 — [V0.12] SI1 Expositions-Check + UFW + +```markdown +## Ziel +Der Expositions-Check zeigt, welche Dienste von außen erreichbar sind, und warnt bei Neuem. + +## Scope +Der privilegierte Report (`read_security_report.py` → `check_security.py`; heute UFW-Heuristik `:34-44`, SSH `:50`) liefert zusätzlich: +- `ss -tulpnH` mit Prozessnamen +- `ufw status verbose` +- Docker-Port-Bindings + +Dart gleicht gegen eine Soll-Liste ab (config `exposure_baseline`): +- Loopback wird ignoriert +- Tailnet-Adressbereiche (CGNAT- und ULA-Präfix) = Info +- LAN oder Bindung auf allen Interfaces = Warnung +- Docker-Freigabe oder Host-Netz = kritisch („umgeht UFW") + +Jeder Eintrag lässt sich als bekannt markieren. polkit-Actions und Exec-Pfade bleiben unverändert; Umsetzung mit Python-TDD. + +## Abnahme +- [ ] Die heutigen sechs Listener erscheinen, bis Basti sie markiert +- [ ] Ein frisch gestarteter Test-Listener auf allen Interfaces wird gemeldet +- [ ] polkit-Actions und Exec-Pfade bleiben unverändert (`git diff` auf `org.linux-assistant.operations.policy` leer) +- [ ] Invarianten-Review vor dem Merge (Root-Report) + +## Andockpunkte (geprüft 2026-09-30) +- `additional/python/read_security_report.py` → `additional/python/check_security.py` (UFW-Heuristik `:34-44`, SSH `:50`); Python-Tests in `additional/python/tests/` +- `org.linux-assistant.operations.policy` (zwei Actions, feste Exec-Pfade) und `_privilegedEntryPoints` in `lib/services/linux.dart` + +## Teststrategie +- Python-TDD: Fixtures für `ss -tulpnH`, `ufw status verbose` und Docker-Bindings bestimmen das Ausgabeformat des Reports +- Dart-Klassifikation: Loopback ignoriert, Tailnet = Info, LAN/alle Interfaces = Warnung, Docker-Freigabe/Host-Netz = kritisch +- `exposure_baseline`: „als bekannt markieren“ wird persistiert und wirkt beim nächsten Lauf + +## Risiko & Fallback +Berührt den privilegierten Pfad → Python-TDD, Invarianten-Review, gleiche Exec-Pfade. Baseline-Listen gehören in die `config.json` des Nutzers, nicht in den Code (öffentliches Repo). + +## Abhängigkeiten +Blockiert durch: #60 (Registry), #68 (Docker-Bindings und Expositions-Flags (laut Abhängigkeitsgraph des Ausbauplans)) +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue Funktion → Zeile in `features.csv` (21 Felder; `yes` nur bei Zorin OS + GNOME, sonst `?`) +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Risiko: berührt den Root-Report — Invarianten-Review vor dem Merge._ +_Quelle: Ausbauplan V0.8.6–V1.0, Epic SI (Teilplan SI1)._ +_MLA-Next-Bezug: GTK-Track: Lagebild #102._ +``` + +--- + +## #81 — [V0.12] SI2 Aktions-Journal (#151) + +```markdown +## Ziel +Jede privilegierte Ausführung ist nachvollziehbar: was, womit, mit welchem Ergebnis. + +## Scope +Die Dart-Seite protokolliert jede Queue-Ausführung als JSONL in `$XDG_STATE_HOME/linux-assistant/actions.jsonl` (mit Rotation): +- Beschreibung und argv +- Env-Schlüssel ohne Werte +- Exit-Code je Befehl und Dauer + +Neuer Screen „Verlauf". Die Exit-Codes kommen aus der Runner-Ausgabe (`run_multiple_commands.py:64-81`: `-- EXIT CODE`, `FINISHED WITH n FAILED`); optional gibt der Runner eine maschinenlesbare `-- RESULT {json}`-Zeile aus (Python-TDD). + +## Abnahme +- [ ] Jede Queue-Ausführung landet mit argv und Exit-Codes im Journal +- [ ] Rotation greift +- [ ] Keine Env-Werte und keine Secrets im Journal + +## Andockpunkte (geprüft 2026-09-30) +- `additional/python/run_multiple_commands.py:64-81` (`-- COMMAND`, `-- EXIT CODE`, `FINISHED WITH n FAILED`); Queue-Aufruf in `lib/services/linux.dart` +- Datei `$XDG_STATE_HOME/linux-assistant/actions.jsonl` mit Rotation + +## Teststrategie +- Parser der Runner-Ausgabe mit Fixture; Rotation (Größe/Anzahl legt der Spec fest) +- Env: nur Schlüssel, nie Werte (Test mit präparierter Umgebung) + +## Abhängigkeiten +Blockiert durch: #55 (WP-S2: argv[0]-Guard und Env-Filter) +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue Funktion → Zeile in `features.csv` (21 Felder; `yes` nur bei Zorin OS + GNOME, sonst `?`) +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Abhängigkeit: nach WP-S2 (argv[0]-Guard und Env-Filter). Liefert ST2 eine Zusatzquelle._ +_Quelle: Ausbauplan V0.8.6–V1.0, Epic SI (Teilplan SI2)._ +_MLA-Next-Bezug: GTK-Track: Audit im Agenten-Gateway #103._ +``` + +--- + +## #82 — [V0.12] SI3 Update-Radar + +```markdown +## Ziel +Ausstehende Updates sind nach Risiko gruppiert sichtbar, bevor man sie einspielt. + +## Scope +- Ausstehende Updates aus dem apt-Cache (python3-apt als Nutzer, ohne `apt update`), gruppiert nach Sicherheit, Kernel, NVIDIA und Rest +- Außerdem: `apt-mark showhold`, Alter des letzten `apt update`, `reboot-required.pkgs` +- Risiko-Hinweis bei Kernel- und NVIDIA-Updates („Neustart, danach ST1 beobachten") +- Die Aktion ist der vorhandene Updater + +## Abnahme +- [ ] Radar zeigt ausstehende Updates gruppiert; reboot-required-Pakete benannt +- [ ] Kein `apt update` im Nutzerkontext; das Alter des letzten Updates wird angezeigt + +## Andockpunkte (geprüft 2026-09-30) +- `python3-apt` (bereits in `deb/DEBIAN/control` unter Depends), `apt-mark showhold`, `/var/run/reboot-required.pkgs`; Aktion = der vorhandene Updater (`lib/layouts/updater/`, `lib/services/updater.dart`) +- Überschneidung mit Q2 (#58, Neustart-Badge): gleiche Datenquelle nutzen + +## Teststrategie +- Fixtures für apt-Cache-Ausgaben (Sicherheit, Kernel, NVIDIA, Rest) + +## Abhängigkeiten +Blockiert durch: — +Reihenfolge (weich): #60 (Screen in der Registry — im Plan nicht ausdrücklich genannt, im Spec bestätigen) +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue Funktion → Zeile in `features.csv` (21 Felder; `yes` nur bei Zorin OS + GNOME, sonst `?`) +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Quelle: Ausbauplan V0.8.6–V1.0, Epic SI (Teilplan SI3)._ +``` + +--- + +## #83 — [V0.12] SP1 Trend & Prognose + +```markdown +## Ziel +Ein voller Datenträger wird Tage vorher gemeldet, nicht erst beim Schreibfehler. + +## Scope +- Stichproben je echtem Mount (bei jedem Wächter-Lauf und App-Start) → `disk-history.jsonl`, 180 Tage +- Lineare Prognose über 14 Tage → „ voll in ~N Tagen", nur bei relevantem Wachstum +- Ampelschwellen 85/90/95 % + +## Abnahme +- [ ] Mit einer Fixture des echten Verlaufs (69 → 100 %) hätte die Prognose mindestens 7 Tage vorher gewarnt +- [ ] Schwellen 85/90/95 % färben die Ampel +- [ ] Aufbewahrung 180 Tage (Beschneidung getestet) + +## Andockpunkte (geprüft 2026-09-30) +- Stichprobe bei jedem Wächter-Lauf (#66) und App-Start; Datei `$XDG_STATE_HOME/linux-assistant/disk-history.jsonl` +- `lib/linux/linux_filesystem.dart` (df-Parser); Fixture-Fälle aus der Crash-Nachlese H3: overlay-, btrfs-, fuse- und Netz-Mounts + +## Teststrategie +- Fixture des echten Verlaufs (69 → 100 %) +- Prognose-Unit-Tests: flach, linear, Sprung; nur echte Mounts + +## Abhängigkeiten +Blockiert durch: #66 (Wächter-Lauf als Stichprobenpunkt) +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue Funktion → Zeile in `features.csv` (21 Felder; `yes` nur bei Zorin OS + GNOME, sonst `?`) +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Abhängigkeit: LB3 (Wächter-Lauf als Stichproben-Punkt)._ +_Quelle: Ausbauplan V0.8.6–V1.0, Epic SP (Teilplan SP1)._ +``` + +--- + +## #84 — [V0.12] SP2 Kategorien & Aufräumen + +```markdown +## Ziel +Aufräumen mit Vorschau: erst sehen, was es bringt und welcher Befehl läuft, dann bestätigen. + +## Scope +Größen je Kategorie mit Vorschau: Docker `system df`, `journalctl --disk-usage`, apt-Cache, alte Kernel (`dpkg` vs. `uname -r`), Papierkorb, Nutzer-Cache. + +- Nutzer-Aktionen laufen direkt (docker builder prune, Papierkorb leeren) +- Root-Aktionen laufen über die Queue (`apt clean`, `journalctl --vacuum-time`, `apt autoremove --purge`); jeder Befehl ist vor pkexec sichtbar +- Baut auf `lib/layouts/disk_cleaner/*` auf + +## Abnahme +- [ ] Kategorien-Übersicht mit korrekten Größen; Cleanup-Aktionen mit Vorschau und sichtbarem Befehl vor pkexec +- [ ] Root-Aktionen nur über die Queue; keine dritte polkit-Action, kein neuer Root-Pfad +- [ ] Der laufende Kernel (`uname -r`) wird nie zur Entfernung angeboten (Sicherheitsvorschlag, im Spec bestätigen) + +## Andockpunkte (geprüft 2026-09-30) +- `lib/layouts/disk_cleaner/{clean_disk,cleaner_select_disk,clean_timeshift,remove_software}.dart` +- Größenquellen: `docker system df`, `journalctl --disk-usage`, apt-Cache, `dpkg` vs. `uname -r`; Root-Aktionen nur über die Queue (`additional/python/run_multiple_commands.py`) + +## Teststrategie +- Fixtures der Größenausgaben +- Vorschau enthält exakt das argv, das später ausgeführt wird + +## Abhängigkeiten +Blockiert durch: #60 (Screen in der Registry) +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue Funktion → Zeile in `features.csv` (21 Felder; `yes` nur bei Zorin OS + GNOME, sonst `?`) +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Quelle: Ausbauplan V0.8.6–V1.0, Epic SP (Teilplan SP2)._ +``` + +--- + +## #85 — [V0.12] WZ1 Systemmonitor ausbauen + +```markdown +## Ziel +Der Systemmonitor zeigt Verläufe über eine Stunde und warnt bei Schwellen, ohne Hintergrunddienst. + +## Scope +Umschnittene Version von V0.9 P1 (Autostart-Option entfällt — der Wächter übernimmt den Hintergrund): + +- Schwellen-Badges für Temperatur, PSI und Platte +- History von 3 min auf 1 h, downsampled (heute `historyLength = 60` bei 3 s, `system_stats_service.dart:139-140`) +- Prozess-Filter und -Sortierung; gefilterte Partitionsnamen (M3) +- n2h: Link auf ein konfigurierbares Grafana-Dashboard (Langzeit-History wird nicht nachgebaut) + +## Abnahme +- [ ] 1-h-History downgesampelt dargestellt; Schwellen-Badges korrekt; Prozess-Filter/-Sortierung funktioniert +- [ ] Der Speicherbedarf der History bleibt durch eine feste Puffergröße begrenzt + +## Andockpunkte (geprüft 2026-09-30) +- `lib/services/system_stats_service.dart:139-140` (`historyLength = 60`, `defaultInterval` 3 s), `lib/layouts/tools/system_monitor.dart`, `test/system_monitor_service_test.dart` +- `lib/linux/linux_process.dart` (Prozessliste) + +## Teststrategie +- Downsampling-Unit-Tests (60 Punkte bei 3 s → 1 h) +- Schwellen-Badges und Filter/Sortierung + +## Risiko & Fallback +Keine Autostart-Option — den Hintergrund übernimmt der Wächter (#66). + +## Abhängigkeiten +Blockiert durch: #60 (Screen in der Registry) +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue Funktion → Zeile in `features.csv` (21 Felder; `yes` nur bei Zorin OS + GNOME, sonst `?`) +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Keine Autostart-Option._ +_Quelle: Ausbauplan V0.8.6–V1.0, Epic WZ (Teilplan WZ1)._ +_MLA-Next-Bezug: GTK-Track: lesender Monitor #96._ +``` + +--- + +## #86 — [V0.12] DS4 Navigation + +```markdown +## Ziel +Einstellungen und Navigation verhalten sich wie der Rest der App. + +## Scope +- Einstellungen als Sektion statt Dialog (UI-Befund N3, `hub_shell.dart:406`) +- Breadcrumb klickbar machen oder entfernen (N1) +- Sidebar einklappbar, falls das Parkliste-Urteil zu 4.8 angenommen wird (Mini-Variante: Sidebar-Breite in `config.json` merken) + +## Abnahme +- [ ] Einstellungen als Sektion erreichbar; Breadcrumb-Entscheidung umgesetzt; Sidebar-Zustand wird gemerkt +- [ ] Fokus und Tastatur in der neuen Sektion in Wayland **und** X11 + +## Andockpunkte (geprüft 2026-09-30) +- `lib/layouts/hub/hub_shell.dart:406` (`showDialog(... SettingsStart())` — Einstellungen als Dialog), Breadcrumb im selben Widget +- Sidebar-Breite in `config.json` merken (Mini-Variante zu Parkliste 4.8) + +## Teststrategie +- Widget-Tests: Einstellungen als Sektion erreichbar, Breadcrumb-Klick oder -Entfernung, Sidebar-Zustand bleibt + +## Risiko & Fallback +Berührt `hub_shell.dart` wie FU1 und DS1 — Reihenfolge im Spec festlegen. + +## Abhängigkeiten +Blockiert durch: — +Reihenfolge (weich): #60 (berührt `hub_shell.dart`; im Plan keine harte Abhängigkeit), #65 (berührt `hub_shell.dart`; im Plan keine harte Abhängigkeit) +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Neue l10n-Keys in allen vier ARB-Dateien (`test/l10n_test.dart`, Budgets de 0 / it 20 / fi 77) +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Quelle: Ausbauplan V0.8.6–V1.0, Epic DS (Teilplan DS4)._ +``` + +--- + +## #87 — [V1.0] QA3 Fixture-Bibliothek + +```markdown +## Ziel +Jeder Parser läuft gegen echte, geschwärzte Ausgaben; neue Fixtures folgen einem dokumentierten Weg. + +## Scope +- Echte Zorin-Ausgaben (geschwärzt) für alle Parser in `test/fixtures/` zusammenführen +- Doku in `docs/wiki/Testing.md` +- Die Regel gilt schon ab FU2 — dieser Teilplan konsolidiert den Bestand + +## Abnahme +- [ ] Alle Parser laufen gegen die Fixture-Bibliothek +- [ ] Testing-Wiki dokumentiert Anlage und Schwärzung neuer Fixtures +- [ ] Jede Fixture ist geschwärzt; Leak-Check leer + +## Andockpunkte (geprüft 2026-09-30) +- `test/fixtures/` existiert noch nicht (Stand 2026-09-30); `docs/wiki/Testing.md`; `test/system_parsers_test.dart`; `packages/la_core/test/` + +## Teststrategie +- Ein Test je Parser gegen die Fixture-Bibliothek + +## Abhängigkeiten +Blockiert durch: — +Reihenfolge (weich): #59 (die Fixture-Regel gilt schon ab FU2; QA3 konsolidiert den Bestand) +Blockiert: — + +## Gates +- [ ] Die fünf CI-Gates grün (`tool/check-versions.sh`, `dart format`, `flutter analyze` mit 0 Findings, `flutter test`, Python-unittest) +- [ ] Abnahme auf dem Referenzsystem (Zorin OS 18.1) mit Beleg; bei UI oder Hotkey in Wayland **und** X11 +- [ ] Leak-Check auf Doku und Issue-Text (Muster aus `release-plaene-v0.8.1-v0.8.5.md`, Task 7): keine Hosts, IPs, Ports, `/home`-Pfade + +_Quelle: Ausbauplan V0.8.6–V1.0, Epic QA (Teilplan QA3)._ +_MLA-Next-Bezug: GTK-Track: gemeinsame Fixtures #94._ +``` + diff --git a/docs/mla-next/ISSUES.md b/docs/mla-next/ISSUES.md new file mode 100644 index 0000000..0017700 --- /dev/null +++ b/docs/mla-next/ISSUES.md @@ -0,0 +1,597 @@ +# MLA-Next — Issues + +> Spiegel der GitHub-Issues mit Label `track:mla-next` (Stand 2026-09-30). Zuordnung Schlüssel → Issue-Nummer steht in der Tabelle. + +| Schlüssel | Issue | Milestone | +|---|---|---| +| A0 | #90 | MLA-Next 0.0.1 – GTK-Fixture-Shell | +| A1 | #91 | MLA-Next 0.0.1 – GTK-Fixture-Shell | +| A2 | #92 | MLA-Next 0.0.1 – GTK-Fixture-Shell | +| A3 | #93 | MLA-Next 0.0.2 – Dart-Kern & Registry | +| FX | #94 | MLA-Next 0.0.3 – Fixtures & Baseline | +| BL | #95 | MLA-Next 0.0.3 – Fixtures & Baseline | +| A5 | #96 | MLA-Next 0.1.x – Lesender Monitor | +| A5B | #97 | MLA-Next 0.1.x – Lesender Monitor | +| A7 | #98 | MLA-Next 0.2.x – Notes, Dateien & Palette | +| A7B | #99 | MLA-Next 0.2.x – Notes, Dateien & Palette | +| A4 | #100 | MLA-Next 0.3.x – Lagebild, Dienste & Backup | +| A6 | #101 | MLA-Next 0.3.x – Lagebild, Dienste & Backup | +| LG | #102 | MLA-Next 0.3.x – Lagebild, Dienste & Backup | +| A8 | #103 | MLA-Next 0.3.x – Lagebild, Dienste & Backup | +| E1 | #104 | MLA-Next 0.4.x – Evaluation & Entscheidung | +| E2 | #105 | MLA-Next 0.4.x – Evaluation & Entscheidung | +| E3 | #106 | MLA-Next 0.4.x – Evaluation & Entscheidung | + +--- + +## #90 — [Next 0.0.1] A0 Baseline & Evidence-Map + +**Milestone:** MLA-Next 0.0.1 – GTK-Fixture-Shell · **Labels:** `track:mla-next`, `size:S` + +### Body + +```markdown +## Scope +Ausgangslage für alle MLA-Next-Pakete festhalten: aktueller SHA, Issues/PRs, Flutter-Gates, Zorin-Matrix und eine Evidence-Map (Aussage → Kommando + Kernausgabe). Ergebnis: `docs/mla-next/BASELINE.md` auf `feature/mla-gtk-scaffold` (Draft-PR #89). Vor jeder neuen Aufgabe wird der SHA neu geprüft. + +## Abnahme +- [x] Zorin-Matrix erfasst (Zorin OS 18.1, Wayland, GTK 4.14.5, libadwaita 1.5.0, Dart 3.13.4, Flutter 3.47.5) — `BASELINE.md` §1 +- [x] Repo-Gates frisch ausgeführt: `check-versions.sh`, `dart format` (118 Dateien, 0 geändert), `flutter analyze` (0 Findings), `flutter test` (+184), Python-Tests (49, OK) — §4; am 2026-09-30 erneut Exit 0 (`VERIFY.md` Gate 1) +- [x] Evidence-Map: jede Aussage mit Kommando und Kernausgabe — §5 +- [ ] Wayland-Screenshot nachholen (nur manuell; `gnome-screenshot` fehlt, D-Bus-API verweigert) — §6 Punkt 2 +- [ ] `-dev`-Pakete für künftige C-Builds klären (`gtk4.pc`/`libadwaita-1.pc` fehlen) — §6 Punkt 3 +- [ ] §6 Punkt 4 (`analysis_options.yaml`-Exclude) abgleichen: Commit `1367d3c` übernimmt die Excludes bereits, der Text in `BASELINE.md` führt den Punkt noch als offen + +## Handoff (Pflicht je Aufgabe, aus `docs/mla-next/VERIFY.md`) +- [ ] Basis-SHA, Pfade, Scope und Failing-Test/Fixture stehen vor der Umsetzung fest +- [ ] Reviewer 1 (Funktion/UX/Races) und Reviewer 2 (Privilegien/Secrets/argv/IPC) haben geprüft +- [ ] Wirklich ausgeführte Gates mit Ausgabe; rote oder übersprungene Gates benannt; Rückfallplan genannt +- [ ] Kein Merge, Release oder Policy-Update ohne gesonderte Freigabe + +## Abhängigkeiten +Blockiert durch: — +Blockiert: alle weiteren MLA-Next-Pakete (Vergleichsgrundlage). + +_Track: MLA-Next — getrenntes Experiment, kein Flutter-Ersatzbeschluss; die Roadmap V0.8.x–V1.0 bleibt gültig._ +_Quelle: `docs/mla-next/AGENT_PLAN.md` (A0), `docs/mla-next/BASELINE.md`._ +``` + +--- + +## #91 — [Next 0.0.1] A1 Tokens & UI-Design + +**Milestone:** MLA-Next 0.0.1 – GTK-Fixture-Shell · **Labels:** `track:mla-next`, `size:M`, `gate:manual` + +### Body + +```markdown +## Scope +Semantische Tokens für die GTK4/libadwaita-Oberfläche (Golden/Cream/Navy, hell und dunkel), Accessibility, Fokus und Screenshot-Regeln. Fensteraufbau laut Plan: `AdwApplicationWindow` → `AdwNavigationSplitView` (Sidebar + `AdwToolbarView` mit HeaderBar), Hauptbereich `GtkStack` plus kontextuelle rechte Leiste. Keine Polls in unsichtbaren Ansichten; Diagramme zeigen Einheit, Messzeit und Quelle. + +Offen (im Task-Spec zu klären): ob Token-Namen an `HermesTokens` der Flutter-App angelehnt werden (Vergleichbarkeit) oder GTK-eigen bleiben. + +**Grenzen (gelten für jedes MLA-Next-Paket):** GTK-UI, Flutter-UI und Dart-Host laufen nie als Root; keine generische Root-IPC, keine Shell-Strings, keine Secrets in Logs, Screenshots oder Commits; die vorhandene polkit-Grenze bleibt unberührt. + +## Abnahme +- [ ] Token-Liste (Farben hell/dunkel, Abstände, Typografie) liegt unter `docs/mla-next/`; die Shell nutzt keine harten Farbwerte außerhalb der Token-Definition +- [ ] Kontrast je Token-Paar geprüft; Zielwert legt der Task-Spec fest +- [ ] Tastatur: vollständige Fokusreihenfolge, sichtbarer Fokusring in allen Bereichen +- [ ] Screenshots hell/dunkel bei 100/125/150 % (ohne Secrets; nur als Artefakt, nicht im Repo) +- [ ] Wayland **und** X11 + +## Handoff (Pflicht je Aufgabe, aus `docs/mla-next/VERIFY.md`) +- [ ] Basis-SHA, Pfade, Scope und Failing-Test/Fixture stehen vor der Umsetzung fest +- [ ] Reviewer 1 (Funktion/UX/Races) und Reviewer 2 (Privilegien/Secrets/argv/IPC) haben geprüft +- [ ] Wirklich ausgeführte Gates mit Ausgabe; rote oder übersprungene Gates benannt; Rückfallplan genannt +- [ ] Kein Merge, Release oder Policy-Update ohne gesonderte Freigabe + +## Abhängigkeiten +Blockiert durch: #90 (Baseline) +Blockiert: #92, #96 + +_Track: MLA-Next — getrenntes Experiment, kein Flutter-Ersatzbeschluss; die Roadmap V0.8.x–V1.0 bleibt gültig._ +_Quelle: `docs/mla-next/AGENT_PLAN.md` (A1, Architektur)._ +``` + +--- + +## #92 — [Next 0.0.1] A2 GTK-Fixture-Shell mit Test-Fakes & Lifecycle + +**Milestone:** MLA-Next 0.0.1 – GTK-Fixture-Shell · **Labels:** `track:mla-next`, `size:M`, `gate:verify`, `gate:manual` + +### Body + +```markdown +## Scope +`prototype/gtk/mla_app.py` als lauffähige Fixture-Shell: Dashboard, Monitor, Backup und Security mit Demo-Daten; Test-Fakes statt Systemzugriff; sauberer Lifecycle (Start, Beenden, Ressourcen). Umsetzung des Gate 0 aus `docs/mla-next/VERIFY.md`. Der Scaffold-Commit ist **kein** Release-Gate. + +**Grenzen (gelten für jedes MLA-Next-Paket):** GTK-UI, Flutter-UI und Dart-Host laufen nie als Root; keine generische Root-IPC, keine Shell-Strings, keine Secrets in Logs, Screenshots oder Commits; die vorhandene polkit-Grenze bleibt unberührt. + +## Abnahme +- [x] `python3 -m py_compile prototype/gtk/mla_app.py` grün — 2026-09-29, `BASELINE.md` §2 +- [x] Start als normaler Nutzer auf Zorin OS 18.1 (GTK 4.14.5, libadwaita 1.5.0) unter Wayland und X11, je ≥ 3 s am Leben, stderr leer — automatisiert, §2; Wayland-Lebensbeleg nur indirekt (§2/§6 Punkt 1) +- [x] Ohne Root, ohne Systemschreibfunktion, ohne Secrets in Screenshots/Logs (Fixture-Only) +- [ ] Dashboard, Monitor, Backup, Security anwählen; Titel, rechte Details und unbekannten Backup-Status prüfen (manuell, `BASELINE.md` §3) +- [ ] Fenster verkleinern/vergrößern; Tastatur/Fokus; Hell/Dunkel; 100/125/150 % (manuell, §3) +- [ ] Test-Fakes: die Shell läuft gegen Fixtures ohne echte Systemaufrufe; Lifecycle (Start/Beenden) getestet — Testform legt der Task-Spec fest + +## Handoff (Pflicht je Aufgabe, aus `docs/mla-next/VERIFY.md`) +- [ ] Basis-SHA, Pfade, Scope und Failing-Test/Fixture stehen vor der Umsetzung fest +- [ ] Reviewer 1 (Funktion/UX/Races) und Reviewer 2 (Privilegien/Secrets/argv/IPC) haben geprüft +- [ ] Wirklich ausgeführte Gates mit Ausgabe; rote oder übersprungene Gates benannt; Rückfallplan genannt +- [ ] Kein Merge, Release oder Policy-Update ohne gesonderte Freigabe + +## Abhängigkeiten +Blockiert durch: #90, #91 (Tokens) +Blockiert: Milestone 0.0.2 und folgende (die Shell ist der UI-Adapter) + +_Track: MLA-Next — getrenntes Experiment, kein Flutter-Ersatzbeschluss; die Roadmap V0.8.x–V1.0 bleibt gültig._ +_Quelle: `docs/mla-next/AGENT_PLAN.md` (A2, 0.0.1), `docs/mla-next/VERIFY.md` (Gate 0)._ +``` + +--- + +## #93 — [Next 0.0.2] A3 la_core-Rest: Parser, DI, Event-Vertrag, Flutter-Adapter + +**Milestone:** MLA-Next 0.0.2 – Dart-Kern & Registry · **Labels:** `track:mla-next`, `size:L`, `gate:test` + +### Body + +```markdown +## Scope +Fortsetzung von #59 (Headless-Probe) und #60 (Registry), deren Kern im Branch `feature/mla-gtk-scaffold` umgesetzt und belegt ist (Gate 1). Rest für den reinen Dart-Kern `packages/la_core` ohne Flutter-/GTK-Typen: +- Parser/Probe-Modelle für `free`, `df`, `uptime`, `ps` (Quellen: `lib/services/system_stats_service.dart`, `lib/linux/linux_filesystem.dart`, `lib/linux/linux_system.dart`, `lib/linux/linux_process.dart`) +- Dependency Injection und Event-Vertrag +- Flutter-Adapter (path-Dependency) für die bestehende App +- Deskriptor: id, titleKey, viewId, kind, capabilities, requires, probeIds, actionIds, subscribedTopics; View-Factories gehören in die UI-Adapter + +Nicht: `linux.dart` komplett zerlegen; dynamische Fremdcode-Plugins. + +## Abnahme +- [ ] Parser mit Fixtures aus echten, geschwärzten Zorin-Ausgaben; `dart test` grün; die bestehenden Flutter-Parser-Tests (`test/system_parsers_test.dart`) laufen unverändert +- [ ] DI und Event-Vertrag: Tests für Registrierung, Auflösung und Fehlerfälle +- [ ] Flutter-Adapter bindet `la_core` ein; Root-`flutter test` bleibt grün (Stand 2026-09-29: +184) +- [ ] `dart compile exe` und Lauf ohne DISPLAY/WAYLAND_DISPLAY bleiben grün (Gate 1) +- [ ] `git diff --stat ..HEAD -- lib/ additional/ deb/ linux/` zeigt nur die beabsichtigten Adapter-Änderungen; der Spike selbst hatte hier einen leeren Diff — die Abweichung wird im Handoff begründet + +## Handoff (Pflicht je Aufgabe, aus `docs/mla-next/VERIFY.md`) +- [ ] Basis-SHA, Pfade, Scope und Failing-Test/Fixture stehen vor der Umsetzung fest +- [ ] Reviewer 1 (Funktion/UX/Races) und Reviewer 2 (Privilegien/Secrets/argv/IPC) haben geprüft +- [ ] Wirklich ausgeführte Gates mit Ausgabe; rote oder übersprungene Gates benannt; Rückfallplan genannt +- [ ] Kein Merge, Release oder Policy-Update ohne gesonderte Freigabe + +## Abhängigkeiten +Blockiert durch: #59 und #60 (Kern; Roadmap-Issues in V0.9) +Blockiert: #96, #100, #94 + +_Track: MLA-Next — getrenntes Experiment, kein Flutter-Ersatzbeschluss; die Roadmap V0.8.x–V1.0 bleibt gültig._ +_Quelle: `docs/mla-next/AGENT_PLAN.md` (A3), `docs/mla-next/VERIFY.md` (Gate 1)._ +``` + +--- + +## #94 — [Next 0.0.3] Gemeinsame Fixtures + Fehler-/Stale-Modelle + +**Milestone:** MLA-Next 0.0.3 – Fixtures & Baseline · **Labels:** `track:mla-next`, `size:M`, `gate:test` + +### Body + +```markdown +## Scope +Eine Fixture-Bibliothek, die Flutter- und GTK-Track gemeinsam nutzen (echte, geschwärzte Zorin-Ausgaben; Ablageort legt der Spec fest — `test/fixtures/` existiert noch nicht). Dazu Fehler- und Stale-Modelle: `unknown`, `stale`, `failed`, `running`, `ok` sind getrennte Zustände; `stale` ist UI-/Transportstatus und wird nie stillschweigend zu `ok` (`IPC_CONTRACT.md`). + +## Abnahme +- [ ] Fixtures liegen an einem Ort; beide Tracks lesen sie, kein Duplikat +- [ ] Schwärzung dokumentiert; Leak-Check (keine Hosts, IPs, Ports, `/home`-Pfade) auf allen Fixtures leer +- [ ] Tests für jeden Zustand, den Übergang ok → stale und den Fehlerpfad +- [ ] Abgrenzung zu QA3 (#87, V1.0): dieser Task liefert die Basis, QA3 konsolidiert den Bestand + +## Handoff (Pflicht je Aufgabe, aus `docs/mla-next/VERIFY.md`) +- [ ] Basis-SHA, Pfade, Scope und Failing-Test/Fixture stehen vor der Umsetzung fest +- [ ] Reviewer 1 (Funktion/UX/Races) und Reviewer 2 (Privilegien/Secrets/argv/IPC) haben geprüft +- [ ] Wirklich ausgeführte Gates mit Ausgabe; rote oder übersprungene Gates benannt; Rückfallplan genannt +- [ ] Kein Merge, Release oder Policy-Update ohne gesonderte Freigabe + +## Abhängigkeiten +Blockiert durch: #93 +Blockiert: #95, #96, #101 + +_Track: MLA-Next — getrenntes Experiment, kein Flutter-Ersatzbeschluss; die Roadmap V0.8.x–V1.0 bleibt gültig._ +_Quelle: `docs/mla-next/AGENT_PLAN.md` (0.0.3), `docs/mla-next/IPC_CONTRACT.md`._ +``` + +--- + +## #95 — [Next 0.0.3] Baseline-Messung Flutter-Release vs. GTK + +**Milestone:** MLA-Next 0.0.3 – Fixtures & Baseline · **Labels:** `track:mla-next`, `size:S`, `gate:verify` + +### Body + +```markdown +## Scope +CPU-, RAM- und Startzeit-Baseline des Flutter-Release-Builds gegen die GTK-Shell auf demselben Zorin-Rechner. Vorhanden: `la_probe` (Binär 6 547 240 Bytes, Median-Startzeit 3 ms, `BASELINE.md` §7) und die Zorin-Matrix (§1). Neu: Leerlauf und Fixture-Last für beide Varianten. + +## Abnahme +- [ ] Messprotokoll in `docs/mla-next/BASELINE.md`: Kommando, Rechner, Sitzungstyp, Datum, Rohwerte +- [ ] Wiederholungen mit Median wie beim `la_probe`-Spike (5 Läufe je Variante) +- [ ] Wayland und X11 getrennt ausgewiesen +- [ ] Das Ergebnis nennt ausdrücklich, was nicht verglichen wurde + +## Handoff (Pflicht je Aufgabe, aus `docs/mla-next/VERIFY.md`) +- [ ] Basis-SHA, Pfade, Scope und Failing-Test/Fixture stehen vor der Umsetzung fest +- [ ] Reviewer 1 (Funktion/UX/Races) und Reviewer 2 (Privilegien/Secrets/argv/IPC) haben geprüft +- [ ] Wirklich ausgeführte Gates mit Ausgabe; rote oder übersprungene Gates benannt; Rückfallplan genannt +- [ ] Kein Merge, Release oder Policy-Update ohne gesonderte Freigabe + +## Abhängigkeiten +Blockiert durch: #94, #92 +Blockiert: #105 (Vergleichsbasis der Regressionsmatrix) + +_Track: MLA-Next — getrenntes Experiment, kein Flutter-Ersatzbeschluss; die Roadmap V0.8.x–V1.0 bleibt gültig._ +_Quelle: `docs/mla-next/AGENT_PLAN.md` (0.0.3), `docs/mla-next/BASELINE.md` §6 Punkt 5._ +``` + +--- + +## #96 — [Next 0.1.x] A5 Lesender Monitor: /proc-Quellen, Ringpuffer, Drill-down + +**Milestone:** MLA-Next 0.1.x – Lesender Monitor · **Labels:** `track:mla-next`, `size:L`, `gate:test` + +### Body + +```markdown +## Scope +Schreibfreier Monitor in der GTK-Shell: Werte aus `/proc` (CPU, RAM, PSI u. a.; Umfang legt der Spec fest), Ringpuffer fester Größe, Drill-down, Diagramme mit Einheit, Messzeit und Quelle. Vergleichsbasis in der Flutter-App: `lib/services/system_stats_service.dart` (`historyLength = 60` bei 3 s); Roadmap-Gegenstück ist WZ1 (#85). + +**Grenzen (gelten für jedes MLA-Next-Paket):** GTK-UI, Flutter-UI und Dart-Host laufen nie als Root; keine generische Root-IPC, keine Shell-Strings, keine Secrets in Logs, Screenshots oder Commits; die vorhandene polkit-Grenze bleibt unberührt. + +## Abnahme +- [ ] Je Wert sind Quelle und Einheit dokumentiert; jedes Diagramm zeigt Einheit, Messzeit und Quelle +- [ ] Ringpuffer: Tests für Überlauf, Zeitmonotonie und Lücken (Lücke = `stale`, nie 0) +- [ ] Plausibilisierung gegen den `/proc`-Rohwert innerhalb einer dokumentierten Toleranz — Toleranz legt der Spec fest +- [ ] Tests laufen gegen Fixtures, ohne echtes System + +## Handoff (Pflicht je Aufgabe, aus `docs/mla-next/VERIFY.md`) +- [ ] Basis-SHA, Pfade, Scope und Failing-Test/Fixture stehen vor der Umsetzung fest +- [ ] Reviewer 1 (Funktion/UX/Races) und Reviewer 2 (Privilegien/Secrets/argv/IPC) haben geprüft +- [ ] Wirklich ausgeführte Gates mit Ausgabe; rote oder übersprungene Gates benannt; Rückfallplan genannt +- [ ] Kein Merge, Release oder Policy-Update ohne gesonderte Freigabe + +## Abhängigkeiten +Blockiert durch: #91, #93, #94 +Blockiert: #97 + +_Track: MLA-Next — getrenntes Experiment, kein Flutter-Ersatzbeschluss; die Roadmap V0.8.x–V1.0 bleibt gültig._ +_Quelle: `docs/mla-next/AGENT_PLAN.md` (A5, 0.1.0–0.1.x)._ +``` + +--- + +## #97 — [Next 0.1.x] Hintergrund-Pause, Langlauf & Accessibility + +**Milestone:** MLA-Next 0.1.x – Lesender Monitor · **Labels:** `track:mla-next`, `size:M`, `gate:verify`, `gate:manual` + +### Body + +```markdown +## Scope +Der Monitor pausiert in unsichtbaren Ansichten und im Hintergrund, hält einen Langlauf ohne Speicherzuwachs durch und ist per Tastatur und Screenreader bedienbar. + +## Abnahme +- [ ] Unsichtbare Ansicht erzeugt nachweislich keine Timer/Polls (Test oder Trace) +- [ ] Langlauf ohne Speicherzuwachs; Dauer und Messverfahren legt der Task fest +- [ ] Diagramme haben Textalternativen; Fokusreihenfolge vollständig; Kontrast geprüft +- [ ] Wayland **und** X11 + +## Handoff (Pflicht je Aufgabe, aus `docs/mla-next/VERIFY.md`) +- [ ] Basis-SHA, Pfade, Scope und Failing-Test/Fixture stehen vor der Umsetzung fest +- [ ] Reviewer 1 (Funktion/UX/Races) und Reviewer 2 (Privilegien/Secrets/argv/IPC) haben geprüft +- [ ] Wirklich ausgeführte Gates mit Ausgabe; rote oder übersprungene Gates benannt; Rückfallplan genannt +- [ ] Kein Merge, Release oder Policy-Update ohne gesonderte Freigabe + +## Abhängigkeiten +Blockiert durch: #96 +Blockiert: Milestone 0.2.x + +_Track: MLA-Next — getrenntes Experiment, kein Flutter-Ersatzbeschluss; die Roadmap V0.8.x–V1.0 bleibt gültig._ +_Quelle: `docs/mla-next/AGENT_PLAN.md` (0.1.0–0.1.x: Pausieren im Hintergrund, Langlauf, Accessibility)._ +``` + +--- + +## #98 — [Next 0.2.x] A7 Notes & Dateiansicht mit Dateisicherheitsgrenzen + +**Milestone:** MLA-Next 0.2.x – Notes, Dateien & Palette · **Labels:** `track:mla-next`, `size:M` + +### Body + +```markdown +## Scope +Notes und Dateiansicht in der GTK-Shell mit ausdrücklichen Dateisicherheitsgrenzen: Pfade werden kanonisiert, Symlinks aus dem Wurzelverzeichnis heraus abgewiesen, Größenlimit, keine Ausführung. Geschrieben wird nur in den gewählten Notizordner. Roadmap-Gegenstück in der Flutter-App: WZ2 (#75, Übergabe an Obsidian per `obsidian://`). + +**Grenzen (gelten für jedes MLA-Next-Paket):** GTK-UI, Flutter-UI und Dart-Host laufen nie als Root; keine generische Root-IPC, keine Shell-Strings, keine Secrets in Logs, Screenshots oder Commits; die vorhandene polkit-Grenze bleibt unberührt. + +## Abnahme +- [ ] Tests für `..`, Symlink aus dem Wurzelverzeichnis heraus, sehr große Datei und Binärdatei +- [ ] Notizordner wählbar; Schreiben nur dort +- [ ] Kein Öffnen oder Ausführen von Dateien ohne Bestätigung +- [ ] Reviewer 2 hat die Dateigrenzen geprüft + +## Handoff (Pflicht je Aufgabe, aus `docs/mla-next/VERIFY.md`) +- [ ] Basis-SHA, Pfade, Scope und Failing-Test/Fixture stehen vor der Umsetzung fest +- [ ] Reviewer 1 (Funktion/UX/Races) und Reviewer 2 (Privilegien/Secrets/argv/IPC) haben geprüft +- [ ] Wirklich ausgeführte Gates mit Ausgabe; rote oder übersprungene Gates benannt; Rückfallplan genannt +- [ ] Kein Merge, Release oder Policy-Update ohne gesonderte Freigabe + +## Abhängigkeiten +Blockiert durch: #97 (Milestone-Reihenfolge) +Blockiert: #99 + +_Track: MLA-Next — getrenntes Experiment, kein Flutter-Ersatzbeschluss; die Roadmap V0.8.x–V1.0 bleibt gültig._ +_Quelle: `docs/mla-next/AGENT_PLAN.md` (A7, 0.2.0–0.2.x)._ +``` + +--- + +## #99 — [Next 0.2.x] Sicherer Dokument-Reviewer, Suchpalette & Browser-/Terminal-Aktionen + +**Milestone:** MLA-Next 0.2.x – Notes, Dateien & Palette · **Labels:** `track:mla-next`, `size:L` + +### Body + +```markdown +## Scope +- **Sicherer Dokument-Reviewer:** zeigt Dokumente an, ohne Skripte, Makros oder Netzzugriffe aus dem Dokument auszuführen. Format-Umfang offen (Spec). +- **Suchpalette:** Aktionen und Treffer per Tastatur; Roadmap-Gegenstück ist TE2 (#73). +- **Kontextuelle Aktionen:** Browser und Terminal über argv, nie über Shell-Strings; der Browser folgt der XDG-Kette aus V0.8.2 (#33/#34); keine stillen Fremdclients. + +**Grenzen (gelten für jedes MLA-Next-Paket):** GTK-UI, Flutter-UI und Dart-Host laufen nie als Root; keine generische Root-IPC, keine Shell-Strings, keine Secrets in Logs, Screenshots oder Commits; die vorhandene polkit-Grenze bleibt unberührt. + +## Abnahme +- [ ] Reviewer öffnet manipulierte Testdokumente ohne Ausführung oder Netzverkehr (Negativfälle als Fixture) +- [ ] Palette: Auswahl per Tastatur; Aktionen mit Seiteneffekt verlangen eine Bestätigung +- [ ] Browser-/Terminal-Aktionen: argv ohne Shell; Test mit Sonderzeichen im Argument +- [ ] Reviewer 2 hat argv-Pfade und Dokumentbehandlung geprüft + +## Handoff (Pflicht je Aufgabe, aus `docs/mla-next/VERIFY.md`) +- [ ] Basis-SHA, Pfade, Scope und Failing-Test/Fixture stehen vor der Umsetzung fest +- [ ] Reviewer 1 (Funktion/UX/Races) und Reviewer 2 (Privilegien/Secrets/argv/IPC) haben geprüft +- [ ] Wirklich ausgeführte Gates mit Ausgabe; rote oder übersprungene Gates benannt; Rückfallplan genannt +- [ ] Kein Merge, Release oder Policy-Update ohne gesonderte Freigabe + +## Abhängigkeiten +Blockiert durch: #98 +Blockiert: Milestone 0.3.x + +_Track: MLA-Next — getrenntes Experiment, kein Flutter-Ersatzbeschluss; die Roadmap V0.8.x–V1.0 bleibt gültig._ +_Quelle: `docs/mla-next/AGENT_PLAN.md` (0.2.0–0.2.x)._ +``` + +--- + +## #100 — [Next 0.3.x] A4 IPC: JSON-RPC-Schema, Peer-UID, Framing, Backpressure + +**Milestone:** MLA-Next 0.3.x – Lagebild, Dienste & Backup · **Labels:** `track:mla-next`, `size:L`, `gate:test` + +### Body + +```markdown +## Scope +Umsetzung des Entwurfs `docs/mla-next/IPC_CONTRACT.md` (noch nicht implementiert): JSON-RPC 2.0 über UTF-8-JSONL auf einem privaten Unix-Socket im `XDG_RUNTIME_DIR`; ein Dart-Host bindet, GTK und ein optionaler Flutter-Adapter sind Clients. `mla.hello` mit Protokollversion und Capabilities zuerst, danach `backup.list`, `backup.watch`, später `backup.startUserUnit`. Schema und positive/negative Contract-Fixtures sind Teil dieses Pakets. Erst nach einem isolierten Spike. + +Same-UID ist keine Sandbox. Nicht: generische Root-IPC, Shell-Execute- oder Root-Methodennamen im Wire, Tokens/Passwörter/rohe Journalausgaben. + +## Abnahme (Gate 2, erster Punkt) +- [ ] Frame über 65 536 Bytes wird **vor** dem Parsen abgewiesen; ungültiges UTF-8, ungültiges JSON, Schemaverletzung, Batch-Request (v1) jeweils mit definiertem Fehler +- [ ] Socket-Verzeichnis 0700, Socket 0600; Eigentümer und Peer-UID geprüft; falsche Peer-UID wird abgewiesen +- [ ] Timeout, Reconnect und Backpressure (begrenzte Schreibqueue, geordnete Writes) +- [ ] Event-Sequenz-/Revisionslücke und Disconnect lösen einen vollständigen Snapshot-Neuabruf aus +- [ ] Schutz gegen doppelte Start-Requests +- [ ] Fehlerdaten nur `code`, `messageKey`, `retryable`; keine Secrets + +## Handoff (Pflicht je Aufgabe, aus `docs/mla-next/VERIFY.md`) +- [ ] Basis-SHA, Pfade, Scope und Failing-Test/Fixture stehen vor der Umsetzung fest +- [ ] Reviewer 1 (Funktion/UX/Races) und Reviewer 2 (Privilegien/Secrets/argv/IPC) haben geprüft +- [ ] Wirklich ausgeführte Gates mit Ausgabe; rote oder übersprungene Gates benannt; Rückfallplan genannt +- [ ] Kein Merge, Release oder Policy-Update ohne gesonderte Freigabe + +## Abhängigkeiten +Blockiert durch: #93, #99 (Milestone-Reihenfolge; Zuordnung zu 0.3.x ist ein Vorschlag) +Blockiert: #101, #103, #102 + +_Track: MLA-Next — getrenntes Experiment, kein Flutter-Ersatzbeschluss; die Roadmap V0.8.x–V1.0 bleibt gültig._ +_Quelle: `docs/mla-next/IPC_CONTRACT.md`, `docs/mla-next/VERIFY.md` (Gate 2)._ +``` + +--- + +## #101 — [Next 0.3.x] A6 Backup nach #63: Fixtures, Zustände, Start nach Bestätigung + +**Milestone:** MLA-Next 0.3.x – Lagebild, Dienste & Backup · **Labels:** `track:mla-next`, `size:M`, `gate:test` + +### Body + +```markdown +## Scope +Backup-Cockpit auf dem Dart-Host nach den Vorgaben von #63 (LB2): User- und System-Units, letzter Lauf, Result/Journal-Evidenz, nächster Timer, Heartbeat. Zustände `unknown`, `stale`, `failed`, `running`, `ok` sind unterscheidbar. `systemctl --user start` nur für eine erneut validierte User-Unit nach Bestätigung; System-Units bleiben lesend. Prozentwerte nur bei echter Datenquelle. Der rsync-Ordnerkopier-Prototyp ist ausdrücklich **nicht** Teil dieses Pakets. + +## Abnahme (Gate 2) +- [ ] Fixtures für User-/System-Units, Timer, Journal-Fehler sowie unknown/stale; keine Zugriffe auf Repos oder Passwörter +- [ ] `systemctl --user start` nur nach Bestätigung und serverseitiger Allowlist; Abbruch, Race und Doppel-Request getestet +- [ ] Ein erfolgreicher Unit-Start macht das Backup nicht grün; ein erfolgreicher Unit-Exit ist kein Restore-Nachweis (Restore wird separat beurteilt) +- [ ] Keine Backups über Repo-Tokens oder Restic-Passwörter + +## Handoff (Pflicht je Aufgabe, aus `docs/mla-next/VERIFY.md`) +- [ ] Basis-SHA, Pfade, Scope und Failing-Test/Fixture stehen vor der Umsetzung fest +- [ ] Reviewer 1 (Funktion/UX/Races) und Reviewer 2 (Privilegien/Secrets/argv/IPC) haben geprüft +- [ ] Wirklich ausgeführte Gates mit Ausgabe; rote oder übersprungene Gates benannt; Rückfallplan genannt +- [ ] Kein Merge, Release oder Policy-Update ohne gesonderte Freigabe + +## Abhängigkeiten +Blockiert durch: #100, #94 +Tracking-Bezug: #63 (Roadmap, Flutter-Seite) + +_Track: MLA-Next — getrenntes Experiment, kein Flutter-Ersatzbeschluss; die Roadmap V0.8.x–V1.0 bleibt gültig._ +_Quelle: `docs/mla-next/AGENT_PLAN.md` (A6, 0.3.x), `docs/mla-next/VERIFY.md` (Gate 2)._ +``` + +--- + +## #102 — [Next 0.3.x] Security-/Operations-Lagebild + Dienste/Timer (GTK) + +**Milestone:** MLA-Next 0.3.x – Lagebild, Dienste & Backup · **Labels:** `track:mla-next`, `size:L` + +### Body + +```markdown +## Scope +Lesendes Lagebild für Sicherheit und Betrieb sowie Dienste/Timer in der GTK-Shell. Roadmap-Gegenstücke in der Flutter-App: LB1 (#62), DI1 (#67), SI1 (#80). Detailumfang offen — der Plan nennt nur „Security-/Operations-Lagebild, Dienste/Timer"; ein kurzer Spec vor Start legt Ampeln, Quellen und Grenzen fest. + +**Grenzen (gelten für jedes MLA-Next-Paket):** GTK-UI, Flutter-UI und Dart-Host laufen nie als Root; keine generische Root-IPC, keine Shell-Strings, keine Secrets in Logs, Screenshots oder Commits; die vorhandene polkit-Grenze bleibt unberührt. + +## Abnahme +- [ ] Ampeln als Registry-Probes im Kern (`la_core`), je mit Ein-Satz-Erklärung +- [ ] Dienste und Timer beider Scopes; Start/Stop nur für `--user`-Units nach Bestätigung +- [ ] Fixtures aus echten, geschwärzten Zorin-Ausgaben +- [ ] Privilegiertes nur über die vorhandene polkit-Grenze: keine dritte Action, kein neuer Root-Pfad + +## Handoff (Pflicht je Aufgabe, aus `docs/mla-next/VERIFY.md`) +- [ ] Basis-SHA, Pfade, Scope und Failing-Test/Fixture stehen vor der Umsetzung fest +- [ ] Reviewer 1 (Funktion/UX/Races) und Reviewer 2 (Privilegien/Secrets/argv/IPC) haben geprüft +- [ ] Wirklich ausgeführte Gates mit Ausgabe; rote oder übersprungene Gates benannt; Rückfallplan genannt +- [ ] Kein Merge, Release oder Policy-Update ohne gesonderte Freigabe + +## Abhängigkeiten +Blockiert durch: #100, #101 +Blockiert: Milestone 0.4.x + +_Track: MLA-Next — getrenntes Experiment, kein Flutter-Ersatzbeschluss; die Roadmap V0.8.x–V1.0 bleibt gültig._ +_Quelle: `docs/mla-next/AGENT_PLAN.md` (0.3.0–0.3.x)._ +``` + +--- + +## #103 — [Next 0.3.x] A8 Agenten-Gateway: Diagnose + einzeln genehmigte Aktionen + +**Milestone:** MLA-Next 0.3.x – Lagebild, Dienste & Backup · **Labels:** `track:mla-next`, `size:L`, `gate:test`, `gate:manual` + +### Body + +```markdown +## Scope +Agenten-Gateway nur für Diagnose und für einzeln genehmigte Aktionen, mit Audit. Ohne Freigabe läuft nichts. Schreibende Aktionen brauchen eine serverseitige Allowlist und eine bestätigte Nutzerauswahl. Roadmap-Gegenstücke: SI2 Aktions-Journal (#81) für das Audit, DI3 (#69) für die Cockpit-Regel „fremde Backends nur read-only und Starten". + +**Grenzen (gelten für jedes MLA-Next-Paket):** GTK-UI, Flutter-UI und Dart-Host laufen nie als Root; keine generische Root-IPC, keine Shell-Strings, keine Secrets in Logs, Screenshots oder Commits; die vorhandene polkit-Grenze bleibt unberührt. + +## Abnahme +- [ ] Diagnose lesend ohne Genehmigung; jede Aktion einzeln genehmigt, gegen die serverseitige Allowlist geprüft +- [ ] Audit-Eintrag je Aktion (argv, Env-Schlüssel ohne Werte, Exit-Code, Dauer) +- [ ] Keine Agenten-Ausführung ohne Nutzerfreigabe; keine Secrets in Logs; ein Stop-Schalter beendet laufende Agenten-Aktionen +- [ ] Nie-Liste beachtet (kein Klartext-Fallback für Secrets, kein Pass-/OTP-Zugriff) +- [ ] Reviewer 2 (Privilegien/Secrets/IPC) hat den Pfad geprüft; manuelle Abnahme durch Basti + +## Handoff (Pflicht je Aufgabe, aus `docs/mla-next/VERIFY.md`) +- [ ] Basis-SHA, Pfade, Scope und Failing-Test/Fixture stehen vor der Umsetzung fest +- [ ] Reviewer 1 (Funktion/UX/Races) und Reviewer 2 (Privilegien/Secrets/argv/IPC) haben geprüft +- [ ] Wirklich ausgeführte Gates mit Ausgabe; rote oder übersprungene Gates benannt; Rückfallplan genannt +- [ ] Kein Merge, Release oder Policy-Update ohne gesonderte Freigabe + +## Abhängigkeiten +Blockiert durch: #100 +Blockiert: #104 + +_Track: MLA-Next — getrenntes Experiment, kein Flutter-Ersatzbeschluss; die Roadmap V0.8.x–V1.0 bleibt gültig._ +_Quelle: `docs/mla-next/AGENT_PLAN.md` (A8, 0.3.0–0.3.x)._ +``` + +--- + +## #104 — [Next 0.4.x] Computer-Use-Evaluation, Wayland-Freigaben & Stop-Schalter + +**Milestone:** MLA-Next 0.4.x – Evaluation & Entscheidung · **Labels:** `track:mla-next`, `size:M`, `gate:manual` + +### Body + +```markdown +## Scope +Bewerten, ob und wie Computer-Use-Werkzeuge gegen die GTK-Shell arbeiten dürfen: Wayland-Freigaben (Portale/Berechtigungen), ein Stop-Schalter, der jede laufende Automatisierung sofort beendet, und ein Audit der Zugriffe. Ergebnis ist eine dokumentierte Evaluation, kein Produktversprechen. + +**Grenzen (gelten für jedes MLA-Next-Paket):** GTK-UI, Flutter-UI und Dart-Host laufen nie als Root; keine generische Root-IPC, keine Shell-Strings, keine Secrets in Logs, Screenshots oder Commits; die vorhandene polkit-Grenze bleibt unberührt. + +## Abnahme +- [ ] Evaluation dokumentiert: getestete Werkzeuge, Freigabeweg unter Wayland, Grenzen +- [ ] Stop-Schalter wirkt nachweislich sofort (Messwert oder Trace) +- [ ] Keine Aufnahme sensibler Bildschirminhalte in Logs oder Commits +- [ ] Manuelle Abnahme durch Basti + +## Handoff (Pflicht je Aufgabe, aus `docs/mla-next/VERIFY.md`) +- [ ] Basis-SHA, Pfade, Scope und Failing-Test/Fixture stehen vor der Umsetzung fest +- [ ] Reviewer 1 (Funktion/UX/Races) und Reviewer 2 (Privilegien/Secrets/argv/IPC) haben geprüft +- [ ] Wirklich ausgeführte Gates mit Ausgabe; rote oder übersprungene Gates benannt; Rückfallplan genannt +- [ ] Kein Merge, Release oder Policy-Update ohne gesonderte Freigabe + +## Abhängigkeiten +Blockiert durch: #103 +Blockiert: #106 + +_Track: MLA-Next — getrenntes Experiment, kein Flutter-Ersatzbeschluss; die Roadmap V0.8.x–V1.0 bleibt gültig._ +_Quelle: `docs/mla-next/AGENT_PLAN.md` (0.4.0–0.4.x)._ +``` + +--- + +## #105 — [Next 0.4.x] Regressionsmatrix (Sicherheit/Performance) + Packaging-/Upgrade-Probe + +**Milestone:** MLA-Next 0.4.x – Evaluation & Entscheidung · **Labels:** `track:mla-next`, `size:L`, `gate:verify` + +### Body + +```markdown +## Scope +Regressionsmatrix für Sicherheit und Performance gegen die Baseline aus 0.0.3 sowie eine Packaging-/Upgrade-Probe. Paket-, Binär-, Policy- und Datenpfadnamen bleiben vorerst `linux-assistant` (AGENT_PLAN). Die Probe prüft, ob eine GTK-Variante neben oder anstelle der Flutter-App installierbar und upgradefähig wäre; Ergebnis ist ein Beleg, keine Paketierung. + +## Abnahme +- [ ] Matrix ausgeführt und belegt (Sicherheit: Privilegiengrenzen, IPC-Tests aus Gate 2; Performance: Werte gegen die Baseline aus #95) +- [ ] Packaging-/Upgrade-Probe belegt: Installation, Upgrade, Rückbau ohne Reste; Policy-Datei und Exec-Pfade unverändert +- [ ] Rote oder übersprungene Zeilen der Matrix benannt + +## Handoff (Pflicht je Aufgabe, aus `docs/mla-next/VERIFY.md`) +- [ ] Basis-SHA, Pfade, Scope und Failing-Test/Fixture stehen vor der Umsetzung fest +- [ ] Reviewer 1 (Funktion/UX/Races) und Reviewer 2 (Privilegien/Secrets/argv/IPC) haben geprüft +- [ ] Wirklich ausgeführte Gates mit Ausgabe; rote oder übersprungene Gates benannt; Rückfallplan genannt +- [ ] Kein Merge, Release oder Policy-Update ohne gesonderte Freigabe + +## Abhängigkeiten +Blockiert durch: #95, #102 +Blockiert: #106 + +_Track: MLA-Next — getrenntes Experiment, kein Flutter-Ersatzbeschluss; die Roadmap V0.8.x–V1.0 bleibt gültig._ +_Quelle: `docs/mla-next/AGENT_PLAN.md` (0.4.0–0.4.x, Nicht-Ziele)._ +``` + +--- + +## #106 — [Next 0.4.x] Doku Flutter-vs-GTK-Entscheidung samt Rückfallplan + +**Milestone:** MLA-Next 0.4.x – Evaluation & Entscheidung · **Labels:** `track:mla-next`, `size:S`, `gate:manual` + +### Body + +```markdown +## Scope +Die Entscheidung Flutter vs. GTK wird anhand der Evidenz aus 0.0.3–0.4.x dokumentiert, samt Rückfallplan. Die Entscheidung trifft Basti; dieses Issue liefert die Entscheidungsgrundlage. Außerdem: 0.5 anhand der 0.4.x-Evidenz definieren (bisher offen) und die Merge-Frage zu Draft-PR #89 beantworten (nicht mergen vor bestandenem Gate 0). + +## Abnahme +- [ ] Entscheidungsdokument: Kriterien, Messwerte aus #95 und #105, Risiken, Rückfallplan +- [ ] 0.5 definiert oder ausdrücklich geparkt +- [ ] Merge-Entscheidung zu PR #89 dokumentiert +- [ ] Freigabe durch Basti (kein Merge, Release oder Policy-Update ohne sie) + +## Abhängigkeiten +Blockiert durch: #104, #105 + +_Track: MLA-Next — getrenntes Experiment, kein Flutter-Ersatzbeschluss; die Roadmap V0.8.x–V1.0 bleibt gültig._ +_Quelle: `docs/mla-next/AGENT_PLAN.md` (0.4.0–0.4.x, 0.5 offen)._ +``` + diff --git a/docs/mla-next/MILESTONES.md b/docs/mla-next/MILESTONES.md new file mode 100644 index 0000000..d6837e4 --- /dev/null +++ b/docs/mla-next/MILESTONES.md @@ -0,0 +1,46 @@ +# MLA-Next — Milestones + +> Spiegel der GitHub-Milestones (Stand 2026-09-30). Track: getrenntes GTK4-Experiment; die Roadmap V0.8.x–V1.0 bleibt gültig. Quelle der Wahrheit ist GitHub; diese Datei ist ein 1:1-kopierbarer Stand. + +## MLA-Next 0.0.1 – GTK-Fixture-Shell + +Isolierte GTK4-Shell (Python/PyGObject, libadwaita) mit Fixture-Dashboard, Backup und Security: nur Demo-Daten, keine schreibende Funktion, kein Root. Teil des getrennten MLA-Next-Experiments (`docs/mla-next/`, Draft-PR #89); die Roadmap V0.8.x–V1.0 bleibt gültig, es gibt keinen Flutter-Ersatzbeschluss. + +Erfolgskriterium: Gate 0 aus `docs/mla-next/VERIFY.md` — `py_compile` grün, Start als normaler Nutzer auf Zorin OS 18.1 in Wayland und X11, manuelle Prüfung (Dashboard/Monitor/Backup/Security, Resize, Tastatur/Fokus, Hell/Dunkel, 100/125/150 %). Stand 2026-09-30: automatisierte Teile belegt, manuelle Liste offen. + +## MLA-Next 0.0.2 – Dart-Kern & Registry + +#59 Headless-Probe (`packages/la_core`, `la_probe`) und #60 Registry-Vertrag (ID-/Zyklenprüfung, Single-Flight-Aktivierung, Rückwärts-Stopp). Reiner Dart-Kern ohne Flutter-/GTK-Typen; die Roadmap-Issues #59/#60 bleiben im Milestone V0.9, hier steht der Rest (A3). + +Erfolgskriterium: Gate 1 — `dart test` und `dart compile exe` grün, Probe läuft ohne DISPLAY/WAYLAND_DISPLAY, bestehende Flutter-Tests unverändert grün. Stand 2026-09-30: Spike und Registry-Kern im Branch belegt (`docs/mla-next/BASELINE.md` §7). + +## MLA-Next 0.0.3 – Fixtures & Baseline + +Gemeinsame Fixtures, Fehler-/Stale-Modelle und die Vergleichsbasis: CPU-, RAM- und Startzeit-Baseline gegen den Flutter-Release-Build auf demselben Zorin-Rechner. + +Erfolgskriterium: Messprotokoll mit Kommando, Sitzungstyp (Wayland/X11) und Rohwerten; die Zustände unknown/stale/failed/running/ok sind per Fixture getestet; das Protokoll nennt, was nicht verglichen wurde. + +## MLA-Next 0.1.x – Lesender Monitor + +Lesender Monitor mit Drill-down und Ringpuffer, Pausieren im Hintergrund, Langlauf und Accessibility. Diagramme zeigen Einheit, Messzeit und Quelle; nichts Schreibendes. + +Erfolgskriterium: Werte gegen `/proc` plausibilisiert; keine Polls in unsichtbaren Ansichten; Langlauf ohne Speicherzuwachs; Bedienung vollständig per Tastatur. + +## MLA-Next 0.2.x – Notes, Dateien & Palette + +Notes, Dateiansicht, sicherer Dokument-Reviewer, Suchpalette sowie kontextuelle Browser-/Terminal-Aktionen. Keine stillen Fremdclients; Dateizugriffe halten dokumentierte Sicherheitsgrenzen. + +Erfolgskriterium: Tests für Pfadtricks (`..`, Symlinks, sehr große und binäre Dateien) grün; jede Aktion mit Seiteneffekt braucht eine Bestätigung; Reviewer 2 hat die Dateigrenzen geprüft. + +## MLA-Next 0.3.x – Lagebild, Dienste & Backup + +Security-/Operations-Lagebild, Dienste/Timer und #63 Backup auf einem unprivilegierten Dart-Host mit IPC (JSON-RPC 2.0 über privaten Unix-Socket). Agenten-Gateway nur für Diagnose und einzeln genehmigte Aktionen mit Audit. Zuordnung der Pakete zu dieser Version ist ein Vorschlag (AGENT_PLAN nennt die Version für IPC nicht). + +Erfolgskriterium: Gate 2 — IPC-Tests (Frame, UTF-8, JSON, Schema, Timeout, Reconnect, Backpressure, falsche Peer-UID, Event-Lücke); #63 mit Unit-/Timer-/Journal-Fixtures; `systemctl --user start` nur nach Bestätigung und Allowlist; kein grünes Backup allein wegen eines erfolgreichen Unit-Starts. + +## MLA-Next 0.4.x – Evaluation & Entscheidung + +Computer-Use-Evaluation, Wayland-Freigaben, Stop-Schalter, Sicherheits-/Performance-Regressionsmatrix, Packaging-/Upgrade-Probe und die dokumentierte Flutter-vs-GTK-Entscheidung samt Rückfallplan. 0.5 ist offen und wird erst anhand der 0.4.x-Evidenz definiert. + +Erfolgskriterium: Regressionsmatrix ausgeführt und belegt; Packaging-/Upgrade-Probe belegt; Entscheidung und Rückfallplan dokumentiert. Merge, Release und Policy-Updates nur nach gesonderter Freigabe von Basti. + From 94b5ef81f188b20e25df856f09ecef758f82b69e Mon Sep 17 00:00:00 2001 From: ElGregor Date: Wed, 30 Sep 2026 02:12:23 +0200 Subject: [PATCH 18/24] refactor(la_core): reine Parser aus der App in den Kern verschieben (#93) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schnitt 1 von #93: es wandern nur Modelle und Parse-Funktionen, die Runner bleiben app-seitig (sie hängen an Linux.runCommandWithCustomArguments bzw. CommandHelper, ein Umzug würde runInShell/expandCommand/kDebugMode verschieben). Neu in packages/la_core: parseDfOutput + DeviceInfo, parsePsOutput + ProcessStat, parseUptime + Uptime, parseLoadAvg, MemoryInfo.parseFreeOutput + MemoryInfo. Die Rümpfe sind 1:1 aus der App übernommen, nur static -> Top-Level; @immutable entfällt, damit der Kern dependency-frei bleibt. Neu: test/parsers_test.dart mit denselben Fixtures wie der App-Test, inline - eine gemeinsame Fixture-Ablage ist #94 und von diesem Schnitt blockiert. dart test 14 -> 30. Das SDK-Bound bleibt bei ^3.11.0 (bf4942b3, deckungsgleich mit packages/la_core/pubspec.lock); ergänzt ist nur ein Kommentar, der erklärt, warum ein Absenken kein No-op wäre: die Spracheversion steuert, welche Formatierer-Regeln `dart format` auf das Paket anwendet. Co-Authored-By: Claude Code --- packages/la_core/lib/la_core.dart | 4 + packages/la_core/lib/src/parsers/df.dart | 75 +++++++ packages/la_core/lib/src/parsers/memory.dart | 62 ++++++ packages/la_core/lib/src/parsers/process.dart | 26 +++ packages/la_core/lib/src/parsers/system.dart | 36 ++++ packages/la_core/pubspec.yaml | 3 + packages/la_core/test/parsers_test.dart | 196 ++++++++++++++++++ 7 files changed, 402 insertions(+) create mode 100644 packages/la_core/lib/src/parsers/df.dart create mode 100644 packages/la_core/lib/src/parsers/memory.dart create mode 100644 packages/la_core/lib/src/parsers/process.dart create mode 100644 packages/la_core/lib/src/parsers/system.dart create mode 100644 packages/la_core/test/parsers_test.dart diff --git a/packages/la_core/lib/la_core.dart b/packages/la_core/lib/la_core.dart index 746fbf5..a2cbf5a 100644 --- a/packages/la_core/lib/la_core.dart +++ b/packages/la_core/lib/la_core.dart @@ -1,3 +1,7 @@ export 'src/module_descriptor.dart'; export 'src/module_registry.dart'; +export 'src/parsers/df.dart'; +export 'src/parsers/memory.dart'; +export 'src/parsers/process.dart'; +export 'src/parsers/system.dart'; export 'src/probe.dart'; diff --git a/packages/la_core/lib/src/parsers/df.dart b/packages/la_core/lib/src/parsers/df.dart new file mode 100644 index 0000000..4e037ac --- /dev/null +++ b/packages/la_core/lib/src/parsers/df.dart @@ -0,0 +1,75 @@ +/// One row of `df -h` output. +class DeviceInfo { + final String filesystem; + final bool isRemovable; + final String mountPoint; + final String size; + final String sizeFree; + final String sizeUsed; + final int usedPercent; + + const DeviceInfo( + this.filesystem, + this.size, + this.sizeUsed, + this.sizeFree, + this.usedPercent, + this.mountPoint, + this.isRemovable, + ); +} + +const List _ignoreDevices = [ + "dev", + "run", + "df:", + "udev", + "tmpfs", + "/dev/loop", + "revokefs-fuse", + "cgroup", + "efivarfs", +]; + +const List _removableDevices = ["/media/", "/mnt/"]; + +/// Pure parser for `df -h` output, split out so it can be tested without +/// shelling out. +/// +/// The mount point is everything after the fifth field, not a sixth +/// whitespace token: mount points like "/media/user/USB Stick" would +/// otherwise shift every column and blow up the Use% parse. +List parseDfOutput(String cmdResult) { + final linePattern = RegExp( + r'^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\d+)%\s+(.+)$', + ); + + final devices = []; + final devicesRead = []; + for (final raw in cmdResult.split("\n").skip(1)) { + final line = raw.trim(); + if (_ignoreDevices.any((y) => line.startsWith(y))) continue; + if (line.length <= 10) continue; + + final match = linePattern.firstMatch(line); + if (match == null) continue; + final source = match.group(1)!; + final size = match.group(2)!; + if (devicesRead.contains(source) || size.endsWith("M")) continue; + + devices.add( + DeviceInfo( + source, + size, + match.group(3)!, + match.group(4)!, + int.parse(match.group(5)!), + match.group(6)!, + _removableDevices.any((x) => match.group(6)!.contains(x)), + ), + ); + devicesRead.add(source); + } + + return devices; +} diff --git a/packages/la_core/lib/src/parsers/memory.dart b/packages/la_core/lib/src/parsers/memory.dart new file mode 100644 index 0000000..2a7e7c4 --- /dev/null +++ b/packages/la_core/lib/src/parsers/memory.dart @@ -0,0 +1,62 @@ +/// Memory figures as reported by `free -m`, in mebibytes. +/// +/// Deliberately not `@immutable`: that annotation is analysis-only, the fields +/// are already final and the constructor const, and importing `package:meta` +/// would break `la_core`'s "no dependencies" contract. +class MemoryInfo { + final int totalMb; + final int usedMb; + final int swapTotalMb; + final int swapUsedMb; + + const MemoryInfo({ + required this.totalMb, + required this.usedMb, + required this.swapTotalMb, + required this.swapUsedMb, + }); + + double get usedRatio => totalMb > 0 ? usedMb / totalMb : 0; + + bool get hasSwap => swapTotalMb > 0; + + double get swapUsedRatio => swapTotalMb > 0 ? swapUsedMb / swapTotalMb : 0; + + /// Pure parser for `free -m`. Returns null when the output is unusable, so + /// callers can keep the previous reading instead of showing a broken tile. + static MemoryInfo? parseFreeOutput(String output) { + final lines = output.split("\n"); + if (lines.length < 2) { + return null; + } + + List columns(String line) => + line.split(" ").where((x) => x.isNotEmpty).toList(); + + try { + final mem = columns(lines[1]); + if (mem.length < 3) { + return null; + } + int swapTotal = 0; + int swapUsed = 0; + if (lines.length >= 3) { + final swap = columns(lines[2]); + if (swap.length >= 3) { + swapTotal = int.parse(swap[1]); + swapUsed = int.parse(swap[2]); + } + } + return MemoryInfo( + totalMb: int.parse(mem[1]), + usedMb: int.parse(mem[2]), + swapTotalMb: swapTotal, + swapUsedMb: swapUsed, + ); + } on FormatException { + return null; + } on RangeError { + return null; + } + } +} diff --git a/packages/la_core/lib/src/parsers/process.dart b/packages/la_core/lib/src/parsers/process.dart new file mode 100644 index 0000000..7250c2f --- /dev/null +++ b/packages/la_core/lib/src/parsers/process.dart @@ -0,0 +1,26 @@ +/// One row of `ps -eo ,args` output. +class ProcessStat { + final String metricValue; + final String processName; + + const ProcessStat(this.metricValue, this.processName); +} + +/// Pure parser for `ps -eo ,args` output, split out so it can be +/// tested without shelling out. +/// +/// Lines that carry a metric but no command are skipped rather than throwing; +/// `ps` emits those for kernel threads on some systems. +List parsePsOutput(String cmdResult, int count) { + var processes = List.empty(growable: true); + for (var line in cmdResult.split("\n").skip(1).take(count)) { + var values = line.split(" "); + values.removeWhere((x) => x == ""); + if (values.length < 2) { + continue; + } + processes.add(ProcessStat(values[0], values[1].split("/").last)); + } + + return processes; +} diff --git a/packages/la_core/lib/src/parsers/system.dart b/packages/la_core/lib/src/parsers/system.dart new file mode 100644 index 0000000..25f38de --- /dev/null +++ b/packages/la_core/lib/src/parsers/system.dart @@ -0,0 +1,36 @@ +/// Uptime as a value+unit pair. The unit is one of `m` (minutes), `h` (hours) +/// or `d` (days). +class Uptime { + final String unit; + final int value; + + const Uptime(this.unit, this.value); +} + +/// Pure parser for `uptime` output, split out so it can be tested without +/// shelling out. +Uptime parseUptime(String output) { + var values = output.replaceAll(RegExp(r" +"), " ").trim().split(" "); + if (values[2].contains(":")) { + var arr = values[2].split(":"); + int hourValue = int.parse(arr[0]); + int minuteValue = int.parse(arr[1].replaceAll(",", "")); + return hourValue == 0 ? Uptime("m", minuteValue) : Uptime("h", hourValue); + } else { + // The new uptime output could be: 1 day, 1:23 + if (output.contains("min")) { + return Uptime("m", int.parse(values[2])); + } + if (output.contains("day")) { + return Uptime("d", int.parse(values[2])); + } + if (output.contains("hour")) { + return Uptime("h", int.parse(values[2])); + } + return Uptime("m", int.parse(values[2])); + } +} + +/// The one-minute figure from the first column of `/proc/loadavg`. +double parseLoadAvg(String content) => + double.parse(content.trim().split(" ")[0]); diff --git a/packages/la_core/pubspec.yaml b/packages/la_core/pubspec.yaml index 0a1d579..e70d318 100644 --- a/packages/la_core/pubspec.yaml +++ b/packages/la_core/pubspec.yaml @@ -4,6 +4,9 @@ version: 0.0.1 publish_to: none environment: + # Aligned with the committed lockfile (>=3.11.0) — see bf4942b3. The bound also + # decides which formatter style `dart format` applies to this package, so + # lowering it silently reformats files rather than being a no-op. sdk: ^3.11.0 dev_dependencies: diff --git a/packages/la_core/test/parsers_test.dart b/packages/la_core/test/parsers_test.dart new file mode 100644 index 0000000..1e7b17b --- /dev/null +++ b/packages/la_core/test/parsers_test.dart @@ -0,0 +1,196 @@ +import 'package:la_core/la_core.dart'; +import 'package:test/test.dart'; + +/// Unit tests for the parsers that moved into this package (#93). +/// +/// The fixtures are inlined on purpose: a shared fixture directory is #94 and +/// is blocked by this move, and the strings are already in the repository in +/// `test/system_parsers_test.dart`. +void main() { + group("df parser", () { + test("keeps real filesystems and drops pseudo ones", () { + const output = ''' +Filesystem Size Used Avail Use% Mounted on +udev 7,8G 0 7,8G 0% /dev +tmpfs 1,6G 2,1M 1,6G 1% /run +/dev/nvme0n1p2 468G 212G 233G 48% / +/dev/nvme0n1p1 511M 6,1M 505M 2% /boot/efi +/dev/sdb1 59G 12G 47G 21% /media/user/USB +'''; + + final disks = parseDfOutput(output); + + expect( + disks.map((d) => d.mountPoint), + containsAll(["/", "/media/user/USB"]), + ); + expect(disks.any((d) => d.mountPoint == "/dev"), isFalse); + expect(disks.any((d) => d.mountPoint == "/run"), isFalse); + + final root = disks.firstWhere((d) => d.mountPoint == "/"); + expect(root.filesystem, "/dev/nvme0n1p2"); + expect(root.usedPercent, 48); + expect(root.sizeFree, "233G"); + expect(root.isRemovable, isFalse); + + final usb = disks.firstWhere((d) => d.mountPoint == "/media/user/USB"); + expect(usb.isRemovable, isTrue); + }); + + test("skips megabyte-sized partitions and duplicate devices", () { + const output = ''' +Filesystem Size Used Avail Use% Mounted on +/dev/sda1 511M 6,1M 505M 2% /boot/efi +/dev/sda2 468G 212G 233G 48% / +/dev/sda2 468G 212G 233G 48% /home +'''; + + final disks = parseDfOutput(output); + + expect(disks.length, 1); + expect(disks.single.mountPoint, "/"); + }); + + test("keeps mount points that contain spaces", () { + // The naive whitespace split shifted every column by one and blew up + // the Use% parse on sticks labeled "USB Stick". + const output = ''' +Filesystem Size Used Avail Use% Mounted on +/dev/sdb1 59G 12G 47G 21% /media/user/USB Stick +'''; + + final disks = parseDfOutput(output); + + expect(disks.length, 1); + expect(disks.single.mountPoint, "/media/user/USB Stick"); + expect(disks.single.usedPercent, 21); + expect(disks.single.isRemovable, isTrue); + }); + + test("returns an empty list for empty output", () { + expect(parseDfOutput(""), isEmpty); + }); + }); + + group("ps parser", () { + test("reads the metric and strips the command path", () { + const output = ''' +%CPU COMMAND + 42.0 /usr/lib/firefox/firefox + 11.5 /usr/bin/gnome-shell + 3.2 /usr/bin/Xorg +'''; + + final processes = parsePsOutput(output, 3); + + expect(processes.length, 3); + expect(processes.first.metricValue, "42.0"); + expect(processes.first.processName, "firefox"); + expect(processes[1].processName, "gnome-shell"); + }); + + test("honours the requested count", () { + const output = ''' +%CPU COMMAND + 42.0 /usr/bin/a + 11.5 /usr/bin/b + 3.2 /usr/bin/c +'''; + expect(parsePsOutput(output, 2).length, 2); + }); + + test("skips lines that carry no command instead of throwing", () { + // ps emits bare metric lines for some kernel threads. + const output = ''' +%CPU COMMAND + 42.0 /usr/bin/a + 0.0 + 3.2 /usr/bin/c +'''; + + final processes = parsePsOutput(output, 3); + expect(processes.map((p) => p.processName), ["a", "c"]); + }); + }); + + group("uptime parser", () { + test("reads hours and minutes from the clock format", () { + final uptime = parseUptime( + " 14:23:01 up 3:45, 1 user, load average: 0,52, 0,58, 0,59", + ); + expect(uptime.unit, "h"); + expect(uptime.value, 3); + }); + + test("reports minutes when the hour component is zero", () { + final uptime = parseUptime( + " 09:12:44 up 0:27, 1 user, load average: 0,10, 0,20, 0,30", + ); + expect(uptime.unit, "m"); + expect(uptime.value, 27); + }); + + test("reads the 'min' wording", () { + final uptime = parseUptime( + " 09:12:44 up 42 min, 1 user, load average: 0,10, 0,20, 0,30", + ); + expect(uptime.unit, "m"); + expect(uptime.value, 42); + }); + + test("reads the 'days' wording", () { + final uptime = parseUptime( + " 09:12:44 up 12 days, 3:21, 2 users, load average: 0,10", + ); + expect(uptime.unit, "d"); + expect(uptime.value, 12); + }); + }); + + group("free parser", () { + test("reads memory and swap totals", () { + const output = ''' + total used free shared buff/cache available +Mem: 15889 6421 1204 512 8263 8901 +Swap: 2047 128 1919 +'''; + + final memory = MemoryInfo.parseFreeOutput(output)!; + + expect(memory.totalMb, 15889); + expect(memory.usedMb, 6421); + expect(memory.swapTotalMb, 2047); + expect(memory.swapUsedMb, 128); + expect(memory.hasSwap, isTrue); + expect(memory.usedRatio, closeTo(6421 / 15889, 0.0001)); + }); + + test("handles a machine without swap", () { + const output = ''' + total used free shared buff/cache available +Mem: 7861 2100 3000 200 2761 5400 +Swap: 0 0 0 +'''; + + final memory = MemoryInfo.parseFreeOutput(output)!; + expect(memory.hasSwap, isFalse); + expect(memory.swapUsedRatio, 0); + }); + + test("returns null rather than throwing on unusable output", () { + expect(MemoryInfo.parseFreeOutput(""), isNull); + expect(MemoryInfo.parseFreeOutput("some error\n"), isNull); + expect(MemoryInfo.parseFreeOutput("header\nMem: not a number\n"), isNull); + }); + }); + + group("loadavg parser", () { + test("takes the one-minute figure", () { + expect(parseLoadAvg("0.52 0.58 0.59 1/1234 5678\n"), 0.52); + }); + + test("tolerates trailing whitespace", () { + expect(parseLoadAvg(" 1.25 0.90 0.75 2/300 111 "), 1.25); + }); + }); +} From 755665c6bf13930724839b870273ab6fe0b9d9b0 Mon Sep 17 00:00:00 2001 From: ElGregor Date: Wed, 30 Sep 2026 02:12:30 +0200 Subject: [PATCH 19/24] refactor(linux): Parser-Aufrufe an la_core delegieren (#93) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die drei Fassaden lib/linux/{linux_filesystem,linux_process,linux_system}.dart halten nur noch Klassenrumpf und Runner; die Parser-statics delegieren prefixed an la_core und die Modelle werden re-exportiert: import 'package:la_core/la_core.dart' as core; export 'package:la_core/la_core.dart' show DeviceInfo; Das Präfix ist Pflicht, nicht Stil: ein unqualifizierter Aufruf im Body eines gleichnamigen static bindet über den Class-Scope auf sich selbst und ergäbe eine Endlosrekursion, die sauber kompiliert und von keinem Analyzer-Gate gefangen wird. lib/services/system_stats_service.dart bekommt import UND export von MemoryInfo: ein export importiert nicht in dieselbe Library, der import deckt den eigenen Scope (Feld SystemStats.memory, Aufruf in refresh), der export hält package:linux_assistant/... als Bezugsweg für den bestehenden Test offen. SystemStats und SystemStatsService bleiben unverändert app-seitig. test/system_parsers_test.dart bleibt byte-identisch (kein Diff). Gates: la_core dart test 30/30, analyze/format/compile exe + Headless-Probe grün; App flutter test +184, gezielter Parser-Test +22, flutter analyze sauber, dart format 0 changed, tool/check-versions.sh konsistent, Python 49 OK. Nebenbei, unabhängig von #93: flutter pub get hat die gegen die installierte Flutter 3.47.5 veraltete pubspec.lock neu aufgelöst - intl 0.20.2 -> 0.20.3 (flutter_localizations verlangt intl ^0.20.3), test_api 0.7.12, matcher 0.12.20, meta 1.19.0, vector_math 2.4.3; die sdks-Zeile steht damit auf dart >=3.11.0, was zum SDK-Bound von la_core passt. Co-Authored-By: Claude Code --- lib/linux/linux_filesystem.dart | 66 +++----------------------- lib/linux/linux_process.dart | 32 ++++--------- lib/linux/linux_system.dart | 41 ++++------------ lib/services/system_stats_service.dart | 64 ++----------------------- pubspec.lock | 29 ++++++----- pubspec.yaml | 4 ++ 6 files changed, 50 insertions(+), 186 deletions(-) diff --git a/lib/linux/linux_filesystem.dart b/lib/linux/linux_filesystem.dart index 1bfa133..b843a65 100644 --- a/lib/linux/linux_filesystem.dart +++ b/lib/linux/linux_filesystem.dart @@ -1,34 +1,10 @@ +import 'package:la_core/la_core.dart' as core; import 'package:linux_assistant/services/linux.dart'; -class DeviceInfo { - final String filesystem; - final bool isRemovable; - final String mountPoint; - final String size; - final String sizeFree; - final String sizeUsed; - final int usedPercent; - - const DeviceInfo(this.filesystem, this.size, this.sizeUsed, this.sizeFree, - this.usedPercent, this.mountPoint, this.isRemovable); -} +export 'package:la_core/la_core.dart' show DeviceInfo; abstract class LinuxFilesystem { - static final List _ignoreDevices = [ - "dev", - "run", - "df:", - "udev", - "tmpfs", - "/dev/loop", - "revokefs-fuse", - "cgroup", - "efivarfs" - ]; - - static final List _removableDevices = ["/media/", "/mnt/"]; - - static Future> disks() async { + static Future> disks() async { var cmdResult = await Linux.runCommandWithCustomArguments( "/usr/bin/df", ["-h"], getErrorMessages: false); @@ -36,40 +12,12 @@ abstract class LinuxFilesystem { return parseDfOutput(cmdResult); } - /// Pure parser for `df -h` output, split out so it can be tested without - /// shelling out. + /// Delegates to the pure parser in `la_core` (#93), which is the one place + /// the format is understood now. /// /// The mount point is everything after the fifth field, not a sixth /// whitespace token: mount points like "/media/user/USB Stick" would /// otherwise shift every column and blow up the Use% parse. - static List parseDfOutput(String cmdResult) { - final linePattern = - RegExp(r'^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\d+)%\s+(.+)$'); - - final devices = []; - final devicesRead = []; - for (final raw in cmdResult.split("\n").skip(1)) { - final line = raw.trim(); - if (_ignoreDevices.any((y) => line.startsWith(y))) continue; - if (line.length <= 10) continue; - - final match = linePattern.firstMatch(line); - if (match == null) continue; - final source = match.group(1)!; - final size = match.group(2)!; - if (devicesRead.contains(source) || size.endsWith("M")) continue; - - devices.add(DeviceInfo( - source, - size, - match.group(3)!, - match.group(4)!, - int.parse(match.group(5)!), - match.group(6)!, - _removableDevices.any((x) => match.group(6)!.contains(x)))); - devicesRead.add(source); - } - - return devices; - } + static List parseDfOutput(String cmdResult) => + core.parseDfOutput(cmdResult); } diff --git a/lib/linux/linux_process.dart b/lib/linux/linux_process.dart index 0b39cf3..6e542f2 100644 --- a/lib/linux/linux_process.dart +++ b/lib/linux/linux_process.dart @@ -1,14 +1,10 @@ +import 'package:la_core/la_core.dart' as core; import 'package:linux_assistant/services/linux.dart'; -class ProcessStat { - final String metricValue; - final String processName; - - const ProcessStat(this.metricValue, this.processName); -} +export 'package:la_core/la_core.dart' show ProcessStat; abstract class LinuxProcess { - static Future> _getTopProcesses( + static Future> _getTopProcesses( String metric, int count) async { var cmdResult = await Linux.runCommandWithCustomArguments( "/usr/bin/ps", ["-eo", "$metric,args", "--sort=-$metric"]); @@ -16,24 +12,12 @@ abstract class LinuxProcess { return parsePsOutput(cmdResult, count); } - /// Pure parser for `ps -eo ,args` output, split out so it can be - /// tested without shelling out. + /// Delegates to the pure parser in `la_core` (#93). /// /// Lines that carry a metric but no command are skipped rather than throwing; /// `ps` emits those for kernel threads on some systems. - static List parsePsOutput(String cmdResult, int count) { - var processes = List.empty(growable: true); - for (var line in cmdResult.split("\n").skip(1).take(count)) { - var values = line.split(" "); - values.removeWhere((x) => x == ""); - if (values.length < 2) { - continue; - } - processes.add(ProcessStat(values[0], values[1].split("/").last)); - } - - return processes; - } + static List parsePsOutput(String cmdResult, int count) => + core.parsePsOutput(cmdResult, count); static Future processCount() async { var cmdResult = @@ -42,10 +26,10 @@ abstract class LinuxProcess { return cmdResult.split("\n").skip(1).length; } - static Future> topProcessesByCpu(int count) async => + static Future> topProcessesByCpu(int count) async => await _getTopProcesses("pcpu", count); - static Future> topProcessesByMemory(int count) async => + static Future> topProcessesByMemory(int count) async => await _getTopProcesses("pmem", count); static Future zombieCount() async { diff --git a/lib/linux/linux_system.dart b/lib/linux/linux_system.dart index a57a94a..64a7304 100644 --- a/lib/linux/linux_system.dart +++ b/lib/linux/linux_system.dart @@ -1,14 +1,10 @@ import 'dart:io'; +import 'package:la_core/la_core.dart' as core; import 'package:linux_assistant/helpers/command_helper.dart'; import 'package:linux_assistant/services/logger.dart'; -class Uptime { - final String unit; - final int value; - - const Uptime(this.unit, this.value); -} +export 'package:la_core/la_core.dart' show Uptime; abstract class LinuxSystem { static Future hasSwap() async { @@ -22,7 +18,7 @@ abstract class LinuxSystem { } /// Might be inaccurate - static Future uptime() async { + static Future uptime() async { var cmdResult = await CommandHelper.run("/usr/bin/uptime", env: {"LC_ALL": "C"}); @@ -33,29 +29,8 @@ abstract class LinuxSystem { return parseUptime(cmdResult.output); } - /// Pure parser for `uptime` output, split out so it can be tested without - /// shelling out. - static Uptime parseUptime(String output) { - var values = output.replaceAll(RegExp(r" +"), " ").trim().split(" "); - if (values[2].contains(":")) { - var arr = values[2].split(":"); - int hourValue = int.parse(arr[0]); - int minuteValue = int.parse(arr[1].replaceAll(",", "")); - return hourValue == 0 ? Uptime("m", minuteValue) : Uptime("h", hourValue); - } else { - // The new uptime output could be: 1 day, 1:23 - if (output.contains("min")) { - return Uptime("m", int.parse(values[2])); - } - if (output.contains("day")) { - return Uptime("d", int.parse(values[2])); - } - if (output.contains("hour")) { - return Uptime("h", int.parse(values[2])); - } - return Uptime("m", int.parse(values[2])); - } - } + /// Delegates to the pure parser in `la_core` (#93). + static core.Uptime parseUptime(String output) => core.parseUptime(output); /// Cached: the CPU thread count cannot change while the app is running, and /// the polling dashboard would otherwise fork `nproc` on every tick. @@ -88,7 +63,7 @@ abstract class LinuxSystem { return load / cpuCount; } - /// The one-minute figure from the first column of `/proc/loadavg`. - static double parseLoadAvg(String content) => - double.parse(content.trim().split(" ")[0]); + /// Delegates to the pure parser in `la_core` (#93): the one-minute figure + /// from the first column of `/proc/loadavg`. + static double parseLoadAvg(String content) => core.parseLoadAvg(content); } diff --git a/lib/services/system_stats_service.dart b/lib/services/system_stats_service.dart index 8e30615..4d9c564 100644 --- a/lib/services/system_stats_service.dart +++ b/lib/services/system_stats_service.dart @@ -1,70 +1,16 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; +// `MemoryInfo` now lives in the Flutter-free core (#93); the import puts it in +// this library's own scope, the export keeps `package:linux_assistant/...` a +// working entry point for it (an export alone does not import). +import 'package:la_core/la_core.dart' show MemoryInfo; import 'package:linux_assistant/linux/linux_filesystem.dart'; import 'package:linux_assistant/linux/linux_process.dart'; import 'package:linux_assistant/linux/linux_system.dart'; import 'package:linux_assistant/services/linux.dart'; -/// Memory figures as reported by `free -m`, in mebibytes. -@immutable -class MemoryInfo { - final int totalMb; - final int usedMb; - final int swapTotalMb; - final int swapUsedMb; - - const MemoryInfo({ - required this.totalMb, - required this.usedMb, - required this.swapTotalMb, - required this.swapUsedMb, - }); - - double get usedRatio => totalMb > 0 ? usedMb / totalMb : 0; - - bool get hasSwap => swapTotalMb > 0; - - double get swapUsedRatio => swapTotalMb > 0 ? swapUsedMb / swapTotalMb : 0; - - /// Pure parser for `free -m`. Returns null when the output is unusable, so - /// callers can keep the previous reading instead of showing a broken tile. - static MemoryInfo? parseFreeOutput(String output) { - final lines = output.split("\n"); - if (lines.length < 2) { - return null; - } - - List columns(String line) => - line.split(" ").where((x) => x.isNotEmpty).toList(); - - try { - final mem = columns(lines[1]); - if (mem.length < 3) { - return null; - } - int swapTotal = 0; - int swapUsed = 0; - if (lines.length >= 3) { - final swap = columns(lines[2]); - if (swap.length >= 3) { - swapTotal = int.parse(swap[1]); - swapUsed = int.parse(swap[2]); - } - } - return MemoryInfo( - totalMb: int.parse(mem[1]), - usedMb: int.parse(mem[2]), - swapTotalMb: swapTotal, - swapUsedMb: swapUsed, - ); - } on FormatException { - return null; - } on RangeError { - return null; - } - } -} +export 'package:la_core/la_core.dart' show MemoryInfo; /// One snapshot of the machine's state. @immutable diff --git a/pubspec.lock b/pubspec.lock index b2c4547..c72a354 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -201,10 +201,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.20.3" json_annotation: dependency: transitive description: @@ -213,6 +213,13 @@ packages: url: "https://pub.dev" source: hosted version: "4.9.0" + la_core: + dependency: "direct main" + description: + path: "packages/la_core" + relative: true + source: path + version: "0.0.1" leak_tracker: dependency: transitive description: @@ -257,10 +264,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.20" material_color_utilities: dependency: transitive description: @@ -273,10 +280,10 @@ packages: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" mutex: dependency: "direct main" description: @@ -414,10 +421,10 @@ packages: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.12" typed_data: dependency: transitive description: @@ -470,10 +477,10 @@ packages: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: "92b9910f66ed1057fd4da7b040ae7c74cafacf885bdc81be496928d5049b032d" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.3" vm_service: dependency: transitive description: @@ -507,5 +514,5 @@ packages: source: hosted version: "6.5.0" sdks: - dart: ">=3.10.0-0 <4.0.0" + dart: ">=3.11.0 <4.0.0" flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index f50f8ff..5fd03b8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,6 +58,10 @@ dependencies: logger: ^2.5.0 mutex: ^3.1.0 + # Flutter-free core (parsers, probe contract). See packages/la_core. + la_core: + path: packages/la_core + dev_dependencies: flutter_test: sdk: flutter From 080becf197e10165390b798eadd552801def734b Mon Sep 17 00:00:00 2001 From: ElGregor Date: Wed, 30 Sep 2026 02:15:02 +0200 Subject: [PATCH 20/24] =?UTF-8?q?docs(mla):=20Gate-1-Nachweis=20f=C3=BCr?= =?UTF-8?q?=20#93=20Schnitt=201=20(Parser-Umzug)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handoff-Abschnitt zu den Commits 94b5ef8 und 755665c: Basis-SHA, Pfade der verschobenen Parser, die zwei Fallen (Class-Scope-Schatten im Delegator, export importiert nicht), alle tatsächlich ausgeführten Gates mit Ausgabe, nicht ausgeführte Gates (CI triggert den Branch nicht; kein Runner- Verhaltensnachweis) und Rückfallplan. Dazu die festgehaltene Planabweichung: das SDK-Bound blieb ^3.11.0 statt wie geplant auf ^3.4.0 zu sinken. Unabhängige Verifikation durch den read-only Workflow mla-93-parser-move-verify (vier Linsen + Synthese, 5 Agenten): Synthese-Verdikt PASS ohne Blocker; Endlosrekursion, gespaltener Typ und Logikdrift gezielt ausgeschlossen, Rümpfe gegen 513def1 byte-identisch. Festgehalten sind auch die Einschränkungen der Runde: leere Belegketten der Linsen 1/2/4, bewegter Arbeitsbaum während der Verifikation, und die Formatierer-Methodik (dart format liest die Spracheversion aus .dart_tool/package_config.json - erst dart pub get, dann formatieren). Co-Authored-By: Claude Code --- docs/mla-next/VERIFY.md | 114 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/docs/mla-next/VERIFY.md b/docs/mla-next/VERIFY.md index 12f450a..3398a4f 100644 --- a/docs/mla-next/VERIFY.md +++ b/docs/mla-next/VERIFY.md @@ -15,6 +15,7 @@ Aktualisierung 2026-09-30: Die GTK-Scaffold-Laufzeit ist auf Zorin verifiziert ( - [x] Issue #59: `dart test` und `dart compile exe`; Probe ohne DISPLAY/WAYLAND_DISPLAY; Binärgröße/Startzeit messen. — 2026-09-30: `dart test` 14/14 in packages/la_core (frisch); `dart compile exe` + Headless-Lauf ohne DISPLAY/WAYLAND_DISPLAY; Binär 6 547 240 Bytes, Median-Startzeit 3 ms — siehe docs/mla-next/BASELINE.md §7 - [x] Issue #60: IDs eindeutig, fehlende/zyklische Abhängigkeiten abgewiesen, Start/Stop/Lazy-Loading getestet; Flutter-Navigation unverändert. — 2026-09-30: `dart test` 14/14 in packages/la_core (doppelte IDs, fehlende/zyklische Abhängigkeiten, Topo-Start/Rückwärts-Stop, Single-Flight); `git diff --stat 92bef60..HEAD -- lib/ additional/ deb/ linux/` leer (Exit 0); Root-`flutter test` +184 bestanden - [x] Bestehende Repo-Gates nach Scope tatsächlich ausführen: `tool/check-versions.sh`, `dart format`, `flutter analyze`, `flutter test`, Python-Tests. — 2026-09-30 frisch ausgeführt, alle Exit 0: `version 0.8.0 is consistent`; `Formatted 118 files (0 changed)`; `No issues found!`; `00:02 +184: All tests passed!`; `Ran 49 tests` / `OK` — vgl. docs/mla-next/BASELINE.md §4 +- [x] Issue #93, Schnitt 1 (Parser-Umzug nach `packages/la_core`): App-Test byte-identisch, alle Gates grün. — 2026-09-30: Handoff-Abschnitt unten. DI/Event-Vertrag und Flutter-`HubModule`-Adapter bleiben offen (spätere Schnitte von #93). ## Gate 2: IPC und Backup @@ -23,6 +24,119 @@ Aktualisierung 2026-09-30: Die GTK-Scaffold-Laufzeit ist auf Zorin verifiziert ( - [ ] `systemctl --user start` nur nach Bestätigung und serverseitiger Allowlist; Abbruch, Race und Doppel-Request prüfen. - [ ] Kein grünes Backup allein wegen eines erfolgreichen Unit-Starts; Restore separat beurteilen. +## Handoff #93 Schnitt 1 — Parser-Umzug nach `packages/la_core` + +**Basis-SHA:** `513def150dbb37ff3883b2061396f7f326e4527b` (= `origin/feature/mla-gtk-scaffold` beim Start). +**Commits:** `94b5ef8` (la_core), `755665c` (App) — **unpushed**. + +**Scope.** Vier reine Parser und ihre Modelle wandern in den Flutter-/GTK-freien Kern; die App-Dateien +werden zu delegierenden Fassaden. Die **Runner bleiben app-seitig** (`disks`, `processCount`, +`zombieCount`, `topProcessesBy*`, `hasSwap`, `uptime`, `getCpuThreadCount`, `getCpuAverageLoad`) — sie +hängen an `Linux.runCommandWithCustomArguments` bzw. `CommandHelper`, ein Umzug würde +`runInShell`/`expandCommand`/`kDebugMode` verschieben und damit echtes Verhalten. + +**Pfade.** + +| Wandert nach `packages/la_core/lib/src/parsers/` | Quelle | +|---|---| +| `df.dart` — `DeviceInfo` + `parseDfOutput` | `lib/linux/linux_filesystem.dart` | +| `process.dart` — `ProcessStat` + `parsePsOutput` | `lib/linux/linux_process.dart` | +| `system.dart` — `Uptime` + `parseUptime` + `parseLoadAvg` | `lib/linux/linux_system.dart` | +| `memory.dart` — `MemoryInfo` + `MemoryInfo.parseFreeOutput` | `lib/services/system_stats_service.dart` | + +Barrel: `packages/la_core/lib/la_core.dart` (+4 Exporte). Neu: `packages/la_core/test/parsers_test.dart`. +App-seitig geändert: die drei `lib/linux/*`-Fassaden, `lib/services/system_stats_service.dart` +(Import **und** Export von `MemoryInfo`), `pubspec.yaml`, `pubspec.lock`. + +**Umsetzung.** Die Rümpfe sind 1:1 übernommen, nur `static` → Top-Level; `@immutable` entfällt, damit +der Kern dependency-frei bleibt (die Felder sind weiterhin `final`, der Konstruktor `const`). +`SystemStats` und `SystemStatsService` bleiben unverändert app-seitig. + +Zwei Fallen, die den Umbau bestimmen und beide **nicht** von einem Analyzer-Gate gefangen werden: + +- **Class-Scope-Schatten.** `static parseDfOutput(s) => parseDfOutput(s)` bindet auf den eigenen + static → Endlosrekursion, die sauber kompiliert. Deshalb `import ... as core;` + `core.parseX(...)` + in jedem Delegator. +- **`export` importiert nicht.** `system_stats_service.dart` braucht beides: `import ... show MemoryInfo` + für den eigenen Scope (Feld `SystemStats.memory`, Aufruf in `refresh`) und `export ... show MemoryInfo`, + damit `test/system_parsers_test.dart` über `package:linux_assistant/...` weiter an den Typ kommt. + +**Gates — tatsächlich ausgeführt (2026-09-30), alle Exit 0.** + +| Gate | Ausgabe | +|---|---| +| `dart pub get` (la_core) | `Got dependencies!`; `packages/la_core/pubspec.lock` unverändert (kein Lock-Churn) | +| `dart analyze` (la_core) | `No issues found!` | +| `dart format --output=none --set-exit-if-changed .` (la_core) | `Formatted 13 files (0 changed)` | +| `dart test` (la_core) | `00:00 +30: All tests passed!` (vorher 14) | +| `dart compile exe bin/la_probe.dart` | `Generated: /tmp/la_probe` | +| `env -u DISPLAY -u WAYLAND_DISPLAY /tmp/la_probe --version` | `la_probe 0.0.1-spike.1 (dart 3.13.4 (stable) … on "linux_x64")`, Exit 0 | +| `flutter pub get` (App) | `Got dependencies!` | +| `bash tool/check-versions.sh` | `version 0.8.0 is consistent` | +| `dart format --output=none --set-exit-if-changed lib test` | `Formatted 118 files (0 changed)` | +| `flutter analyze` | `No issues found! (ran in 1.4s)` | +| `flutter test` | `00:02 +184: All tests passed!` (unverändert) | +| `flutter test test/system_parsers_test.dart` | `00:00 +22: All tests passed!` | +| `python3 -m unittest discover -s tests -t .` | `Ran 49 tests` / `OK` | + +**Abnahme-Diff** (`git diff --stat 513def1..HEAD -- lib/ packages/ pubspec.yaml pubspec.lock tool/ additional/ deb/ linux/`): +13 Dateien, +452/−186 — 4 App-Dateien unter `lib/`, 7 unter `packages/la_core/`, `pubspec.yaml`/`pubspec.lock`. +`test/system_parsers_test.dart` ist **byte-identisch** (leerer Diff). Leak-Grep über die neuen +la_core-Dateien: keine Treffer. + +**Nicht ausgeführt / keine Aussage.** + +- **Kein CI-Grün.** `.github/workflows/build.yml` triggert `feature/mla-gtk-scaffold` nicht. +- Kein Verhaltensnachweis für die Runner — die Tests prüfen `isRunning`/`subscriberCount`, `refresh()` + schluckt Exceptions. In Schnitt 1 unkritisch, weil die Runner unverändert sind; bei einem späteren + Runner-Umbau ist eine echte Verhaltensprobe Pflicht. +- Keine manuelle Zorin-Prüfung nötig (kein UI-Anteil); `install.sh`, `deb/`, `prototype/` unberührt. + +**Planabweichung (festgehalten, weil sie vorher anders geplant war).** Der Plan wollte das SDK-Bound von +`packages/la_core` auf `^3.4.0` senken. Das wurde **verworfen**: `bf4942b3` hat es bewusst auf `^3.11.0` +gehoben, `packages/la_core/pubspec.lock` (getrackt) verlangt `dart: ">=3.11.0 <4.0.0"`, und die +Spracheversion steuert die Formatierer-Regeln — das Absenken hätte `test/module_registry_validate_test.dart` +umformatiert (fremde Datei) statt ein No-op zu sein. `^3.11.0` bleibt; ergänzt ist nur ein Kommentar, +der die Falle festhält. + +**Unabhängige Verifikation.** Workflow `mla-93-parser-move-verify` (read-only, vier Linsen + Synthese, +412 942 Subagenten-Tokens): **Synthese-Verdikt `PASS`, keine Blocker.** Die drei Block-Muster wurden +gezielt geprüft und ausgeschlossen: (1) Endlosrekursion — alle vier Delegatoren rufen prefixed +`core.parseX(...)`, die unqualifizierten Aufrufe in `disks()`/`_getTopProcesses()`/`LinuxSystem` binden +auf den statischen Delegator, nicht auf sich selbst; (2) gespaltener Typ — je **genau eine** Definition +von `DeviceInfo`/`ProcessStat`/`Uptime`/`MemoryInfo`, alle in la_core; (3) Logikdrift — die Rümpfe sind +gegen `513def1` byte-identisch, nur `static` → Top-Level. Alle Gates hat die Synthese **selbst** +ausgeführt und bestätigt (u. a. `+184`, `+22`, `30/30`, `No issues found!`, `0 changed`, +`Generated: /tmp/la_probe_test` + Probelauf `ok:probe.self`). + +Linsen: 1 (Rekursion) `clean` · 2 (Typ-Identität/import-vs-export) `clean` · 4 (Gate-Belege) `clean` · +3 (Flutter-Freiheit/Body-Treue) `befund` **ohne Blocker** — beide Hinweise betreffen nur +`packages/la_core/pubspec.yaml`, keine Parser-Abweichung. + +Belegehrlichkeit, drei Einschränkungen dieser Runde (aus der Synthese übernommen): + +- Die Veredikte der Linsen 1, 2 und 4 tragen **leere Findings-Arrays**, also keine eigene Belegkette. + Ihre Substanz ist durch die Nachprüfung der Synthese gedeckt — nicht durch Linsen-Belege. +- Der Arbeitsbaum hat sich **während** der Verifikation bewegt (Rücknahme der SDK-Absenkung, Rücknahme + des Umformatierens von `module_registry_validate_test.dart`). Dieses `PASS` gilt für den Stand + `513def1` + `94b5ef8` + `755665c`. +- **Formatierer-Methodik:** `dart format` zieht die Spracheversion aus `.dart_tool/package_config.json`, + nicht nur aus der `pubspec.yaml`. Während der Sitzung war la_core dort noch auf `3.4` gepinnt, während + die `pubspec.yaml` `^3.11.0` deklarierte — das Format-Gate hätte die falschen Regeln gemessen. Jetzt + konsistent (package_config 3.11 == pubspec `^3.11.0` == `pubspec.lock` `>=3.11.0`). **Regel für + künftige Floor-Änderungen: erst `dart pub get`, dann `dart format` als Gate.** + +**Rückfallplan.** `git revert 755665c 94b5ef8` (oder `git reset --hard 513def1` bei ungeteiltem Branch) +genügt: kein Migrationsschritt, kein Datenpfad, keine Unit, kein Packaging berührt; `la_core` ist nirgends +installiert. Die fünf neu aufgelösten Transitiven in `pubspec.lock` (`intl` 0.20.3, `test_api` 0.7.12, +`matcher` 0.12.20, `meta` 1.19.0, `vector_math` 2.4.3) kommen durch die installierte Flutter 3.47.5 ohnehin +wieder — die Lockdatei war ihr gegenüber veraltet. + +**Offene Punkte (Schnitt-1-Grenze von #93).** DI (injizierbarer Runner statt static-Global, +`runningInFlatpak`/`_cachedThreadCount` als Instanzzustand), Event-Vertrag (heute nur +`ValueNotifier`; Saat = `ProbeResult`/`ModuleDescriptor`), Flutter-`HubModule`-Adapter, +`la_probe`-Ablage in `build-deb.sh`, Logger-/`CommandHelper`-Umzug. + ## Agenten-Handoff Je Aufgabe: Basis-SHA, Pfade, Scope, Failing-Test/Fixture, Umsetzung, Ergebnis von Reviewer 1 (Funktion/UX) und Reviewer 2 (Sicherheit), **wirklich ausgeführte** Gates mit Ausgaben, rote/übersprungene Gates, manuelle Zorin-Prüfung, Rückfallplan. Kein Merge/Release/Policy-Update ohne gesonderte Freigabe. From 70ddd055598e0b3bccaea4e8a0d667798e5a6bac Mon Sep 17 00:00:00 2001 From: ElGregor Date: Wed, 30 Sep 2026 02:17:12 +0200 Subject: [PATCH 21/24] docs(mla): Push-Status im #93-Handoff nachziehen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Handoff-Abschnitt in VERIFY.md stand nach dem Push noch auf "unpushed" und listete nur zwei der drei Commits. Jetzt: drei Commits, gepusht als Fast-Forward 513def1..080becf. Im Rückfallplan entfällt der reset-Weg, weil der Branch geteilt ist -- revert ist der gangbare Pfad. Co-Authored-By: Claude Code --- docs/mla-next/VERIFY.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/mla-next/VERIFY.md b/docs/mla-next/VERIFY.md index 3398a4f..e614592 100644 --- a/docs/mla-next/VERIFY.md +++ b/docs/mla-next/VERIFY.md @@ -27,7 +27,8 @@ Aktualisierung 2026-09-30: Die GTK-Scaffold-Laufzeit ist auf Zorin verifiziert ( ## Handoff #93 Schnitt 1 — Parser-Umzug nach `packages/la_core` **Basis-SHA:** `513def150dbb37ff3883b2061396f7f326e4527b` (= `origin/feature/mla-gtk-scaffold` beim Start). -**Commits:** `94b5ef8` (la_core), `755665c` (App) — **unpushed**. +**Commits:** `94b5ef8` (la_core), `755665c` (App), `080becf` (dieser Nachweis) — **gepusht** nach +`origin/feature/mla-gtk-scaffold` als Fast-Forward `513def1..080becf` (2026-09-30). **Scope.** Vier reine Parser und ihre Modelle wandern in den Flutter-/GTK-freien Kern; die App-Dateien werden zu delegierenden Fassaden. Die **Runner bleiben app-seitig** (`disks`, `processCount`, @@ -126,9 +127,10 @@ Belegehrlichkeit, drei Einschränkungen dieser Runde (aus der Synthese übernomm konsistent (package_config 3.11 == pubspec `^3.11.0` == `pubspec.lock` `>=3.11.0`). **Regel für künftige Floor-Änderungen: erst `dart pub get`, dann `dart format` als Gate.** -**Rückfallplan.** `git revert 755665c 94b5ef8` (oder `git reset --hard 513def1` bei ungeteiltem Branch) -genügt: kein Migrationsschritt, kein Datenpfad, keine Unit, kein Packaging berührt; `la_core` ist nirgends -installiert. Die fünf neu aufgelösten Transitiven in `pubspec.lock` (`intl` 0.20.3, `test_api` 0.7.12, +**Rückfallplan.** `git revert 080becf 755665c 94b5ef8` genügt: kein Migrationsschritt, kein Datenpfad, +keine Unit, kein Packaging berührt; `la_core` ist nirgends installiert. Der Branch ist seit 2026-09-30 +geteilt (gepusht), deshalb ist `git reset --hard 513def1` hier **kein** gangbarer Weg mehr — er bräuchte +einen Force-Push. Die fünf neu aufgelösten Transitiven in `pubspec.lock` (`intl` 0.20.3, `test_api` 0.7.12, `matcher` 0.12.20, `meta` 1.19.0, `vector_math` 2.4.3) kommen durch die installierte Flutter 3.47.5 ohnehin wieder — die Lockdatei war ihr gegenüber veraltet. From b85f130da6d9144b3c455990cf6e0b66cf980679 Mon Sep 17 00:00:00 2001 From: ElGregor Date: Wed, 30 Sep 2026 03:13:48 +0200 Subject: [PATCH 22/24] feat(mla): Modul-Registry und HubModule-Adapter ersetzen acht Navigations-Switches (#60) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die acht navigationsrelevanten switch-Blöcke in lib/layouts/hub/hub_shell.dart sind durch Lookups in die neue Registry lib/layouts/hub/hub_module.dart ersetzt (HubModule, hubModules, hubModuleOf). Die Enums HubSection/HubTool ziehen dorthin um und werden von hub_shell.dart re-exportiert. Die neun Einträge reichen je einen ModuleDescriptor aus packages/la_core durch — la_core bleibt unangetastet und Flutter-frei. HubModule ergänzt den Kern um Icon, Tier, screenBuilder und isAvailable(Environment). Vier neue ARB-Keys (browser, quickNotes, fileManager, systemMonitor) in allen vier ARB-Dateien plus regenerierte app_localizations*.dart. SystemStatsService bekommt den nur lesenden Test-Seam sectionActive. en/de sind byte-identisch: die Charakterisierungstests H1–H8 wurden zuerst gegen den alten switch-Code geschrieben und blieben unverändert grün. Für it/fi zeigen die vier Werkzeugtitel jetzt echte Übersetzungen statt des englischen _tr-Fallbacks — vom l10n-Gate erzwungen, nicht gewählt. Tests: test/hub_navigation_test.dart (H1–H9), test/hub_module_registry_test.dart (T1–T7), test/hub_test_harness.dart. Voller Lauf +200 grün. Der Abschnitt „Abnahme #60" in docs/mla-next/VERIFY.md belegt alle Gates mit ausgegebener Ausgabe. Co-Authored-By: Claude Code --- lib/l10n/app_de.arb | 24 ++ lib/l10n/app_en.arb | 24 ++ lib/l10n/app_it.arb | 24 ++ lib/l10n/app_localizations.dart | 24 ++ lib/l10n/app_localizations_de.dart | 12 + lib/l10n/app_localizations_en.dart | 12 + lib/l10n/app_localizations_fi.dart | 12 + lib/l10n/app_localizations_it.dart | 12 + lib/l10n/linuxassistant_fi.arb | 24 ++ lib/layouts/hub/hub_module.dart | 253 ++++++++++++++++++++ lib/layouts/hub/hub_shell.dart | 231 +++++------------- lib/services/system_stats_service.dart | 5 + test/hub_module_registry_test.dart | 317 +++++++++++++++++++++++++ test/hub_navigation_test.dart | 317 +++++++++++++++++++++++++ test/hub_test_harness.dart | 74 ++++++ 15 files changed, 1194 insertions(+), 171 deletions(-) create mode 100644 lib/layouts/hub/hub_module.dart create mode 100644 test/hub_module_registry_test.dart create mode 100644 test/hub_navigation_test.dart create mode 100644 test/hub_test_harness.dart diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index d81326b..1eb4b56 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1303,6 +1303,30 @@ "description": "Confirmation before an executable file from the search is run", "type": "text" }, + "browser": "Browser", + "@browser": { + "placeholders": {}, + "description": "Sidebar label for the browser launcher", + "type": "text" + }, + "quickNotes": "Quick Notes", + "@quickNotes": { + "placeholders": {}, + "description": "Sidebar label for the quick notes tool", + "type": "text" + }, + "fileManager": "Dateimanager", + "@fileManager": { + "placeholders": {}, + "description": "Sidebar label for the file manager tool", + "type": "text" + }, + "systemMonitor": "Systemmonitor", + "@systemMonitor": { + "placeholders": {}, + "description": "Sidebar label for the system monitor tool", + "type": "text" + }, "runFileWarning": "Die Datei wird als Programm mit deinen Benutzerrechten ausgeführt. Fahre nur fort, wenn du weißt, was sie tut.", "@runFileWarning": { "placeholders": {}, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3d3c657..16174ca 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1269,6 +1269,30 @@ "description": "Confirmation before an executable file from the search is run", "type": "text" }, + "browser": "Browser", + "@browser": { + "placeholders": {}, + "description": "Sidebar label for the browser launcher", + "type": "text" + }, + "quickNotes": "Quick Notes", + "@quickNotes": { + "placeholders": {}, + "description": "Sidebar label for the quick notes tool", + "type": "text" + }, + "fileManager": "File manager", + "@fileManager": { + "placeholders": {}, + "description": "Sidebar label for the file manager tool", + "type": "text" + }, + "systemMonitor": "System monitor", + "@systemMonitor": { + "placeholders": {}, + "description": "Sidebar label for the system monitor tool", + "type": "text" + }, "runFileWarning": "The file will be executed as a program with your user rights. Only continue if you know what it does.", "@runFileWarning": { "placeholders": {}, diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index b0ef1c2..7ac3bc1 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1289,6 +1289,30 @@ "description": "Confirmation before an executable file from the search is run", "type": "text" }, + "browser": "Browser", + "@browser": { + "placeholders": {}, + "description": "Sidebar label for the browser launcher", + "type": "text" + }, + "quickNotes": "Note rapide", + "@quickNotes": { + "placeholders": {}, + "description": "Sidebar label for the quick notes tool", + "type": "text" + }, + "fileManager": "Gestore file", + "@fileManager": { + "placeholders": {}, + "description": "Sidebar label for the file manager tool", + "type": "text" + }, + "systemMonitor": "Monitor di sistema", + "@systemMonitor": { + "placeholders": {}, + "description": "Sidebar label for the system monitor tool", + "type": "text" + }, "runFileWarning": "Il file verrà eseguito come programma con i tuoi privilegi utente. Continua solo se sai cosa fa.", "@runFileWarning": { "placeholders": {}, diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 82f5387..938bd11 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2118,6 +2118,30 @@ abstract class AppLocalizations { /// **'Run \"{path}\"?'** String runFileQuestion(String path); + /// Sidebar label for the browser launcher + /// + /// In en, this message translates to: + /// **'Browser'** + String get browser; + + /// Sidebar label for the quick notes tool + /// + /// In en, this message translates to: + /// **'Quick Notes'** + String get quickNotes; + + /// Sidebar label for the file manager tool + /// + /// In en, this message translates to: + /// **'File manager'** + String get fileManager; + + /// Sidebar label for the system monitor tool + /// + /// In en, this message translates to: + /// **'System monitor'** + String get systemMonitor; + /// Explains what confirming the dialog above does /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 5d424c6..5baa7a6 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1167,6 +1167,18 @@ class AppLocalizationsDe extends AppLocalizations { return '\"$path\" ausführen?'; } + @override + String get browser => 'Browser'; + + @override + String get quickNotes => 'Quick Notes'; + + @override + String get fileManager => 'Dateimanager'; + + @override + String get systemMonitor => 'Systemmonitor'; + @override String get runFileWarning => 'Die Datei wird als Programm mit deinen Benutzerrechten ausgeführt. Fahre nur fort, wenn du weißt, was sie tut.'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 5de605c..0239d6e 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1158,6 +1158,18 @@ class AppLocalizationsEn extends AppLocalizations { return 'Run \"$path\"?'; } + @override + String get browser => 'Browser'; + + @override + String get quickNotes => 'Quick Notes'; + + @override + String get fileManager => 'File manager'; + + @override + String get systemMonitor => 'System monitor'; + @override String get runFileWarning => 'The file will be executed as a program with your user rights. Only continue if you know what it does.'; diff --git a/lib/l10n/app_localizations_fi.dart b/lib/l10n/app_localizations_fi.dart index f9a0a3f..b355cb0 100644 --- a/lib/l10n/app_localizations_fi.dart +++ b/lib/l10n/app_localizations_fi.dart @@ -1168,6 +1168,18 @@ class AppLocalizationsFi extends AppLocalizations { return 'Suoritetaanko \"$path\"?'; } + @override + String get browser => 'Selain'; + + @override + String get quickNotes => 'Pikamuistiinpanot'; + + @override + String get fileManager => 'Tiedostoselain'; + + @override + String get systemMonitor => 'Järjestelmän seuranta'; + @override String get runFileWarning => 'Tiedosto suoritetaan ohjelmana käyttöoikeuksillasi. Jatka vain, jos tiedät, mitä se tekee.'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 4adfa7b..edff0d0 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1176,6 +1176,18 @@ class AppLocalizationsIt extends AppLocalizations { return 'Eseguire \"$path\"?'; } + @override + String get browser => 'Browser'; + + @override + String get quickNotes => 'Note rapide'; + + @override + String get fileManager => 'Gestore file'; + + @override + String get systemMonitor => 'Monitor di sistema'; + @override String get runFileWarning => 'Il file verrà eseguito come programma con i tuoi privilegi utente. Continua solo se sai cosa fa.'; diff --git a/lib/l10n/linuxassistant_fi.arb b/lib/l10n/linuxassistant_fi.arb index 7a05b21..cc69130 100644 --- a/lib/l10n/linuxassistant_fi.arb +++ b/lib/l10n/linuxassistant_fi.arb @@ -874,6 +874,30 @@ "description": "Confirmation before an executable file from the search is run", "type": "text" }, + "browser": "Selain", + "@browser": { + "placeholders": {}, + "description": "Sidebar label for the browser launcher", + "type": "text" + }, + "quickNotes": "Pikamuistiinpanot", + "@quickNotes": { + "placeholders": {}, + "description": "Sidebar label for the quick notes tool", + "type": "text" + }, + "fileManager": "Tiedostoselain", + "@fileManager": { + "placeholders": {}, + "description": "Sidebar label for the file manager tool", + "type": "text" + }, + "systemMonitor": "Järjestelmän seuranta", + "@systemMonitor": { + "placeholders": {}, + "description": "Sidebar label for the system monitor tool", + "type": "text" + }, "runFileWarning": "Tiedosto suoritetaan ohjelmana käyttöoikeuksillasi. Jatka vain, jos tiedät, mitä se tekee.", "@runFileWarning": { "placeholders": {}, diff --git a/lib/layouts/hub/hub_module.dart b/lib/layouts/hub/hub_module.dart new file mode 100644 index 0000000..90de168 --- /dev/null +++ b/lib/layouts/hub/hub_module.dart @@ -0,0 +1,253 @@ +import 'package:flutter/material.dart'; +import 'package:la_core/la_core.dart' show ModuleDescriptor, ModuleKind; +import 'package:linux_assistant/l10n/app_localizations.dart'; +import 'package:linux_assistant/layouts/hermes_tokens.dart'; +import 'package:linux_assistant/layouts/hub/dashboard_section.dart'; +import 'package:linux_assistant/layouts/hub/storage_section.dart'; +import 'package:linux_assistant/layouts/linux_health/overview.dart'; +import 'package:linux_assistant/layouts/main_screen/main_search.dart'; +import 'package:linux_assistant/layouts/security_check/overview.dart'; +import 'package:linux_assistant/layouts/tools/file_manager.dart'; +import 'package:linux_assistant/layouts/tools/quick_notes.dart'; +import 'package:linux_assistant/layouts/tools/system_monitor.dart'; +import 'package:linux_assistant/models/environment.dart'; + +/// The sections reachable from the sidebar. +enum HubSection { dashboard, search, storage, health, security } + +/// Quick-access tools in the sidebar's "Werkzeuge" section. +/// +/// Two kinds live here: [HubTool.browser] fires a detached process launch and +/// never changes the active section, while screen-based tools +/// ([HubTool.quickNotes], [HubTool.fileManager], [HubTool.systemMonitor]) +/// render inside the hub frame like a section – the frame then tracks them in +/// its `_screenTool` field. +enum HubTool { browser, quickNotes, fileManager, systemMonitor } + +/// Feature tier, the vocabulary `MANIFEST.md` defines for this repo. +/// +/// Nothing reads it yet: it is the seam issue #86 hangs the real navigation +/// (and a tier-ordered sidebar) on, pinned here so the nine modules cannot +/// drift apart in the meantime. +enum HubTier { core, qol, n2h } + +/// What a screen may ask the shell to do. +/// +/// Deliberately one method: a screen that wants to send the user somewhere +/// else (the dashboard's storage and security tiles today, #86 tomorrow) gets +/// exactly that and nothing more. +abstract interface class HubNavigator { + void openSection(HubSection section); +} + +/// Builds the widget for a module. No [BuildContext]: none of the nine +/// screens needs one at construction time. +typedef HubScreenBuilder = Widget Function(HubNavigator nav); + +/// Resolves a module's sidebar and top-bar title. +/// +/// A function rather than a key string on purpose: `l.hubStorage` is checked +/// by the compiler, so a renamed or missing ARB getter is a build error +/// instead of a blank label at runtime. +typedef HubTitle = String Function(AppLocalizations l10n); + +/// Whether a module can be offered on this machine. +typedef HubAvailability = bool Function(Environment environment); + +/// One navigable module: the Flutter half of a [ModuleDescriptor]. +/// +/// Composition, not inheritance — the pure descriptor stays a field. Extending +/// it would drag `la_core` into a Flutter-carrying class and turn every later +/// core change into a breaking change for the UI. +@immutable +class HubModule { + const HubModule({ + required this.descriptor, + required this.icon, + required this.tier, + required this.title, + required this.screenBuilder, + this.usesStats = false, + this.isAvailable = alwaysAvailable, + }); + + /// The Flutter-free half, passed through unchanged (#59/#60 in `packages/la_core`). + final ModuleDescriptor descriptor; + + final IconData icon; + final HubTier tier; + final HubTitle title; + + /// Never null. [HubTool.browser] is a launch module and returns + /// `const SizedBox.shrink()`; its wiring hangs on [startsProcess], not on a + /// missing builder. + final HubScreenBuilder screenBuilder; + + /// Whether the section displays live system stats. Drives + /// `SystemStatsService.setSectionActive` — polling a screen that shows no + /// numbers is pure waste. + final bool usesStats; + + final HubAvailability isAvailable; + + String get id => descriptor.id; + String get titleKey => descriptor.titleKey; + ModuleKind get kind => descriptor.kind; + + /// True for modules that only start a detached process and never occupy the + /// content area. + bool get startsProcess => descriptor.kind == ModuleKind.launch; + + /// Probe and action ids, passed through for #62 / #74. Nothing calls them yet. + Set get probes => descriptor.probeIds; + Set get actions => descriptor.actionIds; + + static bool alwaysAvailable(Environment _) => true; +} + +/// The one place a module's enum value becomes a module. +/// +/// Throws instead of returning null: a miss here means a module was added to +/// an enum without a registry entry, and that should be loud. +HubModule hubModuleOf(Object key) { + if (key is! Enum) { + throw StateError('hubModuleOf expects an enum value, got $key.'); + } + final HubModule? module = hubModules[key.name]; + if (module == null) { + throw StateError( + 'No HubModule registered for ${key.runtimeType}.${key.name}. ' + 'Add it to hubModules in lib/layouts/hub/hub_module.dart.', + ); + } + return module; +} + +/// The module registry. +/// +/// Adding a module is one entry here plus its screen — no `switch` to extend. +/// Keys are the enum names; [hubModuleOf] is the only reader. +final Map hubModules = { + 'dashboard': HubModule( + descriptor: const ModuleDescriptor( + id: 'dashboard', + titleKey: 'dashboard', + viewId: 'dashboard', + kind: ModuleKind.section, + ), + icon: Icons.dashboard_outlined, + tier: HubTier.core, + title: (l10n) => l10n.dashboard, + usesStats: true, + screenBuilder: (nav) => DashboardSection( + onOpenStorage: () => nav.openSection(HubSection.storage), + onOpenSecurity: () => nav.openSection(HubSection.security), + ), + ), + 'search': HubModule( + descriptor: const ModuleDescriptor( + id: 'search', + titleKey: 'hubSearch', + viewId: 'search', + kind: ModuleKind.section, + ), + icon: Icons.search, + tier: HubTier.core, + title: (l10n) => l10n.hubSearch, + // The search screen carries the memory/disk status row. + usesStats: true, + screenBuilder: (_) => MainSearch(embedded: true), + ), + 'storage': HubModule( + descriptor: const ModuleDescriptor( + id: 'storage', + titleKey: 'hubStorage', + viewId: 'storage', + kind: ModuleKind.section, + ), + icon: Icons.storage, + tier: HubTier.core, + title: (l10n) => l10n.hubStorage, + usesStats: true, + screenBuilder: (_) => const StorageSection(), + ), + 'health': HubModule( + descriptor: const ModuleDescriptor( + id: 'health', + titleKey: 'linuxHealth', + viewId: 'health', + kind: ModuleKind.section, + ), + icon: Icons.favorite_outline, + tier: HubTier.core, + title: (l10n) => l10n.linuxHealth, + usesStats: true, + screenBuilder: (_) => const Padding( + padding: EdgeInsets.all(HermesTokens.space4), + child: LinuxHealthContent(), + ), + ), + 'security': HubModule( + descriptor: const ModuleDescriptor( + id: 'security', + titleKey: 'securityCheck', + viewId: 'security', + kind: ModuleKind.section, + ), + icon: Icons.shield_outlined, + tier: HubTier.core, + title: (l10n) => l10n.securityCheck, + screenBuilder: (_) => const SecurityCheckContent(), + ), + // Launches a detached browser process; never becomes the active content key. + 'browser': HubModule( + descriptor: const ModuleDescriptor( + id: 'browser', + titleKey: 'browser', + viewId: 'browser', + kind: ModuleKind.launch, + ), + icon: Icons.public, + tier: HubTier.qol, + title: (l10n) => l10n.browser, + screenBuilder: (_) => const SizedBox.shrink(), + ), + 'quickNotes': HubModule( + descriptor: const ModuleDescriptor( + id: 'quickNotes', + titleKey: 'quickNotes', + viewId: 'quickNotes', + kind: ModuleKind.tool, + ), + icon: Icons.edit_note, + tier: HubTier.qol, + title: (l10n) => l10n.quickNotes, + screenBuilder: (_) => const QuickNotesPage(), + ), + 'fileManager': HubModule( + descriptor: const ModuleDescriptor( + id: 'fileManager', + titleKey: 'fileManager', + viewId: 'fileManager', + kind: ModuleKind.tool, + ), + icon: Icons.folder_open, + tier: HubTier.qol, + title: (l10n) => l10n.fileManager, + screenBuilder: (_) => const FileManagerPage(), + ), + // The system monitor shows live stats, but from its own 1-second sampler – + // the shared 3-second poll stays off for tool screens. + 'systemMonitor': HubModule( + descriptor: const ModuleDescriptor( + id: 'systemMonitor', + titleKey: 'systemMonitor', + viewId: 'systemMonitor', + kind: ModuleKind.tool, + ), + icon: Icons.monitor_heart, + tier: HubTier.qol, + title: (l10n) => l10n.systemMonitor, + screenBuilder: (_) => const SystemMonitorPage(), + ), +}; diff --git a/lib/layouts/hub/hub_shell.dart b/lib/layouts/hub/hub_shell.dart index e87864d..d8c1805 100644 --- a/lib/layouts/hub/hub_shell.dart +++ b/lib/layouts/hub/hub_shell.dart @@ -1,51 +1,22 @@ import 'package:flutter/material.dart'; import 'package:linux_assistant/l10n/app_localizations.dart'; import 'package:linux_assistant/layouts/hermes_tokens.dart'; -import 'package:linux_assistant/layouts/hub/dashboard_section.dart'; -import 'package:linux_assistant/layouts/hub/storage_section.dart'; -import 'package:linux_assistant/layouts/linux_health/overview.dart'; +import 'package:linux_assistant/layouts/hub/hub_module.dart'; import 'package:linux_assistant/layouts/main_screen/main_search.dart'; -import 'package:linux_assistant/layouts/security_check/overview.dart'; import 'package:linux_assistant/layouts/settings/settings_start.dart'; -import 'package:linux_assistant/layouts/tools/file_manager.dart'; -import 'package:linux_assistant/layouts/tools/quick_notes.dart'; -import 'package:linux_assistant/layouts/tools/system_monitor.dart'; import 'package:linux_assistant/main.dart'; import 'package:linux_assistant/services/app_launcher.dart'; +import 'package:linux_assistant/services/linux.dart'; import 'package:linux_assistant/services/system_stats_service.dart'; import 'package:linux_assistant/services/theme_controller.dart'; import 'package:linux_assistant/widgets/hermes/hermes_nav_item.dart'; import 'package:window_manager/window_manager.dart'; -/// The sections reachable from the sidebar. -enum HubSection { dashboard, search, storage, health, security } - -/// Quick-access tools in the sidebar's "Werkzeuge" section. -/// -/// Two kinds live here: [HubTool.browser] fires a detached process launch and -/// never changes the active section, while screen-based tools -/// ([HubTool.quickNotes], [HubTool.fileManager], [HubTool.systemMonitor]) -/// render inside the hub frame like a section – the frame then tracks them in -/// [_screenTool]. -enum HubTool { browser, quickNotes, fileManager, systemMonitor } - -/// Whether a section displays live system stats. -/// -/// Sections that do not are told to the [SystemStatsService] so it can stop -/// polling instead of collecting numbers nobody is looking at. -bool _sectionUsesStats(HubSection section) { - switch (section) { - case HubSection.dashboard: - case HubSection.storage: - case HubSection.health: - return true; - case HubSection.search: - // The search screen carries the memory/disk status row. - return true; - case HubSection.security: - return false; - } -} +// Issue #60 moved both enums into the registry and replaced this file's +// navigation switches with lookups. The enums are re-exported because they are +// part of this file's public surface (`HubShell.initialSection`). +export 'package:linux_assistant/layouts/hub/hub_module.dart' + show HubSection, HubTool; /// The persistent frame of the hub: sidebar, top bar and the active section. /// @@ -67,7 +38,8 @@ class HubShell extends StatefulWidget { } class _HubShellState extends State - with WindowListener, WidgetsBindingObserver { + with WindowListener, WidgetsBindingObserver + implements HubNavigator { static const double _sidebarWidth = 260; static const double _railWidth = 56; @@ -99,7 +71,7 @@ class _HubShellState extends State // manager that reports the state change, and the Flutter lifecycle state // is not delivered by every desktop either. WidgetsBinding.instance.addObserver(this); - SystemStatsService().setSectionActive(_sectionUsesStats(_section)); + SystemStatsService().setSectionActive(hubModuleOf(_section).usesStats); } @override @@ -145,9 +117,14 @@ class _HubShellState extends State _section = section; _screenTool = null; }); - SystemStatsService().setSectionActive(_sectionUsesStats(section)); + SystemStatsService().setSectionActive(hubModuleOf(section).usesStats); } + /// What a screen may ask the shell to do ([HubNavigator]): the dashboard's + /// storage and security tiles use it, and so will the tier navigation of #86. + @override + void openSection(HubSection section) => _select(section); + /// Hands the content area to a screen-based tool. The underlying section /// stays selected underneath, so returning to it loses no state. /// @@ -192,116 +169,19 @@ class _HubShellState extends State ..showSnackBar(SnackBar(content: Text(message))); } - String _titleOf(BuildContext context, HubSection section) { - final l10n = AppLocalizations.of(context)!; - switch (section) { - case HubSection.dashboard: - return l10n.dashboard; - case HubSection.search: - return l10n.hubSearch; - case HubSection.storage: - return l10n.hubStorage; - case HubSection.health: - return l10n.linuxHealth; - case HubSection.security: - return l10n.securityCheck; - } - } - - IconData _iconOf(HubSection section) { - switch (section) { - case HubSection.dashboard: - return Icons.dashboard_outlined; - case HubSection.search: - return Icons.search; - case HubSection.storage: - return Icons.storage; - case HubSection.health: - return Icons.favorite_outline; - case HubSection.security: - return Icons.shield_outlined; - } - } - - IconData _iconOfTool(HubTool tool) { - switch (tool) { - case HubTool.browser: - return Icons.public; - case HubTool.quickNotes: - return Icons.edit_note; - case HubTool.fileManager: - return Icons.folder_open; - case HubTool.systemMonitor: - return Icons.monitor_heart; - } - } - - String _titleOfTool(BuildContext context, HubTool tool) { - switch (tool) { - case HubTool.browser: - return _tr(context, de: 'Browser', en: 'Browser'); - case HubTool.quickNotes: - return _tr(context, de: 'Quick Notes', en: 'Quick Notes'); - case HubTool.fileManager: - return _tr(context, de: 'Dateimanager', en: 'File manager'); - case HubTool.systemMonitor: - return _tr(context, de: 'Systemmonitor', en: 'System monitor'); - } - } - - /// Minimal de/en lookup for the Werkzeuge section. + /// Minimal de/en lookup. /// - /// TODO(l10n): Move these strings into the .arb files - /// (`tools`, `browser`, `quickNotes`, `fileManager`, `systemMonitor`) and - /// regenerate with `flutter gen-l10n`. The .arb files are ~40 KB each and - /// were not editable via API at implementation time. + /// TODO(l10n): The nine module titles come from the .arb files now + /// (`l10n.browser` and friends, #60). What is left here are the "WERKZEUGE" + /// section header and the two browser snackbars — neither is a module title. static String _tr(BuildContext context, {required String de, required String en}) { return Localizations.localeOf(context).languageCode == 'de' ? de : en; } - Widget _buildSection(HubSection section) { - switch (section) { - case HubSection.dashboard: - return DashboardSection( - onOpenStorage: () => _select(HubSection.storage), - onOpenSecurity: () => _select(HubSection.security), - ); - case HubSection.search: - return MainSearch(embedded: true); - case HubSection.storage: - return const StorageSection(); - case HubSection.health: - return const Padding( - padding: EdgeInsets.all(HermesTokens.space4), - child: LinuxHealthContent(), - ); - case HubSection.security: - return const SecurityCheckContent(); - } - } - - Widget _contentFor(Object key) { - if (key is HubTool) { - switch (key) { - case HubTool.quickNotes: - return const QuickNotesPage(); - case HubTool.fileManager: - return const FileManagerPage(); - case HubTool.systemMonitor: - return const SystemMonitorPage(); - case HubTool.browser: - // Never on screen: the browser tool launches an external process - // and is never assigned as the active content key. - return const SizedBox.shrink(); - } - } - return _buildSection(key as HubSection); - } - Widget _content() { final Object active = _screenTool ?? _section; - _built.putIfAbsent(active, () => _contentFor(active)); + _built.putIfAbsent(active, () => hubModuleOf(active).screenBuilder(this)); final visited = _built.keys.toList(); return IndexedStack( @@ -372,26 +252,27 @@ class _HubShellState extends State ), children: [ for (final section in HubSection.values) - HermesNavItem( - icon: _iconOf(section), - label: _titleOf(context, section), - selected: _screenTool == null && _section == section, - collapsed: collapsed, - onTap: () => _select(section), - ), + if (hubModuleOf(section) + .isAvailable(Linux.currentenvironment)) + _navItem( + hubModuleOf(section), + collapsed, + selected: _screenTool == null && _section == section, + onTap: () => _select(section), + ), // Werkzeuge-Sektion (Admin-Hub, Spec: docs/design/feature-spec-admin-hub.md). // Browser startet detached (kein Sectionswechsel); Quick Notes, // Dateimanager und Systemmonitor rendern im Hub-Frame // (Screen-Tools). if (!collapsed) _sectionLabel(context, t), for (final tool in HubTool.values) - HermesNavItem( - icon: _iconOfTool(tool), - label: _titleOfTool(context, tool), - selected: _screenTool == tool, - collapsed: collapsed, - onTap: () => _onToolTap(tool), - ), + if (hubModuleOf(tool).isAvailable(Linux.currentenvironment)) + _navItem( + hubModuleOf(tool), + collapsed, + selected: _screenTool == tool, + onTap: () => _onToolTap(tool), + ), ], ), ), @@ -414,21 +295,31 @@ class _HubShellState extends State ); } + /// One sidebar row for a module. Everything but [selected] and the tap + /// handler comes from the registry. + Widget _navItem( + HubModule module, + bool collapsed, { + required bool selected, + required VoidCallback onTap, + }) { + return HermesNavItem( + icon: module.icon, + label: module.title(AppLocalizations.of(context)!), + selected: selected, + collapsed: collapsed, + onTap: onTap, + ); + } + void _onToolTap(HubTool tool) { - switch (tool) { - case HubTool.browser: - _launchBrowser(); - break; - case HubTool.quickNotes: - _selectTool(HubTool.quickNotes); - break; - case HubTool.fileManager: - _selectTool(HubTool.fileManager); - break; - case HubTool.systemMonitor: - _selectTool(HubTool.systemMonitor); - break; + // The browser is the only tool that is not a screen: it starts a detached + // process and leaves the active section alone. + if (hubModuleOf(tool).startsProcess) { + _launchBrowser(); + return; } + _selectTool(tool); } /// Section header in the style of the storage screen's @@ -527,9 +418,7 @@ class _HubShellState extends State child: Row( children: [ Text( - _screenTool != null - ? _titleOfTool(context, _screenTool!) - : _titleOf(context, _section), + hubModuleOf(_screenTool ?? _section).title(l10n), style: TextStyle( color: t.strong, fontSize: 15, diff --git a/lib/services/system_stats_service.dart b/lib/services/system_stats_service.dart index 4d9c564..9077105 100644 --- a/lib/services/system_stats_service.dart +++ b/lib/services/system_stats_service.dart @@ -109,6 +109,11 @@ class SystemStatsService { @visibleForTesting int get subscriberCount => _subscribers; + /// Read-only view of the section flag, so the coupling between a hub module + /// and this service can be asserted without spawning `ps`/`df`/`free`. + @visibleForTesting + bool get sectionActive => _sectionActive; + /// Registers interest. void acquire() { _subscribers++; diff --git a/test/hub_module_registry_test.dart b/test/hub_module_registry_test.dart new file mode 100644 index 0000000..23f81e6 --- /dev/null +++ b/test/hub_module_registry_test.dart @@ -0,0 +1,317 @@ +// Characterization tests for the hub module registry introduced by issue #60. +// +// `lib/layouts/hub/hub_module.dart` replaced the eight `switch` blocks that +// used to live in `hub_shell.dart`: the enums `HubSection`/`HubTool` moved into +// that file and `hub_shell.dart` re-exports them, and each enum value is now a +// `HubModule` in the top-level `hubModules` map. These cases pin the registry +// itself — bijection with the enums, the exact shape the switches encoded, the +// screen types they built, the `la_core` contract, the probes/actions +// pass-through, the ARB binding of every title, and the availability filter. +// +// Pure Dart: widgets are constructed but never mounted (no `testWidgets`), so +// the two sections that are un-pumpable (security runs pkexec on mount) are +// still covered through their `screenBuilder` return type. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:la_core/la_core.dart'; +import 'package:linux_assistant/l10n/app_localizations.dart'; +import 'package:linux_assistant/layouts/hub/dashboard_section.dart'; +import 'package:linux_assistant/layouts/hub/hub_module.dart'; +import 'package:linux_assistant/layouts/hub/storage_section.dart'; +import 'package:linux_assistant/layouts/linux_health/overview.dart'; +import 'package:linux_assistant/layouts/main_screen/main_search.dart'; +import 'package:linux_assistant/layouts/security_check/overview.dart'; +import 'package:linux_assistant/layouts/tools/file_manager.dart'; +import 'package:linux_assistant/layouts/tools/quick_notes.dart'; +import 'package:linux_assistant/layouts/tools/system_monitor.dart'; +import 'package:linux_assistant/models/environment.dart'; + +/// A navigator that ignores everything: none of the nine `screenBuilder`s may +/// call back during construction, and a local stub keeps the case hermetic. +class _NoopNavigator implements HubNavigator { + const _NoopNavigator(); + + @override + void openSection(HubSection section) {} +} + +/// A no-op activator for the `ModuleRegistry` contract case. +class _NoopActivator implements ModuleActivator { + @override + Future start(ModuleDescriptor module) async {} + + @override + Future stop(ModuleDescriptor module) async {} +} + +/// Reads an ARB file into a `key -> value` map, dropping the `@`-prefixed +/// metadata entries (same idiom as `l10n_test.dart:23`). +Map _arbStrings(String path) { + final Map arb = + jsonDecode(File(path).readAsStringSync()) as Map; + return { + for (final MapEntry e in arb.entries) + if (!e.key.startsWith('@') && e.value is String) e.key: e.value as String, + }; +} + +/// The descriptor every ad-hoc `HubModule` in these cases shares. +const ModuleDescriptor _probeDescriptor = ModuleDescriptor( + id: 'probe', + titleKey: 'probe', + viewId: 'probe', + kind: ModuleKind.tool, + probeIds: {'disk', 'ram'}, + actionIds: {'restart', 'flush'}, +); + +void main() { + group('T1 bijection', () { + test('hubModules keys mirror HubSection then HubTool, exactly nine', () { + final List expected = [ + for (final HubSection s in HubSection.values) s.name, + for (final HubTool t in HubTool.values) t.name, + ]; + expect(hubModules.keys.toList(), expected); + expect(hubModules.length, 9); + + // module.id is the map key; titleKey is only a non-empty string here — + // T2 pins the exact key each id carries. + for (final MapEntry e in hubModules.entries) { + expect(e.value.id, e.key); + expect(e.value.titleKey, isNotEmpty); + } + }); + }); + + group('T2 shape', () { + test('icon, titleKey, kind, flags and tier match the deleted switches', () { + final Map< + String, + ({ + IconData icon, + String titleKey, + ModuleKind kind, + bool usesStats, + bool startsProcess, + HubTier tier, + })> shape = { + 'dashboard': ( + icon: Icons.dashboard_outlined, + titleKey: 'dashboard', + kind: ModuleKind.section, + usesStats: true, + startsProcess: false, + tier: HubTier.core, + ), + 'search': ( + icon: Icons.search, + titleKey: 'hubSearch', + kind: ModuleKind.section, + usesStats: true, + startsProcess: false, + tier: HubTier.core, + ), + 'storage': ( + icon: Icons.storage, + titleKey: 'hubStorage', + kind: ModuleKind.section, + usesStats: true, + startsProcess: false, + tier: HubTier.core, + ), + 'health': ( + icon: Icons.favorite_outline, + titleKey: 'linuxHealth', + kind: ModuleKind.section, + usesStats: true, + startsProcess: false, + tier: HubTier.core, + ), + 'security': ( + icon: Icons.shield_outlined, + titleKey: 'securityCheck', + kind: ModuleKind.section, + usesStats: false, + startsProcess: false, + tier: HubTier.core, + ), + 'browser': ( + icon: Icons.public, + titleKey: 'browser', + kind: ModuleKind.launch, + usesStats: false, + startsProcess: true, + tier: HubTier.qol, + ), + 'quickNotes': ( + icon: Icons.edit_note, + titleKey: 'quickNotes', + kind: ModuleKind.tool, + usesStats: false, + startsProcess: false, + tier: HubTier.qol, + ), + 'fileManager': ( + icon: Icons.folder_open, + titleKey: 'fileManager', + kind: ModuleKind.tool, + usesStats: false, + startsProcess: false, + tier: HubTier.qol, + ), + 'systemMonitor': ( + icon: Icons.monitor_heart, + titleKey: 'systemMonitor', + kind: ModuleKind.tool, + usesStats: false, + startsProcess: false, + tier: HubTier.qol, + ), + }; + + expect(shape.length, hubModules.length); + for (final MapEntry e in hubModules.entries) { + final module = e.value; + final want = shape[e.key]!; + expect(module.icon, want.icon, reason: 'icon for ${e.key}'); + expect(module.titleKey, want.titleKey, reason: 'titleKey for ${e.key}'); + expect(module.kind, want.kind, reason: 'kind for ${e.key}'); + expect(module.usesStats, want.usesStats, + reason: 'usesStats for ${e.key}'); + expect(module.startsProcess, want.startsProcess, + reason: 'startsProcess for ${e.key}'); + expect(module.tier, want.tier, reason: 'tier for ${e.key}'); + } + }); + }); + + group('T3 screen type', () { + const HubNavigator nav = _NoopNavigator(); + + test('each id builds the widget its switch arm used to return', () { + // hubModuleOf() takes the enum value (it throws on a bare String id). + final Map expected = { + HubSection.dashboard: DashboardSection, + HubSection.search: MainSearch, + HubSection.storage: StorageSection, + HubSection.health: Padding, + HubSection.security: SecurityCheckContent, + HubTool.browser: SizedBox, + HubTool.quickNotes: QuickNotesPage, + HubTool.fileManager: FileManagerPage, + HubTool.systemMonitor: SystemMonitorPage, + }; + + for (final MapEntry e in expected.entries) { + final Widget widget = hubModuleOf(e.key).screenBuilder(nav); + expect(widget.runtimeType, e.value, + reason: 'screen type for ${e.key.name}'); + } + + // The health section is a LinuxHealthContent wrapped in a padded box. + final Widget health = hubModuleOf(HubSection.health).screenBuilder(nav); + expect((health as Padding).child, isA()); + }); + }); + + group('T4 la_core contract', () { + test('the nine descriptors validate and duplicates are rejected', () { + final registry = ModuleRegistry(activator: _NoopActivator()); + for (final HubModule module in hubModules.values) { + registry.register(module.descriptor); + } + expect(registry.descriptors.length, 9); + expect(registry.validate, returnsNormally); + + // validate() must be wired, not silently tolerant: re-registering an id + // it already holds is a ModuleRegistryError. + final ModuleDescriptor duplicate = hubModules.values.first.descriptor; + expect( + () => registry.register(duplicate), + throwsA(isA()), + ); + }); + }); + + group('T5 pass-through', () { + test('probes and actions expose exactly the descriptor ids', () { + const HubModule module = HubModule( + descriptor: _probeDescriptor, + icon: Icons.science_outlined, + tier: HubTier.qol, + title: _unusedTitle, + screenBuilder: _emptyScreen, + ); + expect(module.probes, equals(_probeDescriptor.probeIds)); + expect(module.actions, equals(_probeDescriptor.actionIds)); + expect(module.id, _probeDescriptor.id); + expect(module.titleKey, _probeDescriptor.titleKey); + expect(module.kind, _probeDescriptor.kind); + }); + }); + + group('T6 ARB binding', () { + // One ARB file per supported locale; the delegate resolves each tag. + const Map arbFiles = { + 'en': 'lib/l10n/app_en.arb', + 'de': 'lib/l10n/app_de.arb', + 'it': 'lib/l10n/app_it.arb', + 'fi': 'lib/l10n/linuxassistant_fi.arb', + }; + + test('every title reads the ARB value behind its titleKey', () async { + final Map enArb = _arbStrings(arbFiles['en']!); + // A misspelt titleKey would have no template entry at all. + for (final HubModule module in hubModules.values) { + expect(enArb.containsKey(module.titleKey), isTrue, + reason: 'titleKey ${module.titleKey} is not in the EN template'); + } + + for (final MapEntry locale in arbFiles.entries) { + final AppLocalizations l10n = + await AppLocalizations.delegate.load(Locale(locale.key)); + final Map arb = _arbStrings(locale.value); + + for (final HubModule module in hubModules.values) { + // The fi ARB has no entry for the four section keys (dashboard, + // hubSearch, hubStorage, linuxHealth): the generator falls back to + // the ENGLISH template, so that fallback is the expected value there. + final String expected = + arb[module.titleKey] ?? enArb[module.titleKey]!; + expect(module.title(l10n), expected, + reason: '${locale.key}.${module.titleKey}'); + } + } + }); + }); + + group('T7 availability', () { + test('all nine are available, and the false path is reachable', () { + for (final HubModule module in hubModules.values) { + expect(module.isAvailable(Environment()), isTrue, + reason: 'availability of ${module.id}'); + } + + // Today every real module uses alwaysAvailable; a local module with a + // blocking predicate proves the filter branch exists and is honoured. + final HubModule unavailable = HubModule( + descriptor: _probeDescriptor, + icon: Icons.science_outlined, + tier: HubTier.qol, + title: _unusedTitle, + screenBuilder: _emptyScreen, + isAvailable: (_) => false, + ); + expect(unavailable.isAvailable(Environment()), isFalse); + }); + }); +} + +/// Constant title/screen stubs for the ad-hoc modules (never rendered). +String _unusedTitle(AppLocalizations l10n) => ''; +Widget _emptyScreen(HubNavigator nav) => const SizedBox.shrink(); diff --git a/test/hub_navigation_test.dart b/test/hub_navigation_test.dart new file mode 100644 index 0000000..3ef17b8 --- /dev/null +++ b/test/hub_navigation_test.dart @@ -0,0 +1,317 @@ +// Characterization tests for the hub navigation (issue #60, acceptance +// criterion "Verhalten unverändert"). +// +// These are written against TODAY's production code and must stay green, +// unchanged, after `hub_shell.dart` is rebuilt from switch blocks onto a +// registry. If one turns red after the rebuild, the rebuild changed behaviour. +// +// Hermeticity notes: +// * The "Search" and "Security Check" sections are never tapped here: Search +// loads the search index through `main_search_loader`, and Security Check +// starts `pkexec` on mount (lib/layouts/security_check/overview.dart:54). +// * No `pumpAndSettle` anywhere — the system monitor (and section screens) +// can hold live tickers/timers that never settle. +// * QuickNotes/FileManager do real file IO under the fake clock, so those +// pumps go through `drainRealIo`; their contents are never asserted. +import 'dart:ui' show Tristate; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:linux_assistant/layouts/hub/dashboard_section.dart'; +import 'package:linux_assistant/layouts/hub/storage_section.dart'; +import 'package:linux_assistant/layouts/tools/quick_notes.dart'; +import 'package:linux_assistant/services/app_launcher.dart'; +import 'package:linux_assistant/services/system_stats_service.dart'; +import 'package:linux_assistant/widgets/hermes/hermes_nav_item.dart'; + +import 'hub_test_harness.dart'; + +/// The ten sidebar rows in tree order (English locale). +/// +/// The first nine come from `_titleOf`/`_titleOfTool`; "Setting" is the +/// separate entry after the divider (`l10n.settings`) and deliberately lives +/// outside the module list. +const List kSidebarLabels = [ + 'Dashboard', + 'Search', + 'Storage', + 'Linux health', + 'Security Check', + 'Browser', + 'Quick Notes', + 'File manager', + 'System monitor', + 'Setting', +]; + +/// The labels that count as "section entry + settings" in H5 — i.e. every +/// sidebar row except the four tools. +const Set kSectionAndSettingsLabels = { + 'Dashboard', + 'Search', + 'Storage', + 'Linux health', + 'Security Check', + 'Setting', +}; + +List _navItems(WidgetTester tester) => + tester.widgetList(find.byType(HermesNavItem)).toList(); + +HermesNavItem _navItem(WidgetTester tester, String label) => + _navItems(tester).firstWhere((i) => i.label == label); + +Finder _navTap(String label) => find.widgetWithText(HermesNavItem, label); + +IndexedStack _stack(WidgetTester tester) => + tester.widget(find.byType(IndexedStack)); + +void main() { + setUp(() { + // Order matters: resetForTesting restores `_windowVisible = true` + // (system_stats_service.dart:252-260), so the visibility flag has to be + // flipped afterwards. With windowVisible == false `_shouldPoll` stays + // false and no Timer.periodic / ps/df/free fork ever starts. + SystemStatsService().resetForTesting(); + SystemStatsService().setWindowVisible(false); + }); + + tearDown(() { + AppLauncher.resetOverrides(); + SystemStatsService().resetForTesting(); + }); + + testWidgets('H1 sidebar shows the nine modules and settings in order', + (tester) async { + useDesktopView(tester); + await tester.pumpWidget(buildHubHost()); + await tester.pump(); + + expect( + _navItems(tester).map((i) => i.label).toList(), + kSidebarLabels, + ); + + await tester.pumpWidget(const SizedBox()); + }); + + testWidgets('H2 each module carries its own icon', (tester) async { + useDesktopView(tester); + await tester.pumpWidget(buildHubHost()); + await tester.pump(); + + const expected = { + 'Dashboard': Icons.dashboard_outlined, + 'Search': Icons.search, + 'Storage': Icons.storage, + 'Linux health': Icons.favorite_outline, + 'Security Check': Icons.shield_outlined, + 'Browser': Icons.public, + 'Quick Notes': Icons.edit_note, + 'File manager': Icons.folder_open, + 'System monitor': Icons.monitor_heart, + }; + for (final entry in expected.entries) { + expect(_navItem(tester, entry.key).icon, entry.value, reason: entry.key); + } + + await tester.pumpWidget(const SizedBox()); + }); + + testWidgets( + 'H3 "TOOLS" appears once, between the last section and first tool', + (tester) async { + useDesktopView(tester); + await tester.pumpWidget(buildHubHost()); + await tester.pump(); + + expect(find.text('TOOLS'), findsOneWidget); + final double toolsY = tester.getCenter(find.text('TOOLS')).dy; + final double securityY = tester.getCenter(find.text('Security Check')).dy; + final double browserY = tester.getCenter(find.text('Browser')).dy; + expect(toolsY, greaterThan(securityY)); + expect(toolsY, lessThan(browserY)); + + await tester.pumpWidget(const SizedBox()); + }); + + testWidgets('H4 switching to Storage moves the stack, ticker and selection', + (tester) async { + useDesktopView(tester); + await tester.pumpWidget(buildHubHost()); + await tester.pump(); + + expect(_stack(tester).index, 0); + + await tester.tap(_navTap('Storage')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 50)); + + expect(_stack(tester).index, 1); + expect(topBarTitle(tester), 'Storage'); + + // The IndexedStack children are TickerMode wrappers (hub_shell.dart:302-320). + TickerMode tickerFor(Type type) => tester.widget( + find + .ancestor( + of: find.byType(type, skipOffstage: false), + matching: find.byType(TickerMode, skipOffstage: false), + ) + .first, + ); + expect(tickerFor(StorageSection).enabled, isTrue); + expect(tickerFor(DashboardSection).enabled, isFalse); + + expect(_navItem(tester, 'Storage').selected, isTrue); + expect(_navItem(tester, 'Dashboard').selected, isFalse); + + // The nav row reports its selected state to accessibility + // (hermes_nav_item.dart:76). + final handle = tester.ensureSemantics(); + final int storageIndex = + _navItems(tester).indexWhere((i) => i.label == 'Storage'); + final node = + tester.getSemantics(find.byType(HermesNavItem).at(storageIndex)); + expect(node.flagsCollection.isSelected, Tristate.isTrue); + handle.dispose(); + + await tester.tap(_navTap('Dashboard')); + await tester.pump(); + expect(_stack(tester).index, 0); + + await tester.pumpWidget(const SizedBox()); + }); + + testWidgets('H5 Quick Notes is a screen tool and selects no section', + (tester) async { + useDesktopView(tester); + await tester.pumpWidget(buildHubHost()); + await tester.pump(); + + await tester.tap(_navTap('Quick Notes')); + await tester.pump(); + // QuickNotesPage runs real file IO from initState — drain it. + await drainRealIo(tester); + + expect(topBarTitle(tester), 'Quick Notes'); + expect(find.byType(QuickNotesPage), findsOneWidget); + + for (final item in _navItems(tester)) { + if (kSectionAndSettingsLabels.contains(item.label)) { + expect(item.selected, isFalse, reason: item.label); + } + } + // The tool row itself is the selected one while its screen is open. + expect(_navItem(tester, 'Quick Notes').selected, isTrue); + + await tester.tap(_navTap('Dashboard')); + await tester.pump(); + expect(_stack(tester).index, 0); + + await tester.pumpWidget(const SizedBox()); + }); + + testWidgets('H6 Browser launches a process but never changes the section', + (tester) async { + useDesktopView(tester); + final launched = []; + AppLauncher.debugOverride( + whichRunner: (binary) async => binary == 'brave', + processStarter: (binary, args) async { + launched.add(binary); + return true; + }, + ); + + await tester.pumpWidget(buildHubHost()); + await tester.pump(); + + final int childrenBefore = _stack(tester).children.length; + + await tester.tap(_navTap('Browser')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 10)); + + expect(launched, contains('brave')); + // No new screen was registered — the browser is not a screen tool. + expect(_stack(tester).children.length, childrenBefore); + expect(_navItem(tester, 'Dashboard').selected, isTrue); + expect(topBarTitle(tester), 'Dashboard'); + + await tester.pumpWidget(const SizedBox()); + }); + + testWidgets('H7 a narrow window collapses the sidebar to an icon rail', + (tester) async { + useDesktopView(tester, width: 900, height: 800); + await tester.pumpWidget(buildHubHost()); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + + expect(_navItems(tester).every((i) => i.collapsed), isTrue); + // No module label is rendered inside a nav row any more (the top bar's + // title text is a separate, non-nav Text and does not count). + for (final label in kSidebarLabels) { + expect(find.widgetWithText(HermesNavItem, label), findsNothing, + reason: label); + } + // ... but the icons are all there. + expect(find.byIcon(Icons.dashboard_outlined), findsOneWidget); + expect(find.byIcon(Icons.favorite_outline), findsOneWidget); + expect(find.byIcon(Icons.shield_outlined), findsOneWidget); + expect(find.byIcon(Icons.public), findsOneWidget); + expect(find.byIcon(Icons.edit_note), findsOneWidget); + expect(find.byIcon(Icons.folder_open), findsOneWidget); + expect(find.byIcon(Icons.monitor_heart), findsOneWidget); + // The "TOOLS" section header is suppressed in rail mode. + expect(find.text('TOOLS'), findsNothing); + + await tester.pumpWidget(const SizedBox()); + }); + + testWidgets('H8 the tool titles come from the German locale', (tester) async { + useDesktopView(tester); + await tester.pumpWidget(buildHubHost( + locale: const Locale('de', ''), + supportedLocales: const [Locale('de', '')], + )); + await tester.pump(); + + expect(find.text('Dateimanager'), findsOneWidget); + expect(find.text('Systemmonitor'), findsOneWidget); + + await tester.pumpWidget(const SizedBox()); + }); + + // H9 was added after the rebuild: it needs `SystemStatsService.sectionActive`, + // the read-only getter the rebuild introduced. H1..H8 above are unchanged and + // were green against the switch-based code — this one could not be. + // + // The coupling is only observable through that getter (the alternative is + // watching for real ps/df/free forks), so the flag stands in for "the poll + // runs". Security Check and System monitor are avoided on purpose: pkexec on + // mount, and a Ticker-driven sampler. + testWidgets('H9 a stats section turns the poll on, a screen tool off', + (tester) async { + useDesktopView(tester); + await tester.pumpWidget(buildHubHost()); + await tester.pump(); + + expect(SystemStatsService().sectionActive, isTrue, + reason: 'dashboard consumes stats'); + + await tester.tap(_navTap('Quick Notes')); + await tester.pump(); + await drainRealIo(tester); + expect(SystemStatsService().sectionActive, isFalse, + reason: 'a tool screen shows no live stats'); + + await tester.tap(_navTap('Dashboard')); + await tester.pump(); + await drainRealIo(tester); + expect(SystemStatsService().sectionActive, isTrue, + reason: 'returning to a section resumes the poll'); + + await tester.pumpWidget(const SizedBox()); + }); +} diff --git a/test/hub_test_harness.dart b/test/hub_test_harness.dart new file mode 100644 index 0000000..b834e3f --- /dev/null +++ b/test/hub_test_harness.dart @@ -0,0 +1,74 @@ +// Shared host for the hub-navigation characterization tests (issue #60). +// +// Deliberately has no `main()` and no `test()` calls: it is imported by +// `hub_navigation_test.dart`, which owns the cases H1..H8. The cases are +// written against today's production code (`hub_shell.dart` with its switch +// blocks) and must stay green, unchanged, after that file is rebuilt on a +// registry. +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:linux_assistant/l10n/app_localizations.dart'; +import 'package:linux_assistant/layouts/hub/hub_shell.dart'; +import 'package:linux_assistant/layouts/mint_y.dart'; + +/// The app's real theme and localization wiring around a [HubShell]. +/// +/// Mirrors the `_host(...)` helper in `hermes_widgets_test.dart:18`, with the +/// locale/supported-locales lifted to parameters so the German case (H8) can +/// reuse it. +Widget buildHubHost({ + Locale locale = const Locale('en', ''), + List supportedLocales = const [Locale('en', '')], + HubSection initialSection = HubSection.dashboard, +}) { + return MaterialApp( + theme: MintY.theme(), + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: supportedLocales, + locale: locale, + home: Scaffold(body: HubShell(initialSection: initialSection)), + ); +} + +/// Pins the test window to a desktop size. +/// +/// Without this the surface is 800x600 logical pixels by default, which is +/// below the sidebar's `_collapseBreakpoint` (1000, hub_shell.dart:75) and +/// would render the icon rail instead of the labelled sidebar. +void useDesktopView(WidgetTester tester, + {double width = 1200, double height = 900}) { + tester.view.physicalSize = Size(width, height); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.reset); +} + +/// Real file IO started from the fake-async test zone needs several +/// drain-and-pump rounds to work through its await chain; one round only +/// advances it by a single hop (same pattern as `_drainRealIo` in +/// `quick_notes_widget_test.dart:12`). +Future drainRealIo(WidgetTester tester, {int rounds = 8}) async { + for (var i = 0; i < rounds; i++) { + await tester + .runAsync(() => Future.delayed(const Duration(milliseconds: 50))); + await tester.pump(const Duration(milliseconds: 50)); + } +} + +/// Text of the top bar's title. +/// +/// `fontSize: 15` together with `FontWeight.w600` is set by exactly one Text +/// in the app — `HubShell._topBar` (hub_shell.dart:529-538). No section reuses +/// that combination, so the first such Text is unambiguously the title. +String topBarTitle(WidgetTester tester) { + final Text title = tester.widgetList(find.byType(Text)).firstWhere( + (t) => + t.style?.fontSize == 15 && t.style?.fontWeight == FontWeight.w600, + ); + return title.data ?? ''; +} From bad3d5a886d766673ac119531ac3a2e580b6608e Mon Sep 17 00:00:00 2001 From: ElGregor Date: Wed, 30 Sep 2026 03:14:03 +0200 Subject: [PATCH 23/24] =?UTF-8?q?docs(mla):=20Abnahme-Abschnitt=20f=C3=BCr?= =?UTF-8?q?=20#60=20in=20VERIFY.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Basis-SHA, Gate-Tabelle mit tatsächlichen Ausgaben, Rest-Switch-Beweis, Abnahmekriterien, Synthese-Verdikt (PASS), akzeptierte Befunde, nicht ausgeführte Gates, Test-Nebenwirkung, Planabweichungen, Leak-Grep-Einordnung und Rückfallplan. Dazu drei alte Stellen korrigiert: la_core dart test 14 -> 30, Gate-1-Box für #60 mit geliefertem Adapter, HubModule-Adapter nicht mehr als offen geführt. Co-Authored-By: Claude Code --- docs/mla-next/VERIFY.md | 130 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 126 insertions(+), 4 deletions(-) diff --git a/docs/mla-next/VERIFY.md b/docs/mla-next/VERIFY.md index e614592..a64317c 100644 --- a/docs/mla-next/VERIFY.md +++ b/docs/mla-next/VERIFY.md @@ -12,10 +12,10 @@ Aktualisierung 2026-09-30: Die GTK-Scaffold-Laufzeit ist auf Zorin verifiziert ( ## Gate 1: Kern/Registry -- [x] Issue #59: `dart test` und `dart compile exe`; Probe ohne DISPLAY/WAYLAND_DISPLAY; Binärgröße/Startzeit messen. — 2026-09-30: `dart test` 14/14 in packages/la_core (frisch); `dart compile exe` + Headless-Lauf ohne DISPLAY/WAYLAND_DISPLAY; Binär 6 547 240 Bytes, Median-Startzeit 3 ms — siehe docs/mla-next/BASELINE.md §7 -- [x] Issue #60: IDs eindeutig, fehlende/zyklische Abhängigkeiten abgewiesen, Start/Stop/Lazy-Loading getestet; Flutter-Navigation unverändert. — 2026-09-30: `dart test` 14/14 in packages/la_core (doppelte IDs, fehlende/zyklische Abhängigkeiten, Topo-Start/Rückwärts-Stop, Single-Flight); `git diff --stat 92bef60..HEAD -- lib/ additional/ deb/ linux/` leer (Exit 0); Root-`flutter test` +184 bestanden +- [x] Issue #59: `dart test` und `dart compile exe`; Probe ohne DISPLAY/WAYLAND_DISPLAY; Binärgröße/Startzeit messen. — 2026-09-30: `dart test` (damals 14/14, heute 30/30) in packages/la_core; `dart compile exe` + Headless-Lauf ohne DISPLAY/WAYLAND_DISPLAY; Binär 6 547 240 Bytes, Median-Startzeit 3 ms — siehe docs/mla-next/BASELINE.md §7 +- [x] Issue #60: IDs eindeutig, fehlende/zyklische Abhängigkeiten abgewiesen, Start/Stop/Lazy-Loading getestet; Flutter-Navigation unverändert. — Kern-Registry-Teil 2026-09-30: `dart test` 30/30 in packages/la_core (doppelte IDs, fehlende/zyklische Abhängigkeiten, Topo-Start/Rückwärts-Stop, Single-Flight); `git diff --stat 92bef60..HEAD -- lib/ additional/ deb/ linux/` leer (Exit 0). **Der Flutter-`HubModule`-Adapter samt Vollständigkeitstest ist nachgeliefert** — Belege im Abschnitt „Abnahme #60" unten (Root-`flutter test` +200) - [x] Bestehende Repo-Gates nach Scope tatsächlich ausführen: `tool/check-versions.sh`, `dart format`, `flutter analyze`, `flutter test`, Python-Tests. — 2026-09-30 frisch ausgeführt, alle Exit 0: `version 0.8.0 is consistent`; `Formatted 118 files (0 changed)`; `No issues found!`; `00:02 +184: All tests passed!`; `Ran 49 tests` / `OK` — vgl. docs/mla-next/BASELINE.md §4 -- [x] Issue #93, Schnitt 1 (Parser-Umzug nach `packages/la_core`): App-Test byte-identisch, alle Gates grün. — 2026-09-30: Handoff-Abschnitt unten. DI/Event-Vertrag und Flutter-`HubModule`-Adapter bleiben offen (spätere Schnitte von #93). +- [x] Issue #93, Schnitt 1 (Parser-Umzug nach `packages/la_core`): App-Test byte-identisch, alle Gates grün. — 2026-09-30: Handoff-Abschnitt unten. DI/Event-Vertrag bleibt offen (späterer Schnitt von #93); der Flutter-`HubModule`-Adapter ist mit #60 geliefert (Abschnitt unten). ## Gate 2: IPC und Backup @@ -136,9 +136,131 @@ wieder — die Lockdatei war ihr gegenüber veraltet. **Offene Punkte (Schnitt-1-Grenze von #93).** DI (injizierbarer Runner statt static-Global, `runningInFlatpak`/`_cachedThreadCount` als Instanzzustand), Event-Vertrag (heute nur -`ValueNotifier`; Saat = `ProbeResult`/`ModuleDescriptor`), Flutter-`HubModule`-Adapter, +`ValueNotifier`; Saat = `ProbeResult`/`ModuleDescriptor`), Flutter-`HubModule`-Adapter (**mit #60 geliefert**, siehe Abschnitt unten), `la_probe`-Ablage in `build-deb.sh`, Logger-/`CommandHelper`-Umzug. +## Abnahme #60 (FU1) — Modul-Registry und Flutter-`HubModule`-Adapter + +**Basis-SHA:** `70ddd05` (= `HEAD` auf `feature/mla-gtk-scaffold` beim Start), Arbeitsbaum sauber. +**Status:** umgesetzt und **nicht committet** — Push/PR/Bump/Tag warten auf Bastis OK. + +**Scope.** Acht navigationsrelevante `switch`-Blöcke in `lib/layouts/hub/hub_shell.dart` werden durch +Lookups in eine neue Registry ersetzt. Neu: `lib/layouts/hub/hub_module.dart` (`HubModule`, +`hubModules`, `hubModuleOf`; die Enums `HubSection`/`HubTool` ziehen dorthin um, `hub_shell.dart` +re-exportiert sie). Die neun Einträge reichen je einen `ModuleDescriptor` aus `packages/la_core` +**durch**; `la_core` bleibt unangetastet und Flutter-frei. Vier neue ARB-Keys (`browser`, +`quickNotes`, `fileManager`, `systemMonitor`) in allen vier ARB-Dateien + `flutter gen-l10n`. +`SystemStatsService` bekommt einen read-only Test-Seam `sectionActive` (nur lesend, nur für den Test). +Die neun Screens bleiben unverändert. + +**Geänderte/neue Dateien** (`git status --short`): 11 modifiziert (`lib/l10n/*` 9×, +`lib/layouts/hub/hub_shell.dart`, `lib/services/system_stats_service.dart`), 4 neu +(`lib/layouts/hub/hub_module.dart`, `test/hub_module_registry_test.dart`, `test/hub_navigation_test.dart`, +`test/hub_test_harness.dart`). + +**Gates — tatsächlich ausgeführt (2026-09-30), alle Exit 0.** + +| Gate | Ausgabe | +|---|---| +| `bash tool/check-versions.sh` | `version 0.8.0 is consistent` | +| `dart format --output=none --set-exit-if-changed lib test` | `Formatted 122 files (0 changed)` | +| `flutter analyze` | `No issues found!` | +| `flutter test` (voller Lauf) | `00:09 +200: All tests passed!` | +| `flutter test test/hub_navigation_test.dart` | `+9: All tests passed!` (H1–H9) | +| `flutter test test/hub_module_registry_test.dart` | `+7: All tests passed!` (T1–T7) | +| `flutter test test/l10n_test.dart` | `+7: All tests passed!` (Budgets halten) | +| `python3 -m unittest discover -s tests -t .` (additional/python) | `Ran 49 tests` / `OK` | +| la_core: `dart analyze` · `dart format` · `dart test` | `No issues found!` · `13 files (0 changed)` · `+30: All tests passed!` | + +**Rest-Switch-Beweis.** `grep -n "switch (" lib/layouts/hub/hub_shell.dart` → nur `:151` +(`BrowserLaunchResult`) und `:456` (`ThemeMode`), die zwei erlaubten Ausnahmen. Kein +`_sectionUsesStats`/`_titleOf`/`_iconOf`/`_iconOfTool`/`_titleOfTool`/`_buildSection`/`_contentFor` +mehr im Baum. Jede verbleibende `HubSection`/`HubTool`-Nennung in `lib/` ist ein typisiertes Feld, +eine Iteration (`HubSection.values`) oder ein Navigationsaufruf — keine Icon-/Titel-/Screen-/ +Stats-Unterscheidung. + +**Abnahmekriterien → Beleg.** (a) *Verhalten unverändert:* H1–H8 blieben **byte-identisch** und waren +grün **gegen den Switch-Code** (Phase 1) wie danach; sie laufen unter en/de. (b) *Neues Modul = +Listeneintrag + Screen:* Map-Eintrag + `screenBuilder` + ein ARB-Key je Datei. (c) *Vollständigkeits- +test:* T1 (Bijection `hubModules.keys` == Enum-Namen, genau neun, `id == key`), T3 (Enum→Screen-Typ, +deckt die nicht pumpbaren Sektionen ab), T4 (`ModuleRegistry.validate()` über alle neun Deskriptoren). +(d) *Adapter:* `HubModule` ergänzt Icon, Tier, `screenBuilder`, `isAvailable(Environment)`; `la_core` +unberührt (`git status --short packages/` leer) und Flutter-frei. + +**Unabhängige Verifikation.** Workflow `mla-60-phase3-registry` (Phase 3 + drei read-only Linsen + +Synthese, 444 k Subagenten-Tokens, 0 Fehler): **Synthese `PASS`, keine Blocker.** Linse 1 +(Verhaltens-Diff gegen `HEAD`) bestätigt alle acht Switches als zweiggenau ersetzt; Linse 2 (Vertrag/ +Single-Source) bestätigt `hubModules` als Single Source, `id == Enum.name` durch T1 gehalten und +`la_core` unberührt; Linse 3 (Belege) hat alle Gates selbst nachgefahren und dabei meinen +Test-Gesamtwert korrigiert (+200, nicht +193 — die +193 zählten die sieben Registry-Fälle nicht mit). + +**Akzeptierte Befunde (alle minor/info, keiner blockierend).** + +1. **it/fi-Titel-Delta.** Für it/fi zeigen die vier Werkzeugtitel jetzt echte Übersetzungen statt des + englischen `_tr`-Fallbacks (`Dateimanager`→`Gestore file`, `Systemmonitor`→`Monitor di sistema`, + fi `Tiedostoselain`, `Järjestelmän seuranta`). en/de sind **byte-identisch** zu vorher. Das ist die + vom l10n-Gate erzwungene, beabsichtigte Folge des ARB-Umzugs (Planabweichung 3); nur Labels, keine + Zeile/Screen/Selection. H1–H9 decken it/fi **nicht** ab — deshalb Bastis Sichtprüfung. +2. **Tools-`usesStats` hat eine zweite Quelle.** Sektionen lesen die Registry + (`hubModuleOf(...).usesStats`), aber `_selectTool` setzt `setSectionActive(false)` hart. Heute + deckungsgleich (alle vier Tools `false`, von T2 gepinnt), doch ein künftiges Tool mit + `usesStats: true` würde ignoriert. Latent, nicht blockierend. +3. **Sidebar-Verfügbarkeitsfilter** neu (`isAvailable(Linux.currentenvironment)`): heute inert, da alle + neun `alwaysAvailable`. Beobachtbar verhaltensneutral. +4. **Topbar-Such-Icon** dupliziert `Icons.search`/`hubSearch` aus der Registry (Chrome, keine Nav-Zeile). +5. **`hubModuleOf` matcht nur über `key.name`** — ein fremdes Enum mit kollidierendem Namen würde still + falsch auflösen (heute kein solcher Name; latent). +6. **Rest-Enum-Vergleich** `if (_section != HubSection.search)` (`:429`) ist mit `HEAD` identisch und + keiner der acht migrierten Switches (Transparenzhinweis). + +**Nicht ausgeführt / keine Aussage.** + +- **Kein CI-Grün.** `.github/workflows/build.yml` triggert `feature/mla-gtk-scaffold` nicht. + Ersatzwahrheit ist der lokale Lauf oben. +- Keine Messung des 3-s-Polls über echte `ps`/`df`/`free`-Forks — die Stats-Kopplung ist über den + Test-Seam `sectionActive` belegt (H9), nicht über Prozess-Spawns. +- Kein `ModuleActivator` in Produktion, kein `activate`/`deactivate`, keine DI, kein Lebenszyklus; nur + der Test registriert und validiert. `probes`/`actions` sind Getter **ohne Aufrufer** (#62/#74). +- `tier` ist definiert und gepinnt, aber von **keiner** Mechanik gelesen (Andockpunkt #86). +- `install.sh`, `deb/`, `prototype/` unberührt. + +**Nebenwirkung der Tests (melden, nicht beheben).** H5/H9 pumpen `QuickNotesPage`; deren `_ensureDir()` +legt beim Lauf ein echtes Verzeichnis unter dem XDG-Datenpfad an. Inhalt wird nie assertiert. + +**Planabweichungen.** + +1. Das Issue nennt „sechs switch-Blöcke"; tatsächlich sind es **acht** navigationsrelevante (das Issue + bittet selbst um Nachzählung). +2. „Ein neues Modul ist ein Listeneintrag plus Screen" wird zu: Map-Eintrag + Screen-Fabrik + ein + ARB-Key in **vier** Dateien (+ `gen-l10n`). Kein zentraler Titel-Table möglich (Flutters l10n kennt + keinen dynamischen Zugriff). +3. „Verhalten unverändert" gilt **byte-genau für en/de**; für **it/fi** ändern sich vier Werkzeugtitel + (Befund 1). fi-**Sektions**labels bleiben englischer Fallback wie zuvor. +4. `usesStats` und `tier` stehen nicht in der Issue-Feldliste, sind aber durch Entscheid 1 (Enums als + ID-Raum) bzw. die Issue-Nennung „Tier" erzwungen. +5. Der Vollständigkeitstest ist eine **eigene neue Datei** (`test/hub_module_registry_test.dart`), keine + Erweiterung von `hub_navigation_test.dart` — so bleibt H1–H9 byte-identisch als Charakterisierung. + +**Rückfallplan.** `git revert` der (noch zu committenden) #60-Commits genügt: kein Migrationsschritt, +kein Datenpfad, keine Unit, kein Packaging; `la_core` ist unberührt, die generierten l10n-Dateien +laufen mit `gen-l10n` reproduzierbar neu. Der Branch ist seit 2026-09-30 geteilt (gepusht), deshalb +ist `git reset --hard 70ddd05` hier **kein** Weg — er bräuchte einen Force-Push. + +**Leak-Grep (öffentliches Repo).** Über den #60-Diff und die neuen Dateien: keine Treffer. Über +`docs/` insgesamt bleiben Treffer, aber **kein neuer** und **kein echter**: die Muster `~/…` +(generische XDG-Pfade ohne Nutzernamen) und `:[0-9]{4,5}` (Dart-Zeilenreferenzen wie +`mint_y.dart:1054`, eine Prozess-ID) sind Fehlalarme der Plan-Regex, dazu ein bewusst fiktives +`/home/john`-Beispiel in `docs/handoff/analysis/bug-hunt-2026-09-10.md`. Der einzige echte Befund war +der **Nutzername** in drei Handoff-Dateien (Journal-/pkexec-Auszüge) — in einem eigenen Commit +redigiert (`` statt des Namens; `--home=/home/`). Empfehlung: das Gate auf +Nutzername/IP/`localhost` prüfen, nicht auf `~/` und Zeilen-/PID-Ziffern. + +**Manuelle Abnahme (Bastis Teil, Wayland **und** X11).** Browser-Tap in der Sidebar (echter Start bzw. +die drei Snackbar-Varianten) · Sektions- und Tool-Wechsel im echten Fenster (3-s-Poll stoppt in +Security und in Tool-Screens, startet beim Zurückwechseln wieder) · Minimieren/Wiederherstellen · +Sidebar-Kollaps an der 1000-px-Grenze beim echten Resize · **it/fi-Anzeige bestätigen** (Wortwahl ist +Vorschlag, ggf. zu korrigieren) · Optik: keine Verschiebung außer den Labels. + ## Agenten-Handoff Je Aufgabe: Basis-SHA, Pfade, Scope, Failing-Test/Fixture, Umsetzung, Ergebnis von Reviewer 1 (Funktion/UX) und Reviewer 2 (Sicherheit), **wirklich ausgeführte** Gates mit Ausgaben, rote/übersprungene Gates, manuelle Zorin-Prüfung, Rückfallplan. Kein Merge/Release/Policy-Update ohne gesonderte Freigabe. From 8b1b187d37ac5ff10fd493f01478742e61de9c48 Mon Sep 17 00:00:00 2001 From: ElGregor Date: Wed, 30 Sep 2026 03:14:24 +0200 Subject: [PATCH 24/24] =?UTF-8?q?docs:=20Nutzernamen=20aus=20Handoff-Ausz?= =?UTF-8?q?=C3=BCgen=20entfernen=20(=C3=B6ffentliches=20Repo)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Journal- und pkexec-Auszüge in drei Handoff-Dateien trugen den lokalen Nutzernamen (viermal unix-user/pkexec bzw. eine uid-Zeile, dazu ein --home-Pfad) sowie einen absoluten Repo-Pfad. Ersetzt durch bzw. $HOME. Kein Code, keine Aussage geändert. Co-Authored-By: Claude Code --- docs/handoff/00-PLAN.md | 2 +- docs/handoff/analysis/verification-2026-09-11.md | 10 +++++----- docs/handoff/screenshots/v0.7.1/README.md | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/handoff/00-PLAN.md b/docs/handoff/00-PLAN.md index 8112a15..6d60804 100644 --- a/docs/handoff/00-PLAN.md +++ b/docs/handoff/00-PLAN.md @@ -1,7 +1,7 @@ # Plan: Linux-Assistant V0.7.1 → V0.8.0 — Handoff, UI/UX-Analyse, Admin-Hub-Roadmap, Release **Datum:** 2026-09-10 · **Freigegeben von:** Basti (Toqsick) · **Ausführung:** ZCode/GLM, superpowers-SDD -**Repo:** `/home/bratan/10-Projekte/10-active/linux-assistant` (Flutter/Linux, Fork `Toqsick/linux-assistant`, 44 ahead / 0 behind Upstream; Upstream dormat bei 0.6.2) +**Repo:** `$HOME/10-Projekte/10-active/linux-assistant` (Flutter/Linux, Fork `Toqsick/linux-assistant`, 44 ahead / 0 behind Upstream; Upstream dormat bei 0.6.2) ## Ausgangslage (5-Agenten-Recon, 2026-09-10) diff --git a/docs/handoff/analysis/verification-2026-09-11.md b/docs/handoff/analysis/verification-2026-09-11.md index e8d3acc..50491bf 100644 --- a/docs/handoff/analysis/verification-2026-09-11.md +++ b/docs/handoff/analysis/verification-2026-09-11.md @@ -36,14 +36,14 @@ Journal-Evidence (`journalctl`, User in `adm`): ``` Sep 11 10:13:26 polkitd[1290]: Operator of unix-session:3 successfully authenticated as - unix-user:bratan to gain ONE-SHOT authorization for action + unix-user: to gain ONE-SHOT authorization for action org.linux-assistant.read-security-report for unix-process:43860:154605 - [/usr/lib/linux-assistant/linux-assistant] (owned by unix-user:bratan) + [/usr/lib/linux-assistant/linux-assistant] (owned by unix-user:) Sep 11 10:13:26 pkexec[44569]: pam_unix(polkit-1:session): session opened for user - root(uid=0) by bratan(uid=1000) -Sep 11 10:13:26 pkexec[44569]: bratan: Executing command [USER=root] [TTY=unknown] + root(uid=0) by (uid=1000) +Sep 11 10:13:26 pkexec[44569]: : Executing command [USER=root] [TTY=unknown] [CWD=/tmp] [COMMAND=/usr/lib/linux-assistant/additional/python/read_security_report.py - --family=debian --home=/home/bratan] + --family=debian --home=/home/] ``` Das bestätigt drei Fix-Ebenen auf einmal: die dedizierte One-Shot-Polkit-Action diff --git a/docs/handoff/screenshots/v0.7.1/README.md b/docs/handoff/screenshots/v0.7.1/README.md index f220cd4..90ce64d 100644 --- a/docs/handoff/screenshots/v0.7.1/README.md +++ b/docs/handoff/screenshots/v0.7.1/README.md @@ -32,7 +32,7 @@ Laufende Instanz danach im Normalzustand (Wayland) wiederhergestellt. ## Der dokumentierte Bug (Kern-Evidence) `04b` zeigt den Zustand **nach erfolgreicher Passwort-Eingabe**: polkit-Auth -erfolgreich (journal 21:53:19: „successfully authenticated as unix-user:bratan"), +erfolgreich (journal 21:53:19: „successfully authenticated as unix-user:"), `check_security.py` lief als Root, crashte ~1 s später mit `IndexError` an der deb822-Datei `/etc/apt/sources.list.d/graphics-drivers-ubuntu-ppa-noble.sources`, und die UI zeigt danach falsch: