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

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

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

Im Einzelnen:

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Deploymentcenter Bot
2026-08-30 21:06:33 +02:00
co-authored by Claude Opus 5
parent afcbb3c091
commit 3b56839a1e
13 changed files with 566 additions and 50 deletions
+85 -35
View File
@@ -4018,7 +4018,7 @@ chmod +x update-agent
<div class="card" style="padding:0.85rem 1.1rem;">
<form method="GET" action="index.php" style="display:flex; flex-wrap:wrap; gap:0.45rem; align-items:center;">
<select name="rm_project" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="rm_project" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<option value="all">🌐 Alle Projekte</option>
<?php foreach ($projects as $p): ?>
<option value="<?= e($p['slug']) ?>" <?= $rmProject === $p['slug'] ? 'selected' : '' ?>>
@@ -4027,7 +4027,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="rm_lage" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="rm_lage" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<?php
// Voreinstellung ist "aktiv" und nicht "alle": Eine Roadmap,
// die mit sechzig erledigten Eintraegen beginnt, beantwortet
@@ -4046,7 +4046,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="rm_epic" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="rm_epic" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<option value="all">🗂️ Alle Bereiche</option>
<?php foreach ($roadmapEpics as $rmE): ?>
<option value="<?= e((string)$rmE['epic']) ?>" <?= $rmEpic === (string)$rmE['epic'] ? 'selected' : '' ?>>
@@ -4055,7 +4055,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="rm_build" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="rm_build" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<option value="all">🎯 Alle Zielfassungen</option>
<?php foreach ($roadmapBuilds as $rmB): ?>
<option value="<?= e((string)$rmB) ?>" <?= $rmBuild === (string)$rmB ? 'selected' : '' ?>><?= e((string)$rmB) ?></option>
@@ -4641,7 +4641,7 @@ chmod +x update-agent
und ging bei jedem Auto-Refresh verloren.
-->
<form method="GET" action="index.php" style="display:flex; flex-wrap:wrap; gap:0.4rem; align-items:center;">
<select name="bt_project" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="bt_project" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<option value="all">🌐 Alle Projekte</option>
<?php foreach ($projects as $p): ?>
<option value="<?= e($p['slug']) ?>" <?= $btFilters['project_slug'] === $p['slug'] ? 'selected' : '' ?>>
@@ -4650,7 +4650,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="bt_env" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="bt_env" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<?php
$envOptions = [
'all' => '🌍 Alle Umgebungen',
@@ -4664,7 +4664,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="bt_type" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="bt_type" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<?php
$typeOptions = [
'all' => '📂 Alle Typen',
@@ -4676,7 +4676,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="bt_severity" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="bt_severity" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<?php
$sevOptions = [
'all' => '🔥 Alle Schweregrade',
@@ -4692,7 +4692,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="bt_status" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="bt_status" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<?php
$statusOptions = [
'all' => '📌 Alle Status',
@@ -4720,7 +4720,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="bt_order" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="bt_order" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<?php
$orderOptions = [
'newest' => '🕒 Neueste zuerst',
@@ -5026,7 +5026,7 @@ chmod +x update-agent
</h2>
<form method="GET" action="index.php" style="display:flex; flex-wrap:wrap; gap:0.4rem; align-items:center;">
<select name="err_project" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="err_project" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<option value="all">🌐 Alle Projekte</option>
<?php foreach ($projects as $p): ?>
<option value="<?= e($p['slug']) ?>" <?= $errFilters['project_slug'] === $p['slug'] ? 'selected' : '' ?>>
@@ -5035,7 +5035,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="err_level" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="err_level" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<?php
$levelOptions = [
'all' => '🎚️ Alle Klassen',
@@ -5048,7 +5048,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="err_env" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="err_env" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<?php
$errEnvOptions = [
'all' => '🌍 Alle Umgebungen',
@@ -5062,7 +5062,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="err_show" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="err_show" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<?php
$showOptions = [
'active' => '🔔 Nur relevante',
@@ -5074,7 +5074,7 @@ chmod +x update-agent
<?php endforeach; ?>
</select>
<select name="err_hours" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="this.form.submit()">
<select name="err_hours" class="form-input" style="width:auto; padding:0.3rem 0.6rem; font-size:0.8rem;" onchange="dcSubmit(this.form)">
<?php
$hourOptions = [
'0' => '🕒 Gesamter Zeitraum',
@@ -6246,42 +6246,81 @@ SYSTEM
}
/**
* Merkt sich beim Absenden, welche Unteransicht offen war.
* Ansichtsrettung: Merkt sich beim Absenden, welche Unteransicht offen
* war, und stellt sie nach dem Neuladen wieder her.
*
* Der Server haengt diesen Wert nach dem Speichern wieder an die URL.
* Ohne das landete man stets auf dem Dashboard des Moduls - man legte
* einen Monitor an und wurde aus dem Formular geworfen.
* Ohne das landete man stets auf dem Dashboard - man legte einen
* Monitor an und wurde aus dem Formular geworfen, und jede
* Filteraenderung im Bugtracker warf einen dorthin zurueck.
*
* Als zentraler Zuhoerer umgesetzt, damit auch kuenftige Formulare das
* Verhalten automatisch erben.
* Verhalten automatisch erben. Die drei Funktionen darunter gehoeren
* zusammen: dcCurrentView() liest die Ansicht ab, dcRememberView()
* haengt sie an das Formular, dcSubmit() sorgt dafuer, dass beim
* Absenden aus einem onchange ueberhaupt ein submit-Ereignis entsteht.
*/
document.addEventListener('submit', (event) => {
const form = event.target;
if (!(form instanceof HTMLFormElement)) {
return;
}
if ((form.method || '').toLowerCase() !== 'post') {
return;
}
if (form.querySelector('input[name="_return"]')) {
return;
}
function dcCurrentView() {
const activeSub = document.querySelector('.subtab-content.active');
const activeTab = document.querySelector('.main-content > .tab-content.active');
const target = activeSub ? activeSub.id : (activeTab ? activeTab.id : '');
return activeSub ? activeSub.id : (activeTab ? activeTab.id : '');
}
/**
* Haengt die gerade offene Ansicht an ein Formular.
*
* POST: als _return - dc_redirect() macht daraus den Anker der
* Weiterleitung.
* GET: als view - ein Anker ueberlebt die Serialisierung eines
* GET-Formulars nicht, der Query-String schon. Genau daran
* scheiterten die Filter: nach jeder Aenderung stand die Seite
* wieder auf dem Dashboard, weil die neue URL keinen Anker
* mehr trug.
*/
function dcRememberView(form) {
const isPost = (form.method || 'get').toLowerCase() === 'post';
const name = isPost ? '_return' : 'view';
if (form.querySelector('input[name="' + name + '"]')) {
return;
}
const target = dcCurrentView();
if (!target) {
return;
}
const field = document.createElement('input');
field.type = 'hidden';
field.name = '_return';
field.name = name;
field.value = target;
form.appendChild(field);
}
document.addEventListener('submit', (event) => {
const form = event.target;
if (!(form instanceof HTMLFormElement)) {
return;
}
dcRememberView(form);
}, true);
/**
* Absenden aus einem onchange heraus.
*
* form.submit() loest KEIN submit-Ereignis aus - der Zuhoerer oben
* lief deshalb bei den Filtern nie, obwohl es ihn gab.
* requestSubmit() tut es. Wo es fehlt, wird die Ansicht vorher von
* Hand angehaengt.
*/
function dcSubmit(form) {
if (typeof form.requestSubmit === 'function') {
form.requestSubmit();
return;
}
dcRememberView(form);
form.submit();
}
// Sidebar Toggle Handler
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
@@ -6926,7 +6965,18 @@ SYSTEM
* Modulnamen. Der Rueckweg landete deshalb immer auf dem Dashboard.
*/
function restoreViewFromHash() {
const hash = location.hash.replace('#', '');
let hash = location.hash.replace('#', '');
// Ohne Anker: der Wert aus dem Query-String, den dcRememberView()
// an GET-Formulare haengt. Nur eigene Ansichtskennungen zulassen -
// der Wert steuert, was angezeigt wird.
if (!hash) {
const fromQuery = new URLSearchParams(location.search).get('view');
if (fromQuery && /^(?:tab|sub)-[a-z0-9-]{1,60}$/.test(fromQuery)) {
hash = fromQuery;
}
}
if (!hash) {
return;
}