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
+48
-1
@@ -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
@@ -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,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user