{ "_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.7.0", "versions": [ { "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." } ] }, { "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." } ] } ] }