Sammelcommit fuer den ueber mehrere Sitzungen gewachsenen, bisher nicht
eingecheckten Stand. Getrennte Commits waeren ehrlicher gewesen, sind es
nachtraeglich aber nicht mehr: BugRepo, changelog.json und die Doku tragen
Aenderungen aus allen Straengen gleichzeitig, und keine Zwischenstufe waere
lauffaehig.
ROADMAP IM DEPLOYMENTCENTER (2.7.0)
Planungsstand, Reihenfolge und Zustaendigkeit lagen in handgepflegten
Markdown-Dateien neben einem Bugtracker, der genau diese drei Dinge bereits
fuehrt. Zwei Pfleger fuer dasselbe Feld - Drift war eine Frage der Zeit.
Ab jetzt: Zustand im DC, Begruendung in der Datei. Kein Feld hat zwei Pfleger.
- Migration 016: epic, roadmap_rank, target_build, defer_trigger,
doc_anchor, Status "deferred", Tabelle roadmap_epics.
KEINE Roadmap-Tabelle - ein Eintrag ist ein bugtracker_item.
- Zugehoerigkeit ist ein Feld: Ein Item steht auf der Roadmap, wenn es ein
epic hat. Ein zweites Kennzeichen daneben koennte auseinanderlaufen.
- Kennung ist die Item-ID. Handvergebene Kuerzel ("A12") erzeugen die
schlimmste Fehlerart: Beide Seiten stimmig, Verschiedenes gemeint, weil
die Kennung nach einer Loeschung neu vergeben wurde.
- Zurueckstellen verlangt defer_trigger. Ohne benannten Anlass ist ein
Vorhaben nicht zurueckgestellt, sondern vergessen. Die Regel sitzt im
Repository, damit sie auf allen drei Schreibwegen gleich greift.
- Modules/Bugtracker/Roadmap.php, /api/bugtracker/v1/roadmap.php,
WebUI-Modul mit Filtern, cli/roadmap.php --write/--check,
dc_roadmap und dc_roadmap_set im MCP-Adapter.
- cli/roadmap.php --write ERZEUGT den Zustandsteil der Markdown-Datei;
--check prueft nur die Prosa. Was erzeugt wird, kann nicht driften.
- docs/ROADMAP_MIGRATION.md: Uebernahme fuer die anderen Projekte.
TESTINSTANZ UND TESTTOR
- dc-dev.mhdf.de: zweite Installation, eigene DB, eigene Schluessel,
Basic-Auth davor, Rocket.Chat aus.
- 124 Testfaelle in tests/e2e gegen API, Rechte, Migrationen, Sperre.
- scripts/deploy.py bringt vor jedem Upload erst den anstehenden Stand auf
die Testinstanz und laesst dann den Testlauf laufen. Erst bei
Rueckgabewert 0 geht etwas auf die Produktion (--ohne-test als Notausgang).
Andersherum prueft der Lauf aelteren Code als den, der gleich hochgeht.
- tests/ ist aus dem Produktiv-Deploy ausgeschlossen; testctl.php kann
nicht versehentlich auf dc.mhdf.de landen.
- Ein Lauf mit mehr uebersprungenen als geprueften Faellen endet mit
Rueckgabewert 2 statt 0. Nichts ist kaputt, geprueft wurde es aber auch
nicht - der Unterschied darf nicht am Rueckgabewert verschwinden.
SUPERVISOR, PROJEKTLANDSCHAFT, NACHPRUEFUNG (2.6.0/2.6.1)
- Migration 013: projektuebergreifend melden (dev_agent, evidence,
triage_external, affects).
- Migration 014: supervisor_findings, Katalog, fuenf serverseitige
Detektoren im Tick.
- Migration 015: Nachpruefung als Arbeitsschlange statt Benachrichtigung.
- DEV-Lizenzschluessel zur Selbstbedienung, dc-testkit fuer die Test-VM.
- MCP-Adapter: 15 Werkzeuge, ohne Fremdabhaengigkeiten, mit Abgrenzung
von Fremddaten gegen Prompt-Injektion.
BEHOBEN
- observe und status im Supervisor legten den Zustand des Befundes als
"status" in die Antwort. Http::ok() merged ['status' => 'success']
darunter, also gewann der Befundzustand - eine angenommene Meldung kam
als status="open" an. Heisst jetzt finding_status. War Item #59.
Migrationen 013-016 sind auf dem Produktivserver noch NICHT ausgefuehrt.
Ohne sie bricht das WebUI, weil die Roadmap-Ansicht bei jedem Seitenaufruf
gegen bugtracker_items.epic abfragt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
191 lines
7.6 KiB
PHP
191 lines
7.6 KiB
PHP
<?php
|
|
|
|
/**
|
|
* POST /api/bugtracker/v1/report
|
|
*
|
|
* Nimmt Bugs, Feature Requests und Ideen von Agenten und Client-Anwendungen
|
|
* entgegen.
|
|
*
|
|
* SICHERHEITSAENDERUNG: Dieser Endpunkt verlangt jetzt zwingend ein Token mit
|
|
* dem Recht "bugtracker:report". Zuvor wurde ein Token nur geprueft, wenn eines
|
|
* mitgeschickt wurde - damit konnte jeder im Internet Eintraege anlegen
|
|
* (und ueber die Detailansicht Skripte in die Admin-Session einschleusen).
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../../../../src/bootstrap.php';
|
|
|
|
use Deploymentcenter\Core\ApiAuth;
|
|
use Deploymentcenter\Core\Config;
|
|
use Deploymentcenter\Core\Db;
|
|
use Deploymentcenter\Core\Http;
|
|
use Deploymentcenter\Modules\Bugtracker\BugRepo;
|
|
use Deploymentcenter\Modules\License\RateLimiter;
|
|
|
|
/**
|
|
* Mindestlaenge der Begruendung bei einer Meldung gegen ein fremdes Projekt.
|
|
*
|
|
* Kurz genug, dass ein Satz reicht - lang genug, dass "Bug" oder "kaputt"
|
|
* nicht durchgeht.
|
|
*/
|
|
const MIN_EVIDENCE_LENGTH = 40;
|
|
|
|
Http::beginJson(['POST', 'OPTIONS'], true);
|
|
|
|
if (Http::method() !== 'POST') {
|
|
Http::fail(405, 'method_not_allowed', 'Dieser Endpunkt erwartet POST.');
|
|
}
|
|
|
|
$db = Db::init();
|
|
|
|
// Drosselung, damit ein Agent in einer Fehlerschleife den Tracker nicht flutet.
|
|
$limiter = new RateLimiter($db, (int)Config::get('bugtracker.report_rate', 60), 60, 'bt_report');
|
|
if (!$limiter->check(Http::clientIp())) {
|
|
Http::fail(429, 'rate_limited', 'Zu viele Reports. Bitte Sendefrequenz reduzieren.');
|
|
}
|
|
|
|
$data = Http::body();
|
|
if ($data === []) {
|
|
Http::fail(400, 'empty_body', 'Der Request-Body ist leer.');
|
|
}
|
|
|
|
$environment = is_string($data['environment'] ?? null) ? $data['environment'] : null;
|
|
|
|
// Session ist hier bewusst nicht erlaubt: dieser Endpunkt ist die
|
|
// Maschinenschnittstelle. Das WebUI legt Items ueber index.php an.
|
|
$context = ApiAuth::requireScope($db, 'bugtracker:report', $environment, false);
|
|
|
|
$projectSlug = is_string($data['project_slug'] ?? null) ? trim($data['project_slug']) : null;
|
|
|
|
// Werkbank-Token duerfen in ein fremdes Projekt melden - ein Agent, der das SDK
|
|
// in Projekt X integriert und dabei einen Fehler im Deploymentcenter findet,
|
|
// muss ihn dort ablegen koennen. Laufzeit-Token duerfen das nicht: sie stecken
|
|
// im ausgelieferten Client und sind extrahierbar.
|
|
ApiAuth::enforceProject($context, $projectSlug, true);
|
|
|
|
$homeProject = ApiAuth::homeProject($context);
|
|
|
|
// Ein Laufzeit-Token schreibt immer in sein eigenes Projekt.
|
|
if ($homeProject !== null && !ApiAuth::isWorkbench($context)) {
|
|
$data['project_slug'] = $homeProject;
|
|
}
|
|
|
|
// --- Fremdmeldung? ----------------------------------------------------------
|
|
$target = is_string($data['project_slug'] ?? null) ? trim($data['project_slug']) : null;
|
|
$isForeign = $homeProject !== null
|
|
&& $target !== null && $target !== ''
|
|
&& $target !== $homeProject;
|
|
|
|
// Die Herkunft wird aus der Token-Bindung abgeleitet und niemals aus dem Body
|
|
// uebernommen. Nur so ist sie bezeugt statt behauptet - und nur dann traegt das
|
|
// Signal "drei Projekte melden dasselbe" ueberhaupt etwas.
|
|
$rawEvidence = $data['evidence'] ?? null;
|
|
unset($data['reported_from'], $data['evidence']);
|
|
|
|
if ($isForeign) {
|
|
$evidence = is_string($rawEvidence) ? trim($rawEvidence) : null;
|
|
|
|
// Begruendungspflicht. Ein Agent in Projekt X kann kaum unterscheiden, ob
|
|
// das fremde Projekt kaputt ist oder ob X es falsch benutzt; die
|
|
// Voreinstellung eines hilfsbereiten Modells ist im Zweifel "melde es".
|
|
// Etwas ausformulieren zu muessen kostet einen Reflex fast immer den Kopf.
|
|
if ($evidence === null || mb_strlen($evidence) < MIN_EVIDENCE_LENGTH) {
|
|
Http::fail(
|
|
400,
|
|
'missing_evidence',
|
|
sprintf(
|
|
'Eine Meldung gegen das fremde Projekt "%s" verlangt das Feld "evidence" '
|
|
. '(mindestens %d Zeichen): was wurde beobachtet, und warum liegt die '
|
|
. 'Ursache dort und nicht in "%s"?',
|
|
$target,
|
|
MIN_EVIDENCE_LENGTH,
|
|
$homeProject
|
|
),
|
|
null,
|
|
['home_project' => $homeProject, 'target_project' => $target]
|
|
);
|
|
}
|
|
|
|
$data['reported_from'] = $homeProject;
|
|
$data['evidence'] = $evidence;
|
|
}
|
|
|
|
// Der Absender wird aus dem Token abgeleitet und kann nicht frei gewaehlt
|
|
// werden - sonst koennte sich ein Agent als ein anderer ausgeben.
|
|
$data['created_by'] = $context['actor'];
|
|
|
|
// Idempotenz-Schluessel darf auch als Header kommen.
|
|
if (empty($data['client_ref'])) {
|
|
$headerRef = Http::header('idempotency-key');
|
|
if ($headerRef !== null) {
|
|
$data['client_ref'] = $headerRef;
|
|
}
|
|
}
|
|
|
|
$repo = new BugRepo($db);
|
|
$result = $repo->reportItem($data);
|
|
|
|
// Bekannt und in einer spaeteren Fassung behoben. Diese Antwort geht jeder
|
|
// anderen vor: Sie beendet die Frage, statt sie einzusortieren. Der Melder
|
|
// prueft eine Fassung, in der der Fix noch nicht steckt - er soll aufhoeren zu
|
|
// suchen und aktualisieren, nicht ein weiteres Item anlegen.
|
|
// Der Fix hat nicht getragen. Kein neues Item - das bestehende ist zurueck in
|
|
// der Arbeitsschlange, und der Beleg haengt daran.
|
|
if (!empty($result['fix_insufficient'])) {
|
|
$message = sprintf(
|
|
'Der Fix traegt nicht. Item #%d steht wieder auf "fix_insufficient" und liegt in der '
|
|
. 'Arbeitsschlange - kein neues Item angelegt, der Beleg haengt am bestehenden.',
|
|
$result['id']
|
|
);
|
|
} elseif (isset($result['known_fixed_in'])) {
|
|
$message = sprintf(
|
|
'Bekannt und behoben in Build "%s" - du hast "%s" geprueft. Kein neues Item angelegt. '
|
|
. 'Aktualisiere und pruefe erneut; tritt es dann immer noch auf, ist es eine Regression '
|
|
. 'und gehoert gemeldet.',
|
|
$result['known_fixed_in'],
|
|
$result['reported_build']
|
|
);
|
|
} elseif ($isForeign) {
|
|
$message = $result['is_new']
|
|
? sprintf(
|
|
'Im Eingang von "%s" hinterlegt. Wird dort gesichtet und zaehlt erst nach '
|
|
. 'Bestaetigung in die Statistik.',
|
|
$target
|
|
)
|
|
: sprintf('Bereits bekannt - "%s" ist als betroffen vermerkt.', $homeProject);
|
|
} else {
|
|
$message = $result['idempotent_hit']
|
|
? 'Bereits erfasst (identische client_ref) - kein Duplikat angelegt.'
|
|
: ($result['is_new']
|
|
? ($result['type'] === 'bug' ? 'Bug erfasst.' : 'Feature Request erfasst.')
|
|
: 'Wiederkehrendes Vorkommnis - Zaehler erhoeht.');
|
|
}
|
|
|
|
Http::ok([
|
|
'item_id' => $result['id'],
|
|
'is_new' => $result['is_new'],
|
|
'idempotent_hit' => $result['idempotent_hit'],
|
|
'occurrence_count' => $result['occurrence_count'],
|
|
'dedup_key' => $result['dedup_key'],
|
|
'error_hash' => $result['error_hash'],
|
|
'type' => $result['type'],
|
|
'item_status' => $result['status'],
|
|
'environment' => $result['environment'],
|
|
'push_id' => $result['push_id'] ?? null,
|
|
'regression_of' => $result['regression_of'] ?? null,
|
|
// Gesetzt, wenn der Fehler bereits in einer spaeteren Fassung behoben ist
|
|
// als der, die gemeldet wurde. Dann wurde NICHTS angelegt und nichts
|
|
// hochgezaehlt - die Antwort ist eine Auskunft.
|
|
'known_fixed_in' => $result['known_fixed_in'] ?? null,
|
|
'reported_build' => $result['reported_build'] ?? null,
|
|
// Gesetzt, wenn der gemeldete Fehler ein Item betrifft, dessen Fix noch
|
|
// nicht bestaetigt war. Dann wurde kein neues Item angelegt - das
|
|
// bestehende wechselte auf fix_insufficient.
|
|
'fix_insufficient' => !empty($result['fix_insufficient']),
|
|
'reported_from' => $result['reported_from'] ?? null,
|
|
'affects' => $result['affects'] ?? [],
|
|
'url' => Http::baseUrl() . '/index.php#tab-bugtracker',
|
|
'message' => $message,
|
|
], $result['is_new'] ? 201 : 200);
|