Git-Deploy per Tag, Betriebsüberwachung und Release-Seeder für den Livegang
- dev/betrieb/deploy.sh: Deploy nur per Tag, mit --seed (Seeder im Wartungsmodus) und --initial (Vorschau/Live einmalig an Ort und Stelle auf Git umstellen) - dev/betrieb/vorschau-auffrischen.sh: Live-DB und Live-Medien in die Vorschau (Generalprobe) - InteriorReleaseSeeder: alle Inhaltsschritte des Relaunchs in einem Lauf, idempotent; Displays, Playlists und Mediathek bleiben unberührt - DisplayInteriorBrandingSeeder überschreibt keinen inzwischen gepflegten Claim mehr - Monitoring: GlitchTip (Server + Browser), /health mit Token, Scheduler-Herzschlag, Alarme per Mail und ntfy, Version in about und /health - public/build versioniert; .gitignore: macOS-Muster Icon traf resources/views/flux/icon/ - Runbook: dev/betrieb/README.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fcb10be120
commit
b5ed756d67
98 changed files with 3947 additions and 82 deletions
39
app/Http/Controllers/HealthController.php
Normal file
39
app/Http/Controllers/HealthController.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\Monitoring\HealthProbe;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* Tiefer Health-Check für den externen Monitor.
|
||||
*
|
||||
* `/up` zeigt nur, dass die App bootet. `/health` prüft zusätzlich DB,
|
||||
* Cache, Queue-Rückstau, Scheduler-Herzschlag und Plattenplatz: 200 solange
|
||||
* nichts kritisch ist, sonst 503. Zugang nur mit MONITORING_HEALTH_TOKEN
|
||||
* (Bearer oder `?token=`); ohne konfiguriertes Token gibt es den Endpunkt nicht.
|
||||
*/
|
||||
class HealthController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, HealthProbe $probe): JsonResponse
|
||||
{
|
||||
$token = (string) config('monitoring.health.token');
|
||||
$given = (string) ($request->bearerToken() ?? $request->query('token', ''));
|
||||
|
||||
abort_if($token === '' || ! hash_equals($token, $given), 404);
|
||||
|
||||
$checks = $probe->checks();
|
||||
$status = HealthProbe::worstStatus($checks);
|
||||
|
||||
return response()->json([
|
||||
'status' => $status,
|
||||
'version' => config('app.version'),
|
||||
'checked_at' => now()->toIso8601String(),
|
||||
'checks' => collect($checks)->mapWithKeys(fn (array $check): array => [
|
||||
$check['key'] => ['status' => $check['status'], 'detail' => $check['detail']],
|
||||
]),
|
||||
], $status === HealthProbe::StatusCritical ? 503 : 200)
|
||||
->header('Cache-Control', 'no-store');
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,12 @@ class BasicAuthMiddleware
|
|||
return $next($request);
|
||||
}
|
||||
|
||||
// Skip Basic Auth für den Health-Check: Uptime Kuma schickt seinen eigenen
|
||||
// Authorization-Header (Bearer-Token), /health prüft ihn selbst.
|
||||
if ($path === 'health' || $path === 'up') {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Skip Basic Auth für Flux UI Assets (flux.js, flux.min.js, editor.js, etc.)
|
||||
if (str_starts_with($path, 'flux/')) {
|
||||
return $next($request);
|
||||
|
|
|
|||
96
app/Http/Middleware/ReportErrorSignals.php
Normal file
96
app/Http/Middleware/ReportErrorSignals.php
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Support\Monitoring\ErrorReportScrubber;
|
||||
use App\Support\Monitoring\ErrorSignal;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
use function Illuminate\Support\defer;
|
||||
|
||||
/**
|
||||
* Meldet Antworten, hinter denen ein Bedien- oder Programmierfehler steckt,
|
||||
* obwohl keine Exception gemeldet wird:
|
||||
*
|
||||
* - 404 mit Referrer der eigenen Domain — irgendwo bei uns steht ein toter Link
|
||||
* - 403/429 für angemeldete Nutzer — die Oberfläche führt sie dorthin
|
||||
* - 419 — Formular oder Livewire-Komponente abgelaufen, Eingaben sind verloren
|
||||
*
|
||||
* Gesendet wird nach der Antwort (`defer()`), der Nutzer wartet nicht auf GlitchTip.
|
||||
*/
|
||||
class ReportErrorSignals
|
||||
{
|
||||
public function __construct(private readonly ErrorSignal $errorSignal) {}
|
||||
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
$signal = $this->signalFor($request, $response->getStatusCode());
|
||||
|
||||
if ($signal !== null) {
|
||||
$location = $this->locationOf($request);
|
||||
$context = [
|
||||
'status' => $response->getStatusCode(),
|
||||
'method' => $request->method(),
|
||||
'path' => '/'.ltrim($request->path(), '/'),
|
||||
'referer' => $this->refererPath($request),
|
||||
'livewire' => $request->hasHeader('X-Livewire'),
|
||||
];
|
||||
|
||||
defer(fn () => $this->errorSignal->report($signal, $location, $context));
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function signalFor(Request $request, int $status): ?string
|
||||
{
|
||||
return match (true) {
|
||||
$status === 404 && $this->hasInternalReferer($request) => ErrorSignal::DeadInternalLink,
|
||||
$status === 403 && $request->user() !== null => ErrorSignal::AccessDenied,
|
||||
$status === 419 => ErrorSignal::PageExpired,
|
||||
$status === 429 && $request->user() !== null => ErrorSignal::Throttled,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Livewire-Requests laufen alle über denselben Update-Endpunkt — der Ort
|
||||
* ist dort die Seite, auf der der Nutzer stand.
|
||||
*/
|
||||
private function locationOf(Request $request): string
|
||||
{
|
||||
if ($request->hasHeader('X-Livewire')) {
|
||||
return 'livewire '.($this->refererPath($request) ?? '/');
|
||||
}
|
||||
|
||||
$routeName = $request->route()?->getName();
|
||||
|
||||
return $routeName !== null
|
||||
? $routeName
|
||||
: '/'.ltrim($request->path(), '/');
|
||||
}
|
||||
|
||||
private function hasInternalReferer(Request $request): bool
|
||||
{
|
||||
$referer = $request->headers->get('referer');
|
||||
|
||||
return is_string($referer) && parse_url($referer, PHP_URL_HOST) === $request->getHost();
|
||||
}
|
||||
|
||||
private function refererPath(Request $request): ?string
|
||||
{
|
||||
$referer = $request->headers->get('referer');
|
||||
|
||||
if (! is_string($referer) || $referer === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$path = parse_url($referer, PHP_URL_PATH);
|
||||
|
||||
return is_string($path) ? ErrorReportScrubber::scrubPath($path) : null;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue