fix(2.7.1): Auskuenfte, die stimmen - Fassung, Health, doctor, Ansicht

Der erste Durchlauf der Testinstanz hat neun Bugs zutage gefoerdert, und fast
alle waren dieselbe Sorte Fehler: eine Auskunft, die nicht stimmte.

Am folgenreichsten die Fassung. config/config.php fuehrte app.version als feste
Zeichenkette und stand seit 2.0.0 darauf, waehrend 2.7.0 lief - unbemerkt, weil
die Datei gitignored ist und in keinem Diff auftaucht. Zwei gemeldete Bugs sind
daran fehldiagnostiziert worden ("der Server ist zu alt"). src/bootstrap.php
leitet die Fassung jetzt aus "current" in public/docs/changelog.json ab.

Im Einzelnen:

- #346 Fassung aus dem Changelog statt aus der Konfiguration
- #60  /api/health ueberlebt eine leere Datenbank: eigener Check "schema",
       healthy=false mit Begruendung statt HTTP 500
- #54  dc-testkit doctor prueft Mindestfassung (semantisch) und Rechte, statt
       sie nur anzuzeigen - dafuer neu: GET /api/tokens/v1/whoami
- #53  dc-testkit.sh unter /testkit/ erreichbar (Route fehlte, der
       dokumentierte Bezugsweg hat nie funktioniert) und mit LF ausgeliefert
       (deploy.py/deploy_dev.py entfernen CR bei .sh/.bash beim Upload)
- #288 Filter und Formulare werfen nicht mehr aufs Dashboard zurueck: die
       offene Ansicht wandert bei GET als "view" in den Query-String, und die
       Filter loesen ueber requestSubmit() aus - form.submit() erzeugt kein
       submit-Ereignis, weshalb der zentrale Zuhoerer dort nie lief
- #347 /api/bugtracker/v1/roadmap auch ohne .php erreichbar
- #55  Fassungshinweis in TESTUMGEBUNG.md 4.6 (serverseitig war das mit 2.6.0
       bereits behoben)

Nicht geprueft: php -l. Auf dem Arbeitsplatz ist kein PHP installiert; die
Abnahme laeuft ueber die Testinstanz.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Deploymentcenter Bot
2026-08-30 21:06:33 +02:00
co-authored by Claude Opus 5
parent afcbb3c091
commit 3b56839a1e
13 changed files with 566 additions and 50 deletions
+14
View File
@@ -28,11 +28,13 @@ Options -Indexes
RewriteRule ^api/bugtracker/v1/report/?$ public/api/bugtracker/v1/report.php [L,QSA]
RewriteRule ^api/bugtracker/v1/projects/?$ public/api/bugtracker/v1/projects.php [L,QSA]
RewriteRule ^api/bugtracker/v1/manage(?:/(.*))?$ public/api/bugtracker/v1/manage/index.php [L,QSA]
RewriteRule ^api/bugtracker/v1/roadmap/?$ public/api/bugtracker/v1/roadmap.php [L,QSA]
RewriteRule ^api/license/v1(?:/(.*))?$ public/api/license/v1/index.php [L,QSA]
RewriteRule ^api/watchdog/v1(?:/(.*))?$ public/api/watchdog/v1/index.php [L,QSA]
RewriteRule ^api/updateservice/v1(?:/(.*))?$ public/api/updateservice/v1/index.php [L,QSA]
RewriteRule ^api/tokens/v1/provision/?$ public/api/tokens/v1/provision.php [L,QSA]
RewriteRule ^api/tokens/v1/whoami/?$ public/api/tokens/v1/whoami.php [L,QSA]
RewriteRule ^api/setup/v1(?:/(.*))?$ public/api/setup/v1/index.php [L,QSA]
RewriteRule ^api/supervisor/v1(?:/(.*))?$ public/api/supervisor/v1/index.php [L,QSA]
@@ -46,6 +48,18 @@ Options -Indexes
RewriteRule ^docs/?$ public/docs/index.php [L,QSA]
RewriteRule ^docs/(.+)$ public/docs/$1 [L,QSA]
# ------------------------------------------------------------------
# 3b. Werkzeugkasten fuer die Test-VM (public/testkit/)
#
# TESTUMGEBUNG.md nennt als Bezugsweg
# curl -fsSL https://dc.mhdf.de/testkit/dc-testkit.sh
# Ohne diese Regel lief der Aufruf in ein 404: ausgeliefert war die
# Datei nur unter /public/testkit/. Der dokumentierte Weg hat also
# nie funktioniert - was als "Server ist noch zu alt" gedeutet
# wurde und in den Behelf per scp fuehrte.
# ------------------------------------------------------------------
RewriteRule ^testkit/(.+)$ public/testkit/$1 [L,QSA]
# ------------------------------------------------------------------
# 4. WebUI
# ------------------------------------------------------------------
+4
View File
@@ -26,6 +26,10 @@ if (!function_exists('dc_env')) {
return [
'app' => [
'name' => 'Deploymentcenter',
// Rueckfallebene. Massgeblich ist "current" aus
// public/docs/changelog.json - src/bootstrap.php setzt den Wert von
// dort. Hier wurde der Wert nicht nachgezogen - /api/health meldete
// deshalb bis 2.7.0 unveraendert 2.0.0.
'version' => '2.0.0',
'url' => dc_env('DC_APP_URL', 'https://dc.example.com'),
'timezone' => 'Europe/Berlin',
+15 -5
View File
@@ -126,12 +126,16 @@ chmod +x ~/bin/dc-testkit
```
> ⚠️ **Beides — dieser Download und `dc-testkit license` — setzt Deploymentcenter 2.6.1 voraus.**
> Solange die Fassung auf dem Server älter ist, gibt es die Datei unter `/testkit/` nicht, und der
> Endpunkt `/api/license/v1/dev-key` fehlt. Der fehlende Endpunkt fällt dabei **nicht** mit 404
> auf: Die Lizenz-API antwortet bei unbekannten Pfadsegmenten mit ihrer Verfügbarkeitsprüfung, also
> mit HTTP 200. Das Testkit erkennt das und sagt es; wer von Hand `curl`t, sucht sonst lange.
> Ist die Fassung auf dem Server älter, fehlt der Endpunkt `/api/license/v1/dev-key`. Er fällt
> dabei **nicht** mit 404 auf: Die Lizenz-API antwortet bei unbekannten Pfadsegmenten mit ihrer
> Verfügbarkeitsprüfung, also mit HTTP 200. Das Testkit erkennt das und sagt es; wer von Hand
> `curl`t, sucht sonst lange. Seit 2.7.1 vergleicht `dc-testkit doctor` die Fassung von sich aus,
> statt sie nur anzuzeigen.
>
> Bis dahin geht es auch per `scp` aus dem Repo:
> **Der Download unter `/testkit/` funktionierte bis 2.7.1 gar nicht** — und zwar unabhängig von
> der Fassung: Im Webroot lag die Datei nur unter `/public/testkit/`, die Route auf `/testkit/`
> fehlte in der `.htaccess`. Wer vor 2.7.1 ein 404 sah, hat deshalb nicht unbedingt einen zu alten
> Server vor sich. Ausweichweg für alte Stände:
> `scp public/testkit/dc-testkit.sh testvm:~/bin/dc-testkit`
Dann je Projekt eine Konfiguration — **das Token gehört in die Datei, nicht in die Befehlszeile**,
@@ -397,6 +401,12 @@ am selben Problem. Antwortet der Server mit `409 already_claimed`, nimm das näc
### 4.6 Befunde, die ein anderes Projekt betreffen
> ⚠️ **Alles in diesem Abschnitt setzt Deploymentcenter 2.6.0 voraus** (`triage_external`,
> Deckelung, `reported_from`, die Ablehnung von Laufzeit-Token). Ein älterer Server nimmt eine
> Fremdmeldung an, legt sie aber als gewöhnliches Item mit `status=open` und ohne `reported_from`
> ab — ohne das zu sagen. Wer das beobachtet, hat nicht drei Fehler vor sich, sondern einen zu
> alten Server; `dc-testkit doctor` sagt es seit 2.7.1 von sich aus.
**Das ist der wichtigste Abschnitt dieser Datei.** Ein Agent auf der Test-VM stolpert
zwangsläufig über Dinge, die nicht seinem Projekt gehören: Der PolyTrader-Agent findet einen
Fehler im `update-agent`. Ihm fällt auf, dass diese Anleitung an einer Stelle lügt. Er hätte
+48 -1
View File
@@ -47,6 +47,34 @@ try {
$logDir = DC_VAR . '/log';
$checks['log_writable'] = ['ok' => is_dir($logDir) ? is_writable($logDir) : is_writable(DC_ROOT)];
// --- Schema vorhanden? ---
// Dass die Verbindung steht, heisst noch nicht, dass die Migrationen gelaufen
// sind: SELECT 1 gelingt auch auf einer leeren Datenbank. Vor der ersten
// Migration gibt es dc_tokens nicht - und daran starb die Rechtepruefung
// weiter unten mit einer PDOException. Ausgerechnet der Endpunkt, der ueber
// kaputte Zustaende Auskunft geben soll, antwortete dann mit HTTP 500 statt
// mit healthy=false und einer Begruendung. Betroffen war jede Neuinstallation
// zwischen Deployment und erster Migration.
$schemaReady = false;
if ($db !== null) {
try {
$schemaReady = $db->query("SHOW TABLES LIKE 'dc_tokens'")->fetchColumn() !== false;
} catch (Throwable $e) {
Logger::error('Health-Check: Schemapruefung fehlgeschlagen', ['error' => $e->getMessage()]);
}
$checks['schema'] = $schemaReady
? ['ok' => true]
: [
'ok' => false,
'message' => 'Datenbankschema fehlt - die Migrationen wurden noch nicht ausgefuehrt (install_db.php aufrufen).',
];
if (!$schemaReady) {
$healthy = false;
}
}
$response = [
'healthy' => $healthy,
'app' => Config::get('app.name', 'Deploymentcenter'),
@@ -56,7 +84,26 @@ $response = [
];
// --- Detailinformationen nur fuer Authentifizierte ---
if ($db !== null && ApiAuth::resolve($db, 'bugtracker:read') !== null) {
// Die Rechtepruefung selbst bleibt zusaetzlich abgesichert: ein halb
// migriertes Schema besteht die Pruefung oben, kann hier aber trotzdem ueber
// eine fehlende Spalte stolpern. Ein Fehlschlag wird wie "nicht
// authentifiziert" behandelt - dann kommt die Kurzfassung heraus, und die
// Antwort bleibt auswertbar.
$authenticated = false;
if ($db !== null && $schemaReady) {
try {
$authenticated = ApiAuth::resolve($db, 'bugtracker:read') !== null;
} catch (Throwable $e) {
Logger::warning('Health-Check: Rechtepruefung nicht moeglich', ['error' => $e->getMessage()]);
$response['checks']['schema'] = [
'ok' => false,
'message' => 'Datenbankschema unvollstaendig - die Rechtepruefung war nicht moeglich.',
];
$response['healthy'] = false;
}
}
if ($authenticated) {
try {
$status = Migrator::status($db);
$response['schema'] = [
+42 -1
View File
@@ -406,7 +406,7 @@ $spec = [
],
],
'/api/bugtracker/v1/roadmap.php' => [
'/api/bugtracker/v1/roadmap' => [
'get' => [
'tags' => ['Bugtracker'],
'summary' => 'Roadmap lesen',
@@ -1229,6 +1229,47 @@ $spec = [
'responses' => ['201' => ['description' => 'Sub-Token erstellt'], '403' => $errorResponse],
],
],
'/api/tokens/v1/whoami' => [
'get' => [
'tags' => ['Tokens'],
'summary' => 'Auskunft ueber das eigene Token',
'description' =>
"Meldet Besitzerart, Projektbindung, Rechte, Umgebung und Frist des uebergebenen Tokens.\n\n"
. "Gedacht fuer Werkzeuge, die ihre Voraussetzungen pruefen wollen, BEVOR sie etwas tun. "
. "Ohne diesen Endpunkt liesse sich ein Recht nur durch Ausprobieren feststellen - und das "
. "hat Nebenwirkungen: Ein Test auf `bugtracker:manage` uebernaehme ein Item, einer auf "
. "`license:dev` stellte einen Schluessel aus.\n\n"
. "`is_workbench` ist der Wert, der ueber Fremdmeldungen entscheidet: nur ein Token der "
. "Besitzerart `dev_agent` darf in ein fremdes Projekt melden.\n\n"
. "Der Klartextwert des Tokens wird nicht ausgeliefert. Ein widerrufenes oder abgelaufenes "
. "Token bekommt `401`.",
'responses' => [
'200' => [
'description' => 'Angaben zum Token',
'content' => ['application/json' => ['schema' => [
'type' => 'object',
'properties' => [
'token_id' => ['type' => 'string'],
'name' => ['type' => 'string'],
'type' => ['type' => 'string', 'enum' => ['master', 'sub']],
'owner_type' => ['type' => 'string', 'enum' => TokenManager::OWNER_TYPES],
'owner_identity' => ['type' => 'string', 'nullable' => true],
'is_workbench' => ['type' => 'boolean', 'description' => 'owner_type = dev_agent'],
'project_slug' => ['type' => 'string', 'nullable' => true],
'environment' => ['type' => 'string', 'enum' => TokenManager::ENVIRONMENTS],
'scopes' => ['type' => 'array', 'items' => ['type' => 'string']],
'created_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true],
'last_used_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true, 'description' => 'Stand VOR diesem Aufruf.'],
'expires_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true],
'parent_token_id' => ['type' => 'string', 'nullable' => true],
],
]]],
],
'401' => $errorResponse,
],
],
],
],
];
+68
View File
@@ -0,0 +1,68 @@
<?php
/**
* GET /api/tokens/v1/whoami
*
* Auskunft ueber das eigene Token: Besitzerart, Projektbindung, Rechte,
* Umgebung, Frist.
*
* Warum es das gibt: Bis hierher konnte ein Aufrufer nur ausprobieren, was
* sein Token darf - und das geht nicht ohne Nebenwirkung. "Darf ich
* bugtracker:manage?" haette geheissen, ein Item zu uebernehmen; "habe ich
* license:dev?" haette einen Schluessel ausgestellt. Also meldete
* dc-testkit doctor nur "gueltig - der Bugtracker antwortet" und lief mit
* einem Laufzeit-Token glatt durch, dem beides fehlte. Der Lauf scheiterte
* dann beim ersten Schritt, der die Rechte wirklich brauchte.
*
* Ausgeliefert wird ausschliesslich, was der Aufrufer ohnehin besitzt. Der
* Klartextwert des Tokens ist nicht dabei.
*/
declare(strict_types=1);
require_once __DIR__ . '/../../../../src/bootstrap.php';
use Deploymentcenter\Core\Db;
use Deploymentcenter\Core\Http;
use Deploymentcenter\Core\TokenManager;
Http::beginJson(['GET', 'OPTIONS'], true);
$presented = Http::bearerToken();
if ($presented === null) {
Http::fail(401, 'unauthorized', 'Kein Token uebergeben. Erwartet als Authorization: Bearer <token> oder X-Agent-Token.');
}
$db = Db::init();
$manager = new TokenManager($db);
// Ohne geforderten Scope: die Frage lautet "wer bin ich", nicht "darf ich
// etwas". Widerruf, kaskadierender Widerruf und Ablauf werden trotzdem
// geprueft - ein entwertetes Token bekommt hier keine Auskunft.
$token = $manager->validateToken($presented, null);
if ($token === null) {
Http::fail(401, 'invalid_token', 'Token unbekannt, widerrufen oder abgelaufen.');
}
$scopes = TokenManager::decodeScopes($token['scopes']);
$isWorkbench = ($token['owner_type'] ?? null) === 'dev_agent';
Http::ok([
'token_id' => (string)$token['token_id'],
'name' => (string)$token['name'],
'type' => (string)$token['type'],
'owner_type' => (string)$token['owner_type'],
'owner_identity' => $token['owner_identity'] !== null ? (string)$token['owner_identity'] : null,
// Werkbank-Token duerfen in fremde Projekte melden, Laufzeit-Token nicht.
// Der Unterschied entscheidet ueber Fremdmeldungen und ist deshalb der
// erste Wert, den ein Werkzeug pruefen sollte.
'is_workbench' => $isWorkbench,
'project_slug' => $token['project_slug'] !== null ? (string)$token['project_slug'] : null,
'environment' => (string)$token['environment'],
'scopes' => $scopes,
'created_at' => $token['created_at'] !== null ? (string)$token['created_at'] : null,
'last_used_at' => $token['last_used_at'] !== null ? (string)$token['last_used_at'] : null,
'expires_at' => $token['expires_at'] !== null ? (string)$token['expires_at'] : null,
'parent_token_id' => $token['parent_token_id'] !== null ? (string)$token['parent_token_id'] : null,
]);
+32
View File
@@ -70,6 +70,38 @@ Das zurückgegebene `sub_token` wird **nur einmal** ausgeliefert.
Ist ein Token an ein Projekt gebunden, greifen alle Aufrufe automatisch nur
auf dieses Projekt zu — ein Zugriff auf ein anderes liefert `403 project_forbidden`.
### Was kann mein Token? (ab 2.7.1)
```bash
curl https://dc.mhdf.de/api/tokens/v1/whoami \
-H "Authorization: Bearer $DC_TOKEN"
```
```json
{
"status": "success",
"name": "Werkbank polytrader",
"type": "master",
"owner_type": "dev_agent",
"is_workbench": true,
"project_slug": "polytrader",
"environment": "all",
"scopes": ["bugtracker:manage", "license:dev"],
"last_used_at": "2026-08-30 09:12:44",
"expires_at": null
}
```
Gedacht für Werkzeuge, die ihre Voraussetzungen prüfen wollen, **bevor** sie
etwas tun. Ohne diese Auskunft ließe sich ein Recht nur durch Ausprobieren
feststellen — und das hat Nebenwirkungen: eine Probe auf `bugtracker:manage`
übernähme ein Item, eine auf `license:dev` stellte einen Schlüssel aus.
`is_workbench` ist der Wert, der über Fremdmeldungen entscheidet: nur ein Token
der Besitzerart `dev_agent` darf in ein fremdes Projekt melden. Der
Klartextwert des Tokens wird nicht ausgeliefert; ein widerrufenes oder
abgelaufenes Token bekommt `401`.
---
## 2. Projekte finden
+51 -1
View File
@@ -1,8 +1,58 @@
{
"_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",
"current": "2.7.1",
"versions": [
{
"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. Fuer POST-Formulare gab es dafuer laengst eine Loesung (_return und dc_redirect), die Filter fielen aber durch beide Netze: 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. Nach jeder Filteraenderung stand man wieder im Dashboard und musste zurueckklicken - beim Filtern nach mehreren Parametern jedes Mal. Jetzt haengt der zentrale Zuhoerer die offene Ansicht bei GET als Parameter 'view' an den Query-String, ausgeloest wird ueber requestSubmit(), und beim Laden wird die Ansicht aus 'view' wiederhergestellt, wenn kein Anker da ist."
},
{
"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."
}
]
},
{
"version": "2.7.0",
"date": "2026-08-29",
+85 -35
View File
@@ -4018,7 +4018,7 @@ chmod +x update-agent
<div class="card" style="padding:0.85rem 1.1rem;">
<form method="GET" action="index.php" style="display:flex; flex-wrap:wrap; gap:0.45rem; align-items:center;">
<select name="rm_project" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="rm_project" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<option value="all">🌐 Alle Projekte</option>
<?php foreach ($projects as $p): ?>
<option value="<?= e($p['slug']) ?>" <?= $rmProject === $p['slug'] ? 'selected' : '' ?>>
@@ -4027,7 +4027,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="rm_lage" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="rm_lage" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<?php
// Voreinstellung ist "aktiv" und nicht "alle": Eine Roadmap,
// die mit sechzig erledigten Eintraegen beginnt, beantwortet
@@ -4046,7 +4046,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="rm_epic" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="rm_epic" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<option value="all">🗂️ Alle Bereiche</option>
<?php foreach ($roadmapEpics as $rmE): ?>
<option value="<?= e((string)$rmE['epic']) ?>" <?= $rmEpic === (string)$rmE['epic'] ? 'selected' : '' ?>>
@@ -4055,7 +4055,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="rm_build" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="rm_build" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<option value="all">🎯 Alle Zielfassungen</option>
<?php foreach ($roadmapBuilds as $rmB): ?>
<option value="<?= e((string)$rmB) ?>" <?= $rmBuild === (string)$rmB ? 'selected' : '' ?>><?= e((string)$rmB) ?></option>
@@ -4641,7 +4641,7 @@ chmod +x update-agent
und ging bei jedem Auto-Refresh verloren.
-->
<form method="GET" action="index.php" style="display:flex; flex-wrap:wrap; gap:0.4rem; align-items:center;">
<select name="bt_project" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="bt_project" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<option value="all">🌐 Alle Projekte</option>
<?php foreach ($projects as $p): ?>
<option value="<?= e($p['slug']) ?>" <?= $btFilters['project_slug'] === $p['slug'] ? 'selected' : '' ?>>
@@ -4650,7 +4650,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="bt_env" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="bt_env" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<?php
$envOptions = [
'all' => '🌍 Alle Umgebungen',
@@ -4664,7 +4664,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="bt_type" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="bt_type" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<?php
$typeOptions = [
'all' => '📂 Alle Typen',
@@ -4676,7 +4676,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="bt_severity" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="bt_severity" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<?php
$sevOptions = [
'all' => '🔥 Alle Schweregrade',
@@ -4692,7 +4692,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="bt_status" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="bt_status" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<?php
$statusOptions = [
'all' => '📌 Alle Status',
@@ -4720,7 +4720,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="bt_order" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="bt_order" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<?php
$orderOptions = [
'newest' => '🕒 Neueste zuerst',
@@ -5026,7 +5026,7 @@ chmod +x update-agent
</h2>
<form method="GET" action="index.php" style="display:flex; flex-wrap:wrap; gap:0.4rem; align-items:center;">
<select name="err_project" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="err_project" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<option value="all">🌐 Alle Projekte</option>
<?php foreach ($projects as $p): ?>
<option value="<?= e($p['slug']) ?>" <?= $errFilters['project_slug'] === $p['slug'] ? 'selected' : '' ?>>
@@ -5035,7 +5035,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="err_level" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="err_level" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<?php
$levelOptions = [
'all' => '🎚️ Alle Klassen',
@@ -5048,7 +5048,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="err_env" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="err_env" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<?php
$errEnvOptions = [
'all' => '🌍 Alle Umgebungen',
@@ -5062,7 +5062,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="err_show" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="err_show" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<?php
$showOptions = [
'active' => '🔔 Nur relevante',
@@ -5074,7 +5074,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="err_hours" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="err_hours" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<?php
$hourOptions = [
'0' => '🕒 Gesamter Zeitraum',
@@ -6246,42 +6246,81 @@ SYSTEM
}
/**
* Merkt sich beim Absenden, welche Unteransicht offen war.
* Ansichtsrettung: Merkt sich beim Absenden, welche Unteransicht offen
* war, und stellt sie nach dem Neuladen wieder her.
*
* Der Server haengt diesen Wert nach dem Speichern wieder an die URL.
* Ohne das landete man stets auf dem Dashboard des Moduls - man legte
* einen Monitor an und wurde aus dem Formular geworfen.
* Ohne das landete man stets auf dem Dashboard - man legte einen
* Monitor an und wurde aus dem Formular geworfen, und jede
* Filteraenderung im Bugtracker warf einen dorthin zurueck.
*
* Als zentraler Zuhoerer umgesetzt, damit auch kuenftige Formulare das
* Verhalten automatisch erben.
* Verhalten automatisch erben. Die drei Funktionen darunter gehoeren
* zusammen: dcCurrentView() liest die Ansicht ab, dcRememberView()
* haengt sie an das Formular, dcSubmit() sorgt dafuer, dass beim
* Absenden aus einem onchange ueberhaupt ein submit-Ereignis entsteht.
*/
document.addEventListener('submit', (event) => {
const form = event.target;
if (!(form instanceof HTMLFormElement)) {
return;
}
if ((form.method || '').toLowerCase() !== 'post') {
return;
}
if (form.querySelector('input[name="_return"]')) {
return;
}
function dcCurrentView() {
const activeSub = document.querySelector('.subtab-content.active');
const activeTab = document.querySelector('.main-content > .tab-content.active');
const target = activeSub ? activeSub.id : (activeTab ? activeTab.id : '');
return activeSub ? activeSub.id : (activeTab ? activeTab.id : '');
}
/**
* Haengt die gerade offene Ansicht an ein Formular.
*
* POST: als _return - dc_redirect() macht daraus den Anker der
* Weiterleitung.
* GET: als view - ein Anker ueberlebt die Serialisierung eines
* GET-Formulars nicht, der Query-String schon. Genau daran
* scheiterten die Filter: nach jeder Aenderung stand die Seite
* wieder auf dem Dashboard, weil die neue URL keinen Anker
* mehr trug.
*/
function dcRememberView(form) {
const isPost = (form.method || 'get').toLowerCase() === 'post';
const name = isPost ? '_return' : 'view';
if (form.querySelector('input[name="' + name + '"]')) {
return;
}
const target = dcCurrentView();
if (!target) {
return;
}
const field = document.createElement('input');
field.type = 'hidden';
field.name = '_return';
field.name = name;
field.value = target;
form.appendChild(field);
}
document.addEventListener('submit', (event) => {
const form = event.target;
if (!(form instanceof HTMLFormElement)) {
return;
}
dcRememberView(form);
}, true);
/**
* Absenden aus einem onchange heraus.
*
* form.submit() loest KEIN submit-Ereignis aus - der Zuhoerer oben
* lief deshalb bei den Filtern nie, obwohl es ihn gab.
* requestSubmit() tut es. Wo es fehlt, wird die Ansicht vorher von
* Hand angehaengt.
*/
function dcSubmit(form) {
if (typeof form.requestSubmit === 'function') {
form.requestSubmit();
return;
}
dcRememberView(form);
form.submit();
}
// Sidebar Toggle Handler
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
@@ -6926,7 +6965,18 @@ SYSTEM
* Modulnamen. Der Rueckweg landete deshalb immer auf dem Dashboard.
*/
function restoreViewFromHash() {
const hash = location.hash.replace('#', '');
let hash = location.hash.replace('#', '');
// Ohne Anker: der Wert aus dem Query-String, den dcRememberView()
// an GET-Formulare haengt. Nur eigene Ansichtskennungen zulassen -
// der Wert steuert, was angezeigt wird.
if (!hash) {
const fromQuery = new URLSearchParams(location.search).get('view');
if (fromQuery && /^(?:tab|sub)-[a-z0-9-]{1,60}$/.test(fromQuery)) {
hash = fromQuery;
}
}
if (!hash) {
return;
}
+124 -4
View File
@@ -46,6 +46,13 @@ DC_PROJECT="${DC_PROJECT:-}"
DC_TOKEN="${DC_TOKEN:-}"
DC_CHANNEL="${DC_CHANNEL:-prod}"
# Aelteste Fassung des Deploymentcenters, mit der dieses Testkit vollstaendig
# arbeiten kann. Darunter fehlen /api/license/v1/dev-key (dc-testkit license),
# der Bezug ueber /testkit/ und /api/tokens/v1/whoami (Rechtepruefung in
# doctor). Die Zahl stand vorher nur in einer Fehlermeldung mitten im Ablauf -
# doctor kannte sie nicht und meldete "Bereit" auf einem zu alten Server.
DC_MIN_SERVER="${DC_MIN_SERVER:-2.6.1}"
# Die Maschinenkennung entscheidet, welchen DEV-Schluessel der Server
# heraussucht. Sie muss ueber Laeufe hinweg gleich bleiben - sonst sammelt sich
# je Lauf ein weiterer Schluessel an, bis die Grenze je Projekt greift.
@@ -241,6 +248,52 @@ ensure_state() {
chmod 700 "$dir" 2>/dev/null || true
}
# ---------------------------------------------------------------------------
# Fassungen und Rechte
# ---------------------------------------------------------------------------
# Ist $1 aelter als $2? 0 = ja, 1 = nein, 2 = nicht vergleichbar.
#
# Nicht "sort -V": das fehlt auf BusyBox-Systemen, und die Antwort waere dort
# still falsch statt laut fehlend. Ein Vorabkennzeichen (1.2.3-rc1) wird
# abgeschnitten - fuer die Frage "reicht der Server aus" genuegt das.
version_lt() {
local a="${1%%-*}" b="${2%%-*}" i av bv
for i in 1 2 3; do
av=$(printf '%s' "$a" | cut -d. -f$i)
bv=$(printf '%s' "$b" | cut -d. -f$i)
av="${av:-0}"; bv="${bv:-0}"
case "$av$bv" in
''|*[!0-9]*) return 2 ;;
esac
[ "$av" -lt "$bv" ] && return 0
[ "$av" -gt "$bv" ] && return 1
done
return 1
}
# Deckt die Rechteliste $1 (JSON-Array aus whoami) das Recht $2 ab?
#
# Die Wildcards muessen mitgedacht werden: "*" deckt alles ab, "bugtracker:*"
# alles darunter. Sonst meldete das Werkzeug ein Recht als fehlend, das der
# Server anerkennt - und schickte jemanden ein Token neu bauen, das passt.
has_scope() {
local list="$1" want="$2" prefix
prefix="${2%%:*}"
case "$list" in
*'"*"'*) return 0 ;;
*"\"$want\""*) return 0 ;;
*"\"$prefix:*\""*) return 0 ;;
esac
return 1
}
# ---------------------------------------------------------------------------
# doctor - laeuft hier ueberhaupt etwas?
#
@@ -263,9 +316,34 @@ cmd_doctor() {
step "Deploymentcenter unter $DC_URL"
if api GET /api/health; then
local healthy
local healthy server_version
healthy=$(printf '%s' "$API_BODY" | json_get '.healthy' || echo '?')
ok "erreichbar, healthy=$healthy, Fassung $(printf '%s' "$API_BODY" | json_get '.version' || echo '?')"
server_version=$(printf '%s' "$API_BODY" | json_get '.version' || echo '')
ok "erreichbar, healthy=$healthy, Fassung ${server_version:-?}"
# Die Fassung stand doctor immer schon vor Augen - verglichen wurde sie
# nur nie. Deshalb meldete er "Bereit" und der erste Schritt, der den
# Server wirklich braucht, scheiterte danach mit "kennt
# /api/license/v1/dev-key noch nicht".
if [ -z "$server_version" ]; then
warn "der Server nennt keine Fassung - $DC_MIN_SERVER nicht pruefbar"
else
version_lt "$server_version" "$DC_MIN_SERVER"
case $? in
0)
warn "zu alt: $server_version - gebraucht wird mindestens $DC_MIN_SERVER"
warn " Es fehlen: dc-testkit license (dev-key), der Bezug ueber /testkit/"
warn " und die Rechtepruefung. Der Server muss deployt werden."
failed=1
;;
2)
warn "Fassung \"$server_version\" ist nicht auswertbar - $DC_MIN_SERVER ungeprueft"
;;
*)
ok "erfuellt die Mindestfassung $DC_MIN_SERVER"
;;
esac
fi
else
warn "nicht erreichbar: $(api_error)"
failed=1
@@ -275,8 +353,50 @@ cmd_doctor() {
if [ -z "$DC_TOKEN" ]; then
warn "kein Token gesetzt (DC_TOKEN)"
failed=1
elif api GET /api/bugtracker/v1/projects; then
ok "gueltig - der Bugtracker antwortet"
elif api GET /api/tokens/v1/whoami; then
# Fruehere Fassungen meldeten hier nur "gueltig - der Bugtracker
# antwortet". Das lief auch mit einem Laufzeit-Token durch, dem
# bugtracker:manage und license:dev fehlten - der Lauf scheiterte dann
# erst dort, wo die Rechte wirklich gebraucht wurden.
local tok_name tok_owner tok_project tok_scopes tok_workbench want
tok_name=$(printf '%s' "$API_BODY" | json_get '.name' || echo '?')
tok_owner=$(printf '%s' "$API_BODY" | json_get '.owner_type' || echo '?')
tok_project=$(printf '%s' "$API_BODY" | json_get '.project_slug' || echo '')
tok_scopes=$(printf '%s' "$API_BODY" | json_get '.scopes' || echo '[]')
tok_workbench=$(printf '%s' "$API_BODY" | json_get '.is_workbench' || echo 'false')
ok "gueltig - \"$tok_name\", Besitzerart $tok_owner, Projekt ${tok_project:-keins}"
dim " Rechte: $tok_scopes"
if [ "$tok_workbench" != 'true' ]; then
warn "kein Werkbank-Token: Besitzerart ist $tok_owner, gebraucht wird dev_agent"
warn " Ein Laufzeit-Token darf nicht in fremde Projekte melden (TESTUMGEBUNG.md 4.2)."
failed=1
fi
for want in bugtracker:manage license:dev updateservice:read; do
if ! has_scope "$tok_scopes" "$want"; then
warn "Recht fehlt: $want"
failed=1
fi
done
if [ -n "$tok_project" ] && [ -n "$DC_PROJECT" ] && [ "$tok_project" != "$DC_PROJECT" ]; then
warn "Token gehoert zu $tok_project, gearbeitet wird an $DC_PROJECT"
warn " Je Projekt ein eigenes Token - sonst ist die Herkunft einer Meldung nicht mehr ablesbar."
failed=1
fi
elif [ "$API_STATUS" = '404' ]; then
# Server ohne whoami. Dann bleibt nur die alte, grobe Pruefung - und
# der ausdrueckliche Hinweis, dass die Rechte ungeprueft sind. Still
# "gueltig" zu melden waere genau der Fehler, der hier behoben wird.
if api GET /api/bugtracker/v1/projects; then
warn "gueltig, aber die Rechte sind ungeprueft"
warn " /api/tokens/v1/whoami gibt es erst ab $DC_MIN_SERVER."
else
warn "Token wird nicht angenommen: $(api_error)"
failed=1
fi
else
warn "Token wird nicht angenommen: $(api_error)"
failed=1
+31 -1
View File
@@ -17,6 +17,7 @@ gibt gruenes Licht fuer Code, den er nie gesehen hat.
import os
import json
import ftplib
import io
import subprocess
import sys
import hashlib
@@ -110,6 +111,35 @@ def should_ignore(rel_path):
return True
return False
# Dateien, die auf dem Server ausgefuehrt oder von einer Shell gelesen werden.
# Fuer sie ist CRLF kein Schoenheitsfehler, sondern ein Abbruch.
LF_REQUIRED_SUFFIXES = {'.sh', '.bash'}
def open_for_upload(full_path):
"""
Oeffnet eine Datei zum Hochladen und entfernt bei Shell-Skripten das CR.
Warum das noetig ist, obwohl .gitattributes "*.sh text eol=lf" fuehrt:
Normalisiert wird beim EINCHECKEN. Eine Arbeitskopie, die vorher schon
CRLF hatte, bleibt unberuehrt - und genau die laedt dieses Skript hoch,
denn es liest den Arbeitsbaum, nicht den Index. storbinary uebertraegt
Byte fuer Byte. So landete public/testkit/dc-testkit.sh mit CRLF im
Webroot und scheiterte auf der Linux-VM mit
"env: bash\r: No such file or directory".
Die Umwandlung hier schuetzt unabhaengig davon, von welchem Rechner
ausgeliefert wird und in welchem Zustand die Arbeitskopie ist.
"""
if os.path.splitext(full_path)[1].lower() not in LF_REQUIRED_SUFFIXES:
return open(full_path, 'rb')
with open(full_path, 'rb') as handle:
data = handle.read()
return io.BytesIO(data.replace(b'\r\n', b'\n'))
def collect_changes(cache):
"""Ermittelt alle Dateien, die sich seit dem letzten Deployment geaendert haben."""
files_to_upload = []
@@ -216,7 +246,7 @@ def deploy():
ftp.cwd('/')
print(f"Uploading {rel_file} ...")
with open(full_path, 'rb') as f:
with open_for_upload(full_path) as f:
ftp.storbinary(f"STOR {remote_file_path}", f)
new_cache[rel_file] = file_hash
+15 -2
View File
@@ -203,6 +203,19 @@ def should_ignore(rel_path):
return False
# Dateien, die auf der Test-VM ausgefuehrt werden. CRLF ist dort kein
# Schoenheitsfehler, sondern ein Abbruch: "env: bash\r: No such file or
# directory". .gitattributes normalisiert erst beim Einchecken - hochgeladen
# wird der Arbeitsbaum. Deshalb hier, wie in deploy.py.
LF_REQUIRED_SUFFIXES = {'.sh', '.bash'}
def normalize_for_upload(rel, data):
if os.path.splitext(rel)[1].lower() not in LF_REQUIRED_SUFFIXES:
return data
return data.replace(b'\r\n', b'\n')
def collect(cache, generated, full=False):
"""(remote_pfad, bytes, hash) fuer alles, was uebertragen werden muss."""
items = []
@@ -214,10 +227,10 @@ def collect(cache, generated, full=False):
rel = ('%s/%s' % (rel_dir, f)).lstrip('/')
if should_ignore(rel):
continue
items.append((rel, (Path(root) / f).read_bytes()))
items.append((rel, normalize_for_upload(rel, (Path(root) / f).read_bytes())))
for rel in sorted(generated):
items.append((rel, generated[rel]))
items.append((rel, normalize_for_upload(rel, generated[rel])))
out = []
for rel, data in items:
+37
View File
@@ -50,6 +50,43 @@ if (!is_array($dcConfig)) {
exit(1);
}
// --- Fassung: aus dem Changelog, nicht aus der Konfiguration ---
// Die ausgelieferte Fassung ist eine Eigenschaft des Standes, keine
// Einstellung der Installation. In config/config.php stand sie als
// Zeichenkette - und blieb dort auf 2.0.0 stehen, waehrend laengst 2.7.0 lief.
// Das war keine Schoenheitsfrage: /api/health meldete 2.0.0, Fehlermeldungen
// trugen eine falsche build_version, und wer die Fassung gegen eine
// Mindestanforderung prueft (dc-testkit doctor), bekam auf einem aktuellen
// Server eine Absage. public/docs/changelog.json wird bei jedem Release
// ohnehin gepflegt und ist damit die belastbarere Quelle. Der Wert aus der
// Konfiguration bleibt Rueckfallebene, falls die Datei fehlt.
//
// Gelesen wird nur der Kopf der Datei: sie ist rund 55 KB gross, und dafuer
// bei jedem Aufruf einen json_decode zu bezahlen, waere fuer eine einzelne
// Zeichenkette zu teuer. Findet sich der Schluessel dort nicht - etwa weil
// jemand die Datei umsortiert hat -, wird sie doch vollstaendig gelesen.
$dcChangelogFile = DC_ROOT . '/public/docs/changelog.json';
if (is_file($dcChangelogFile)) {
$dcVersion = null;
$dcHead = (string)file_get_contents($dcChangelogFile, false, null, 0, 4096);
if (preg_match('/"current"\s*:\s*"([^"]+)"/', $dcHead, $dcMatch) === 1) {
$dcVersion = $dcMatch[1];
} else {
$dcMeta = json_decode((string)file_get_contents($dcChangelogFile), true);
if (is_array($dcMeta) && is_string($dcMeta['current'] ?? null)) {
$dcVersion = $dcMeta['current'];
}
}
if (is_string($dcVersion) && $dcVersion !== '') {
if (!isset($dcConfig['app']) || !is_array($dcConfig['app'])) {
$dcConfig['app'] = [];
}
$dcConfig['app']['version'] = $dcVersion;
}
}
Deploymentcenter\Core\Config::load($dcConfig);
date_default_timezone_set((string)Deploymentcenter\Core\Config::get('app.timezone', 'UTC'));