{ "_comment": "Einzige Quelle der Wahrheit fuer den Changelog. Wird von /docs/changelog.php gerendert und von GET /api/updateservice/v1/changelog?since=X ausgeliefert. Neue Eintraege oben einfuegen.", "schema": 1, "current": "2.8.2", "versions": [ { "version": "2.8.2", "date": "2026-09-08", "summary": "Sicherheits-Hardening: Proaktive Kontosperren nach 5 Fehlversuchen, Fail2Ban-Integration mit standardisiertem SecurityLogger, Rocket.Chat- und Webhook-Alarmierung bei Angriffen und Token-Brute-Force-Schutz für die API.", "actionRequired": true, "changes": [ { "area": "auth", "breaking": false, "title": "Proaktive Accountsperren nach 5 Fehlversuchen", "text": "dc_users fuehrt jetzt failed_login_count und locked_until (Migration 018). Nach fuenf aufeinanderfolgenden Fehlversuchen wird das Konto fuer 15 Minuten gesperrt. Gilt fuer das WebUI und die Setup-API. Erfolgreicher Login setzt den Zaehler zurueck; Administratoren koennen Konten im WebUI unter System -> Benutzer mit einem Klick entsperren.", "action": null }, { "area": "security", "breaking": false, "title": "Fail2Ban-Standardisierung und dedizierter SecurityLogger", "text": "Sicherheitsereignisse (AUTH_FAIL, ACCOUNT_LOCKED, IP_LOCKED, API_AUTH_FAIL, API_BRUTE_FORCE, RATE_LIMITED) werden im standardisierten Format in var/log/security.log geschrieben. Vorkonfigurierte Filter- und Jail-Dateien sowie Dokumentation liegen unter config/fail2ban/ und docs/FAIL2BAN.md bereit.", "action": "Auf dem Host-Server den Fail2Ban-Filter deploymentcenter.conf und die Jail hinterlegen." }, { "area": "notify", "breaking": false, "title": "Rocket.Chat- und Webhook-Alarmierung bei Angriffen", "text": "Wird ein Konto oder eine IP gesperrt oder ein Brute-Force-Angriff auf die API erkannt, erfolgt sofort eine Benachrichtigung im Rocket.Chat Alarm-Kanal (#DC-Alerts) sowie ueber das Webhook-Event security.lockout bzw. security.attack. Ein 15-Minuten-Cooldown verhindert Alert-Spam.", "action": null }, { "area": "api", "breaking": false, "title": "API-Hardening und zentraler Core RateLimiter", "text": "ApiAuth drosselt wiederholte ungueltige Token-Uebermittlungen pro IP (HTTP 429 bei >20 Fehlern in 5 Minuten). Ein universeller Core\\RateLimiter sichert Endpunkte ueber die neue Tabelle dc_rate_limits ab.", "action": null } ] }, { "version": "2.8.1", "date": "2026-08-31", "summary": "Sechs Befunde aus der Fremdmeldung, und fast alle gehen auf dieselbe Bauart zurueck: eine Voreinstellung, die einsprang, wo die richtige Antwort bekannt gewesen waere - meist das Projekt, an das der Aufrufer ohnehin gebunden ist. dc_next_task antwortete durchgaengig mit 500. Meldungen ohne project_slug landeten im Scheinprojekt \"default\" und waren danach fuer ihren eigenen Melder gesperrt; Watchdog-Monitore landeten dort auf einem zweiten Weg. Der Lesepfad lieferte ohne Slug alle Projekte statt des eigenen - ausgerechnet beim ersten Aufruf jeder Sitzung, und in einer Groesse, die das Kontextfenster fuellte. Und wo er richtig verengte, sagte er es nicht.", "actionRequired": true, "changes": [ { "area": "api", "breaking": false, "title": "dc_next_task antwortete immer mit HTTP 500", "text": "POST manage?action=next scheiterte bei jedem Aufruf, mit und ohne Parameter, waehrend claim, release, list und stats am selben Endpunkt mit demselben Token durchliefen. Ursache war ein benannter Platzhalter, der in derselben Abfrage zweimal stand: BugRepo::buildFilter() setzte :project_scope fuer den Rueckkanal (fremde Items, die auf Nachpruefung warten) an zwei Stellen ein. Mit PDO::ATTR_EMULATE_PREPARES = false bereitet MySQL die Anweisung selbst vor, PDO bindet jeden Namen genau einmal - zwei Marker, ein Wert, SQLSTATE[HY093]. Getroffen war nur \"next\", weil nur dort der Rueckkanal-Filter gesetzt wird. Damit gab es keine exklusive Uebernahme; zwei Agenten konnten unbemerkt am selben Item arbeiten. Behoben durch zwei Platzhalter fuer denselben Wert.", "action": "Wer wegen des 500ers ohne Uebernahme gearbeitet hat: laufende Arbeit jetzt mit dc_next_task absichern." }, { "area": "api", "breaking": true, "title": "Meldungen ohne project_slug gehen ins Heimatprojekt, nicht nach \"default\"", "text": "POST /report legte eine Meldung ohne project_slug im Projekt \"default\" ab. Das ist kein Projekt: Es hat keine dc_projects-Zeile, project_id blieb NULL, und das Item tauchte in keiner Projektansicht auf. Schlimmer noch war die Folge - dasselbe Token, das die Meldung angelegt hatte, bekam beim naechsten Zugriff 403 project_forbidden, weil es an sein eigenes Projekt gebunden ist. Der Server legte Items dort ab, wo der Melder sie nicht mehr anfassen durfte, und die Antwort sah wie ein Erfolg aus. Betroffen war der dokumentierte Normalfall: Das Weglassen des Slugs ist ausdruecklich vorgesehen. Ursache war eine Bedingung, die nur Laufzeit-Token auf ihr Heimatprojekt festnagelte; ein Werkbank-Token ohne Slug fiel durch bis zur Voreinstellung. Jetzt gilt: genannter Slug vor Heimatprojekt, und ohne beides eine Absage (400 missing_project) statt eines Ratens. Ein ausdruecklich genanntes, unbekanntes Projekt wird ebenfalls abgewiesen (400 unknown_project) statt stillschweigend angelegt. Dieselbe Falltuer war in POST /api/errors/v1/report und im WebUI-Formular offen und ist dort mitgeschlossen.", "action": "Bereits in \"default\" gelandete Items sind ueber die API nicht erreichbar und muessen mit Adminrechten umgehaengt oder geloescht werden." }, { "area": "api", "breaking": true, "title": "Ohne project_slug gilt das eigene Projekt - beim Lesen wie beim Melden", "text": "dc_roadmap, dc_search, manage?action=list und ?action=stats lieferten ohne project_slug die Eintraege ALLER Projekte. Die Werkzeugbeschreibung sagte das Gegenteil (\"Ohne project_slug das eigene Projekt\"), was den Widerspruch lange verdeckt hat. Grund war eine Voreinstellung auf \"all\": Ein Werkbank-Token ist an ein Projekt gebunden, faellt beim Lesen aber absichtlich nicht unter die Projektschranke - es soll die Landschaft sehen duerfen. Damit war der Zwangsfilter leer, die Vorgabe griff, und ein Agent bekam einen fremden Backlog vorgelegt, den er nach derselben Regel nicht bearbeiten darf. Da CLAUDE.md dc_roadmap zum ersten Aufruf jeder Sitzung macht, war das der erste Eindruck. Jetzt gilt: genannter Slug, sonst Heimatprojekt, und erst ohne beides \"alle\". project_slug=all bleibt moeglich und ist damit eine Ansage statt eines Nebeneffekts.", "action": "Wer bisher auf die projektuebergreifende Antwort gebaut hat, schickt jetzt ausdruecklich project_slug=all." }, { "area": "mcp", "breaking": true, "title": "dc_roadmap antwortet schmal statt vollstaendig", "text": "Die Roadmap lieferte je Eintrag den vollen Datensatz mit rund fuenfzig Feldern, darunter description, stack_trace, evidence und context_json - fuer 57 Eintraege rund 137.000 Zeichen. Ausgerechnet der erste Aufruf einer Sitzung fuellte damit das Kontextfenster, bevor die Arbeit begann. Eine Uebersicht beantwortet \"was ist dran und wer sitzt dran\"; dafuer braucht es keinen Stacktrace. dc_roadmap liefert deshalb standardmaessig die Uebersichtsfelder (BugRepo::COMPACT_FIELDS); Einzelheiten holt dc_get_item, die Begruendung steht ohnehin hinter doc_anchor. fields=full liefert weiterhin alles, dc_search hat fields=compact als Gegenstueck.", "action": null }, { "area": "testkit", "breaking": false, "title": "dc-testkit 1.1.1 - und ein Riegel gegen die doppelt ausgelieferte Fassung", "text": "1.1.0 wurde zweimal ausgeliefert: mit 1898 und, im Zuge von 2.8.0, mit 1930 Zeilen. Der Unterschied sass vollstaendig in cmd_confirm - die aeltere Kopie kannte nur den Weg ueber status=closed, nicht den ueber verified_in_build. Jede Nachpruefung von so einer Maschine landete still im alten Verfahren, und selfupdate sah \"gleiche Fassung\" und tat nichts. Dasselbe war 1.0.0 schon passiert (1215 und 1335 Zeilen). Der Absatz ueber DC_TESTKIT_VERSION stand die ganze Zeit im Skript; er hat nicht getragen. scripts/deploy.py vergleicht jetzt vor dem Hochladen gegen den zuletzt ausgelieferten Stand und bricht ab, wenn sich der Inhalt geaendert hat, waehrend die Fassungsnummer stehen blieb.", "action": "Auf allen Testmaschinen dc-testkit selfupdate ausfuehren; erwartet wird danach 1.1.1." }, { "area": "docs", "breaking": false, "title": "Die SDK-Fassung wird gemeldet, nicht abgeschrieben", "text": "Die Agentenvorlage zeigte die Zeile \"Deploymentcenter-Anbindung: SDK 2.5.1\" mit einer konkreten Zahl. Sie war als Beispiel gemeint und wurde als Wert uebernommen - in Projekt-CLAUDE.md-Dateien stand danach 2.5.1, waehrend dc_link_project laengst 2.6.0 auswies. Eine Angabe, die an N Stellen von Hand nachgezogen werden muss, ist nach dem ersten Vergessen falsch, und zwar schweigend. Die Vorlage nennt jetzt dc_link_project als die belastbare Auskunft und benutzt sichtbare Platzhalter, wo eine Fassung hingehoert.", "action": null }, { "area": "api", "breaking": false, "title": "Watchdog-Monitore erben das Projekt ihres Ping-Tokens", "text": "watchdog_monitors.project_slug gibt es seit Migration 014, geschrieben hat die Spalte nie jemand: upsertHeartbeat() bekam das Projekt gar nicht erst uebergeben. Der Monitor entstand mit project_slug NULL, und jeder daraus abgeleitete Supervisor-Befund fiel auf das Scheinprojekt \"default\" zurueck - dieselbe Endstation wie bei den Meldungen ohne project_slug, nur ueber einen anderen Weg. Wirkung: dc_findings(project_slug=...) lieferte fuer eine laufende, meldende Instanz null Befunde. Wer dem Leitfaden folgt und dort anfaengt, schliesst daraus \"nichts auffaellig\" - waehrend ein deviation.self-Befund mit elf Vorkommnissen bereitstand. Die Zuordnung war dabei die ganze Zeit bekannt: Das Ping-Token ist an ein Projekt gebunden, und authorizeSource() liest die Bindung ohnehin - sie wurde nur nicht weitergereicht. Das Projekt kommt jetzt aus der Token-Bindung, nie aus dem Body, und ein Ping ohne Bindung (Shared Key, Alt-Token) ueberschreibt eine bekannte Zuordnung nicht. Die Ping-Antwort nennt project_slug.", "action": "Bestandsmonitore behalten project_slug NULL, bis ihre Anwendung das naechste Mal mit einem projektgebundenen Token pingt." }, { "area": "api", "breaking": false, "title": "Ein verengter Leseweg sagt jetzt, dass er verengt hat", "text": "Ein an Projekt X gebundenes Token, das nach Projekt Y fragte, bekam beim Schreiben eine klare Absage (403 project_forbidden) und beim Lesen HTTP 200 - mit den Items von X und einem Umschlag, in dem der Projektfilter bereits auf X umgeschrieben war. Die Antwort sah damit aus wie eine gueltige Auskunft ueber Y. Das zaehlt, weil die Prompt-Vorlage jeden Agenten vor jeder Meldung durch dc_search schickt (\"gibt es das schon?\"): Auf \"gibt es das im Deploymentcenter schon?\" kam eine leere, plausibel aussehende Antwort - und das Duplikat wurde angelegt. Einem Melder ist genau das beinahe passiert; tatsaechlich waren es 71 Items. Die Schranke selbst bleibt unveraendert; list, stats und die Roadmap legen bei einer Verengung jetzt scoped_to und scope_note in den Umschlag. Ohne Verengung steht dort nichts - ein Hinweis, der immer kommt, wird nicht gelesen.", "action": null } ] }, { "version": "2.8.0", "date": "2026-08-31", "summary": "Remote-Testen, das traegt. Die Testwege waren beschrieben, aber nicht begehbar: Auf der Test-VM lag die Anleitung von vor zwei Fassungen, ein Token ohne die noetigen Rechte und ein Testkit, das aelter war als das ausgelieferte - beide meldeten dieselbe Fassungsnummer. Deshalb sagte doctor dort \"Bereit\", waehrend kein einziger Auslieferungstest laufen konnte. Dazu die Trennung, die dem Pruefverfahren bisher fehlte: bugtracker:verify erlaubt, eine Nachpruefung zu bestaetigen, ohne zu erlauben, den Fehler selbst zu beheben.", "actionRequired": false, "changes": [ { "area": "server", "breaking": false, "title": "bugtracker:verify - pruefen duerfen, ohne beheben zu duerfen", "text": "bugtracker:manage war ein einziger Schreib-Scope: Wer eine Nachpruefung bestaetigen durfte, durfte damit auch resolve, update, bulk_update und die Roadmap umschreiben. Fuer einen Pruefstand ist das zu viel - und wer einen Fix schreibt und ihn selbst abnimmt, prueft gegen die Erwartung, die er beim Schreiben hatte. Genau dagegen ist das Verfahren aus 2.6.1 gebaut, aber die Grenze stand nur in der Anleitung. bugtracker:verify deckt jetzt ab, was ein Pruefer braucht: uebernehmen, kommentieren, bestaetigen, zurueckgeben. bugtracker:manage schliesst es ein, vorhandene Token verlieren also nichts. \"status\" bleibt bewusst bei manage - darueber liesse sich sonst jeder Zustand setzen und die Trennung umgehen." }, { "area": "server", "breaking": false, "title": "confirm ist eine eigene Aktion und hinterlaesst, wogegen geprueft wurde", "text": "Eine bestaetigte Nachpruefung war im Datenbestand nicht von \"jemand hat das Item zugemacht\" zu unterscheiden: dc-testkit confirm setzte status=closed und schrieb den Rest in einen Kommentar. Damit war die eine Frage, fuer die das Verfahren gebaut ist, nicht beantwortbar - wurde dieses Item je gegen ein ausgeliefertes Paket geprueft? Neu: POST .../manage?action=confirm mit verified_in_build. Migration 017 legt verified_at, verified_in_build und verified_by an. Weicht die geprueften Fassung von resolved_in_build ab, wurde gegen etwas anderes geprueft als angekuendigt, und man sieht es. Bestaetigen laesst sich nur, was als behoben ausgegeben wurde; ein offenes Item zu \"bestaetigen\" antwortet 409 not_verifiable." }, { "area": "mcp", "breaking": false, "title": "dc_confirm - Pruefauftraege liessen sich annehmen, aber nicht abschliessen", "text": "Der Adapter hatte dc_next_task und dc_resolve, aber keinen Weg, eine Nachpruefung zu bestaetigen; dc_comment kennt nur action_taken, keinen Status. Ein Pruefagent ueber MCP konnte einen Prueffauftrag also uebernehmen und dann nichts damit anfangen. Jetzt 16 Werkzeuge statt 15." }, { "area": "testkit", "breaking": false, "title": "dc-testkit 1.1.0: kennt seinen eigenen Stand, prueft Rechte und Display", "text": "Vier Fehler: doctor brach unter \"set -e\" wortlos nach der Health-Pruefung ab, sobald der Server neu genug war - Token und Projekt wurden nie geprueft. Die Konfigurationsdatei ueberschrieb die Umgebung statt umgekehrt, \"DC_PROJECT=x dc-testkit report\" meldete also stillschweigend in das Projekt aus der Datei. Der Bericht ging teils auf stderr und stand ueber SSH unter der falschen Ueberschrift. Und \"~/bin\" liegt ueber SSH nicht im PATH, weil .profile nur von einer Login-Shell gelesen wird. Neu: selfupdate (mit Richtungspruefung - eine neuere lokale Fassung ist ein Entwicklungsstand und kein Grund zum Update), doctor prueft den eigenen Stand, die Token-Herkunft, die fehlende Projektbindung und das eigene Display, sowie display start|stop|status und shot fuer die Oberflaechenpruefung." }, { "area": "testkit", "breaking": false, "title": "dc-testkit-remote: der Agent laeuft auf dem Arbeitsplatz, der Test auf der VM", "text": "Claude Code laesst sich auf der Test-VM nicht betreiben, Agenten arbeiten also ueber SSH von aussen - dort liegen auch die Projekt-Repos und damit die Werkbank-Token in deren .mcp.json. Der Wrapper haelt das Token aus \"ps aux\" der geteilten Maschine (es geht ueber die Standardeingabe in die Umgebung), ruft bash -l, schuetzt Argumente vor drei Ebenen Quoting und buendelt ganze Pruefplaene in eine Verbindung. Auf der VM wird keine Konfiguration mehr gelesen: Sonst gilt drueben, was irgendwann jemand eingetragen hat - genau daran ist es gescheitert." }, { "area": "security", "breaking": false, "title": "Interne Anleitungen bleiben aus dem Webroot", "text": "docs/TESTUMGEBUNG.md und docs/TESTINSTANZ.md gingen bei jedem Deployment mit hoch. Erreichbar waren sie dort nicht - aber nur, weil die Rewrite-Regel ^docs/ auf public/docs/ umleitet und dort nichts Gleichnamiges liegt. Faellt mod_rewrite aus oder aendert jemand die Reihenfolge, liegen interne Adresse, Benutzername und der Weg zur sudo-Freischaltung offen. Beide Dateien sagen in ihrem eigenen Kopf, dass sie da nicht hingehoeren; deploy.py schliesst sie jetzt pfadgenau aus." }, { "area": "docs", "breaking": false, "title": "Baustein E: ein Agent, der nur prueft", "text": "Die Prompt-Vorlage hatte nur den Entwickleragenten, der nebenbei testet. Baustein E beschreibt das Umgekehrte und zieht die Rollengrenze ausdruecklich: kein resolve, keine Roadmap, und bei fehlendem Token eine Meldung statt eines Ratespiels - .mcp.json ist gitignored und kommt ueber Git nie mit. TESTUMGEBUNG.md 4.8 beschreibt die Rolle, 4.2 den tatsaechlichen Ablageort der Token, und die Belegung in 7 ist von der Maschine erhoben statt fortgeschrieben - sie war vier Projekte im Rueckstand." } ] }, { "version": "2.7.1", "date": "2026-08-30", "summary": "Aufraeumen nach dem ersten Durchlauf der Testinstanz: Was dort auffiel, war fast durchweg dieselbe Sorte Fehler - eine Auskunft, die nicht stimmte. Der Server nannte eine falsche Fassung, doctor meldete Bereitschaft ohne zu pruefen, /api/health starb an dem Zustand, ueber den es Auskunft geben sollte, und das WebUI zeigte nach jedem Filter eine andere Ansicht als die verlangte.", "actionRequired": false, "changes": [ { "area": "server", "breaking": false, "title": "Die gemeldete Fassung kommt aus dem Changelog statt aus der Konfiguration", "text": "config/config.php fuehrte 'version' als Zeichenkette, und die stand seit jeher auf 2.0.0 - waehrend 2.7.0 lief. Das war keine Schoenheitsfrage: /api/health meldete 2.0.0, selbst gemeldete Fehler trugen eine falsche build_version, und jede Pruefung gegen eine Mindestfassung schlug auf einem aktuellen Server fehl. Genau daraus entstand der Eindruck, die Testinstanz laufe auf veralteter Software. src/bootstrap.php liest die Fassung jetzt aus 'current' in public/docs/changelog.json - der Datei, die bei jedem Release ohnehin gepflegt wird. Der Wert in der Konfiguration bleibt Rueckfallebene." }, { "area": "api", "breaking": false, "title": "/api/health ueberlebt eine leere Datenbank", "text": "Auf einer frisch aufgesetzten Instanz - Anwendung deployt, Migrationen noch nicht gelaufen - endete GET /api/health mit HTTP 500: die Rechtepruefung griff auf dc_tokens zu, und diese Tabelle gab es noch nicht. Ausgerechnet der Endpunkt, der ueber kaputte Zustaende Auskunft geben soll, fiel bei einem kaputten Zustand aus. Neu ist eine eigene Pruefung 'schema': fehlt das Schema, kommt healthy=false mit dem Hinweis auf install_db.php statt eines Serverfehlers. Die Rechtepruefung selbst ist zusaetzlich abgesichert, damit auch ein halb migrierter Stand eine auswertbare Antwort liefert." }, { "area": "api", "breaking": false, "title": "Neu: GET /api/tokens/v1/whoami - Auskunft ueber das eigene Token", "text": "Meldet Besitzerart, Projektbindung, Rechte, Umgebung und Frist des uebergebenen Tokens; der Klartextwert wird nicht ausgeliefert, ein widerrufenes oder abgelaufenes Token bekommt 401. Bis hierher liess sich ein Recht nur durch Ausprobieren feststellen - und das hat Nebenwirkungen: eine Probe auf bugtracker:manage uebernaehme ein Item, eine auf license:dev stellte einen Schluessel aus. Deshalb pruefte kein Werkzeug seine Voraussetzungen, sondern lief hinein. 'is_workbench' ist der Wert, der ueber Fremdmeldungen entscheidet." }, { "area": "tooling", "breaking": false, "title": "dc-testkit doctor prueft Mindestfassung und Rechte, statt sie nur anzuzeigen", "text": "doctor meldete 'Bereit.', obwohl der Server die benoetigten Endpunkte nicht hatte und das Token die noetigen Rechte nicht trug - der Lauf scheiterte dann beim ersten Schritt, der beides wirklich brauchte. Die Fassung stand dabei schon in der Ausgabe, verglichen wurde sie nur nie. Jetzt: Vergleich gegen DC_MIN_SERVER (2.6.1) nach semantischer Ordnung, und ueber /api/tokens/v1/whoami die Pruefung auf Besitzerart dev_agent sowie auf bugtracker:manage, license:dev und updateservice:read - fehlende Rechte werden einzeln benannt. Gehoert das Token zu einem anderen Projekt als dem bearbeiteten, sagt doctor das ebenfalls. Gegen einen aelteren Server faellt er auf die grobe Pruefung zurueck und weist ausdruecklich darauf hin, dass die Rechte ungeprueft blieben." }, { "area": "betrieb", "breaking": false, "title": "dc-testkit.sh: erreichbar unter /testkit/ und mit Unix-Zeilenenden", "text": "Zwei Fehler auf demselben Weg. Erstens fehlte die Route: TESTUMGEBUNG.md nennt 'curl https://dc.mhdf.de/testkit/dc-testkit.sh', ausgeliefert war die Datei aber nur unter /public/testkit/ - der dokumentierte Bezugsweg hat nie funktioniert und wurde als 'Server noch zu alt' gedeutet. Zweitens die Zeilenenden: .gitattributes fuehrt *.sh als eol=lf, normalisiert wird aber erst beim Einchecken - hochgeladen wird der Arbeitsbaum. Von einem Windows-Rechner aus landete das Skript deshalb mit CRLF im Webroot und scheiterte auf Linux mit 'env: bash\\r: No such file or directory'. scripts/deploy.py und scripts/deploy_dev.py entfernen das CR bei .sh und .bash jetzt beim Hochladen, unabhaengig vom Zustand der Arbeitskopie." }, { "area": "webui", "breaking": false, "title": "Filter und Formulare werfen nicht mehr aufs Dashboard zurueck", "text": "Welche Ansicht sichtbar ist, hing allein am Anker der URL - und die Ansichtsrettung, die das auffangen sollte, griff aus zwei Gruenden nicht.\n\nErstens fragte sie dokumentweit nach der offenen Unteransicht. Im Markup tragen aber ACHT Unteransichten die Klasse \"active\": jedes Modul hat seine eigene Startansicht, und switchMainTab() raeumt nur innerhalb des Moduls auf, das es oeffnet. Zurueck kam deshalb immer die erste in Dokumentreihenfolge, unabhaengig davon, was gerade sichtbar war. Dass die Rettung trotzdem zu funktionieren schien, lag an den wenigen Formularen, die ihren Anker fest in der action stehen haben. Gesucht wird jetzt erst das aktive Modul und darin die Unteransicht.\n\nZweitens fielen die Filter durch das Netz: Es sind GET-Formulare, und ein GET-Formular verwirft beim Absenden den Anker seiner action; ausserdem loesten sie ueber form.submit() aus, was gar kein submit-Ereignis erzeugt. Der zentrale Zuhoerer haengt die offene Ansicht bei GET jetzt als Parameter \"view\" an den Query-String, ausgeloest wird ueber requestSubmit(), und beim Laden wird die Ansicht daraus wiederhergestellt. Die drei Filterformulare tragen das Feld zusaetzlich fest im Markup - dort ist ohne Zweifel bekannt, wo sie stehen, und das haengt an keiner DOM-Abfrage mehr." }, { "area": "api", "breaking": false, "title": "/api/bugtracker/v1/roadmap ist jetzt auch ohne .php erreichbar", "text": "Jeder andere Endpunkt kommt ohne Dateiendung aus, dieser nicht: Die Route fehlte in der .htaccess, der Aufruf lief in ein 404, und die OpenAPI-Beschreibung fuehrte deshalb als einzigen Pfad einen mit .php. Wer dem Muster der uebrigen API folgte, lief auf. Die Route ist nachgetragen, die Beschreibung nennt jetzt den Pfad ohne Endung. Der alte Pfad mit .php funktioniert unveraendert weiter - er faellt auf die allgemeine Regel fuer /api/ zurueck." }, { "area": "webui", "breaking": false, "title": "Die Suche findet Eintraege ueber ihre Nummer", "text": "Die Nummer ist das, was ueberall zitiert wird - in Commits, in Kommentaren, im Changelog - und war ausgerechnet ueber die Suche nicht auffindbar: Sie steht in keiner der durchsuchten Spalten. Wer \"288\" eingab, bekam nichts und musste die Liste durchblaettern. Jetzt sucht die Nummer mit, im Bugtracker wie im Fehler-Stream und ebenso ueber den Parameter search der API. Zwei Schreibweisen, zwei Bedeutungen: \"288\" findet das Item mit dieser Nummer UND alles, was die Zahl im Text stehen hat; \"#288\" ist eindeutig gemeint und liefert ausschliesslich dieses eine Item." }, { "area": "api", "breaking": false, "title": "Der Parameter search ist jetzt beschrieben", "text": "action=list kennt search seit jeher, in der OpenAPI-Beschreibung stand er nicht. Nachgetragen, mitsamt dem Verhalten bei Zahlen." }, { "area": "webui", "breaking": false, "title": "Statusfilter als Mehrfachauswahl, mit Vorgabe auf das Laufende", "text": "Vorher liess sich genau eine Lage waehlen; wer \"offen und in Nachpruefung\" sehen wollte, brauchte eine vorgefertigte Kombination - stand sie nicht in der Liste, ging es nicht. Jetzt Ankreuzfelder, gruppiert nach Laufend / Verlangt Aufmerksamkeit / Abgeschlossen. Angehakt ist ab Werk, was laeuft oder Aufmerksamkeit verlangt (open, planned, in_progress, triage_external, awaiting_verification, in_verification, fix_insufficient); Erledigtes, Geschlossenes und Abgelehntes bleibt draussen - es ist der groesste Teil des Bestandes und verdeckt sonst das, was noch zu tun ist. \"deferred\" fehlt mit Absicht: Der Sinn dieses Status ist \"nicht jetzt\". Kein Haken bedeutet keine Einschraenkung; das versteckte Feld im Formular macht \"alle Haken entfernt\" von \"erster Aufruf\" unterscheidbar, denn HTML sendet fuer leere Ankreuzfelder nichts." }, { "area": "webui", "breaking": false, "title": "Die Suche sagt, wenn ein Filter Treffer verschluckt", "text": "Eine Suche, die schweigt, obwohl sie etwas gefunden hat, ist schlimmer als gar keine Suche: Wer \"288\" eingibt und nichts bekommt, schliesst daraus, es gebe den Eintrag nicht - dabei ist er nur erledigt und deshalb ausgeblendet. Bei aktiver Suche wird jetzt zusaetzlich gezaehlt, wie viele Treffer die aktuellen Einschraenkungen wegnehmen, und das Ergebnis steht als Hinweis ueber der Liste, mit einem Verweis \"Ueberall suchen\". Die zweite Abfrage laeuft nur bei aktiver Suche und holt eine einzige Zeile." }, { "area": "webui", "breaking": false, "title": "Roadmap-Eintraege stehen nicht mehr in der Bugliste", "text": "Seit 2.7.0 IST ein Roadmap-Eintrag ein bugtracker_item - richtig so, es gibt keine zweite Ablage. Nur landet damit die gesamte Planung aller Projekte in derselben Liste wie die Meldungen: Nach der Uebernahme der Projekt-Roadmaps waren 217 von 229 Feature Requests Roadmap-Eintraege, und die zwoelf Vorschlaege, um die es in dieser Liste geht, lagen darunter begraben. Die Bugliste zeigt jetzt ab Werk nur, was NICHT auf der Roadmap steht (Kennzeichen ist unveraendert der Themenbereich). Sichtbar bleibt beides, nur an getrennten Orten; der neue Filter holt die Planung jederzeit wieder herein, und die Suche weist auf verdeckte Treffer hin." }, { "area": "docs", "breaking": false, "title": "Was ein Feature Request ist - und was nicht", "text": "Ein feature_request ist ein Vorschlag an ein FREMDES Projekt: etwas, das dort gebaut werden muesste, damit die Zusammenarbeit besser wird. Die eigene Planung ist es nicht - die gehoert als Roadmap-Eintrag in den Tracker, also mit Themenbereich. Der Unterschied war nirgends aufgeschrieben und ist am type auch nicht ablesbar, weil der nur bug und feature_request kennt; unterschieden wird am Themenbereich. Steht jetzt in der Agent-Vorlage, im Agenten-Handbuch und in der OpenAPI-Beschreibung. ROADMAP_MIGRATION.md warnt zusaetzlich, dass Schritt 3 und 4 zusammengehoeren: Wer nach dem Anlegen abbricht, hinterlaesst Dutzende vermeintlicher Feature Requests im Eingang des Projekts." } ] }, { "version": "2.7.0", "date": "2026-08-29", "summary": "Die Roadmap zieht in das Deploymentcenter: Planungsstand, Reihenfolge und Zustaendigkeit stehen dort, wo sie ohnehin gefuehrt werden, und nicht mehr in handgepflegten Markdown-Dateien daneben. Die Dateien behalten, was eine Tabellenzelle nicht traegt - die Begruendung.", "actionRequired": true, "changes": [ { "area": "api", "breaking": false, "title": "Roadmap als Sicht auf den Bugtracker - keine zweite Ablage", "text": "Migration 016 legt KEINE Roadmap-Tabelle an. Ein Roadmap-Eintrag IST ein bugtracker_item; es fehlten nur vier Angaben, die ein Backlog nicht braucht und eine Planung schon: epic (Themenbereich), roadmap_rank (Reihenfolge - Schweregrad ist keine Prioritaet), target_build (Zielfassung VOR der Umsetzung, abgegrenzt von resolved_in_build danach) und defer_trigger. Dazu doc_anchor: der Verweis auf die Stelle in der Projektdatei, an der die Begruendung steht. Neu ausserdem der Status \"deferred\" und die winzige Tabelle roadmap_epics fuer die Bereichstitel - stuenden die an jedem Item, waeren sie in jeder Zeile anders schreibbar. Die Zugehoerigkeit ist EIN Feld: Ein Item steht auf der Roadmap, wenn es ein epic hat. Ein zweites Kennzeichen daneben waeren zwei Angaben, die dasselbe behaupten und auseinanderlaufen koennen.", "action": "Migration 016 ausfuehren. Wer eigene Auswertungen auf bugtracker_items.status hat, muss \"deferred\" einordnen: Es zaehlt NICHT als offener Punkt und taucht bewusst nicht in ?action=next auf - genau das ist sein Zweck. Abzugrenzen von \"rejected\": verworfen heisst \"nicht erneut vorschlagen\", zurueckgestellt heisst \"es gibt einen Anlass, an dem es wieder ansteht\"." }, { "area": "api", "breaking": false, "title": "Zurueckstellen ohne Ausloeser wird abgelehnt", "text": "status=deferred verlangt defer_trigger mit mindestens 10 Zeichen: Woran erkennt man, dass \"spaeter\" jetzt ist? Ohne benannten Anlass ist ein Vorhaben nicht zurueckgestellt, sondern vergessen - und ein vergessener Punkt sieht in jeder Liste aus wie ein geplanter. Die Regel sitzt im Repository und gilt damit auf allen drei Wegen gleich (WebUI, /manage?action=update, roadmap.php?action=set); in einem Formular allein waere sie in dem Moment wirkungslos, in dem ein Agent die API benutzt. Abgelehnt wird mit 400 invalid_update statt 404 - ein fehlender Ausloeser ist kein fehlendes Item. Ein zurueckgestelltes Item verliert ausserdem seine Lease: Wer etwas vertagt, arbeitet nicht mehr daran.", "action": null }, { "area": "api", "breaking": false, "title": "Neuer Endpunkt /api/bugtracker/v1/roadmap.php", "text": "Lesend (bugtracker:read): action=list liefert die Eintraege nach Bereich und Reihenfolge, epics die Bereiche samt Zaehlung je Lage, next die vorderen Eintraege der Warteschlange, active wer gerade welches Item haelt, render den erzeugten Markdown-Block, check den Abgleich einer mitgeschickten Datei. Schreibend (bugtracker:manage, nur im eigenen Projekt): set, move, reorder, epic. Der status-Parameter nimmt eine Lage statt eines ENUM-Werts - aktiv, offen, arbeit, erledigt, zurueckgestellt, verworfen; zwoelf Status sind fuer die Ablage richtig und fuer eine Auswahlliste zu viele. Angelegt wird weiterhin ueber /report und abgeschlossen ueber /manage: Ein zweiter Schreibweg auf dieselben Spalten waere eine zweite Stelle, an der Regeln gelten muessten.", "action": null }, { "area": "mcp", "breaking": false, "title": "dc_roadmap und dc_roadmap_set - der Planungsstand als Werkzeug", "text": "Der Adapter hat jetzt 15 Werkzeuge. dc_roadmap beantwortet zu Sitzungsbeginn die zwei Fragen, die man sich sonst falsch beantwortet: was ist als Naechstes dran, und arbeitet schon jemand daran. dc_roadmap_set hebt ein Item auf die Roadmap (epic setzen), reiht es ein und traegt die Zielfassung nach. Beide bewusst getrennt von dc_report: Anlegen und Einplanen sind zwei Entscheidungen, und ein Werkzeug, das beides zugleich tut, trifft die zweite ungefragt mit.", "action": "In den Projekten mit eingerichtetem Adapter den Roadmap-Abschnitt aus docs/AGENT_PROMPT_TEMPLATE.md in CLAUDE.md bzw. AGENTS.md uebernehmen. Ohne ihn weiss ein Agent nicht, dass er vor der Arbeit nachsehen soll." }, { "area": "webui", "breaking": false, "title": "Neues Modul \"Roadmap\" mit Filtern nach Bereich, Lage, Zielfassung und Agent", "text": "Vier Unteransichten: Uebersicht (Kennzahlen, \"Als Naechstes\", wer arbeitet gerade woran, Fortschritt je Themenbereich), Alle Vorhaben (die Liste mit Reihenfolge-Pfeilen und Bearbeitungsdialog), Aufnahmeliste (offene Vorhaben ohne Bereich - der Weg vom Backlog auf die Planung) und Themenbereiche. Gefiltert wird serverseitig ueber GET-Parameter, die Ansicht ist damit als Link teilbar. Voreinstellung der Lage ist \"aktiv\" und nicht \"alle\": Eine Roadmap, die mit sechzig erledigten Eintraegen beginnt, beantwortet die Frage nicht, fuer die man sie aufschlaegt. Der Fortschrittsbalken rechnet Zurueckgestelltes und Verworfenes aus dem Nenner heraus - ein Bereich, in dem die Haelfte verworfen wurde, ist nicht zur Haelfte fertig, er ist kleiner geworden.", "action": null }, { "area": "cli", "breaking": false, "title": "cli/roadmap.php: Zustandsblock erzeugen, Prosa abgleichen", "text": "Die Antwort auf die Frage, wie Datenbank und Markdown-Datei zusammenbleiben, ist nicht \"besser aufpassen\", sondern zweigeteilt. --write ERZEUGT den Zustandsteil der Datei zwischen -Markern; dort kann nichts driften, weil dort niemand tippt, und ausserhalb wird nichts angefasst. --check prueft, was von Hand geschrieben ist: Verweist ein Absatz auf ein Item, das es nicht gibt (unknown_item)? Gehoert es einem anderen Projekt (foreign_item)? Fehlt zu einer zurueckgestellten Entscheidung die Begruendung (decision_undocumented)? Rueckgabewert 1 bei Fehlern, damit sich der Aufruf in deploy.py haengen laesst. Ein Item OHNE Absatz ist bewusst kein Befund - nicht jedes Vorhaben braucht eine Herleitung. Handvergebene Kennungen entfallen: Die Kennung ist die Item-ID, denn AUTO_INCREMENT vergibt nie zweimal dieselbe Nummer, waehrend ein neu vergebenes \"A12\" auf beiden Seiten stimmig aussieht und Verschiedenes meint.", "action": "Vorhandene ROADMAP.md, TODO.md oder PLAN.md je Projekt einmalig uebernehmen. Sechs Schritte, beschrieben in docs/ROADMAP_MIGRATION.md." }, { "area": "api", "breaking": true, "title": "observe und status: der Zustand des Befundes heisst jetzt finding_status", "text": "Beide Endpunkte legten das Feld \"status\" des Befundes in dieselbe Ebene wie das Umschlagfeld \"status\" der API. Http::ok() arbeitet mit array_merge(['status' => 'success'], $data) - der Wert aus dem Body gewann. Eine angenommene Meldung kam beim Client also als status=\"open\" an. Jeder Client, der wie ueberall sonst auf status === \"success\" prueft, hielt eine erfolgreiche Meldung fuer einen Fehlschlag, und zwar ausgerechnet an dem Endpunkt, den ein meldender Client am haeufigsten aufruft. Das Feld heisst jetzt finding_status; der Umschlag sagt wieder \"success\". Zwei Testfaelle halten beides fest - dass der Umschlag stimmt UND dass die Angabe nicht einfach verschwunden ist, denn ein geloeschtes Feld haette denselben Test bestanden.", "action": "Wer die Antwort von /api/supervisor/v1/observe oder /status auswertet, liest den Befundzustand nun aus finding_status. In der Praxis war das Feld vorher nicht benutzbar - es kollidierte ja gerade mit dem Umschlag." }, { "area": "betrieb", "breaking": false, "title": "Testinstanz dc-dev.mhdf.de und ein Testtor vor jedem Produktiv-Upload", "text": "Eine vollstaendige zweite Installation, die jederzeit geloescht und neu aufgebaut werden darf: eigene Datenbank, eigene Schluessel, Basic-Auth davor, Rocket.Chat aus. Dazu 124 Testfaelle in tests/e2e gegen API, Rechte, Migrationen und die Zugangssperre. scripts/deploy.py bringt vor jedem Upload erst den anstehenden Stand auf die Testinstanz und laesst dann den Testlauf darauf laufen; erst bei Rueckgabewert 0 geht etwas auf die Produktion (--ohne-test ist der Notausgang). Die Reihenfolge ist der Punkt: Liefe der Test gegen den vorhandenen Dev-Stand, prueft er aelteren Code als den, der gleich hochgeht. Gefundene, noch offene Fehler stehen mit Item-Nummer in tests/e2e/bekannte_befunde.json - sie laufen als BEK mit und blockieren nicht; besteht so ein Fall wieder, sagt der Lauf das ausdruecklich. Ein Lauf, in dem mehr uebersprungen als geprueft wurde, endet mit Rueckgabewert 2 statt 0: nichts ist kaputt, geprueft wurde es aber auch nicht, und der Unterschied darf nicht am Rueckgabewert verschwinden. Anleitung: docs/TESTINSTANZ.md.", "action": "Wer deployt, braucht scripts/deploy_dev_config.json (Vorlage: ...example.json). Ohne sie bricht scripts/deploy.py ab und verweist auf --ohne-test." }, { "area": "mcp", "breaking": false, "title": "MCP-Adapter 1.1: Ressourcen, Prompts und eine echte Protokollaushandlung", "text": "Werkzeuge ruft ein Modell von sich aus auf - Ressourcen und Prompts waehlt ein MENSCH. In Claude Desktop sind das das Anhang-Menue und die Schraegstrich-Befehle, und genau die fehlten. Neu: vier Ressourcen (dc://roadmap als fertiger Markdown-Block, dc://naechste-schritte, dc://projekte, dc://befunde) und vier Prompts (arbeit-aufnehmen, befunde-sichten, fehler-melden, roadmap-pflegen). Der Wert der Prompts liegt darin, dass die Hausregeln mitkommen: erst nachsehen wer dransitzt, Fremdmeldung nur mit Begruendung, verworfene Vorhaben nicht ohne neues Argument wiedervorschlagen. Bisher standen die nur in Werkzeugbeschreibungen - und die liest ein Modell erst, wenn es das Werkzeug schon gewaehlt hat. Ressourcen laufen durch dieselbe Fremddaten-Abgrenzung wie Werkzeugergebnisse; sie sind sogar eher ein Einfallstor, weil sie ungefragt im Kontext landen. BEHOBEN dabei: \"initialize\" spiegelte die vom Client verlangte Protokollfassung einfach zurueck. Ein Client, der 2025-06-18 anfragte, bekam sie bestaetigt, obwohl der Adapter fuer 2024-11-05 geschrieben war. Jetzt wird gegen eine Liste tatsaechlich bedienter Fassungen ausgehandelt. Ebenfalls behoben: Beim Beenden rief der Adapter process.exit() mitten in den schliessenden libuv-Handles - unter Windows eine Assertion und ein Rueckgabewert ungleich null, also ein sauber beendeter Server, der wie ein abgestuerzter aussah.", "action": "Fuer Claude Desktop: Eintrag in claude_desktop_config.json (%APPDATA%\\Claude\\ bzw. ~/Library/Application Support/Claude/), absoluter Pfad, DC_PROJECT setzen, Desktop vollstaendig neu starten. Anleitung in mcp/README.md." }, { "area": "api", "breaking": false, "title": "Behoben: Werkbank-Token konnte seine eigene Roadmap nicht rendern", "text": "roadmap.php?action=render und ?action=check verlangen ein konkretes Projekt. Beim Lesen laesst projectFilter() ein Werkbank-Token aber bewusst durch - es soll die ganze Landschaft sehen - und liefert deshalb null. Damit wurde der Projektbezug zu \"all\" und der Aufruf mit 400 missing_project abgelehnt. Betroffen war ausgerechnet der Tokentyp, den der MCP-Adapter und dc-testkit verlangen. Das Token ist trotzdem an ein Projekt gebunden; dieser Bezug dient jetzt als Rueckfall. \"Meine Roadmap\" ohne Angabe heisst genau das.", "action": null }, { "area": "api", "breaking": false, "title": "Behoben: Projektliste warf 500 fuer Werkbank-Token (HY093)", "text": "ProjectLinks::between() benutzte :a dreimal und :b zweimal im selben Statement, forProject() :slug zweimal. Bei ATTR_EMULATE_PREPARES = false ist das unzulaessig - PDO reicht die Namen an den Server durch, und der zaehlt Parameter, statt sie zu benennen. Ergebnis: SQLSTATE[HY093] und ein 500 auf /api/bugtracker/v1/projects. Die Abfrage laeuft nur, wenn ein Heimatprojekt gesetzt ist, also nur mit einem Werkbank-Token - fuer das Master-Token des Testlaufs wurde der Zweig uebersprungen. Derselbe Fehler steckte schon einmal in der Volltextsuche des Bugtrackers; ein Suchlauf ueber alle prepare()-Aufrufe der Codebasis fand jetzt keine weitere Stelle.", "action": null }, { "area": "betrieb", "breaking": false, "title": "Testlauf kennt jetzt den Werkbank-Token - der Tokentyp, den die Agenten benutzen", "text": "Es gibt keine API, ein dev_agent-Token anzulegen; in der Produktion geschieht das von Hand in der Token-Verwaltung, und das bleibt auch so. Fuer den Testlauf fehlte damit ausgerechnet der Tokentyp, den MCP-Adapter und dc-testkit verwenden - mit der Folge, dass seine Sonderwege niemand prueft. Genau dort steckten die beiden Befunde oben. testctl.php kann jetzt action=workbench_token, und vier Testfaelle decken die Wege ab: Roadmap ohne Projektangabe rendern, Projektliste mit Beziehungen, und die Gegenprobe, dass der Rueckfall aufs Heimatprojekt das Lesen ueber alle Projekte nicht einschraenkt. Der Testlauf steht damit bei 128 Faellen.", "action": null } ] }, { "version": "2.6.1", "date": "2026-08-28", "summary": "Agenten koennen auf der Linux-Testumgebung selbstaendig testen: DEV-Lizenzschluessel zur Selbstbedienung, ein Werkzeug fuer Auslieferung und Meldung, und eine Nachpruefung, die als Arbeit zurueckkommt statt als Benachrichtigung.", "actionRequired": true, "changes": [ { "area": "api", "breaking": true, "title": "Nachpruefung: der Fix ist erst fertig, wenn ihn jemand gegen ein Paket geprueft hat", "text": "Drei neue Zustaende (Migration 015): awaiting_verification, in_verification, fix_insufficient. Ein Item, dessen Fix veroeffentlicht wurde, geht nicht mehr direkt zu, sondern auf awaiting_verification - und taucht damit in ?action=next als Aufgabe auf. Das IST der Rueckkanal: Die Zustellung existierte laengst, es fehlte nur ein Status, der in der Warteschlangenabfrage vorkommt. Uebernehmen befoerdert nach in_verification, ueber denselben Claim wie gewoehnliche Arbeit. Bestaetigt wird mit status=closed; wer stattdessen erneut meldet, schiebt das Item selbsttaetig auf fix_insufficient - kein neues Item, Versuch und Fehlschlag bleiben auf einem Strang. Eine Regression bleibt davon unberuehrt und bekommt weiterhin ein eigenes Item: Sie betrifft ein bestaetigt geschlossenes Item, das zurueckkommt. Gegen das Verrotten: Laeuft die Lease ab, geht es zurueck in die Schlange; nach drei abgebrochenen Anlaeufen holt ein Vermerk einen Menschen; nach 30 Tagen ohne Pruefung wird ungeprueft geschlossen, mit einem Kommentar, der genau das sagt. Neu auch die Spalten status_changed_at (traegt den Timeout) und verification_cycles. Der Verlauf entsteht ohne zusaetzliche Tabelle: Jeder Statuswechsel schreibt jetzt strukturiertes meta_json (from/to/actor/since) in den vorhandenen Kommentarstrang - zuvor stand dort nur deutscher Fliesstext und der alte Status gar nicht.", "action": "Migration 015 ausfuehren. Wer eigene Auswertungen auf bugtracker_items.status hat, muss die drei neuen Werte einordnen: awaiting_verification und in_verification zaehlen NICHT als offene Fehler (der Fix ist ausgeliefert, offen ist nur die Frage), fix_insufficient dagegen schon - er ist ein offener Fehler, und zwar ein besonders aergerlicher. Wer Items ueber ?action=next abholt, muss den Status pruefen: in_verification heisst 'pruefe, ob der Fix traegt', nicht 'behebe das'." }, { "area": "api", "breaking": false, "title": "Behoben: liegengelassene Items verschwanden lautlos aus der Warteschlange", "text": "Uebernehmen befoerdert ein Item von 'open' nach 'in_progress'. Lief die Lease ab, gab expireStaleLeases() zwar claimed_by, claimed_at und lease_until frei - liess den Status aber stehen. Da claimNext() nur nach 'open,planned' fragt, war ein liegengelassenes Item danach unauffindbar: kein Besitzer, nicht in der Schlange, kein Hinweis darauf, dass es je jemand angefasst hatte. Es blieb fuer immer liegen. Die Freigabe stuft den Status jetzt denselben Weg zurueck, den das Uebernehmen ihn befoerdert hat (in_progress -> open, in_verification -> awaiting_verification). Der Fehler bestand unabhaengig von der Nachpruefung und betraf jedes Projekt mit Agentenbetrieb.", "action": null }, { "area": "api", "breaking": false, "title": "Wer einen Fremdbefund gemeldet hat, bekommt den Pruefauftrag", "text": "Beim Schreiben gilt sonst ohne Ausnahme: Ein Agent uebernimmt nur Items seines eigenen Projekts. Genau das machte den Rueckkanal unmoeglich - meldet PolyTrader einen Fehler im Deploymentcenter, gehoert das Item dem Deploymentcenter, und PolyTraders Agent bekaeme es nie zu sehen, obwohl er oft der Einzige ist, der bestaetigen kann, dass der Fix bei ihm wirkt. Geoeffnet wird deshalb genau ein Spalt: fremde Items, die auf Nachpruefung warten UND bei denen das eigene Projekt in bugtracker_item_affects vermerkt ist. Wer auf diesem Weg etwas haelt, darf es kommentieren, im Status setzen und zurueckgeben - aber nicht resolve oder update. Was in einem fremden Projekt als 'behoben in Build X' gilt, entscheidet dieses Projekt.", "action": null }, { "area": "api", "breaking": false, "title": "Regression oder nur ein veralteter Pruefstand? Der Tracker vergleicht jetzt die Fassungen", "text": "Trifft eine Meldung auf ein bereits erledigtes Item, wurde daraus bisher IMMER eine Regression - ein neues Item mit Verweis auf das alte. Verglichen wurde nichts. Auf einer Testumgebung ist der haeufigste Fall aber ein anderer: Ein Lauf gegen 1.4.2 findet einen Fehler, der in 1.4.3 behoben wurde. Das ist kein Rueckschritt der Software, sondern eine veraltete Installation. Waere jede solche Meldung eine 'Regression', waere die Kennzeichnung wertlos fuer die echten. Neu wird build_version gegen resolved_in_build des erledigten Items verglichen (ueber den vorhandenen semantischen Vergleicher UpdateService\\Version, nicht ueber einen zweiten). Ist die gemeldete Fassung AELTER, entsteht nichts: kein Item, kein Kommentar, kein hochgezaehlter Zaehler. Die Antwort traegt stattdessen known_fixed_in und reported_build und sagt, dass aktualisiert und erneut geprueft werden soll. Ist sie gleich oder neuer, bleibt es bei der Regression - der Systemkommentar nennt jetzt beide Fassungen. Nicht vergleichbare Angaben ('dev', 'nightly', 'unknown', fehlendes resolved_in_build) fuehren weiterhin zur Regression; im Zweifel wird gemeldet und nicht verschluckt.", "action": "Wer eigene Meldewege gebaut hat, schickt build_version mit. Ohne diese Angabe kann der Server nicht unterscheiden und legt weiterhin im Zweifel ein Regressionsitem an. dc-testkit traegt sie automatisch aus der manifest.json des geprueften Verzeichnisses nach." }, { "area": "api", "breaking": false, "title": "DEV-Lizenzschluessel zur Selbstbedienung", "text": "Neu: POST /api/license/v1/dev-key, Scope license:dev. Eine Testmaschine stellt sich damit fuer ihr Projekt selbst einen befristeten Lizenzschluessel aus - Vorgabe 30 Tage, hoechstens 90, drei Aktivierungen, hoechstens zwoelf gleichzeitig je Projekt. Der Aufruf ist wiederholbar: Fuer dieselbe Kombination aus project_slug und instance_id kommt derselbe Schluessel zurueck und seine Frist wird verlaengert; ein Testskript darf ihn deshalb an den Anfang jedes Laufs setzen. Der Schluessel wird beim Ausstellen sofort in die .htpasswd des Produkts nachgezogen und oeffnet damit auch die Release-Ablage. Er ist markiert (customer_email dev-agent@testumgebung.invalid, Name 'DEV '), damit Testhardware in der Aktivierungsliste von echten Installationen zu unterscheiden ist. Abgelaufene Schluessel werden nach vierzehn Tagen automatisch geloescht. ACHTUNG beim Anbinden: Dieser Endpunkt antwortet im Hausformat (status: success), nicht im abweichenden Altformat von validate und deactivate daneben.", "action": "Wer auf der Linux-Test-VM testet, braucht je Projekt ein Werkbank-Token mit den Rechten bugtracker:manage und license:dev (Besitzerart dev_agent, an das Projekt gebunden). Bis das angelegt ist, laufen 'dc-testkit license' und 'dc-testkit report' ins Leere. Anleitung: docs/TESTUMGEBUNG.md, Abschnitt 4.2." }, { "area": "tooling", "breaking": false, "title": "dc-testkit - ein Weg fuer Lizenz, Auslieferung und Meldung", "text": "Neu unter public/testkit/dc-testkit.sh, auf der Testmaschine zu holen ueber https://dc.mhdf.de/testkit/dc-testkit.sh. Deckt den ganzen Kreis eines Testlaufs ab: doctor prueft Werkzeuge, Netz, Token und Projekt; license holt und verlaengert den DEV-Schluessel; agent laedt den update-agent und gleicht die Pruefsumme gegen installer.json ab; install liefert in einen leeren Sandkasten aus; update, repair und list decken die uebrigen Wege ab; verify vergleicht das geschriebene manifest.json mit dem, was der Server als neueste Fassung fuehrt; report, next, comment, resolve und unclaim bedienen den Bugtracker; run macht alles nacheinander und meldet, was scheitert; cleanup raeumt auf und gibt den Schluessel zurueck. Braucht nur curl und python3 - jq ist auf der VM nicht installiert und wird nicht vorausgesetzt. Der Lizenzschluessel geht ueber DC_LICENSE_KEY an den Agenten und nicht ueber --license-key: Auf einer geteilten Maschine sieht jeder andere Benutzer fremde Befehlszeilen in 'ps aux'. Meldungen tragen ein aus Projekt, Titel und Fundstelle abgeleitetes client_ref - derselbe Befund aus zehn Laeufen legt ein Item an und nicht zehn.", "action": null }, { "area": "docs", "breaking": false, "title": "Anleitung fuer die Linux-Testumgebung, projektuebergreifend", "text": "Neu: docs/TESTUMGEBUNG.md. Beschreibt die geteilte Test-VM vollstaendig - Zugang, vorhandene Werkzeuge, Testkit, Konventionen fuer den Parallelbetrieb mehrerer Projekte (eigene Datenbank, eigenes X-Display, Projektpraefix in /tmp), Sichtpruefung von Oberflaechen ueber Xvfb, xdotool und ImageMagick, sowie Belegungsliste und Aufraeumen. Bewusst NICHT unter public/: Die Datei nennt eine interne Adresse, einen Benutzernamen und den Weg, sudo zeitweise freizuschalten; /docs/ ist oeffentlich erreichbar. Zuvor lag die Beschreibung im PolyTrader-Repo, obwohl sie fuer alle Projekte gilt; dort steht jetzt nur noch ein Verweis samt der PolyTrader-eigenen Angaben.", "action": null }, { "area": "docs", "breaking": false, "title": "Bekannte Luecke: der Installationsdialog ist nicht kopflos pruefbar", "text": "update-agent --action install ist ein Dialog mit Auswahllisten und Passwortabfrage. Ein Agent kann ihn nicht treiben - Auswahllisten brauchen Pfeiltasten an einem echten Terminal, und ein Agent gibt grundsaetzlich kein Passwort ein. 'dc-testkit install' prueft deshalb die Auslieferung in ein leeres Verzeichnis: denselben Pfad, den der Installer intern nimmt, mit Download, SHA256, Signatur, Staging und Rollback. Ungeprueft bleiben die Aufloesung von setup.json und das Schreiben der Konfiguration. Als Vorhaben in der Roadmap unter G1 vermerkt.", "action": null }, { "area": "docs", "breaking": false, "title": "Fremdmeldungen: was dabei anders ist, steht jetzt dort, wo Agenten es lesen", "text": "Projektuebergreifendes Melden gibt es seit 2.6.0 (C1), beschrieben war es aber nirgends dort, wo ein meldender Agent hinsieht. Drei Eigenheiten fuehrten deshalb verlaesslich in die Irre: evidence ist bei einer Fremdmeldung PFLICHT (mind. 40 Zeichen, sonst 400 missing_evidence); der Schweregrad wird serverseitig auf medium gedeckelt, weil ein fremder Melder die Dringlichkeit nicht beurteilen kann; und die Meldung landet in triage_external statt in open, wird in der offenen Liste des Zielprojekts also nicht gefunden. Steht jetzt in TESTUMGEBUNG.md Abschnitt 4.6 und in der Agent-Prompt-Vorlage. Neu im Testkit: 'dc-testkit projects' zeigt die gueltigen Slugs, 'report --project --evidence ...' meldet dorthin, und die Deckelung wird vorher angesagt statt hinterher entdeckt.", "action": "Wer die Agent-Prompt-Vorlage in ein Projekt kopiert hat, zieht den Abschnitt 'Projekt bestimmen' nach - die alte Fassung sagte zwar, dass DC-Fehler unter 'deploymentcenter' gehoeren, verschwieg aber die Pflichtangabe evidence. Ein Agent, der ihr folgte, lief in 400 missing_evidence und legte den Befund danach im eigenen Projekt ab, wo ihn niemand behebt." }, { "area": "tooling", "breaking": false, "title": "Testkit: 'feature' wurde stillschweigend zu 'bug'", "text": "Der Bugtracker kennt genau zwei Typen, bug und feature_request, und deutet alles Uebrige wortlos in 'bug' um (BugRepo Zeile 92). Ein '--type feature' haette Feature Requests also als Fehler abgelegt, ohne dass es jemandem aufgefallen waere. Das Testkit uebersetzt jetzt feature/fr/wunsch/verbesserung nach feature_request und lehnt unbekannte Typen ab, statt sie umzudeuten. 'idea' wird gesondert abgefangen: Das ist ein Schweregrad, kein Typ. Schweregrade werden ebenfalls gegen die erlaubte Liste geprueft.", "action": null } ] }, { "version": "2.6.0", "date": "2026-08-21", "summary": "Supervisor-Befunde, MCP-Anbindung und projektuebergreifendes Melden. Der Installer prueft die Laufzeit und richtet die Ueberwachung des Hosts ein.", "actionRequired": true, "changes": [ { "area": "server", "breaking": false, "title": "Serverseitige Detektoren finden, woran niemand gedacht hat", "text": "Fuenf Detektoren laufen im minuetlichen Tick (gedrosselt auf zehn Minuten) und arbeiten ausschliesslich auf Daten, die ohnehin hereinkommen: novel.error_signature meldet Fehlersignaturen, die es in dieser Fassung noch nie gab; silence.heartbeat verstummte Instanzen; deviation.self Abweichungen vom eigenen Verlauf; deviation.fleet Ausreisser gegenueber den anderen Installationen derselben Fassung; regression.build Verschiebungen zwischen zwei Fassungen. Keiner davon muss den gesuchten Fehler vorher kennen - der Massstab kommt aus der Wirklichkeit statt aus einem Kopf. Sie brauchen keine Client-Aenderung.", "action": "Fuer deviation.fleet und regression.build den Monitoren ein Projekt zuordnen (watchdog_monitors.project_slug). Ohne Zuordnung laufen die uebrigen Detektoren weiter. Ein Beobachter in der ausgelieferten Anwendung ist NICHT noetig und auch nicht vorgesehen - diese Detektoren arbeiten auf Daten, die ohnehin hereinkommen." }, { "area": "api", "breaking": false, "title": "/api/supervisor/v1/ - Befunde einliefern und abfragen", "text": "POST observe nimmt Befunde entgegen (Scope supervisor:report), GET findings und summary liefern sie aus (supervisor:read), catalog nennt alle bekannten Detektoren, POST status stuft ein (supervisor:manage). Ein Befund ist keine Fehlermeldung: er behauptet nicht, dass etwas kaputt ist, sondern dass etwas ungewoehnlich ist - deshalb loest er keine Benachrichtigung aus. Idempotent ueber client_ref, damit ein minuetlich pruefender Beobachter einen Eintrag mit hohem Zaehler erzeugt und nicht 1.440 am Tag. Wer einliefert, sind Pruefwerkzeuge und Testlaeufe (dc-testkit ab 2.6.1) - ein Beobachter im ausgelieferten Programm war erwogen und ist verworfen worden, siehe docs/ROADMAP.md Abschnitt H.", "action": "" }, { "area": "api", "breaking": false, "title": "Projektuebergreifend melden", "text": "Ein Token vom Typ dev_agent darf jetzt in ein fremdes Projekt melden - der Fall, den die Agenten-Prompt-Vorlage seit jeher verlangte und den ApiAuth::enforceProject() mit 403 verweigerte. Laufzeit-Token duerfen es weiterhin nicht: sie stecken im ausgelieferten Client und sind extrahierbar. Fuer Fremdmeldungen ist evidence Pflicht, reported_from wird aus der Token-Bindung abgeleitet statt aus dem Body uebernommen, das Item landet im Status triage_external und wird auf Schweregrad medium gedeckelt. Meldet ein zweites Projekt dasselbe, entsteht kein neues Item - es waechst die Liste der Betroffenen.", "action": "Fuer Claude Code je Projekt ein Token vom Typ dev_agent anlegen, gebunden an das jeweilige Projekt." }, { "area": "api", "breaking": false, "title": "MCP-Adapter fuer Claude Code und andere Werkzeuge", "text": "Unter mcp/ liegt ein Adapter ohne Fremdabhaengigkeiten, der 13 Werkzeuge bereitstellt: Bugtracker (Suchen, Uebernehmen, Melden, Kommentieren, Abschliessen), Supervisor (Befunde, Zaehlung je Detektor, Kennzahlverlauf) und die Projektlandschaft. Fremddaten - Fehlermeldungen, Stacktraces, Kommentare - werden im Werkzeugergebnis abgegrenzt gekennzeichnet: sie entstehen auf fremden Maschinen und sind Daten, keine Anweisungen. Damit entfaellt das Einkopieren der Agenten-Prompt-Vorlage in jedes Projekt.", "action": "Adapter je Projekt in .mcp.json eintragen, siehe mcp/README.md." }, { "area": "api", "breaking": false, "title": "Beziehungen zwischen Projekten", "text": "dc_project_links haelt fest, welches Projekt welches nutzt und in welcher Fassung. Damit ersetzt POST /api/bugtracker/v1/projects die von Hand gepflegte Zeile \"Deploymentcenter-Anbindung: SDK x.y.z\" in jeder Projekt-CLAUDE.md - eine Angabe, die an N Stellen nachgezogen werden muss und nach dem ersten Vergessen schweigend falsch ist. Erstmals beantwortbar: welche meiner Projekte sind von diesem SDK-Fehler betroffen?", "action": "Nach jeder Aenderung an der Anbindung dc_link_project aufrufen." }, { "area": "client", "breaking": false, "title": "DeploymentcenterSdk.Version stand auf 2.5.1", "text": "Die Konstante war beim Sprung auf 2.6.0 nicht mitgezogen worden. Wer den Changelog wie vorgesehen mit ?since=DeploymentcenterSdk.Version abfragte, bekam 2.6.0 dauerhaft als neu gemeldet, auch nach dem Aktualisieren. Jetzt korrekt.", "action": "" }, { "area": "server", "breaking": false, "title": "Projektgebundene Token kamen an fremde Statistiken", "text": "?action=stats nahm project_slug aus der Anfrage, ohne die Bindung des Tokens zu erzwingen - ein an Projekt X gebundenes Token konnte damit die Zahlen jedes anderen Projekts abrufen. Die Bindung geht der Anfrage jetzt vor.", "action": "" }, { "area": "client", "breaking": false, "title": "Installer prueft die .NET-Laufzeit", "text": "Bisher endete eine gelungene Installation auf einer Maschine ohne .NET mit einer Anwendung, die sich nicht starten laesst - und die Fehlersuche begann beim Deploymentcenter, weil das der letzte bewusste Schritt war. Nach dem Uebertragen der Dateien liest der Installer jetzt die runtimeconfig.json der Anwendung, vergleicht sie mit \"dotnet --list-runtimes\" und nennt bei Bedarf den Installationsbefehl fuer diese Plattform. Eigenstaendig veroeffentlichte Pakete bringen ihre Laufzeit mit und werden nicht bemaengelt; rollForward wird beachtet.", "action": "" }, { "area": "client", "breaking": false, "title": "Ueberwachung der Maschine direkt aus dem Installer", "text": "Am Ende der Installation genuegen zwei Fragen - Name im Dashboard und ob eingeplant werden soll. Monitor, Token, Agent-Skript, Dateirechte, ein erster Heartbeat zur Probe und der Cron-Eintrag bzw. die geplante Aufgabe entstehen daraus. Fuer Maschinen ohne Installation, etwa den Hypervisor darunter, gibt es denselben Weg als \"--action monitor\".", "action": "Keine. Wer die Frage verneint, holt es spaeter mit --action monitor nach." }, { "area": "server", "breaking": false, "title": "Agent-Skripte melden jetzt Kennzahlen", "text": "Das aus der Oberflaeche heruntergeladene Skript schickte nur \"status: ok\" - damit beantwortete ein Host-Monitor genau eine Frage und keine der Fragen, wegen derer man auf einen Host schaut. Die Skripte melden jetzt Last, Speicherbelegung, Plattenbelegung und Laufzeit mit und stufen sich ab 90 Prozent Plattenbelegung selbst auf \"warning\". Erzeugt werden sie an einer Stelle (Modules/Watchdog/AgentScript.php), die Oberflaeche und Installer gemeinsam nutzen.", "action": "" }, { "area": "api", "breaking": false, "title": "POST /api/setup/v1/agent", "text": "Legt den Monitor an, stellt ein Token mit genau watchdog:ping aus und liefert das fertige Skript fuer den Host. Ein bereits vorhandener Monitor gleichen Namens wird mit 409 abgelehnt, damit nicht eine zweite Maschine stillschweigend die Historie der ersten uebernimmt; overwrite=true erlaubt es ausdruecklich. Im SDK: SetupClient.RequestWatchdogAgentAsync(...).", "action": "" } ] }, { "version": "2.5.3", "date": "2026-08-21", "summary": "Administratorkonten kommen an die Release-Ablage.", "actionRequired": false, "changes": [ { "area": "server", "breaking": false, "title": "Administratorkonten im Release-Zugangsschutz", "text": "Der Zugangsschutz nahm bisher nur Konten der Rolle installer auf, waehrend die Installationsskripte und der Installer ausdruecklich sagten, ein Administratorkonto tue es auch. Eine Erstinstallation mit einem Administratorkonto meldete sich erfolgreich an, zeigte den Katalog und scheiterte erst beim Herunterladen mit einer Meldung ueber abgelaufene Lizenzen. Administratoren zaehlen jetzt zu den Installationskonten; die .htpasswd-Dateien werden beim naechsten Tick neu erzeugt. Neu angelegte Konten landen ausserdem sofort darin - zuvor galt das nur fuer die Rolle installer, ein frisches Administratorkonto wartete bis zum naechsten turnusmaessigen Lauf.", "action": "" }, { "area": "client", "breaking": false, "title": "Ehrliche Meldung bei abgelehntem Zugang", "text": "Ein 401 der Release-Ablage nannte immer Lizenzgruende - abgelaufen, widerrufen, falsches Produkt. Bei einer Erstinstallation gibt es noch gar keine Lizenz; die Meldung schickte damit in die falsche Richtung. Wurde mit Benutzername und Passwort zugegriffen, nennt sie jetzt das Konto und die Rollen, die Zugang haben. Ausserdem bricht der Agent bei 401 sofort ab, statt ueber die API weiterzusuchen und dieselbe Meldung ein paar Schritte spaeter ein zweites Mal zu zeigen.", "action": "" } ] }, { "version": "2.5.2", "date": "2026-08-15", "summary": "LaunchUpdateAgent reicht --require-signature durch.", "actionRequired": true, "changes": [ { "area": "sdk", "breaking": false, "title": "requireSignature-Parameter auf LaunchUpdateAgent", "text": "--require-signature gab es bisher nur als Kommandozeilenschalter. Jede Anwendung, die dem Leitfaden folgt und ueber LaunchUpdateAgent startet - der empfohlene Weg - aktualisierte damit ungeprueft, waehrend derselbe Vorgang von Hand auf der Kommandozeile geschuetzt gewesen waere. Der oeffentliche Schluessel muss dafuer nicht separat verwaltet werden, der Agent holt und cached ihn ohnehin selbst.", "action": "LaunchUpdateAgent(..., requireSignature: true) setzen, sobald server.security.release_private_key hinterlegt ist und alle ausgelieferten Releases signiert wurden." } ] }, { "version": "2.5.1", "date": "2026-08-14", "summary": "Zugangsschutz je Produkt abschaltbar, Dienst-Betrieb dokumentiert, inkrementelle Builds repariert.", "actionRequired": true, "changes": [ { "area": "server", "breaking": false, "title": "Produkte lassen sich vom Zugangsschutz ausnehmen", "text": "Der Schutz entstand beim ersten Lauf nach dem Deployment von selbst - auch fuer Produkte, deren ausgelieferte Installationen noch ohne Lizenzschluessel bauen. Die bekamen ab dem Moment 401 statt Updates; bei Predictalytics ist genau das passiert. Unter UpdateService -> Zugangsschutz laesst sich je Produkt umschalten.", "action": "Wer eine Installationsbasis ohne SDK 2.4 hat: Produkt ausnehmen, Build mit Schluessel ausliefern, danach wieder einschalten." }, { "area": "sdk", "breaking": false, "title": "BuildInfo.targets verhindert keine inkrementellen Builds mehr", "text": "BuildDateUtc trug die volle Uhrzeit und aenderte sich bei jedem Build; WriteOnlyWhenDifferent griff nie, und jedes einbindende Projekt wurde jedes Mal neu uebersetzt. Jetzt tagesgenau - hoechstens ein Neubau pro Tag.", "action": "Keine. BuildInfo.BuildDateUtc enthaelt nur noch das Datum, nicht die Uhrzeit." }, { "area": "sdk", "breaking": true, "title": "LicenseConfig.SetStorageDirectory haengt jetzt /license an", "text": "Zuvor wurde der gesetzte Pfad roh benutzt, waehrend der Weg ueber DEPLOYMENTCENTER_STORAGE_DIR /license anhaengte. Zwei Produkte im selben Prozess mit gesetztem Override schrieben damit in dieselbe state.dat.", "action": "Wer SetStorageDirectory benutzt: Der Cache liegt jetzt eine Ebene tiefer. Ein vorhandener Cache wird einmalig neu aufgebaut." }, { "area": "sdk", "breaking": false, "title": "Lizenz-Cache faellt auf /var/lib/ zurueck", "text": "Eine systemd-Unit mit User= und ohne eigenes Heimatverzeichnis hat weder XDG_CONFIG_HOME noch HOME. Der Rueckfall landete im Installationsverzeichnis, unter /opt fuer den Dienstbenutzer nicht beschreibbar - also kein Offline-Cache, ausgerechnet im kopflosen Betrieb.", "action": "Wenn /var/lib nicht beschreibbar ist: DEPLOYMENTCENTER_STORAGE_DIR setzen." }, { "area": "docs", "breaking": false, "title": "Lueckenschluss in den Leitfaeden", "text": "Woher die Anwendung den Lizenzschluessel fuer den Update-Zugang nimmt (TryGetCachedKey), wie ein laufender systemd-Dienst aktualisiert wird, was statt Environment.Exit(1) zu tun ist, wenn Beenden die gefaehrlichere Option waere, und die Drosselungsgrenzen aller Endpunkte.", "action": null } ] }, { "version": "2.5.0", "date": "2026-08-14", "summary": "Geheimnisse raus aus der Kommandozeile, Packager sperrt statt zu warnen, ILicensePrompt lebt.", "actionRequired": true, "changes": [ { "area": "sdk", "breaking": false, "title": "Lizenzschluessel wird nicht mehr als Argument uebergeben", "text": "Der Agent nahm --license-key nur aus argv. 'ps' zeigt das jedem Benutzer der Maschine - exakt die Begruendung, mit der UPGRADE.md §5 den Crontab-Weg verwirft. Der Agent liest jetzt DC_LICENSE_KEY, DC_DOWNLOAD_USER und DC_DOWNLOAD_PASSWORD; Umgebung vor Argument. LaunchUpdateAgent setzt die Variable auf dem eigenen Prozess, das Kind erbt sie.", "action": "Keine Aenderung noetig, wenn ihr licenseKey an LaunchUpdateAgent uebergebt. Wer den Agenten selbst aufruft, sollte von --license-key auf DC_LICENSE_KEY umstellen." }, { "area": "sdk", "breaking": false, "title": "waitTimeoutSeconds an LaunchUpdateAgent", "text": "Der Agent kannte --wait-timeout, das SDK reichte es nicht durch - es galten fest 60 Sekunden. Laeuft die Zeit ab, bricht der Agent ab, ohne etwas zu veraendern.", "action": "Wenn euer Herunterfahren laenger als etwa 40 Sekunden dauert: waitTimeoutSeconds heraufsetzen." }, { "area": "sdk", "breaking": false, "title": "Environment.Exit(0) ist dokumentiert, nicht geaendert", "text": "exitCurrentApp:true beendet den Prozess hart - laufende finally-Bloecke, IHostApplicationLifetime und Destruktoren kommen nicht mehr zum Zug.", "action": "Bei offenem Zustand (Positionen, Transaktionen, ungeschriebene Puffer) exitCurrentApp:false setzen und selbst geordnet herunterfahren. Der Agent wartet ohnehin auf das Prozessende." }, { "area": "sdk", "breaking": false, "title": "EnsureLicensedAsync - ILicensePrompt wird endlich benutzt", "text": "Der Konstruktor nahm ILicensePrompt entgegen, legte es ab und rief es nie auf. Neu: EnsureLicensedAsync() nimmt den zwischengespeicherten Schluessel, fragt sonst nach, prueft, und fragt bei Ablehnung erneut. allowPrompt:false lehnt ohne Cache ab, statt im Dienst auf eine Eingabe zu warten, die nie kommt.", "action": "Optional. Wer eine eigene Abfrage gebaut hat, kann sie behalten." }, { "area": "packager", "breaking": true, "title": "Bricht bei Zugangsdaten im Paket ab", "text": "Zuvor nur eine Warnung - und die war ausgerechnet unterdrueckt, wenn die Datei auf der preserve-Liste stand. So geriet ein echter API-Schluessel in ein oeffentlich abrufbares Paket. Geprueft werden Dateiname (appsettings.Local.json, master.key, *.pfx, *.db, server_settings.xml) und Inhalt (gefuelltes Password=, sk-, ghp_, dc_master_, AKIA, private Schluessel). Platzhalter loesen nicht aus.", "action": "Pruefen, was im Publish-Verzeichnis landet - haeufig eine CopyToOutputDirectory-Regel in der csproj. Notausgang: --allow-secrets." }, { "area": "sdk", "breaking": false, "title": "BuildInfo.targets liegt im NuGet-Paket", "text": "Die Anleitung empfahl einen per relativem Pfad ins Nachbar-Repository - das setzt voraus, dass beide Arbeitskopien nebeneinander liegen und in derselben Fassung stehen. Das Target liegt jetzt unter build/ im Paket, NuGet importiert es selbst.", "action": "Den aus der csproj entfernen und stattdessen PackageReference auf Deploymentcenter.Client 2.5.0 setzen." }, { "area": "sdk", "breaking": false, "title": "Unauthorized auch im API-Zweig", "text": "Ein 401 wurde nur auf dem statischen Weg als Lizenzproblem erkannt; ueber die API kam er als gewoehnlicher HTTP-Fehler an.", "action": null }, { "area": "tooling", "breaking": false, "title": "pack-and-deploy ist beziehbar, Release-Vorlage vorhanden", "text": "Das Werkzeug wurde in der Anleitung benutzt, als laege es im PATH - beziehbar war es nirgends. Es steht jetzt unter /installer/ neben dem Agenten. Dazu eine Vorlage zum Kopieren ins eigene Projekt, die je Plattform dotnet publish und pack-and-deploy verkettet und sich das Werkzeug selbst holt.", "action": "Empfohlen: Vorlage von /docs/release-template/ holen. Anleitung unter /docs/release.md." } ] }, { "version": "2.4.0", "date": "2026-08-13", "summary": "Release-Ablage liegt hinter Zugangsschutz. Ohne Lizenzschluessel keine Updates mehr.", "actionRequired": true, "changes": [ { "area": "server", "breaking": true, "title": "/releases/ verlangt Zugangsdaten", "text": "Zuvor konnte jeder im Internet die vollstaendigen Pakete herunterladen. Zugang haben jetzt gueltige Lizenzschluessel des jeweiligen Produkts sowie die Installationskonten. Je Produkt eine eigene .htpasswd - eine gemeinsame wuerde bedeuten, dass eine Lizenz fuer Produkt A auch Produkt B oeffnet.", "action": "PFLICHT: credentials: ReleaseCredentials.FromLicenseKey(schluessel) an CheckForUpdateAsync, und licenseKey an LaunchUpdateAgent. Ohne das bekommt die Anwendung 401 und keine Updates mehr." }, { "area": "server", "breaking": true, "title": "Benutzername ist eine Ableitung, nicht der Schluessel", "text": "Das htpasswd-Format hasht nur die Passwortspalte. Stuende der Lizenzschluessel auch als Benutzername darin, waere die Datei eine Klartext-Kundenliste. Der Benutzername ist deshalb lic_.", "action": "Nur relevant, wenn ihr Basic Auth selbst baut statt ReleaseCredentials zu benutzen." }, { "area": "sdk", "breaking": false, "title": "UpdateCheckResult.Unauthorized", "text": "Trennt 'Lizenz traegt nicht mehr' von einem Netzwerkfehler. Ohne diese Unterscheidung sieht ein abgelaufener Vertrag aus wie eine Stoerung, und man sucht an der falschen Stelle.", "action": "Empfohlen: Unauthorized abfragen und dem Benutzer als Lizenzhinweis zeigen." }, { "area": "server", "breaking": false, "title": "Reihenfolge bei Neuprodukten", "text": "Fuer ein Produkt ohne Release existiert /releases// nicht und wird uebersprungen. Das Verzeichnis entsteht mit dem ersten Upload, der naechste Abgleich schuetzt es. Es gibt also kein Fenster, um ein ungeschuetztes Release zu ziehen und danach das SDK nachzuruesten.", "action": "Der erste ausgelieferte Build eines neuen Produkts muss die Zugangsdaten schon mitbringen." } ] }, { "version": "2.3.0", "date": "2026-08-13", "summary": "Erstinstallation ueber den Update-Agent, setup.json, Installationskonto.", "actionRequired": false, "changes": [ { "area": "agent", "breaking": false, "title": "update-agent --action install", "text": "Fuehrt durch Anmeldung, Auswahl aus dem Katalog, Zielverzeichnis, Installation und Einrichtung. Die Dateien kommen ueber denselben Pfad wie ein Update - mit Pruefsumme, Signatur, Staging und Rollback.", "action": "Optional: setup.json ins Publish-Verzeichnis legen, damit die Erstinstallation die noetigen Werte abfragen kann." }, { "area": "agent", "breaking": false, "title": "setup.json beschreibt die einzurichtenden Werte", "text": "Gefragt wird nur, was uebrig bleibt: bereits gesetzt -> detect:... -> provision -> fragen. Ziele haben ein 'location' (install, config, data, home) und optional fileWindows/fileLinux/fileMacOS. Dateien mit geheimen Werten werden auf den eigenen Benutzer beschraenkt.", "action": "Optional. Ohne setup.json laesst sich die Anwendung installieren, aber nicht einrichten." }, { "area": "server", "breaking": false, "title": "Rolle 'installer' und Benutzerverwaltung", "text": "Konten dieser Rolle koennen sich ausschliesslich ueber /api/setup/v1/login anmelden und Anwendungen einrichten - nicht am WebUI. Die Zugangsdaten werden auf jedem Zielsystem eingetippt; mit einem Administratorkonto verteilte man den Zugang zur gesamten Verwaltung.", "action": "Migration 012 einspielen. Fuer Erstinstallationen ein installer-Konto anlegen." } ] }, { "version": "2.2.0", "date": "2026-08-09", "summary": "Plattform-Dimension, signierte Releases, Update mit Rollback, geschuetzte Konfigurationsdateien.", "actionRequired": true, "changes": [ { "area": "server", "breaking": true, "title": "Releases tragen eine Plattform", "text": "Zuvor gab es die Dimension nicht: win-x64 und linux-x64 landeten unter derselben Version im selben Kanal und ueberschrieben sich: ein Linux-System zog sich das Windows-Paket. Ein Client, der keine Plattform mitschickt, sieht ausschliesslich Releases mit platform=any.", "action": "PFLICHT beim Veroeffentlichen: --platform win-x64 (o. ae.) an pack-and-deploy. Clientseitig passiert es von selbst - CheckForUpdateAsync schickt ohne Angabe die Kennung des laufenden Systems." }, { "area": "packager", "breaking": true, "title": "Version wird gegen die Hauptassembly geprueft", "text": "Weicht --version von der einkompilierten ab, bricht der Vorgang ab. Wird 1.0.1 als 1.0.2 veroeffentlicht, aktualisieren alle Clients, melden danach weiter 1.0.1, halten das Release erneut fuer neu - eine Endlosschleife ueber die gesamte Installationsbasis.", "action": " in die Directory.Build.props, nicht in einzelne csproj-Dateien. Notausgang: --ignore-version-mismatch." }, { "area": "packager", "breaking": true, "title": "preservePatterns schuetzt Konfigurationsdateien", "text": "Zuvor ueberschrieb jedes Update die eingerichteten Werte des Zielsystems. Ausschluss und Schutz sind zwei verschiedene Dinge: excludePatterns haelt eine Datei aus dem Paket, preservePatterns liefert sie aus, laesst am Ziel aber die vorhandene Fassung in Ruhe. Die Muster sind jetzt echte Globs - 'logs/**' traf zuvor nie zu.", "action": "preservePatterns in die packager.config.json aufnehmen. Pruefen, ob eure Konfigurationsdateien bisher ueberschrieben wurden." }, { "area": "agent", "breaking": false, "title": "Anwenden mit Backup und Rollback", "text": "Die Stelle war als 'Atomic Replace with Backup' kommentiert und war eine Kopierschleife. Bricht sie ab, blieb eine halb aktualisierte Installation zurueck. Jetzt: Plan, Backup, Anwenden, bei Fehler vollstaendiger Rollback. Verwaiste Dateien werden entfernt - aber nur solche aus dem Manifest der Vorversion.", "action": null }, { "area": "sdk", "breaking": true, "title": "LaunchUpdateAgent uebergibt Neustart und Prozesskennung", "text": "--restart wurde nie uebergeben - die Anwendung schloss sich nach 'Jetzt installieren' und blieb zu. --wait-for-pid gab es nicht: der Agent kopierte bei langsamem Herunterfahren ueber gesperrte Dateien. ResolveAgentPath() liefert den plattformrichtigen Namen; ein fest verdrahtetes update-agent.exe wird unter Linux nie gefunden.", "action": "UpdateClient.ResolveAgentPath() statt eines festen Pfads benutzen. currentVersion: BuildInfo.Version mitgeben - fuer Installationen ohne manifest.json." }, { "area": "server", "breaking": false, "title": "Releases werden signiert", "text": "Der SHA256 stammt aus derselben Quelle wie das Paket. Wer den Webroot kontrolliert, tauscht beide gemeinsam aus. Der Server signiert jetzt mit RSA-SHA256, der Agent prueft gegen /api/updateservice/v1/pubkey. Bewusst asymmetrisch: bei HMAC braeuchte der pruefende Agent denselben geheimen Schluessel.", "action": "Serverseitig security.release_private_key hinterlegen. Ohne Schluessel bleiben Releases unsigniert und installierbar." }, { "area": "sdk", "breaking": true, "title": "CheckForUpdateAsync hat einen Parameter mehr", "text": "platform steht vor dem CancellationToken. Wer den Token positionell uebergeben hat, bekommt einen Uebersetzungsfehler - kein stilles Fehlverhalten.", "action": "Benannte Argumente benutzen." } ] }, { "version": "2.1.0", "date": "2026-08-08", "summary": "Korrekturen im .NET-SDK, app_version am Heartbeat.", "actionRequired": true, "changes": [ { "area": "sdk", "breaking": true, "title": "BuildInfo.targets erzeugt im Namensraum des Projekts", "text": "Die vorherige Fassung erzeugte fest in Deploymentcenter.Client.Models und war damit nicht einbindbar (CS0433).", "action": "Ein etwaiges 'using Deploymentcenter.Client.Models' fuer BuildInfo entfernen." }, { "area": "sdk", "breaking": false, "title": "API-Rueckfall der Update-Pruefung liefert vollstaendige Daten", "text": "Fehlte die latest.json, kamen ueber die API weder Download-Adresse noch Pruefsumme, Changelog oder Kritikalitaet an - nur 'version' stimmte in beiden Formaten ueberein.", "action": "Eigene Umgehungen koennen entfallen." }, { "area": "sdk", "breaking": true, "title": "unknown_error entfaellt, cache_ttl_hours wird ausgewertet", "text": "Der Cache-Rueckfall greift jetzt bei jedem HTTP-Fehler, nicht nur bei bestimmten. app_version ist Parameter statt fest.", "action": "Wer unknown_error abfaengt, prueft stattdessen IsTransient. LicenseClient.DefaultAppVersion beim Start setzen." } ] }, { "version": "2.0.0", "date": "2026-08-07", "summary": "Sicherheitsumstellung: Veroeffentlichen braucht ein Token, Versionsvergleich nach Semver.", "actionRequired": true, "changes": [ { "area": "server", "breaking": true, "title": "Veroeffentlichen verlangt updateservice:publish", "text": "Zuvor voellig ungeschuetzt - jeder konnte download_url und sha256_hash eines bestehenden Releases ueberschreiben und allen Clients ein beliebiges Paket unterschieben.", "action": "Token mit dem Recht updateservice:publish erzeugen und als DC_TOKEN hinterlegen." }, { "area": "server", "breaking": true, "title": "Versionsvergleich folgt der semantischen Ordnung", "text": "Zuvor verglich SQL lexikografisch: 1.9.0 galt als neuer als 1.10.0, und Clients bekamen ein Downgrade als Update angeboten.", "action": null }, { "area": "server", "breaking": true, "title": "Einheitliches Antwortformat", "text": "Alle JSON-Endpunkte antworten mit {\"status\":\"success\",...} bzw. {\"status\":\"error\",\"error\":{\"code\":...}}. Der code ist stabil und fuer Programme gedacht, die message richtet sich an Menschen.", "action": "Auswertung der Antworten auf das neue Format umstellen." } ] } ] }