SSR control center: auto-purge webhook, dashboard tiles, backend module
Package 1: SsrPurgeService posts to the node /__purge (endpoint+secret in var/vitec-ssr-purge.env, outside git) and logs every attempt to tx_vitec_ssrpurgelog. DataHandler hooks queue per request and flush once: pages with nav-relevant field changes and all tx_news_/tx_vitec_ records purge everything, other page/content edits purge the hosting page path. Gated by the routing flag and the new Site Set switch vitec.ssr.autoPurge. Flipping the SSR switch ON purges everything once. CLI: vitec:ssr-purge, vitec:ssr-health (samples /__health into tx_vitec_ssrhealthlog, 14 days kept, scheduler-ready). Package 2: dashboard widgets vitecSsrStatus (live health, release, switch, last purge) and vitecSsrStats (purge counts, failures, uptime, response time). Package 3: backend module web_vitecssr with manual purge (paths or everything) and both protocols. Tested end to end against the running node (purge single/all 200, health sample logged). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d52d49974f
commit
63f950f9db
14 files changed
+1003
No files matched your search
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Controller\Backend;
|
||||
|
||||
use Evomedien\Vitec\Service\SsrPurgeService;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Http\RequestFactory;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Backend module "VITEC SSR": live state of the SSR node, manual purge
|
||||
* (single paths or everything) and the purge/health protocol.
|
||||
*/
|
||||
#[AsController]
|
||||
final class SsrController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ModuleTemplateFactory $moduleTemplateFactory,
|
||||
private readonly SsrPurgeService $purgeService,
|
||||
private readonly RequestFactory $requestFactory,
|
||||
private readonly ConnectionPool $connectionPool,
|
||||
private readonly FlashMessageService $flashMessageService,
|
||||
private readonly UriBuilder $uriBuilder,
|
||||
) {}
|
||||
|
||||
public function indexAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->moduleTemplateFactory->create($request);
|
||||
$view->setTitle('VITEC SSR');
|
||||
|
||||
$health = null;
|
||||
$healthError = '';
|
||||
$endpoint = $this->purgeService->getHealthEndpoint();
|
||||
if ($endpoint === '') {
|
||||
$healthError = 'var/vitec-ssr-purge.env fehlt oder ist unvollständig.';
|
||||
} else {
|
||||
$start = microtime(true);
|
||||
try {
|
||||
$response = $this->requestFactory->request($endpoint, 'GET', [
|
||||
'timeout' => 3, 'connect_timeout' => 2, 'http_errors' => false,
|
||||
]);
|
||||
if ($response->getStatusCode() === 200) {
|
||||
$health = json_decode((string)$response->getBody(), true) ?: null;
|
||||
if ($health !== null) {
|
||||
$health['responseMs'] = (int)round((microtime(true) - $start) * 1000);
|
||||
}
|
||||
} else {
|
||||
$healthError = 'HTTP ' . $response->getStatusCode();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$healthError = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$view->assignMultiple([
|
||||
'flagOn' => file_exists(Environment::getVarPath() . '/vitec-ssr-on.flag'),
|
||||
'configured' => $this->purgeService->isConfigured(),
|
||||
'health' => $health,
|
||||
'healthError' => $healthError,
|
||||
'purgeLog' => $this->rows('tx_vitec_ssrpurgelog', 30),
|
||||
'healthLog' => $this->rows('tx_vitec_ssrhealthlog', 10),
|
||||
]);
|
||||
return $view->renderResponse('Ssr/Index');
|
||||
}
|
||||
|
||||
public function purgeAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$body = (array)$request->getParsedBody();
|
||||
$mode = (string)($body['mode'] ?? 'paths');
|
||||
$user = (string)($GLOBALS['BE_USER']->user['username'] ?? '?');
|
||||
|
||||
if ($mode === 'all') {
|
||||
$paths = ['*'];
|
||||
} else {
|
||||
$paths = preg_split('/[\s,]+/', (string)($body['paths'] ?? ''), -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
||||
}
|
||||
|
||||
if ($paths === []) {
|
||||
$this->flash('Keine Pfade angegeben.', 'Purge', false);
|
||||
} else {
|
||||
$result = $this->purgeService->purge($paths, 'module:' . $user);
|
||||
$this->flash(
|
||||
sprintf('HTTP %d — %s', $result['httpCode'], $result['message']),
|
||||
$result['success'] ? 'Purge erfolgreich' : 'Purge fehlgeschlagen',
|
||||
$result['success']
|
||||
);
|
||||
}
|
||||
return new RedirectResponse((string)$this->uriBuilder->buildUriFromRoute('web_vitecssr'), 303);
|
||||
}
|
||||
|
||||
private function rows(string $table, int $limit): array
|
||||
{
|
||||
try {
|
||||
$qb = $this->connectionPool->getQueryBuilderForTable($table);
|
||||
return $qb->select('*')->from($table)
|
||||
->orderBy('uid', 'DESC')->setMaxResults($limit)
|
||||
->executeQuery()->fetchAllAssociative();
|
||||
} catch (\Throwable) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function flash(string $message, string $title, bool $ok): void
|
||||
{
|
||||
$flash = GeneralUtility::makeInstance(
|
||||
FlashMessage::class,
|
||||
$message,
|
||||
$title,
|
||||
$ok ? ContextualFeedbackSeverity::OK : ContextualFeedbackSeverity::ERROR,
|
||||
true
|
||||
);
|
||||
$this->flashMessageService->getMessageQueueByIdentifier()->addMessage($flash);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user