Files
Deploymentcenter BotandClaude Opus 5 a8b9f6f7c9 feat(docs): Changelog mit "was ist seit meiner Fassung neu"
Bisher musste ein Agent, der eine Anbindung aktualisiert, die gesamte Historie
lesen - oder er las gar nichts und uebersah eine brechende Aenderung. Beides
schlecht.

- public/docs/changelog.json ist die einzige Quelle. Je Fassung eine
  Zusammenfassung, je Aenderung Bereich, ein "breaking"-Kennzeichen und vor
  allem ein Feld "action" mit dem, was konkret zu tun ist. Steht dort null,
  ist nichts zu tun - das ist die haeufigste und nuetzlichste Antwort.
- GET /api/updateservice/v1/changelog?since=2.2.0 liefert nur die neueren
  Fassungen, dazu die Anzahl der Punkte mit Handlungsbedarf und der
  brechenden Aenderungen. count:0 heisst "du bist auf Stand" - dann muss gar
  nichts gelesen werden. Optional nach Bereich filterbar (?area=packager).
- /docs/changelog.php rendert dieselbe Datei fuer Menschen, mit Eingabefeld
  fuer die eigene Fassung. Bewusst dieselbe Quelle: zwei Fassungen zu pflegen
  hiesse, sie auseinanderlaufen zu lassen.
- DeploymentcenterSdk.Version im SDK ist der Bezugspunkt. Damit muss die
  Fassung nicht abgetippt werden.
- AGENT_PROMPT_TEMPLATE.md verpflichtet dazu, sie in der AGENTS.md des
  Projekts festzuhalten und vor jeder Aenderung an der Anbindung den
  Unterschied abzufragen. Auch in der Kurzfassung fuer knappe Prompt-Budgets.

Die Historie ist rueckwirkend bis 2.0.0 gefuellt: 6 Fassungen, 25 Punkte mit
Handlungsbedarf, 13 brechende Aenderungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 16:45:50 +02:00

213 lines
7.9 KiB
PHP

<?php
declare(strict_types=1);
/**
* Lesbare Ansicht des Changelogs.
*
* Quelle ist changelog.json - dieselbe Datei, die
* GET /api/updateservice/v1/changelog ausliefert. Zwei Fassungen zu pflegen
* hiesse, sie frueher oder spaeter auseinanderlaufen zu lassen.
*
* /docs/changelog.php alles
* /docs/changelog.php?since=2.2.0 nur was seitdem kam
* /docs/changelog.php?raw die rohe JSON-Datei
*/
$path = __DIR__ . '/changelog.json';
$raw = is_file($path) ? (string)file_get_contents($path) : '';
$raw = preg_replace('/^\xEF\xBB\xBF/', '', $raw) ?? $raw;
if (isset($_GET['raw'])) {
header('Content-Type: application/json; charset=utf-8');
echo $raw;
exit;
}
$decoded = json_decode($raw, true);
// Auch bei kaputter Datei ein Array - sonst greift die Ausgabe unten auf null zu.
$data = is_array($decoded) ? $decoded : [];
$versions = isset($data['versions']) && is_array($data['versions'])
? $data['versions']
: [];
$since = isset($_GET['since']) ? trim((string)$_GET['since']) : '';
/** Semantischer Vergleich - dieselbe Ordnung wie serverseitig. */
function cl_core(string $v): array
{
$v = ltrim(trim($v), 'vV');
$plus = strpos($v, '+');
if ($plus !== false) { $v = substr($v, 0, $plus); }
$dash = strpos($v, '-');
if ($dash !== false) { $v = substr($v, 0, $dash); }
$out = [];
foreach (explode('.', $v) as $part) {
$out[] = (int)preg_replace('/\D/', '', $part);
}
return $out;
}
function cl_newer(string $candidate, string $than): bool
{
$a = cl_core($candidate);
$b = cl_core($than);
for ($i = 0; $i < max(count($a), count($b)); $i++) {
$x = $a[$i] ?? 0;
$y = $b[$i] ?? 0;
if ($x !== $y) { return $x > $y; }
}
return false;
}
if ($since !== '') {
$versions = array_values(array_filter(
$versions,
static fn(array $v): bool => cl_newer((string)($v['version'] ?? '0'), $since)
));
}
$actionItems = 0;
$breaking = 0;
foreach ($versions as $v) {
foreach ($v['changes'] ?? [] as $c) {
if (!empty($c['action'])) { $actionItems++; }
if (!empty($c['breaking'])) { $breaking++; }
}
}
function e(?string $s): string
{
return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
}
$areaLabels = [
'server' => 'Server',
'sdk' => 'SDK',
'agent' => 'Update-Agent',
'packager' => 'Packager',
'tooling' => 'Werkzeuge',
'docs' => 'Dokumentation',
];
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Deploymentcenter — Änderungen</title>
<style>
:root {
--bg: #0b0f19; --card: #141b2d; --accent: #5b9dff;
--text: #e2e8f0; --muted: #94a3b8; --border: #1f2937;
--warn: #fbbf24; --break: #f87171; --ok: #4ade80;
}
* { box-sizing: border-box; }
body { margin: 0; padding: 2rem 1rem; background: var(--bg); color: var(--text);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif; line-height: 1.6; }
.wrap { max-width: 60rem; margin: 0 auto; }
h1 { margin: 0 0 .25rem; font-size: 1.75rem; }
.lead { color: var(--muted); margin: 0 0 1.5rem; }
code { font-family: "Fira Code", ui-monospace, monospace; font-size: .85em;
background: rgba(91,157,255,.12); padding: .1rem .35rem; border-radius: 3px; }
pre code { display: block; padding: .75rem; overflow-x: auto; }
.box { background: var(--card); border: 1px solid var(--border);
border-radius: 8px; padding: 1rem 1.25rem; margin-bottom: 1.25rem; }
.filter { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }
input[type=text] { background: var(--bg); border: 1px solid var(--border); color: var(--text);
padding: .4rem .6rem; border-radius: 5px; font-family: inherit; }
button { background: var(--accent); border: 0; color: #041225; font-weight: 600;
padding: .45rem .9rem; border-radius: 5px; cursor: pointer; font-family: inherit; }
.ver { border-left: 3px solid var(--accent); padding-left: 1rem; margin: 2rem 0; }
.ver h2 { margin: 0; font-size: 1.3rem; }
.date { color: var(--muted); font-size: .85rem; }
.summary { margin: .4rem 0 1rem; }
.chg { border-top: 1px solid var(--border); padding: .9rem 0; }
.chg:first-of-type { border-top: 0; }
.chg h3 { margin: 0 0 .35rem; font-size: 1rem; }
.tag { display: inline-block; font-size: .7rem; font-weight: 700; letter-spacing: .03em;
padding: .1rem .45rem; border-radius: 4px; margin-right: .4rem; vertical-align: middle; }
.t-area { background: rgba(148,163,184,.18); color: var(--muted); }
.t-break { background: rgba(248,113,113,.16); color: var(--break); }
.t-act { background: rgba(251,191,36,.16); color: var(--warn); }
.action { background: rgba(251,191,36,.08); border-left: 2px solid var(--warn);
padding: .5rem .75rem; margin-top: .5rem; font-size: .92rem; }
.action strong { color: var(--warn); }
.none { color: var(--muted); font-style: italic; }
a { color: var(--accent); }
</style>
</head>
<body>
<div class="wrap">
<h1>Änderungen am Deploymentcenter</h1>
<p class="lead">
Aktuelle Fassung: <code><?= e($data['current'] ?? '?') ?></code>.
Trage ein, gegen welche Fassung deine Anbindung gebaut wurde — dann steht hier
nur, was seitdem dazugekommen ist.
</p>
<div class="box">
<form method="get" class="filter">
<label for="since">Meine Fassung:</label>
<input type="text" id="since" name="since" value="<?= e($since) ?>" placeholder="2.2.0" size="10">
<button type="submit">Unterschied zeigen</button>
<?php if ($since !== ''): ?>
<a href="changelog.php" style="margin-left:.5rem;">alles zeigen</a>
<?php endif; ?>
</form>
<p style="margin:.75rem 0 0; color:var(--muted); font-size:.9rem;">
Maschinenlesbar: <code>GET /api/updateservice/v1/changelog?since=<?= e($since !== '' ? $since : '2.2.0') ?></code>
· <a href="changelog.php?raw">rohes JSON</a>
</p>
</div>
<?php if ($versions === []): ?>
<p class="none">
<?= $since !== ''
? 'Seit ' . e($since) . ' hat sich nichts geändert.'
: 'Es ist nichts hinterlegt.' ?>
</p>
<?php else: ?>
<div class="box">
<strong><?= count($versions) ?></strong> Fassung(en)<?= $since !== '' ? ' neuer als ' . e($since) : '' ?>,
davon <strong style="color:var(--warn);"><?= $actionItems ?></strong> Punkt(e) mit Handlungsbedarf
und <strong style="color:var(--break);"><?= $breaking ?></strong> mit Bruch.
</div>
<?php foreach ($versions as $v): ?>
<div class="ver">
<h2><?= e($v['version'] ?? '?') ?>
<span class="date">— <?= e($v['date'] ?? '') ?></span>
</h2>
<p class="summary"><?= e($v['summary'] ?? '') ?></p>
<?php foreach ($v['changes'] ?? [] as $c): ?>
<div class="chg">
<h3>
<span class="tag t-area"><?= e($areaLabels[$c['area'] ?? ''] ?? ($c['area'] ?? '?')) ?></span>
<?php if (!empty($c['breaking'])): ?><span class="tag t-break">BRUCH</span><?php endif; ?>
<?php if (!empty($c['action'])): ?><span class="tag t-act">TUN</span><?php endif; ?>
<?= e($c['title'] ?? '') ?>
</h3>
<div><?= e($c['text'] ?? '') ?></div>
<?php if (!empty($c['action'])): ?>
<div class="action"><strong>Zu tun:</strong> <?= e($c['action']) ?></div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
<p style="margin-top:2.5rem; color:var(--muted); font-size:.9rem;">
<a href="/docs/">Agenten-Handbuch</a> ·
<a href="/docs/release.md">Veröffentlichen</a> ·
<a href="/api/openapi.json">OpenAPI</a>
</p>
</div>
</body>
</html>