Files
Deploymentcenter/docs/ROADMAP_MIGRATION.md
Deploymentcenter BotandClaude Opus 5 f0ed67ad29 feat(2.9.0): Hardwarekey an der WebUI-Anmeldung und die Roadmap als Tafel
Zwei angeforderte Funktionen, dazu zwei Befunde, die ihnen im Weg lagen.

== Zweite Stufe der Anmeldung (#666) ==
Nach korrektem Passwort verlangt das WebUI einen registrierten FIDO2-Schluessel.
Die Pflicht haengt je Konto daran, OB ein Schluessel registriert ist - nicht an
einem Schalter. Ein Schalter, den man vor der Registrierung umlegt, sperrt den
Betreiber aus seiner eigenen Verwaltung aus.

Ohne Fremdbibliothek gebaut (das Projekt hat bewusst kein Composer):
src/Core/Webauthn/ mit CBOR-Leser, COSE-nach-DER-Wandlung und Pruefung ueber
openssl_verify (ES256, RS256). Auth::login() heisst jetzt Auth::startLogin()
und liefert drei Zustaende statt eines bool - aus "wahr heisst drin" ist "wahr
heisst vielleicht drin" geworden, und ein uebersehener Aufrufer soll an einem
fehlenden Methodennamen scheitern statt an einer truthy Zeichenkette. Die
Setup-API der Zielsysteme geht ueber verifyCredentials() daran vorbei und
bleibt unberuehrt.

Notfalltoken als Ersatzweg: nur als Hash in der Datenbank, Klartext einmalig
angezeigt, Einmalgebrauch. Laeuft als gewoehnliches Formular ohne JavaScript -
ein Ersatzweg mit denselben Voraussetzungen wie der Hauptweg waere keiner.
Fehlversuche auf der zweiten Stufe laufen in dieselbe Konto- und IP-Sperre wie
das Passwort; zuvor zaehlte nur die erste Stufe.

Benutzerverwaltung kann Konten umbenennen. Beim Loeschen eines Kontos und beim
Zuruecksetzen der Testinstanz werden seine Schluessel mitgenommen: credential_id
ist geraeteweit eindeutig, eine verwaiste Zeile haette denselben Yubikey
dauerhaft unbrauchbar gemacht.

== Roadmap-Tafel (#664) ==
Neuer Unterreiter Roadmap -> Tafel. Der Server liefert die fertige Gliederung
(RoadmapBoard, ?action=board), die Seite rendert nur - sonst schneidet das
WebUI anders als die API und ein spaeterer Export ein drittes Mal anders.

Phasen stehen nicht im Code: gepflegt in der neuen Tabelle roadmap_phases,
sonst an den groessten Luecken der Rangfolge geschnitten und sichtbar als
geraten gekennzeichnet. Links die gekappte Spalte des Erledigten, rechts
"Wird ueberlegt". Nachpruefung ist ein eigener Zustand und wird nicht als
erledigt gezeichnet.

Klick oeffnet ein Seitenpanel: Volltext, Verlauf, Kommentieren mit
action_taken, Aendern von Bereich, Rang, Zielfassung, Status und Ausloeser -
ohne Seitenwechsel. Das war der eigentliche Gewinn: Wer etwas bemerkte, musste
sich bisher die Nummer merken und im Bugtracker danach suchen. Tiefe Verweise
#roadmap/<projekt>/item/<id>. Ziehen zwischen Phasen ueber ?action=rank_between.

Fremdtext wird ausschliesslich ueber textContent gesetzt, nie ueber innerHTML.

== Befund: roadmap_rank war projektweit und bereichslokal zugleich (#177) ==
Entschieden: projektweit. move() sucht den Nachbarn ohne Bereichsfilter,
reorder() schreibt keinen ganzen Bereich mehr auf 10/20/30 um, sondern
vertauscht nur die Raenge, die die Eintraege ohnehin halten. rank_between
rechnet die Mitte zwischen zwei Nachbarn und nummeriert nur dann einmal neu -
unter Erhalt der Reihenfolge -, wenn dort kein Platz mehr ist; die Antwort
sagt es dann.

== Befund: CSRF an sitzungsauthentifizierten API-Schreibzugriffen ==
ApiAuth akzeptiert die WebUI-Sitzung, die Endpunkte prueften dabei kein
CSRF-Token. Der Kommentar in Http::beginJson - "ausschliesslich per
Token-Header, niemals per Cookie" - traf auf die API nie zu. Schreibende
Anfragen im Sitzungsweg verlangen jetzt csrf_token bzw. X-CSRF-Token.
Token-Clients sind nicht betroffen und wurden gegengeprueft.

== Geprueft ==
tests/e2e/suite_tafel.py neu (16 Faelle), Suite gesamt 172 von 172 gegen die
Testinstanz. Die WebAuthn-Zeremonien zusaetzlich gegen einen Software-
Authenticator mit echtem P-256 und echtem CBOR: Registrieren, Anmelden,
verfaelschte Signatur, veraltete Herausforderung und Zaehler-Rueckfall als
Klonverdacht werden je mit 403 abgewiesen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 18:16:51 +02:00

14 KiB
Raw Permalink Blame History

Roadmap ins Deploymentcenter übernehmen

Für Agenten und Entwickler in den angebundenen Projekten. Diese Anleitung beschreibt, wie eine vorhandene ROADMAP.md, TODO.md, PLAN.md oder eine Sammlung von Entwurfsdokumenten in das Roadmap-System des Deploymentcenters überführt wird — einmalig, pro Projekt.

Verfügbar ab 2.7.0. Voraussetzung: Migration 016_roadmap.sql ist auf dem Server gelaufen.


Warum überhaupt

Vorhaben lagen bisher in Markdown-Dateien: Status, Reihenfolge und Zuständigkeit von Hand gepflegt, neben einem Bugtracker, der genau diese drei Dinge bereits führt. Zwei Pfleger für dasselbe Feld — Drift war keine Möglichkeit, sondern eine Frage der Zeit.

Konkret ging dabei verloren:

  • Ein Punkt stand als offen in der Datei, obwohl der Code ihn längst erledigt hatte. Wer die Datei las, arbeitete an etwas Fertigem.
  • Zwei Agenten nahmen sich dasselbe Vorhaben vor, weil die Datei nicht sagen kann, dass jemand daran sitzt.
  • Ein verworfener Vorschlag kam nach drei Monaten wieder, weil die Begründung in einem Entwurfsdokument stand, das niemand mehr aufschlug.

Die Arbeitsteilung

Kein Feld hat zwei Pfleger. Das ist die ganze Regel; alles Weitere folgt daraus.

Wahrheit Die andere Seite hält
Status, Reihenfolge, Zielfassung, Bearbeiter Deploymentcenter eine maschinell erzeugte Kopie zwischen Markern
Begründung, Auslöser, Verworfenes, Herleitung Die Datei nur einen Verweis (doc_anchor)

Die Datei verschwindet also nicht. Sie verliert nur die Hälfte, die sie ohnehin nie zuverlässig getragen hat.

Die Kennung

Handvergebene Kennungen (A12, TODO-7) sind der Grund für die schlimmste Fehlerart: Beide Seiten sind für sich stimmig und meinen Verschiedenes, weil die Kennung nach einer Löschung neu vergeben wurde. Niemand bemerkt das.

Deshalb ist die Kennung die Item-ID aus dem Deploymentcenter. AUTO_INCREMENT vergibt nie zweimal dieselbe Nummer; eine gelöschte Zeile hinterlässt eine hängende Referenz, und die meldet der Abgleich, statt sie stillschweigend umzudeuten.

Der Themenbuchstabe bleibt — aber nur als Gruppe:

### [A] #142 — Symbol-Upload und dc_symbolicate

Übernahme in sechs Schritten

1. Bestandsaufnahme

Sortiere jeden Eintrag deiner Datei in genau eine der vier Kisten:

Kiste Kennzeichen Wohin
Vorhaben hat einen Zustand, jemand könnte es übernehmen Item im DC
Zurückgestellt bewusst nicht jetzt, mit benanntem Anlass Item (deferred) + Absatz
Verworfen geprüft und abgelehnt, mit Begründung Item (rejected) + Absatz
Erledigt schon ausgeliefert Item (resolved) — oder ersatzlos streichen

Was in keine Kiste passt, ist meist ein Gedanke und kein Vorhaben. Der gehört in die Prosa, nicht in die Datenbank. Faustregel: Wenn niemand es übernehmen kann, ist es kein Item.

2. Themenbereiche anlegen

Die Abschnitte deiner Datei werden Themenbereiche. Kurzes Kürzel, sprechender Titel:

curl -X POST "https://dc.mhdf.de/api/bugtracker/v1/roadmap.php?action=epic" \
  -H "Authorization: Bearer $DC_TOKEN" -H "Content-Type: application/json" \
  -d '{"project_slug":"meinprojekt","epic":"A","title":"Handelslogik","sort_order":10}'

Oder im WebUI unter Roadmap → Themenbereiche.

Halte die Zahl klein. Fünf bis zehn Bereiche sind übersichtlich, zwanzig sind eine zweite Ablage, die selbst wieder sortiert werden will.

Abkürzung: Schritte 2–4 als Skript

Für eine Datei im Format dieses Projekts — Bereichstabellen mit | A4 | Titel | 📋 |, Auslöser als **Auslöser:** … — nimmt scripts/roadmap_import.py die drei Schritte ab:

python scripts/roadmap_import.py --lesen

Zeigt, was es gefunden hat, ohne etwas zu senden. Immer zuerst. Dabei fällt auf, was die Datei falsch behauptet — bei der Übernahme hier waren drei Einträge längst erledigt und einer doppelt vergeben.

python scripts/roadmap_import.py --uebernehmen

DC_TOKEN muss dabei ein Werkbank-Token des Projekts sein. Das Skript legt die Themenbereiche mit an, ist über client_ref wiederholbar und wartet bei Drosselung, statt abzubrechen: /report lässt 60 Meldungen je Minute zu, und eine Roadmap mit mehr Einträgen liegt darüber.

Die Prioritätsreihenfolge liest es aus dem Abschnitt „Als Nächstes" der Datei — solange die Datei die Quelle ist, soll auch die Priorität von dort kommen und nicht aus einer zweiten Liste daneben.

Für eine Datei in anderem Format ist das Skript die Vorlage, nicht das Werkzeug: Die Auswertung steht in auswerten() und ist auf etwa achtzig Zeilen angepasst.

3. Vorhaben anlegen

Jedes Vorhaben wird ein Item. Mit MCP-Adapter:

⚠️ type: "feature_request" ist hier ein Behelf, keine Aussage. type kennt nur bug und feature_request — ein Vorhaben muss sich deshalb als Wunsch ausgeben, obwohl es die eigene Planung ist und kein Vorschlag an jemanden. Entscheidend ist Schritt 4: Erst der Themenbereich macht das Item zum Roadmap-Eintrag, und erst dann verschwindet es aus der Bugliste und erscheint im Roadmap-Modul.

Wer die Übernahme nach Schritt 3 abbricht, hinterlässt Dutzende vermeintlicher Feature Requests im Eingang des Projekts — Schritt 3 und 4 gehören zusammen.

Bei der ersten Übernahme (August 2026, vier Projekte) ist das gutgegangen: 217 von 229 Einträgen haben ihren Themenbereich bekommen. Auffällig wurde trotzdem etwas anderes — die Bugliste zeigte Roadmap-Einträge damals noch mit an, und die zwölf echten Vorschläge lagen unter der Planung begraben. Seit 2.7.1 blendet die Bugliste sie ab Werk aus.

Dass der type zu eng ist, ist als Item #285 gemeldet.

dc_report(title: "Symbol-Upload und dc_symbolicate",
          type: "feature_request",
          description: "…",
          client_ref: "roadmap-a4")

client_ref immer setzen: Ein wiederholter Lauf legt dann kein Duplikat an — und bei einer Übernahme von zwanzig Einträgen läuft irgendetwas immer zweimal.

Ohne MCP:

curl -X POST "https://dc.mhdf.de/api/bugtracker/v1/report" \
  -H "Authorization: Bearer $DC_TOKEN" -H "Content-Type: application/json" \
  -d '{"project_slug":"meinprojekt","type":"feature_request",
       "title":"…","description":"…","client_ref":"roadmap-a4"}'

4. Auf die Roadmap heben

Ein Vorhaben steht auf der Roadmap, sobald es einen Themenbereich hat. Das ist das einzige Kennzeichen — ein zweites Flag daneben gäbe es zwei Angaben, die dasselbe behaupten und auseinanderlaufen können.

dc_roadmap_set(id: 142, epic: "A", roadmap_rank: 10, target_build: "2.7.0",
               doc_anchor: "docs/ROADMAP.md#142-symbol-upload")

Zur Reihenfolge: Zehnerschritte. Die Lücken sind Absicht — ein Eintrag lässt sich später auf Rang 15 dazwischenschieben, ohne dass alle folgenden neu nummeriert werden müssen.

Der Rang gilt projektweit, nicht innerhalb des Bereichs. Hier stand einmal „Reihenfolge im Bereich“, und das war die Hälfte eines Widerspruchs (#177): dc_next_task und die Roadmap-Tafel lesen den Rang ohne jede Gruppierung nach epic. Wer je Bereich bei 10 anfängt, setzt damit das erste Vorhaben jedes Bereichs gleichauf an die Spitze — und dann entscheidet der Schweregrad die Reihenfolge, also ausgerechnet die Größe, von der hier steht, sie sei keine Priorität. Eine durchgehende Liste über mehrere Bereiche hinweg ist deshalb genau richtig: A kann auf 10 stehen, C auf 20, A wieder auf 30.

Zurückgestelltes braucht den Auslöser, sonst wird der Aufruf abgelehnt:

dc_roadmap_set(id: 147, epic: "A", status: "deferred",
               defer_trigger: "sobald Logauszüge strukturiert hereinkommen")

Ohne benannten Anlass ist ein Vorhaben nicht zurückgestellt, sondern vergessen. Genau deshalb ist das Feld Pflicht und nicht bloß empfohlen.

5. Die Datei umbauen

Drei Änderungen:

a) Zustandstabellen raus. Jede von Hand gepflegte Tabelle mit Status-Spalte wird ersatzlos gelöscht. Sie kommt gleich erzeugt zurück.

b) Überschriften auf Item-IDs umstellen.

### [A] #142 — Symbol-Upload und dc_symbolicate

Der Packager lädt beim `publish` die PDB mit hoch, verknüpft mit `commit_sha`.
Ausgeliefert wird ein gestripptes Binary. …

Kein Status in der Überschrift, kein Häkchen, keine Zielfassung — das steht alles im erzeugten Block und wäre hier eine zweite Wahrheit.

c) Den Block einsetzen. Beim ersten Lauf wird er ans Dateiende gehängt; verschiebe ihn samt Markern dorthin, wo er stehen soll. Künftige Läufe finden ihn dort wieder.

php cli/roadmap.php --write --project=meinprojekt --file=/pfad/zu/ROADMAP.md

Liegt das Projekt auf einer anderen Maschine als das Deploymentcenter, holst du den Block über die API:

curl -s "https://dc.mhdf.de/api/bugtracker/v1/roadmap.php?action=render&project_slug=meinprojekt" \
  -H "Authorization: Bearer $DC_TOKEN" | jq -r .markdown

6. Abgleich einrichten

php cli/roadmap.php --check --project=meinprojekt --file=/pfad/zu/ROADMAP.md

Was geprüft wird — nur die Prosa, denn der erzeugte Block kann nicht abweichen:

Befund Bedeutung Grad
unknown_item Absatz nennt #142, das Item gibt es nicht Fehler
foreign_item #142 gehört einem anderen Projekt Fehler
not_on_roadmap Absatz vorhanden, aber kein Themenbereich gesetzt Warnung
title_drift Überschrift und Item-Titel haben nichts mehr gemeinsam Warnung
decision_undocumented zurückgestellt/verworfen, aber keine Begründung in der Datei Warnung

Rückgabewert 0 heißt in Ordnung (Warnungen erlaubt), 1 heißt Fehler. Damit lässt sich der Aufruf in einen Deploy-Schritt oder Pre-Commit-Hook hängen.

Ein Item ohne Absatz ist bewusst kein Befund: Nicht jedes Vorhaben braucht eine Herleitung, die meisten sind eine Zeile wert und mehr nicht.


Nach der Übernahme: die Regeln im Alltag

Ein Vorhaben entsteht immer im DC, nie in der Datei. dc_report legt das Item an → --write schreibt die Zeile → danach schreibst du die Begründung darunter. So kann ein Eintrag, der nur in der Datei steht, gar nicht erst entstehen — und damit auch kein offener Punkt, den niemand angeht.

Die Arbeitsschlange ist die Wahrheit über Arbeit. Was nicht über ?action=next ausgegeben wird, macht niemand — unabhängig davon, wie es in einer Datei markiert ist. Ein Eintrag, der etwas werden soll, muss ein Item sein.

Vor dem Anfangen dc_roadmap aufrufen. Es beantwortet „was ist als Nächstes dran" und „arbeitet schon jemand daran". Beides, bevor du beginnst.

Vor dem Wiedervorschlagen die Begründung lesen. Steht ein Vorhaben auf rejected oder deferred, führt doc_anchor zu dem Absatz, der sagt warum. Ein erneuter Vorschlag braucht ein Argument, das dort noch nicht steht.

Der erzeugte Block darf altern. Er ist eine Kopie, und Kopien altern. Der Live-Stand kommt aus dc_roadmap; der Block ist für Menschen, die die Datei aufschlagen. --check weist auf einen Block hin, der älter als ein Tag ist.


Was nicht ins DC gehört

Der Umzug ist keine Einladung, alles in die Datenbank zu kippen.

  • Architekturentscheidungen mit Herleitung bleiben in der Datei. Ein Absatz, der eine Abwägung erklärt, überlebt keine Tabellenzelle.
  • Verworfenes mit Begründung bleibt in der Datei. Der Wert des Eintrags ist die Begründung, nicht die Ablehnung. Im DC steht nur der Status daneben.
  • Lose Gedanken gehören weder hierhin noch dorthin. Wenn niemand sie übernehmen kann, sind sie kein Vorhaben — im Zweifel als Notiz ins Archiv.
  • Alte Entwurfsdokumente nach docs/archiv/ verschieben, nicht löschen. Sie enthalten die ausführliche Begründung, aber keinen gültigen Stand mehr. Ein Hinweis darauf gehört an den Kopf der Roadmap-Datei.

Werkzeuge im Überblick

Werkzeug Wofür
dc_roadmap lesen: was ist dran, wer sitzt dran (MCP)
dc_roadmap_set Bereich, Rang, Zielfassung, Anker setzen (MCP)
GET /api/bugtracker/v1/roadmap.php?action=list dasselbe über REST
?action=render den Markdown-Block holen
?action=check Abgleich mit mitgeschickter Datei (content)
php cli/roadmap.php --write|--check|--list von der DC-Maschine aus
WebUI → Roadmap Übersicht, Filter, Reihenfolge, Aufnahmeliste

Token braucht bugtracker:read zum Lesen und bugtracker:manage zum Schreiben. Ein Werkbank-Token (dev_agent) hat beides.


Beispiel: eine Datei vorher und nachher

Vorher — Zustand und Begründung durcheinander, beides von Hand:

## A — Supervisor

| | Vorhaben | Stand |
|---|---|---|
| A4 | Symbol-Upload | 📋 geplant für 2.7.0 |
| A5 | Logzeilen-Schablonen | 🧊 später |

### A4 — Symbol-Upload
Der Packager lädt die PDB mit hoch…

Nachher — Zustand erzeugt, Begründung von Hand, verbunden über die ID:

<!-- dc:roadmap stand=2026-08-29T14:02Z items=7 projekt=deploymentcenter -->

### A — Supervisor und Befunde

| | Vorhaben | Stand | Ziel |
|---|---|---|---|
| #142 | Symbol-Upload und dc_symbolicate | 📋 Offen | `2.7.0` |
| #143 | novel.log_template | 🧊 Zurückgestellt — Auslöser: sobald Logauszüge strukturiert hereinkommen | — |

<!-- /dc:roadmap -->

## Begründungen

### [A] #142 — Symbol-Upload
Der Packager lädt die PDB mit hoch, verknüpft mit `commit_sha`. Ausgeliefert
wird ein gestripptes Binary. Der Kunde bekommt `at 0x4a2f`, du bekommst
`UserAuthService.cs:42` — ohne dass Quelltext das Haus verlässt.

Die Statuszeile stimmt jetzt, weil sie niemand mehr pflegt.