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:
co-authored by
Claude Opus 5
parent
afcbb3c091
commit
3b56839a1e
@@ -28,11 +28,13 @@ Options -Indexes
|
|||||||
RewriteRule ^api/bugtracker/v1/report/?$ public/api/bugtracker/v1/report.php [L,QSA]
|
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/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/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/license/v1(?:/(.*))?$ public/api/license/v1/index.php [L,QSA]
|
||||||
RewriteRule ^api/watchdog/v1(?:/(.*))?$ public/api/watchdog/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/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/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/setup/v1(?:/(.*))?$ public/api/setup/v1/index.php [L,QSA]
|
||||||
RewriteRule ^api/supervisor/v1(?:/(.*))?$ public/api/supervisor/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/index.php [L,QSA]
|
||||||
RewriteRule ^docs/(.+)$ public/docs/$1 [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. WebUI
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ if (!function_exists('dc_env')) {
|
|||||||
return [
|
return [
|
||||||
'app' => [
|
'app' => [
|
||||||
'name' => 'Deploymentcenter',
|
'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',
|
'version' => '2.0.0',
|
||||||
'url' => dc_env('DC_APP_URL', 'https://dc.example.com'),
|
'url' => dc_env('DC_APP_URL', 'https://dc.example.com'),
|
||||||
'timezone' => 'Europe/Berlin',
|
'timezone' => 'Europe/Berlin',
|
||||||
|
|||||||
+15
-5
@@ -126,12 +126,16 @@ chmod +x ~/bin/dc-testkit
|
|||||||
```
|
```
|
||||||
|
|
||||||
> ⚠️ **Beides — dieser Download und `dc-testkit license` — setzt Deploymentcenter 2.6.1 voraus.**
|
> ⚠️ **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
|
> Ist die Fassung auf dem Server älter, fehlt der Endpunkt `/api/license/v1/dev-key`. Er fällt
|
||||||
> Endpunkt `/api/license/v1/dev-key` fehlt. Der fehlende Endpunkt fällt dabei **nicht** mit 404
|
> dabei **nicht** mit 404 auf: Die Lizenz-API antwortet bei unbekannten Pfadsegmenten mit ihrer
|
||||||
> auf: Die Lizenz-API antwortet bei unbekannten Pfadsegmenten mit ihrer Verfügbarkeitsprüfung, also
|
> Verfügbarkeitsprüfung, also mit HTTP 200. Das Testkit erkennt das und sagt es; wer von Hand
|
||||||
> mit HTTP 200. Das Testkit erkennt das und sagt es; wer von Hand `curl`t, sucht sonst lange.
|
> `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`
|
> `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**,
|
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
|
### 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
|
**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
|
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
|
Fehler im `update-agent`. Ihm fällt auf, dass diese Anleitung an einer Stelle lügt. Er hätte
|
||||||
|
|||||||
+48
-1
@@ -47,6 +47,34 @@ try {
|
|||||||
$logDir = DC_VAR . '/log';
|
$logDir = DC_VAR . '/log';
|
||||||
$checks['log_writable'] = ['ok' => is_dir($logDir) ? is_writable($logDir) : is_writable(DC_ROOT)];
|
$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 = [
|
$response = [
|
||||||
'healthy' => $healthy,
|
'healthy' => $healthy,
|
||||||
'app' => Config::get('app.name', 'Deploymentcenter'),
|
'app' => Config::get('app.name', 'Deploymentcenter'),
|
||||||
@@ -56,7 +84,26 @@ $response = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
// --- Detailinformationen nur fuer Authentifizierte ---
|
// --- 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 {
|
try {
|
||||||
$status = Migrator::status($db);
|
$status = Migrator::status($db);
|
||||||
$response['schema'] = [
|
$response['schema'] = [
|
||||||
|
|||||||
+42
-1
@@ -406,7 +406,7 @@ $spec = [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
'/api/bugtracker/v1/roadmap.php' => [
|
'/api/bugtracker/v1/roadmap' => [
|
||||||
'get' => [
|
'get' => [
|
||||||
'tags' => ['Bugtracker'],
|
'tags' => ['Bugtracker'],
|
||||||
'summary' => 'Roadmap lesen',
|
'summary' => 'Roadmap lesen',
|
||||||
@@ -1229,6 +1229,47 @@ $spec = [
|
|||||||
'responses' => ['201' => ['description' => 'Sub-Token erstellt'], '403' => $errorResponse],
|
'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,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
]);
|
||||||
@@ -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
|
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`.
|
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
|
## 2. Projekte finden
|
||||||
|
|||||||
@@ -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.",
|
"_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,
|
"schema": 1,
|
||||||
"current": "2.7.0",
|
"current": "2.7.1",
|
||||||
"versions": [
|
"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",
|
"version": "2.7.0",
|
||||||
"date": "2026-08-29",
|
"date": "2026-08-29",
|
||||||
|
|||||||
+85
-35
@@ -4018,7 +4018,7 @@ chmod +x update-agent
|
|||||||
|
|
||||||
<div class="card" style="padding:0.85rem 1.1rem;">
|
<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;">
|
<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>
|
<option value="all">🌐 Alle Projekte</option>
|
||||||
<?php foreach ($projects as $p): ?>
|
<?php foreach ($projects as $p): ?>
|
||||||
<option value="<?= e($p['slug']) ?>" <?= $rmProject === $p['slug'] ? 'selected' : '' ?>>
|
<option value="<?= e($p['slug']) ?>" <?= $rmProject === $p['slug'] ? 'selected' : '' ?>>
|
||||||
@@ -4027,7 +4027,7 @@ chmod +x update-agent
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</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
|
<?php
|
||||||
// Voreinstellung ist "aktiv" und nicht "alle": Eine Roadmap,
|
// Voreinstellung ist "aktiv" und nicht "alle": Eine Roadmap,
|
||||||
// die mit sechzig erledigten Eintraegen beginnt, beantwortet
|
// die mit sechzig erledigten Eintraegen beginnt, beantwortet
|
||||||
@@ -4046,7 +4046,7 @@ chmod +x update-agent
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</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>
|
<option value="all">🗂️ Alle Bereiche</option>
|
||||||
<?php foreach ($roadmapEpics as $rmE): ?>
|
<?php foreach ($roadmapEpics as $rmE): ?>
|
||||||
<option value="<?= e((string)$rmE['epic']) ?>" <?= $rmEpic === (string)$rmE['epic'] ? 'selected' : '' ?>>
|
<option value="<?= e((string)$rmE['epic']) ?>" <?= $rmEpic === (string)$rmE['epic'] ? 'selected' : '' ?>>
|
||||||
@@ -4055,7 +4055,7 @@ chmod +x update-agent
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</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>
|
<option value="all">🎯 Alle Zielfassungen</option>
|
||||||
<?php foreach ($roadmapBuilds as $rmB): ?>
|
<?php foreach ($roadmapBuilds as $rmB): ?>
|
||||||
<option value="<?= e((string)$rmB) ?>" <?= $rmBuild === (string)$rmB ? 'selected' : '' ?>><?= e((string)$rmB) ?></option>
|
<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.
|
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;">
|
<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>
|
<option value="all">🌐 Alle Projekte</option>
|
||||||
<?php foreach ($projects as $p): ?>
|
<?php foreach ($projects as $p): ?>
|
||||||
<option value="<?= e($p['slug']) ?>" <?= $btFilters['project_slug'] === $p['slug'] ? 'selected' : '' ?>>
|
<option value="<?= e($p['slug']) ?>" <?= $btFilters['project_slug'] === $p['slug'] ? 'selected' : '' ?>>
|
||||||
@@ -4650,7 +4650,7 @@ chmod +x update-agent
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</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
|
<?php
|
||||||
$envOptions = [
|
$envOptions = [
|
||||||
'all' => '🌍 Alle Umgebungen',
|
'all' => '🌍 Alle Umgebungen',
|
||||||
@@ -4664,7 +4664,7 @@ chmod +x update-agent
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</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
|
<?php
|
||||||
$typeOptions = [
|
$typeOptions = [
|
||||||
'all' => '📂 Alle Typen',
|
'all' => '📂 Alle Typen',
|
||||||
@@ -4676,7 +4676,7 @@ chmod +x update-agent
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</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
|
<?php
|
||||||
$sevOptions = [
|
$sevOptions = [
|
||||||
'all' => '🔥 Alle Schweregrade',
|
'all' => '🔥 Alle Schweregrade',
|
||||||
@@ -4692,7 +4692,7 @@ chmod +x update-agent
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</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
|
<?php
|
||||||
$statusOptions = [
|
$statusOptions = [
|
||||||
'all' => '📌 Alle Status',
|
'all' => '📌 Alle Status',
|
||||||
@@ -4720,7 +4720,7 @@ chmod +x update-agent
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</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
|
<?php
|
||||||
$orderOptions = [
|
$orderOptions = [
|
||||||
'newest' => '🕒 Neueste zuerst',
|
'newest' => '🕒 Neueste zuerst',
|
||||||
@@ -5026,7 +5026,7 @@ chmod +x update-agent
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<form method="GET" action="index.php" style="display:flex; flex-wrap:wrap; gap:0.4rem; align-items:center;">
|
<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>
|
<option value="all">🌐 Alle Projekte</option>
|
||||||
<?php foreach ($projects as $p): ?>
|
<?php foreach ($projects as $p): ?>
|
||||||
<option value="<?= e($p['slug']) ?>" <?= $errFilters['project_slug'] === $p['slug'] ? 'selected' : '' ?>>
|
<option value="<?= e($p['slug']) ?>" <?= $errFilters['project_slug'] === $p['slug'] ? 'selected' : '' ?>>
|
||||||
@@ -5035,7 +5035,7 @@ chmod +x update-agent
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</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
|
<?php
|
||||||
$levelOptions = [
|
$levelOptions = [
|
||||||
'all' => '🎚️ Alle Klassen',
|
'all' => '🎚️ Alle Klassen',
|
||||||
@@ -5048,7 +5048,7 @@ chmod +x update-agent
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</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
|
<?php
|
||||||
$errEnvOptions = [
|
$errEnvOptions = [
|
||||||
'all' => '🌍 Alle Umgebungen',
|
'all' => '🌍 Alle Umgebungen',
|
||||||
@@ -5062,7 +5062,7 @@ chmod +x update-agent
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</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
|
<?php
|
||||||
$showOptions = [
|
$showOptions = [
|
||||||
'active' => '🔔 Nur relevante',
|
'active' => '🔔 Nur relevante',
|
||||||
@@ -5074,7 +5074,7 @@ chmod +x update-agent
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</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
|
<?php
|
||||||
$hourOptions = [
|
$hourOptions = [
|
||||||
'0' => '🕒 Gesamter Zeitraum',
|
'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 - man legte einen
|
||||||
* Ohne das landete man stets auf dem Dashboard des Moduls - man legte
|
* Monitor an und wurde aus dem Formular geworfen, und jede
|
||||||
* einen Monitor an und wurde aus dem Formular geworfen.
|
* Filteraenderung im Bugtracker warf einen dorthin zurueck.
|
||||||
*
|
*
|
||||||
* Als zentraler Zuhoerer umgesetzt, damit auch kuenftige Formulare das
|
* 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) => {
|
function dcCurrentView() {
|
||||||
const form = event.target;
|
|
||||||
if (!(form instanceof HTMLFormElement)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ((form.method || '').toLowerCase() !== 'post') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (form.querySelector('input[name="_return"]')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeSub = document.querySelector('.subtab-content.active');
|
const activeSub = document.querySelector('.subtab-content.active');
|
||||||
const activeTab = document.querySelector('.main-content > .tab-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) {
|
if (!target) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const field = document.createElement('input');
|
const field = document.createElement('input');
|
||||||
field.type = 'hidden';
|
field.type = 'hidden';
|
||||||
field.name = '_return';
|
field.name = name;
|
||||||
field.value = target;
|
field.value = target;
|
||||||
form.appendChild(field);
|
form.appendChild(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('submit', (event) => {
|
||||||
|
const form = event.target;
|
||||||
|
if (!(form instanceof HTMLFormElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dcRememberView(form);
|
||||||
}, true);
|
}, 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
|
// Sidebar Toggle Handler
|
||||||
function toggleSidebar() {
|
function toggleSidebar() {
|
||||||
const sidebar = document.getElementById('sidebar');
|
const sidebar = document.getElementById('sidebar');
|
||||||
@@ -6926,7 +6965,18 @@ SYSTEM
|
|||||||
* Modulnamen. Der Rueckweg landete deshalb immer auf dem Dashboard.
|
* Modulnamen. Der Rueckweg landete deshalb immer auf dem Dashboard.
|
||||||
*/
|
*/
|
||||||
function restoreViewFromHash() {
|
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) {
|
if (!hash) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ DC_PROJECT="${DC_PROJECT:-}"
|
|||||||
DC_TOKEN="${DC_TOKEN:-}"
|
DC_TOKEN="${DC_TOKEN:-}"
|
||||||
DC_CHANNEL="${DC_CHANNEL:-prod}"
|
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
|
# Die Maschinenkennung entscheidet, welchen DEV-Schluessel der Server
|
||||||
# heraussucht. Sie muss ueber Laeufe hinweg gleich bleiben - sonst sammelt sich
|
# heraussucht. Sie muss ueber Laeufe hinweg gleich bleiben - sonst sammelt sich
|
||||||
# je Lauf ein weiterer Schluessel an, bis die Grenze je Projekt greift.
|
# 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
|
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?
|
# doctor - laeuft hier ueberhaupt etwas?
|
||||||
#
|
#
|
||||||
@@ -263,9 +316,34 @@ cmd_doctor() {
|
|||||||
|
|
||||||
step "Deploymentcenter unter $DC_URL"
|
step "Deploymentcenter unter $DC_URL"
|
||||||
if api GET /api/health; then
|
if api GET /api/health; then
|
||||||
local healthy
|
local healthy server_version
|
||||||
healthy=$(printf '%s' "$API_BODY" | json_get '.healthy' || echo '?')
|
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
|
else
|
||||||
warn "nicht erreichbar: $(api_error)"
|
warn "nicht erreichbar: $(api_error)"
|
||||||
failed=1
|
failed=1
|
||||||
@@ -275,8 +353,50 @@ cmd_doctor() {
|
|||||||
if [ -z "$DC_TOKEN" ]; then
|
if [ -z "$DC_TOKEN" ]; then
|
||||||
warn "kein Token gesetzt (DC_TOKEN)"
|
warn "kein Token gesetzt (DC_TOKEN)"
|
||||||
failed=1
|
failed=1
|
||||||
elif api GET /api/bugtracker/v1/projects; then
|
elif api GET /api/tokens/v1/whoami; then
|
||||||
ok "gueltig - der Bugtracker antwortet"
|
# 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
|
else
|
||||||
warn "Token wird nicht angenommen: $(api_error)"
|
warn "Token wird nicht angenommen: $(api_error)"
|
||||||
failed=1
|
failed=1
|
||||||
|
|||||||
+31
-1
@@ -17,6 +17,7 @@ gibt gruenes Licht fuer Code, den er nie gesehen hat.
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import ftplib
|
import ftplib
|
||||||
|
import io
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -110,6 +111,35 @@ def should_ignore(rel_path):
|
|||||||
return True
|
return True
|
||||||
return False
|
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):
|
def collect_changes(cache):
|
||||||
"""Ermittelt alle Dateien, die sich seit dem letzten Deployment geaendert haben."""
|
"""Ermittelt alle Dateien, die sich seit dem letzten Deployment geaendert haben."""
|
||||||
files_to_upload = []
|
files_to_upload = []
|
||||||
@@ -216,7 +246,7 @@ def deploy():
|
|||||||
|
|
||||||
ftp.cwd('/')
|
ftp.cwd('/')
|
||||||
print(f"Uploading {rel_file} ...")
|
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)
|
ftp.storbinary(f"STOR {remote_file_path}", f)
|
||||||
|
|
||||||
new_cache[rel_file] = file_hash
|
new_cache[rel_file] = file_hash
|
||||||
|
|||||||
+15
-2
@@ -203,6 +203,19 @@ def should_ignore(rel_path):
|
|||||||
return False
|
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):
|
def collect(cache, generated, full=False):
|
||||||
"""(remote_pfad, bytes, hash) fuer alles, was uebertragen werden muss."""
|
"""(remote_pfad, bytes, hash) fuer alles, was uebertragen werden muss."""
|
||||||
items = []
|
items = []
|
||||||
@@ -214,10 +227,10 @@ def collect(cache, generated, full=False):
|
|||||||
rel = ('%s/%s' % (rel_dir, f)).lstrip('/')
|
rel = ('%s/%s' % (rel_dir, f)).lstrip('/')
|
||||||
if should_ignore(rel):
|
if should_ignore(rel):
|
||||||
continue
|
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):
|
for rel in sorted(generated):
|
||||||
items.append((rel, generated[rel]))
|
items.append((rel, normalize_for_upload(rel, generated[rel])))
|
||||||
|
|
||||||
out = []
|
out = []
|
||||||
for rel, data in items:
|
for rel, data in items:
|
||||||
|
|||||||
@@ -50,6 +50,43 @@ if (!is_array($dcConfig)) {
|
|||||||
exit(1);
|
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);
|
Deploymentcenter\Core\Config::load($dcConfig);
|
||||||
|
|
||||||
date_default_timezone_set((string)Deploymentcenter\Core\Config::get('app.timezone', 'UTC'));
|
date_default_timezone_set((string)Deploymentcenter\Core\Config::get('app.timezone', 'UTC'));
|
||||||
|
|||||||
Reference in New Issue
Block a user