feat(setup): Erstinstallation ueber den Update-Agent, Installationskonto, Downloads

Bisher gab es nur den Update-Weg: eine Anwendung musste bereits installiert und
eingerichtet sein, damit sich etwas aktualisieren liess. Die Erstinstallation
auf einem neuen System war Handarbeit - Paket kopieren, Konfiguration
abtippen, Token besorgen.

Setup-API (neu)
- POST /api/setup/v1/login tauscht Benutzername und Passwort gegen ein Token
  mit 30 Minuten Gueltigkeit und ausschliesslich setup:install. Es wird nicht
  mitgeschrieben und lebt im Installer nur im Speicher.
- GET /api/setup/v1/catalog zeigt nur, was zur Laufzeitkennung des anfragenden
  Systems passt. Ein Projekt mit ausschliesslich Windows-Paket taucht auf einem
  Linux-Rechner gar nicht erst auf.
- POST /api/setup/v1/token stellt das Dauertoken der Anwendung aus. Welche
  Rechte vergeben werden, entscheidet der Server; die Anfrage kann nur
  einschraenken. Sonst waere der Umweg ueber ein kurzlebiges Token wirkungslos.

Rollentrennung (Migration 012)
- dc_users bekommt role, disabled und last_login_at. Die Rolle "installer"
  darf sich ueber den Setup-Weg anmelden und nicht am WebUI. Die Zugangsdaten
  werden auf jedem Zielsystem eingetippt; mit einem Administratorkonto
  verteilte man damit den Zugang zu Tokens, Lizenzen und Monitoren auf jeden
  Rechner, auf dem je etwas installiert wurde.
- Auth::verifyCredentials() prueft sessionfrei, damit Setup- und WebUI-Login
  nicht zwei verschiedene Haertungsgrade haben (Drosselung, Timing-Angleichung,
  Rehash gelten fuer beide).
- Konten mit hinterlegtem TOTP-Geheimnis werden am Setup-Weg mit 501
  abgewiesen. Eine TOTP-Pruefung gibt es im Deploymentcenter noch nicht; sie
  stillschweigend zu uebergehen waere ein Rueckschritt.
- Benutzerverwaltung im WebUI - es gab bisher gar keine, nur den einen von
  install_db.php angelegten Admin. Das letzte aktive Administratorkonto laesst
  sich weder deaktivieren noch loeschen.

Installer
- update-agent --action install fuehrt durch Anmeldung, Auswahl,
  Zielverzeichnis, Installation und Einrichtung. Die Dateien kommen ueber
  denselben Pfad wie ein Update - mit Pruefsumme, Signatur, Staging und
  Rollback. Ein zweiter Download-Weg waere ein zweiter Ort fuer dieselben
  Fehler.
- --action configure holt die Einrichtung nachtraeglich.
- setup.json im Paket beschreibt die benoetigten Werte. Bewusst im Paket und
  nicht zentral: so ist sie mit der Anwendung versioniert.
- Gefragt wird nur, was uebrig bleibt: bereits gesetzt -> detect:... ->
  provision -> fragen. Platzhalter wie changeme oder <dein-wert> gelten dabei
  nicht als eingerichtet, sonst liefe die Anwendung mit der Vorlage los.
- SetupWriter erhaelt vorhandene Inhalte. Eine appsettings.json fuehrt neben
  den abgefragten Werten meist Logging und anderes; sie neu zu erzeugen waere
  bequemer und verloere das - bei einer Neuinstallation ohne Backup.
  int und bool landen als JSON-Typ, nicht als Zeichenkette.

Downloads
- scripts/build_installer.ps1 baut selbstenthaltende Einzeldateien fuer
  win-x64, linux-x64 und linux-arm64 (rund 34 MB, .NET-Laufzeit inbegriffen).
  Ohne NativeAOT und ohne Trimming: Spectre.Console loest ueber Reflexion auf
  und braeche sonst erst beim Anwender.
- scripts/upload_installer.py laedt sie nach /installer/. Getrennt von
  deploy.py, das client-dotnet bewusst ausklammert.
- Bereich "Installer" auf der UpdateService-Seite mit Groessen, Pruefsummen
  und den wget-Befehlen; die Angaben stammen aus installer.json statt aus fest
  eingetragenem Text.
- install.sh und install.ps1 laden, pruefen die Pruefsumme und legen ab -
  sie richten bewusst nichts selbst ein. Das Manifest wird BOM-frei
  geschrieben, sonst scheitert json_decode() daran.

Enthaelt ausserdem die bislang nicht committete Arbeit an den
RocketChat-Benachrichtigungen (Migrationen 010 und 011) sowie die Loesch- und
Editierfunktion des UpdateService; die betroffenen Dateien liessen sich nicht
getrennt stagen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Deploymentcenter Bot
2026-08-13 10:33:26 +02:00
co-authored by Claude Opus 5
parent 2388b5abe1
commit c8f3e78635
34 changed files with 4704 additions and 21 deletions
+148
View File
@@ -515,6 +515,154 @@ $spec = [
],
],
'/api/setup/v1/login' => [
'post' => [
'tags' => ['Setup'],
'summary' => 'Anmelden und ein kurzlebiges Setup-Token holen',
'description' =>
"Ohne Token erreichbar - hier entsteht das Token erst.\n\n"
. "Gedacht fuer Konten der Rolle `installer`, die sich ihrerseits **nicht** am "
. "WebUI anmelden koennen. Die Zugangsdaten werden auf jedem Zielsystem "
. "eingegeben; mit einem Administratorkonto verteilte man damit den Zugang zur "
. "gesamten Verwaltung.\n\n"
. "Das Token laeuft nach 30 Minuten ab, traegt nur `setup:install` und wird "
. "nicht mitgeschrieben - es ist nach der Ausgabe nirgends mehr abrufbar.\n\n"
. "Konten mit hinterlegtem TOTP-Geheimnis werden mit 501 abgewiesen: eine "
. "TOTP-Pruefung gibt es noch nicht, und sie stillschweigend zu uebergehen "
. "waere ein Rueckschritt.",
'security' => [],
'requestBody' => [
'required' => true,
'content' => ['application/json' => ['schema' => [
'type' => 'object',
'required' => ['username', 'password'],
'properties' => [
'username' => ['type' => 'string'],
'password' => ['type' => 'string', 'format' => 'password'],
'hostname' => ['type' => 'string', 'description' => 'Rechnername, erscheint im Token-Namen und im Protokoll.'],
],
]]],
],
'responses' => [
'201' => [
'description' => 'Angemeldet',
'content' => ['application/json' => ['schema' => [
'type' => 'object',
'properties' => [
'setup_token' => ['type' => 'string'],
'expires_at' => ['type' => 'string', 'format' => 'date-time'],
'expires_in' => ['type' => 'integer', 'example' => 1800],
'scopes' => ['type' => 'array', 'items' => ['type' => 'string']],
'role' => ['type' => 'string', 'enum' => ['admin', 'installer']],
],
]]],
],
'401' => $errorResponse,
'429' => $errorResponse,
'501' => $errorResponse,
],
],
],
'/api/setup/v1/catalog' => [
'get' => [
'tags' => ['Setup'],
'summary' => 'Installierbare Anwendungen fuer dieses System',
'description' =>
"Scope `setup:catalog`. Zeigt ausschliesslich, was zur angefragten "
. "Laufzeitkennung passt - ein Projekt, von dem es nur ein Windows-Paket gibt, "
. "taucht auf einem Linux-Rechner gar nicht erst auf.\n\n"
. "Eine WebUI-Session wird hier bewusst **nicht** akzeptiert.",
'parameters' => [
['name' => 'platform', 'in' => 'query', 'schema' => ['type' => 'string', 'example' => 'win-x64'],
'description' => 'Laufzeitkennung des Zielsystems. Ohne Angabe gelten nur Releases mit platform=any.'],
],
'responses' => [
'200' => [
'description' => 'Katalog',
'content' => ['application/json' => ['schema' => [
'type' => 'object',
'properties' => [
'platform' => ['type' => 'string'],
'count' => ['type' => 'integer'],
'projects' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'slug' => ['type' => 'string'],
'name' => ['type' => 'string'],
'notes' => ['type' => 'string', 'nullable' => true],
'channels' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'channel' => ['type' => 'string'],
'version' => ['type' => 'string'],
'platform' => ['type' => 'string'],
'size_bytes' => ['type' => 'integer'],
'is_critical' => ['type' => 'boolean'],
'signed' => ['type' => 'boolean'],
'download_url' => ['type' => 'string'],
],
],
],
],
],
],
],
]]],
],
'401' => $errorResponse,
],
],
],
'/api/setup/v1/token' => [
'post' => [
'tags' => ['Setup'],
'summary' => 'Dauertoken fuer die eingerichtete Anwendung',
'description' =>
"Scope `setup:install`. Stellt das Token aus, mit dem die frisch installierte "
. "Anwendung spaeter Heartbeats sendet und Fehler meldet.\n\n"
. "**Welche Rechte vergeben werden, entscheidet der Server.** Die angefragten "
. "`scopes` koennen nur einschraenken; erlaubt sind ausschliesslich "
. "`watchdog:ping`, `watchdog:read`, `bugtracker:report` und "
. "`updateservice:read`. Andernfalls waere der Umweg ueber ein kurzlebiges "
. "Setup-Token wirkungslos - wer einmal installieren darf, stellte sich sonst "
. "ein Token mit allen Rechten aus und behielte es.",
'requestBody' => [
'required' => true,
'content' => ['application/json' => ['schema' => [
'type' => 'object',
'required' => ['project'],
'properties' => [
'project' => ['type' => 'string'],
'scopes' => ['type' => 'array', 'items' => ['type' => 'string']],
'hostname' => ['type' => 'string'],
],
]]],
],
'responses' => [
'201' => [
'description' => 'Token ausgestellt - wird nur einmal ausgeliefert',
'content' => ['application/json' => ['schema' => [
'type' => 'object',
'properties' => [
'token' => ['type' => 'string'],
'project' => ['type' => 'string'],
'scopes' => ['type' => 'array', 'items' => ['type' => 'string']],
],
]]],
],
'401' => $errorResponse,
'403' => $errorResponse,
'404' => $errorResponse,
],
],
],
'/api/watchdog/v1/ping' => [
'post' => [
'tags' => ['Watchdog'],
+253
View File
@@ -0,0 +1,253 @@
<?php
/**
* Setup API - Erstinstallation von Anwendungen
*
* POST /api/setup/v1/login Benutzername + Passwort -> kurzlebiges Token
* GET /api/setup/v1/catalog Was ist hier installierbar?
* POST /api/setup/v1/token Dauertoken fuer die eingerichtete Anwendung
*
* Warum ein eigener Anmeldeweg statt eines Dauertokens: Ein Token muesste
* vorher jemand erzeugen und auf das Zielsystem bringen - genau die Huerde,
* die eine Erstinstallation umstaendlich macht. Zugangsdaten hat der
* Einrichtende ohnehin.
*
* Das ausgestellte Token laeuft nach 30 Minuten ab und traegt nur
* setup:install. Es liegt auf einem fremden Rechner; alles Langlebige waere
* dort falsch aufgehoben. Was nach der Installation zurueckbleibt, ist das
* Sub-Token der Anwendung selbst mit genau den Rechten, die sie braucht.
*
* ROLLENTRENNUNG: Anmelden darf sich hier jedes Konto, aber gedacht ist der
* Weg fuer Konten der Rolle "installer" - die sich ihrerseits nicht am WebUI
* anmelden koennen. Wer auf jedem Zielsystem sein Admin-Passwort eingibt,
* verteilt den Zugang zur gesamten Verwaltung.
*/
declare(strict_types=1);
require_once __DIR__ . '/../../../../src/bootstrap.php';
use Deploymentcenter\Core\ApiAuth;
use Deploymentcenter\Core\Auth;
use Deploymentcenter\Core\Db;
use Deploymentcenter\Core\Http;
use Deploymentcenter\Core\Logger;
use Deploymentcenter\Core\TokenManager;
use Deploymentcenter\Modules\License\RateLimiter;
use Deploymentcenter\Modules\Setup\SetupCatalog;
use Deploymentcenter\Modules\UpdateService\UpdateManager;
Http::beginJson(['GET', 'POST', 'OPTIONS'], true);
$db = Db::init();
/** Gueltigkeit des ausgestellten Tokens in Sekunden. */
const SETUP_TOKEN_TTL = 1800;
switch (resolveSetupAction()) {
case 'login':
if (Http::method() !== 'POST') {
Http::fail(405, 'method_not_allowed', 'Die Anmeldung erwartet POST.');
}
// Deutlich enger als die uebrigen Endpunkte: hier werden Passwoerter
// geprueft. Die Sperre aus Auth greift zusaetzlich pro IP, sie zaehlt
// aber erst ab dem zehnten Fehlversuch.
$limiter = new RateLimiter($db, 10, 60, 'setup_login');
if (!$limiter->check(Http::clientIp())) {
Http::fail(429, 'rate_limited', 'Zu viele Anmeldeversuche. Bitte kurz warten.');
}
$username = Http::str('username');
$password = Http::str('password');
if ($username === null || $password === null || $username === '' || $password === '') {
Http::fail(400, 'missing_credentials', 'Benutzername und Passwort werden benoetigt.');
}
$lockout = Auth::lockoutSeconds($db, Http::clientIp());
if ($lockout > 0) {
Http::fail(429, 'locked_out', sprintf(
'Zu viele fehlgeschlagene Anmeldungen. Bitte in %d Minute(n) erneut versuchen.',
(int)ceil($lockout / 60)
));
}
$user = Auth::verifyCredentials($db, $username, $password);
if ($user === null) {
// Bewusst dieselbe Meldung fuer falschen Benutzer, falsches
// Passwort und deaktiviertes Konto.
Http::fail(401, 'invalid_credentials', 'Ungueltige Anmeldedaten.');
}
// TOTP ist in dc_users vorgesehen, wird aber noch von keinem
// Anmeldeweg geprueft. Ein Konto mit hinterlegtem Geheimnis hier
// durchzulassen, ohne den zweiten Faktor zu verlangen, waere ein
// stiller Rueckschritt - deshalb wird es ausdruecklich abgelehnt.
if (!empty($user['totp_secret'])) {
Logger::warning('Setup-Anmeldung mit TOTP-Konto abgelehnt', [
'username' => $username,
'ip' => Http::clientIp(),
]);
Http::fail(
501,
'totp_not_supported',
'Fuer dieses Konto ist ein zweiter Faktor hinterlegt. Die Setup-Anmeldung '
. 'unterstuetzt ihn noch nicht - bitte ein eigenes Installationskonto verwenden.'
);
}
$manager = new TokenManager($db);
$hostname = Http::str('hostname') ?? Http::str('instance_id');
$token = $manager->createEphemeralToken(
sprintf('Setup %s%s', $username, $hostname !== null ? ' @ ' . $hostname : ''),
['setup:install'],
SETUP_TOKEN_TTL,
$hostname
);
Logger::info('Setup-Anmeldung erfolgreich', [
'username' => $username,
'role' => Auth::roleOf($user),
'hostname' => $hostname,
'ip' => Http::clientIp(),
'token_id' => $token['token_id'],
]);
Http::ok([
'setup_token' => $token['raw_token'],
'token_id' => $token['token_id'],
'expires_at' => $token['expires_at'],
'expires_in' => SETUP_TOKEN_TTL,
'scopes' => $token['scopes'],
'role' => Auth::roleOf($user),
'message' => 'Angemeldet. Das Token laeuft nach 30 Minuten ab und wird nicht gespeichert.',
], 201);
case 'catalog':
// allowSession bewusst aus: dieser Weg ist fuer den Installer, nicht
// fuer einen angemeldeten Browser.
$context = ApiAuth::requireScope($db, 'setup:catalog', null, false);
$platform = Http::str('platform') ?? Http::str('rid');
$catalog = (new SetupCatalog($db))->forPlatform(
$platform,
ApiAuth::projectFilter($context)
);
Http::ok([
'platform' => UpdateManager::normalizePlatform($platform),
'count' => count($catalog),
'projects' => $catalog,
'message' => $catalog === []
? 'Fuer diese Plattform ist nichts hinterlegt. Releases ohne Plattformangabe gelten als "any".'
: sprintf('%d Anwendung(en) installierbar.', count($catalog)),
]);
case 'token':
if (Http::method() !== 'POST') {
Http::fail(405, 'method_not_allowed', 'Dieser Endpunkt erwartet POST.');
}
$context = ApiAuth::requireScope($db, 'setup:install', null, false);
$project = Http::str('project') ?? Http::str('product_slug');
if ($project === null || $project === '') {
Http::fail(400, 'missing_project', 'Der Parameter "project" wird benoetigt.');
}
ApiAuth::enforceProject($context, $project);
$exists = $db->prepare('SELECT slug FROM dc_projects WHERE slug = :slug LIMIT 1');
$exists->execute([':slug' => $project]);
if ($exists->fetch() === false) {
Http::fail(404, 'unknown_project', sprintf('Ein Projekt "%s" ist nicht angelegt.', $project));
}
$requested = Http::input('scopes', []);
if (is_string($requested)) {
$requested = array_map('trim', explode(',', $requested));
}
if (!is_array($requested) || $requested === []) {
$requested = ['watchdog:ping'];
}
// Was ein Installer ausstellen darf, ist fest umrissen und nicht
// verhandelbar. Wuerde die Anfrage die Rechte bestimmen, waere der
// Umweg ueber ein kurzlebiges Setup-Token wirkungslos: wer einmal
// installieren darf, koennte sich sonst ein Token mit allen Rechten
// ausstellen und behalten.
$allowed = ['watchdog:ping', 'watchdog:read', 'bugtracker:report', 'updateservice:read'];
$granted = array_values(array_intersect(
array_map(static fn($s): string => strtolower(trim((string)$s)), $requested),
$allowed
));
if ($granted === []) {
Http::fail(403, 'scopes_not_allowed', 'Keines der angeforderten Rechte darf ueber den Setup-Weg vergeben werden.', null, [
'allowed' => $allowed,
]);
}
$hostname = Http::str('hostname') ?? Http::str('instance_id');
$manager = new TokenManager($db);
$appToken = $manager->createMasterToken(
sprintf('%s @ %s', $project, $hostname ?? 'unbekannt'),
$project,
null,
'host',
$hostname,
$granted,
Http::str('environment') ?? 'all'
);
Logger::info('Anwendungstoken ueber den Setup-Weg ausgestellt', [
'project' => $project,
'hostname' => $hostname,
'scopes' => $granted,
'actor' => $context['actor'],
'token_id' => $appToken['token_id'],
]);
Http::ok([
'token' => $appToken['raw_token'],
'token_id' => $appToken['token_id'],
'project' => $project,
'scopes' => $granted,
'message' => 'Token ausgestellt. Es wird nur einmal ausgeliefert.',
], 201);
default:
Http::fail(404, 'unknown_action', 'Endpunkt nicht gefunden.', null, [
'available' => ['login', 'catalog', 'token'],
]);
}
/**
* Bestimmt die Aktion aus dem Pfadsegment oder ?action=.
* Feste Liste statt Teilstring-Suche.
*/
function resolveSetupAction(): string
{
$explicit = Http::str('action');
if ($explicit !== null) {
return strtolower($explicit);
}
$segments = array_values(array_filter(
explode('/', trim(Http::path(), '/')),
static fn(string $s): bool => $s !== ''
));
$last = strtolower((string)end($segments));
return match ($last) {
'login', 'catalog', 'token' => $last,
default => 'unknown',
};
}
+125 -2
View File
@@ -244,9 +244,132 @@ switch ($action) {
),
], $result['created'] ? 201 : 200);
case 'update':
case 'edit':
if (Http::method() !== 'POST') {
Http::fail(405, 'method_not_allowed', 'Das Bearbeiten eines Releases erwartet POST.');
}
$context = ApiAuth::requireScope($db, 'updateservice:publish');
$releaseId = Http::int('release_id', Http::int('id', 0));
if ($releaseId <= 0) {
Http::fail(400, 'missing_release_id', 'Der Parameter "release_id" oder "id" ist erforderlich.');
}
$existing = $manager->getReleaseById($releaseId);
if ($existing === null) {
Http::fail(404, 'release_not_found', sprintf('Release mit ID %d wurde nicht gefunden.', $releaseId));
}
$product = Http::str('product_slug') ?? Http::str('product') ?? $existing['product_slug'];
$version = Http::str('version') ?? $existing['version'];
$url = Http::str('download_url') ?? $existing['download_url'];
$channel = Http::str('channel') ?? $existing['channel'];
$platform = UpdateManager::normalizePlatform(Http::str('platform') ?? Http::str('rid') ?? $existing['platform']);
$notes = Http::str('release_notes') ?? $existing['release_notes'];
$hash = Http::str('sha256_hash') ?? $existing['sha256_hash'];
$git = Http::str('git_commit') ?? $existing['git_commit'];
$size = Http::int('size_bytes', (int)$existing['size_bytes']);
$critical = Http::input('is_critical', null) !== null
? filter_var(Http::input('is_critical'), FILTER_VALIDATE_BOOLEAN)
: (bool)$existing['is_critical'];
ApiAuth::enforceProject($context, (string)$product);
if ($hash !== null && $hash !== '' && preg_match('/^[0-9a-f]{64}$/i', $hash) !== 1) {
Http::fail(400, 'invalid_hash', 'sha256_hash muss 64 Hexadezimalzeichen enthalten.');
}
$signature = ReleaseSigner::sign(ReleaseSigner::canonical(
(string)$product,
(string)$version,
(string)$channel,
(string)$platform,
$hash !== '' ? $hash : null,
(string)$url,
$size
));
try {
$ok = $manager->updateRelease(
$releaseId,
(string)$product,
(string)$version,
(string)$channel,
$notes,
(string)$url,
$hash !== '' ? $hash : null,
$git,
$size,
$existing['manifest_json'] ?? null,
$critical,
$context['actor'],
(string)$platform,
$signature
);
} catch (\PDOException $e) {
if (str_contains($e->getMessage(), 'uq_prod_ver_chan_plat') || $e->getCode() === '23000') {
Http::fail(409, 'conflict', 'Ein Release mit dieser Kombination aus Produkt, Version, Kanal und Plattform existiert bereits.');
}
throw $e;
}
Http::ok([
'release_id' => $releaseId,
'updated' => $ok,
'platform' => $platform,
'signed' => $signature !== null,
'message' => sprintf('Release %s (%s, %s) fuer "%s" erfolgreich aktualisiert.', $version, $channel, $platform, $product),
]);
case 'delete':
if (Http::method() !== 'POST' && Http::method() !== 'DELETE') {
Http::fail(405, 'method_not_allowed', 'Das Loeschen erwartet POST oder DELETE.');
}
$context = ApiAuth::requireScope($db, 'updateservice:publish');
$releaseId = Http::int('release_id', Http::int('id', 0));
if ($releaseId <= 0) {
$product = Http::str('product_slug') ?? Http::str('product');
$version = Http::str('version');
$channel = Http::str('channel') ?? 'prod';
$platform = Http::str('platform') ?? Http::str('rid');
if ($product !== null && $version !== null) {
$found = $manager->findRelease($product, $version, $channel, $platform);
if ($found !== null) {
$releaseId = (int)$found['id'];
}
}
}
if ($releaseId <= 0) {
Http::fail(400, 'missing_release_id', 'Ungueltige oder fehlende release_id.');
}
$existing = $manager->getReleaseById($releaseId);
if ($existing === null) {
Http::fail(404, 'release_not_found', sprintf('Release mit ID %d wurde nicht gefunden.', $releaseId));
}
ApiAuth::enforceProject($context, (string)$existing['product_slug']);
$deleted = $manager->deleteRelease($releaseId);
if (!$deleted) {
Http::fail(500, 'delete_failed', 'Das Release konnte nicht geloescht werden.');
}
Http::ok([
'release_id' => $releaseId,
'deleted' => true,
'message' => sprintf('Release v%s (%s, %s) fuer "%s" geloescht.', $existing['version'], $existing['channel'], $existing['platform'] ?? 'any', $existing['product_slug']),
]);
default:
Http::fail(404, 'unknown_action', 'Endpunkt nicht gefunden.', null, [
'available' => ['check', 'latest', 'releases', 'pubkey', 'publish'],
'available' => ['check', 'latest', 'releases', 'pubkey', 'publish', 'update', 'delete'],
]);
}
@@ -270,7 +393,7 @@ function resolveUpdateAction(): string
$last = strtolower((string)end($segments));
return match ($last) {
'check', 'latest', 'releases', 'pubkey', 'publish' => $last,
'check', 'latest', 'releases', 'pubkey', 'publish', 'update', 'edit', 'delete' => $last === 'edit' ? 'update' : $last,
'publish_release' => 'publish',
default => 'check',
};
+13 -5
View File
@@ -188,12 +188,20 @@ switch ($action) {
// ein Agenten-Token soll den Zustand aller Monitore nicht umschreiben.
ApiAuth::requireScope($db, 'watchdog:evaluate');
$reportRes = null;
if (Http::str('force_report') === '1' || Http::str('report') === '1') {
$reportRes = \Deploymentcenter\Modules\Notify\RocketChatNotifier::sendStatusReport($db, true);
}
$result = Evaluator::run($db);
Http::ok($result + ['message' => sprintf(
'%d Monitor(e) geprueft, %d Zustandswechsel.',
$result['checked'],
$result['changed']
)]);
Http::ok($result + [
'rocketchat_report' => $reportRes,
'message' => sprintf(
'%d Monitor(e) geprueft, %d Zustandswechsel.',
$result['checked'],
$result['changed']
)
]);
default:
Http::fail(404, 'unknown_action', 'Endpunkt nicht gefunden.', null, [