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,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Command;
|
||||||
|
|
||||||
|
use Evomedien\Vitec\Service\SsrPurgeService;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Http\RequestFactory;
|
||||||
|
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'vitec:ssr-health',
|
||||||
|
description: 'Sample the SSR node /__health into tx_vitec_ssrhealthlog (run via scheduler for history)',
|
||||||
|
)]
|
||||||
|
final class SsrHealthSampleCommand extends Command
|
||||||
|
{
|
||||||
|
private const KEEP_DAYS = 14;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly SsrPurgeService $purgeService,
|
||||||
|
private readonly RequestFactory $requestFactory,
|
||||||
|
private readonly ConnectionPool $connectionPool,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$endpoint = $this->purgeService->getHealthEndpoint();
|
||||||
|
if ($endpoint === '') {
|
||||||
|
$output->writeln('<error>SSR_HEALTH_ENDPOINT missing in var/vitec-ssr-purge.env</error>');
|
||||||
|
return Command::INVALID;
|
||||||
|
}
|
||||||
|
$row = ['crdate' => time(), 'ok' => 0, 'release_id' => '', 'cached_pages' => 0, 'cache_mb' => 0, 'response_ms' => 0];
|
||||||
|
$start = microtime(true);
|
||||||
|
try {
|
||||||
|
$response = $this->requestFactory->request($endpoint, 'GET', [
|
||||||
|
'timeout' => 4, 'connect_timeout' => 2, 'http_errors' => false,
|
||||||
|
]);
|
||||||
|
$row['response_ms'] = (int)round((microtime(true) - $start) * 1000);
|
||||||
|
if ($response->getStatusCode() === 200) {
|
||||||
|
$data = json_decode((string)$response->getBody(), true);
|
||||||
|
$row['ok'] = !empty($data['ok']) ? 1 : 0;
|
||||||
|
$row['release_id'] = substr((string)($data['release']['release'] ?? ''), 0, 100);
|
||||||
|
$row['cached_pages'] = (int)($data['cachedPages'] ?? 0);
|
||||||
|
$row['cache_mb'] = (int)($data['cacheMB'] ?? 0);
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$row['response_ms'] = (int)round((microtime(true) - $start) * 1000);
|
||||||
|
}
|
||||||
|
$conn = $this->connectionPool->getConnectionForTable('tx_vitec_ssrhealthlog');
|
||||||
|
$conn->insert('tx_vitec_ssrhealthlog', $row);
|
||||||
|
$conn->executeStatement(
|
||||||
|
'DELETE FROM tx_vitec_ssrhealthlog WHERE crdate < ?',
|
||||||
|
[time() - self::KEEP_DAYS * 86400]
|
||||||
|
);
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
'%s release=%s pages=%d cache=%dMB %dms',
|
||||||
|
$row['ok'] ? '<info>ok</info>' : '<error>DOWN</error>',
|
||||||
|
$row['release_id'] ?: '-',
|
||||||
|
$row['cached_pages'],
|
||||||
|
$row['cache_mb'],
|
||||||
|
$row['response_ms']
|
||||||
|
));
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Command;
|
||||||
|
|
||||||
|
use Evomedien\Vitec\Service\SsrPurgeService;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'vitec:ssr-purge',
|
||||||
|
description: 'Purge paths (or everything with --all) from the SSR node page cache',
|
||||||
|
)]
|
||||||
|
final class SsrPurgeCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(private readonly SsrPurgeService $purgeService)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->addArgument('paths', InputArgument::IS_ARRAY, 'Paths to purge, e.g. /products/aligo');
|
||||||
|
$this->addOption('all', null, InputOption::VALUE_NONE, 'Purge the whole cache ("*")');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$paths = $input->getOption('all') ? ['*'] : (array)$input->getArgument('paths');
|
||||||
|
if ($paths === []) {
|
||||||
|
$output->writeln('<error>No paths given (use arguments or --all).</error>');
|
||||||
|
return Command::INVALID;
|
||||||
|
}
|
||||||
|
$result = $this->purgeService->purge($paths, 'cli');
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
'%s HTTP %d - %s (paths: %s)',
|
||||||
|
$result['success'] ? '<info>OK</info>' : '<error>FAILED</error>',
|
||||||
|
$result['httpCode'],
|
||||||
|
$result['message'],
|
||||||
|
implode(', ', $result['paths'])
|
||||||
|
));
|
||||||
|
return $result['success'] ? Command::SUCCESS : Command::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Hooks;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Core\Environment;
|
||||||
|
use Evomedien\Vitec\Service\SsrPurgeService;
|
||||||
|
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
||||||
|
use TYPO3\CMS\Core\Site\SiteFinder;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-purge: tells the SSR node which pages to drop from its full-page
|
||||||
|
* cache after editors change content (ticket 0054-FE section 3).
|
||||||
|
*
|
||||||
|
* Mapping rules (v1, deliberately conservative):
|
||||||
|
* - pages: navigation-relevant field changed (title, nav_title, slug,
|
||||||
|
* hidden, nav_hide, doktype) -> '*' (menus are embedded in EVERY page),
|
||||||
|
* otherwise just that page's path.
|
||||||
|
* - tt_content: the hosting page's path.
|
||||||
|
* - tx_news_* and tx_vitec_*: '*' (news feed the megamenu spotlight,
|
||||||
|
* domain models render on many pages).
|
||||||
|
* - delete/move/undelete commands: pages -> '*', records -> hosting page.
|
||||||
|
*
|
||||||
|
* Changes are queued per request and flushed ONCE in afterAllOperations,
|
||||||
|
* so bulk operations (importer!) end up as a single purge call.
|
||||||
|
* Runs only while the SSR routing flag is ON and the Site Set switch
|
||||||
|
* vitec.ssr.autoPurge is enabled.
|
||||||
|
*/
|
||||||
|
final class DataHandlerSsrPurge
|
||||||
|
{
|
||||||
|
private const NAV_FIELDS = ['title', 'nav_title', 'slug', 'hidden', 'nav_hide', 'doktype', 'fe_group', 'starttime', 'endtime'];
|
||||||
|
|
||||||
|
/** @var array<string,true> */
|
||||||
|
private static array $queue = [];
|
||||||
|
|
||||||
|
public function processDatamap_afterDatabaseOperations(
|
||||||
|
string $status,
|
||||||
|
string $table,
|
||||||
|
$id,
|
||||||
|
array $fieldArray,
|
||||||
|
DataHandler $dataHandler
|
||||||
|
): void {
|
||||||
|
if (!$this->isActive()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($table === 'pages') {
|
||||||
|
if (array_intersect(array_keys($fieldArray), self::NAV_FIELDS) !== []) {
|
||||||
|
self::$queue['*'] = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$pageId = $this->realId($id, $dataHandler);
|
||||||
|
if ($pageId > 0) {
|
||||||
|
$this->queuePagePath($pageId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($table === 'tt_content') {
|
||||||
|
$recordId = $this->realId($id, $dataHandler);
|
||||||
|
$pid = $fieldArray['pid'] ?? null;
|
||||||
|
if (!is_numeric($pid) || (int)$pid <= 0) {
|
||||||
|
$pid = $this->pidOfRecord($table, $recordId);
|
||||||
|
}
|
||||||
|
if ((int)$pid > 0) {
|
||||||
|
$this->queuePagePath((int)$pid);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (str_starts_with($table, 'tx_news_') || str_starts_with($table, 'tx_vitec_')) {
|
||||||
|
self::$queue['*'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processDatamap_afterAllOperations(DataHandler $dataHandler): void
|
||||||
|
{
|
||||||
|
$this->flush('editor-save');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processCmdmap_postProcess(
|
||||||
|
string $command,
|
||||||
|
string $table,
|
||||||
|
$id,
|
||||||
|
$value,
|
||||||
|
DataHandler $dataHandler
|
||||||
|
): void {
|
||||||
|
if (!$this->isActive()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($table === 'pages') {
|
||||||
|
// Delete/move/copy of a page changes menus everywhere.
|
||||||
|
self::$queue['*'] = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($table === 'tt_content') {
|
||||||
|
$pid = $this->pidOfRecord($table, (int)$id);
|
||||||
|
if ($pid > 0) {
|
||||||
|
$this->queuePagePath($pid);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (str_starts_with($table, 'tx_news_') || str_starts_with($table, 'tx_vitec_')) {
|
||||||
|
self::$queue['*'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processCmdmap_afterFinish(DataHandler $dataHandler): void
|
||||||
|
{
|
||||||
|
$this->flush('editor-command');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function flush(string $trigger): void
|
||||||
|
{
|
||||||
|
if (self::$queue === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$paths = array_keys(self::$queue);
|
||||||
|
self::$queue = [];
|
||||||
|
GeneralUtility::makeInstance(SsrPurgeService::class)->purgeQuietly($paths, $trigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isActive(): bool
|
||||||
|
{
|
||||||
|
if (!file_exists(Environment::getVarPath() . '/vitec-ssr-on.flag')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
foreach (GeneralUtility::makeInstance(SiteFinder::class)->getAllSites() as $site) {
|
||||||
|
if ($site->getSettings()->get('vitec.ssr.autoPurge', true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function queuePagePath(int $pageId): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($pageId);
|
||||||
|
$uri = $site->getRouter()->generateUri($pageId);
|
||||||
|
$path = $uri->getPath() ?: '/';
|
||||||
|
self::$queue[$path] = true;
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// Page outside a site (folders etc.) - nothing cached for it.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function realId($id, DataHandler $dataHandler): int
|
||||||
|
{
|
||||||
|
if (is_numeric($id)) {
|
||||||
|
return (int)$id;
|
||||||
|
}
|
||||||
|
return (int)($dataHandler->substNEWwithIDs[$id] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pidOfRecord(string $table, int $uid): int
|
||||||
|
{
|
||||||
|
if ($uid <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
|
||||||
|
$qb->getRestrictions()->removeAll();
|
||||||
|
$pid = $qb->select('pid')->from($table)
|
||||||
|
->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid, \Doctrine\DBAL\ParameterType::INTEGER)))
|
||||||
|
->executeQuery()->fetchOne();
|
||||||
|
return (int)$pid;
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,6 +81,10 @@ final class SsrFlagService
|
|||||||
. "flip the switch in Site Management > Settings instead.\n"
|
. "flip the switch in Site Management > Settings instead.\n"
|
||||||
. 'Last sync: ' . date('c') . "\n"
|
. 'Last sync: ' . date('c') . "\n"
|
||||||
);
|
);
|
||||||
|
// Fresh ON: whatever the node cached while the switch was OFF
|
||||||
|
// is stale by definition - drop its whole page cache once.
|
||||||
|
\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\Evomedien\Vitec\Service\SsrPurgeService::class)
|
||||||
|
->purgeQuietly(['*'], 'switch-on');
|
||||||
} else {
|
} else {
|
||||||
@unlink($this->getFlagPath());
|
@unlink($this->getFlagPath());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Service;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Core\Environment;
|
||||||
|
use TYPO3\CMS\Core\Http\RequestFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Talks to the SSR node's POST /__purge endpoint (contract 0054-FE) and
|
||||||
|
* keeps a log of every attempt in tx_vitec_ssrpurgelog.
|
||||||
|
*
|
||||||
|
* Endpoint + secret live OUTSIDE the repository in var/vitec-ssr-purge.env
|
||||||
|
* (KEY=VALUE lines: SSR_PURGE_ENDPOINT, SSR_PURGE_SECRET, SSR_HEALTH_ENDPOINT)
|
||||||
|
* - same pattern as the routing flag file, nothing secret in git.
|
||||||
|
*/
|
||||||
|
final class SsrPurgeService
|
||||||
|
{
|
||||||
|
private ?array $config = null;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly RequestFactory $requestFactory,
|
||||||
|
private readonly ConnectionPool $connectionPool,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function getConfigPath(): string
|
||||||
|
{
|
||||||
|
return Environment::getVarPath() . '/vitec-ssr-purge.env';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isConfigured(): bool
|
||||||
|
{
|
||||||
|
$c = $this->getConfig();
|
||||||
|
return ($c['SSR_PURGE_ENDPOINT'] ?? '') !== '' && ($c['SSR_PURGE_SECRET'] ?? '') !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHealthEndpoint(): string
|
||||||
|
{
|
||||||
|
return $this->getConfig()['SSR_HEALTH_ENDPOINT'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes, dedupes and sends the paths; logs the outcome.
|
||||||
|
* Never throws - editors' saves must not fail on a purge problem.
|
||||||
|
*
|
||||||
|
* @param string[] $paths page paths, or ['*'] for everything
|
||||||
|
* @return array{success:bool, httpCode:int, message:string, paths:string[]}
|
||||||
|
*/
|
||||||
|
public function purge(array $paths, string $trigger): array
|
||||||
|
{
|
||||||
|
$paths = $this->normalize($paths);
|
||||||
|
if ($paths === []) {
|
||||||
|
return ['success' => true, 'httpCode' => 0, 'message' => 'nothing to purge', 'paths' => []];
|
||||||
|
}
|
||||||
|
if (!$this->isConfigured()) {
|
||||||
|
$result = ['success' => false, 'httpCode' => 0, 'message' => 'not configured (var/vitec-ssr-purge.env missing)', 'paths' => $paths];
|
||||||
|
$this->log($trigger, $result, 0);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$c = $this->getConfig();
|
||||||
|
$start = microtime(true);
|
||||||
|
try {
|
||||||
|
$response = $this->requestFactory->request($c['SSR_PURGE_ENDPOINT'], 'POST', [
|
||||||
|
'headers' => [
|
||||||
|
'X-Purge-Secret' => $c['SSR_PURGE_SECRET'],
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
],
|
||||||
|
'body' => json_encode(['paths' => $paths], JSON_THROW_ON_ERROR),
|
||||||
|
'timeout' => 4,
|
||||||
|
'connect_timeout' => 2,
|
||||||
|
'http_errors' => false,
|
||||||
|
]);
|
||||||
|
$code = $response->getStatusCode();
|
||||||
|
$body = substr((string)$response->getBody(), 0, 200);
|
||||||
|
$result = [
|
||||||
|
'success' => $code === 200,
|
||||||
|
'httpCode' => $code,
|
||||||
|
'message' => $body,
|
||||||
|
'paths' => $paths,
|
||||||
|
];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$result = ['success' => false, 'httpCode' => 0, 'message' => substr($e->getMessage(), 0, 200), 'paths' => $paths];
|
||||||
|
}
|
||||||
|
$this->log($trigger, $result, (int)round((microtime(true) - $start) * 1000));
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function purgeQuietly(array $paths, string $trigger): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->purge($paths, $trigger);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// Logged inside purge(); never disturb the caller.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path rules from 0055-BE Q3 / 0054-FE section 3: path only, query
|
||||||
|
* stripped, no trailing slash (root stays '/'), '*' wins over all.
|
||||||
|
*
|
||||||
|
* @param string[] $paths
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function normalize(array $paths): array
|
||||||
|
{
|
||||||
|
$out = [];
|
||||||
|
foreach ($paths as $p) {
|
||||||
|
$p = trim((string)$p);
|
||||||
|
if ($p === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($p === '*') {
|
||||||
|
return ['*'];
|
||||||
|
}
|
||||||
|
$p = parse_url($p, PHP_URL_PATH) ?: '/';
|
||||||
|
if ($p !== '/' ) {
|
||||||
|
$p = '/' . trim($p, '/');
|
||||||
|
}
|
||||||
|
$out[$p] = true;
|
||||||
|
}
|
||||||
|
return array_keys($out);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getConfig(): array
|
||||||
|
{
|
||||||
|
if ($this->config !== null) {
|
||||||
|
return $this->config;
|
||||||
|
}
|
||||||
|
$this->config = [];
|
||||||
|
$file = self::getConfigPath();
|
||||||
|
if (is_readable($file)) {
|
||||||
|
foreach (file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
|
||||||
|
if (str_starts_with(ltrim($line), '#') || !str_contains($line, '=')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
[$k, $v] = explode('=', $line, 2);
|
||||||
|
$this->config[trim($k)] = trim($v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this->config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function log(string $trigger, array $result, int $durationMs): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->connectionPool->getConnectionForTable('tx_vitec_ssrpurgelog')->insert(
|
||||||
|
'tx_vitec_ssrpurgelog',
|
||||||
|
[
|
||||||
|
'crdate' => time(),
|
||||||
|
'trigger_source' => substr($trigger, 0, 100),
|
||||||
|
'paths' => implode(', ', $result['paths']),
|
||||||
|
'success' => $result['success'] ? 1 : 0,
|
||||||
|
'http_code' => $result['httpCode'],
|
||||||
|
'message' => substr($result['message'], 0, 255),
|
||||||
|
'duration_ms' => $durationMs,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// Table missing (before schema update) must not break anything.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Widgets;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Dashboard\Widgets\WidgetConfigurationInterface;
|
||||||
|
use TYPO3\CMS\Dashboard\Widgets\WidgetInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dashboard tile: purge and health statistics from the two protocol
|
||||||
|
* tables. History depth = what vitec:ssr-health sampling (scheduler)
|
||||||
|
* and the purge log hold (14 days).
|
||||||
|
*/
|
||||||
|
final class SsrStatsWidget implements WidgetInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly WidgetConfigurationInterface $configuration,
|
||||||
|
private readonly ConnectionPool $connectionPool,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function renderWidgetContent(): string
|
||||||
|
{
|
||||||
|
$now = time();
|
||||||
|
$d1 = $now - 86400;
|
||||||
|
$d7 = $now - 7 * 86400;
|
||||||
|
|
||||||
|
$purges24h = $this->count('tx_vitec_ssrpurgelog', 'crdate > ' . $d1);
|
||||||
|
$purges7d = $this->count('tx_vitec_ssrpurgelog', 'crdate > ' . $d7);
|
||||||
|
$failed7d = $this->count('tx_vitec_ssrpurgelog', 'crdate > ' . $d7 . ' AND success = 0');
|
||||||
|
$full7d = $this->count('tx_vitec_ssrpurgelog', "crdate > " . $d7 . " AND paths = '*'");
|
||||||
|
|
||||||
|
$samples24h = $this->count('tx_vitec_ssrhealthlog', 'crdate > ' . $d1);
|
||||||
|
$ok24h = $this->count('tx_vitec_ssrhealthlog', 'crdate > ' . $d1 . ' AND ok = 1');
|
||||||
|
$uptime = $samples24h > 0 ? round($ok24h / $samples24h * 100, 1) . ' %' : '-';
|
||||||
|
$avgMs = $this->scalar('SELECT ROUND(AVG(response_ms)) FROM tx_vitec_ssrhealthlog WHERE crdate > ' . $d1 . ' AND ok = 1');
|
||||||
|
$maxCache = $this->scalar('SELECT MAX(cache_mb) FROM tx_vitec_ssrhealthlog WHERE crdate > ' . $d7);
|
||||||
|
|
||||||
|
$rows = [
|
||||||
|
['Purges (24 h / 7 Tage)', $purges24h . ' / ' . $purges7d],
|
||||||
|
['davon Komplett-Purges (7 T.)', (string)$full7d],
|
||||||
|
['Fehlgeschlagene Purges (7 T.)', $failed7d > 0
|
||||||
|
? '<span class="badge badge-danger">' . $failed7d . '</span>'
|
||||||
|
: '<span class="badge badge-success">0</span>'],
|
||||||
|
['Verfügbarkeit (24 h)', htmlspecialchars($uptime)
|
||||||
|
. ' <small>(' . $ok24h . '/' . $samples24h . ' Messungen)</small>'],
|
||||||
|
['∅ Antwortzeit (24 h)', $avgMs !== null ? $avgMs . ' ms' : '-'],
|
||||||
|
['Max. Cache-Größe (7 T.)', $maxCache !== null ? $maxCache . ' MB' : '-'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$html = '<table class="table table-striped table-sm"><tbody>';
|
||||||
|
foreach ($rows as [$label, $value]) {
|
||||||
|
$html .= '<tr><th style="width:55%">' . $label . '</th><td>' . $value . '</td></tr>';
|
||||||
|
}
|
||||||
|
$html .= '</tbody></table>';
|
||||||
|
if ($samples24h === 0) {
|
||||||
|
$html .= '<p class="text-muted" style="font-size:11px">Noch keine Messwerte: Scheduler-Task für <code>vitec:ssr-health</code> einrichten (z. B. alle 5 Minuten).</p>';
|
||||||
|
}
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOptions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function count(string $table, string $where): int
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return (int)$this->connectionPool->getConnectionForTable($table)
|
||||||
|
->executeQuery('SELECT COUNT(*) FROM ' . $table . ' WHERE ' . $where)->fetchOne();
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function scalar(string $sql): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$v = $this->connectionPool->getConnectionForTable('tx_vitec_ssrhealthlog')
|
||||||
|
->executeQuery($sql)->fetchOne();
|
||||||
|
return $v === null || $v === false ? null : (string)$v;
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Widgets;
|
||||||
|
|
||||||
|
use Evomedien\Vitec\Service\SsrPurgeService;
|
||||||
|
use TYPO3\CMS\Core\Core\Environment;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Http\RequestFactory;
|
||||||
|
use TYPO3\CMS\Dashboard\Widgets\WidgetConfigurationInterface;
|
||||||
|
use TYPO3\CMS\Dashboard\Widgets\WidgetInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dashboard tile: live state of the SSR node on the Solr VPS plus the
|
||||||
|
* delivery switch - green/red at a glance after backend login.
|
||||||
|
*/
|
||||||
|
final class SsrStatusWidget implements WidgetInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly WidgetConfigurationInterface $configuration,
|
||||||
|
private readonly SsrPurgeService $purgeService,
|
||||||
|
private readonly RequestFactory $requestFactory,
|
||||||
|
private readonly ConnectionPool $connectionPool,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function renderWidgetContent(): string
|
||||||
|
{
|
||||||
|
$flagOn = file_exists(Environment::getVarPath() . '/vitec-ssr-on.flag');
|
||||||
|
$health = $this->fetchHealth();
|
||||||
|
$lastPurge = $this->lastPurgeRow();
|
||||||
|
|
||||||
|
$badge = static function (bool $ok, string $yes, string $no): string {
|
||||||
|
return $ok
|
||||||
|
? '<span class="badge badge-success">' . htmlspecialchars($yes) . '</span>'
|
||||||
|
: '<span class="badge badge-danger">' . htmlspecialchars($no) . '</span>';
|
||||||
|
};
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
$rows[] = ['SSR-Auslieferung', $badge($flagOn, 'AN - Seiten kommen vom VPS', 'AUS - SPA wie bisher')];
|
||||||
|
if ($health === null) {
|
||||||
|
$rows[] = ['SSR-Server', '<span class="badge badge-danger">NICHT ERREICHBAR</span>'];
|
||||||
|
} else {
|
||||||
|
$rows[] = ['SSR-Server', $badge((bool)($health['ok'] ?? false), 'läuft (' . (int)$health['responseMs'] . ' ms)', 'meldet Fehler')];
|
||||||
|
$rows[] = ['Release', htmlspecialchars((string)($health['release']['release'] ?? '?'))
|
||||||
|
. ' <small>(Commit ' . htmlspecialchars((string)($health['release']['commit'] ?? '?'))
|
||||||
|
. ', gebaut ' . htmlspecialchars($this->fmtDate((string)($health['release']['builtAt'] ?? ''))) . ')</small>'];
|
||||||
|
$rows[] = ['Seiten-Cache', (int)($health['cachedPages'] ?? 0) . ' Seiten / ' . (int)($health['cacheMB'] ?? 0) . ' MB'];
|
||||||
|
}
|
||||||
|
if ($lastPurge !== null) {
|
||||||
|
$rows[] = ['Letzter Purge', htmlspecialchars(date('d.m. H:i', (int)$lastPurge['crdate']))
|
||||||
|
. ' <small>(' . htmlspecialchars((string)$lastPurge['trigger_source']) . ', '
|
||||||
|
. ($lastPurge['success'] ? 'ok' : 'FEHLER') . ', '
|
||||||
|
. htmlspecialchars(mb_strimwidth((string)$lastPurge['paths'], 0, 60, '…')) . ')</small>'];
|
||||||
|
}
|
||||||
|
if (!$this->purgeService->isConfigured()) {
|
||||||
|
$rows[] = ['Auto-Purge', '<span class="badge badge-warning">nicht konfiguriert (var/vitec-ssr-purge.env fehlt)</span>'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$html = '<table class="table table-striped table-sm"><tbody>';
|
||||||
|
foreach ($rows as [$label, $value]) {
|
||||||
|
$html .= '<tr><th style="width:38%">' . htmlspecialchars($label) . '</th><td>' . $value . '</td></tr>';
|
||||||
|
}
|
||||||
|
$html .= '</tbody></table>';
|
||||||
|
$html .= '<p class="text-muted" style="font-size:11px">Schalter: Site Management › Settings › VITEC. Modul "VITEC SSR" für Purge & Protokoll.</p>';
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOptions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fetchHealth(): ?array
|
||||||
|
{
|
||||||
|
$endpoint = $this->purgeService->getHealthEndpoint();
|
||||||
|
if ($endpoint === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$start = microtime(true);
|
||||||
|
try {
|
||||||
|
$response = $this->requestFactory->request($endpoint, 'GET', [
|
||||||
|
'timeout' => 3, 'connect_timeout' => 2, 'http_errors' => false,
|
||||||
|
]);
|
||||||
|
if ($response->getStatusCode() !== 200) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$data = json_decode((string)$response->getBody(), true);
|
||||||
|
if (!is_array($data)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$data['responseMs'] = (int)round((microtime(true) - $start) * 1000);
|
||||||
|
return $data;
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function lastPurgeRow(): ?array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$qb = $this->connectionPool->getQueryBuilderForTable('tx_vitec_ssrpurgelog');
|
||||||
|
$row = $qb->select('*')->from('tx_vitec_ssrpurgelog')
|
||||||
|
->orderBy('uid', 'DESC')->setMaxResults(1)
|
||||||
|
->executeQuery()->fetchAssociative();
|
||||||
|
return $row ?: null;
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fmtDate(string $iso): string
|
||||||
|
{
|
||||||
|
$ts = strtotime($iso);
|
||||||
|
return $ts ? date('d.m.Y H:i', $ts) : '?';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -127,4 +127,26 @@ return [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'web_vitecssr' => [
|
||||||
|
'parent' => 'web',
|
||||||
|
'position' => ['after' => 'web_vitecimport'],
|
||||||
|
'access' => 'user',
|
||||||
|
'workspaces' => 'live',
|
||||||
|
'path' => '/module/web/vitec-ssr',
|
||||||
|
'labels' => [
|
||||||
|
'title' => 'VITEC SSR',
|
||||||
|
'shortDescription' => 'SSR delivery: VPS status, manual purge, purge/health protocol',
|
||||||
|
],
|
||||||
|
'extensionName' => 'Vitec',
|
||||||
|
'iconIdentifier' => 'actions-system-cache-clear-impact-medium',
|
||||||
|
'routes' => [
|
||||||
|
'_default' => [
|
||||||
|
'target' => \Evomedien\Vitec\Controller\Backend\SsrController::class . '::indexAction',
|
||||||
|
],
|
||||||
|
'purge' => [
|
||||||
|
'target' => \Evomedien\Vitec\Controller\Backend\SsrController::class . '::purgeAction',
|
||||||
|
'methods' => ['POST'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
@@ -22,3 +22,32 @@ services:
|
|||||||
# the final page title (seo.title) into meta.title as well.
|
# the final page title (seo.title) into meta.title as well.
|
||||||
FriendsOfTYPO3\Headless\Seo\MetaHandlerInterface:
|
FriendsOfTYPO3\Headless\Seo\MetaHandlerInterface:
|
||||||
alias: Evomedien\Vitec\Seo\VitecMetaHandler
|
alias: Evomedien\Vitec\Seo\VitecMetaHandler
|
||||||
|
|
||||||
|
# The DataHandler hook fetches this via GeneralUtility::makeInstance,
|
||||||
|
# which resolves constructor arguments only for container-public services.
|
||||||
|
Evomedien\Vitec\Service\SsrPurgeService:
|
||||||
|
public: true
|
||||||
|
|
||||||
|
# SSR dashboard tiles (the WidgetConfigurationInterface argument is
|
||||||
|
# injected by the dashboard compiler pass via the tag).
|
||||||
|
Evomedien\Vitec\Widgets\SsrStatusWidget:
|
||||||
|
tags:
|
||||||
|
- name: dashboard.widget
|
||||||
|
identifier: vitecSsrStatus
|
||||||
|
groupNames: systemInformation
|
||||||
|
title: 'VITEC: SSR / VPS-Status'
|
||||||
|
description: 'Live-Zustand des SSR-Servers (Release, Cache, Erreichbarkeit) und der Auslieferungs-Schalter'
|
||||||
|
iconIdentifier: 'content-widget-text'
|
||||||
|
height: 'medium'
|
||||||
|
width: 'medium'
|
||||||
|
|
||||||
|
Evomedien\Vitec\Widgets\SsrStatsWidget:
|
||||||
|
tags:
|
||||||
|
- name: dashboard.widget
|
||||||
|
identifier: vitecSsrStats
|
||||||
|
groupNames: systemInformation
|
||||||
|
title: 'VITEC: SSR-Statistik'
|
||||||
|
description: 'Purge-Protokoll und Verfügbarkeit des SSR-Servers (14 Tage Historie)'
|
||||||
|
iconIdentifier: 'content-widget-number'
|
||||||
|
height: 'medium'
|
||||||
|
width: 'medium'
|
||||||
@@ -127,3 +127,10 @@ settings:
|
|||||||
category: VITEC
|
category: VITEC
|
||||||
type: bool
|
type: bool
|
||||||
default: false
|
default: false
|
||||||
|
|
||||||
|
vitec.ssr.autoPurge:
|
||||||
|
label: 'SSR auto-purge on editor changes'
|
||||||
|
description: 'ON: saving pages/content/news automatically evicts the affected pages from the SSR page cache (menu-relevant changes evict everything). OFF: the SSR cache refreshes only by its TTL (5 min) or manual purge. Only relevant while SSR delivery is ON.'
|
||||||
|
category: VITEC
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
|
||||||
|
|
||||||
|
<h1>VITEC SSR — Steuerung & Protokoll</h1>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h3>Status</h3>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<tr>
|
||||||
|
<th>SSR-Auslieferung</th>
|
||||||
|
<td>
|
||||||
|
<f:if condition="{flagOn}">
|
||||||
|
<f:then><span class="badge badge-success">AN — Seiten kommen vom VPS</span></f:then>
|
||||||
|
<f:else><span class="badge badge-secondary">AUS — SPA wie bisher</span></f:else>
|
||||||
|
</f:if>
|
||||||
|
<br><small>Umschalten: Site Management › Settings › VITEC</small>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>SSR-Server</th>
|
||||||
|
<td>
|
||||||
|
<f:if condition="{health}">
|
||||||
|
<f:then>
|
||||||
|
<span class="badge badge-success">läuft</span> {health.responseMs} ms<br>
|
||||||
|
Release <strong>{health.release.release}</strong>
|
||||||
|
(Commit {health.release.commit})<br>
|
||||||
|
Cache: {health.cachedPages} Seiten / {health.cacheMB} MB
|
||||||
|
</f:then>
|
||||||
|
<f:else>
|
||||||
|
<span class="badge badge-danger">nicht erreichbar</span> {healthError}
|
||||||
|
</f:else>
|
||||||
|
</f:if>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Purge-Konfiguration</th>
|
||||||
|
<td>
|
||||||
|
<f:if condition="{configured}">
|
||||||
|
<f:then><span class="badge badge-success">ok</span></f:then>
|
||||||
|
<f:else><span class="badge badge-warning">var/vitec-ssr-purge.env fehlt</span></f:else>
|
||||||
|
</f:if>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h3>Cache leeren (Purge)</h3>
|
||||||
|
<form action="{f:be.uri(route: 'web_vitecssr.purge')}" method="post" class="form-group">
|
||||||
|
<label for="purge-paths">Pfade (einer je Zeile oder kommagetrennt):</label>
|
||||||
|
<textarea class="form-control" id="purge-paths" name="paths" rows="3"
|
||||||
|
placeholder="/products/aligo /insights/news"></textarea>
|
||||||
|
<input type="hidden" name="mode" value="paths">
|
||||||
|
<div style="margin-top:8px">
|
||||||
|
<button type="submit" class="btn btn-primary">Diese Pfade purgen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<form action="{f:be.uri(route: 'web_vitecssr.purge')}" method="post"
|
||||||
|
onsubmit="return confirm('Wirklich den KOMPLETTEN SSR-Cache leeren?');">
|
||||||
|
<input type="hidden" name="mode" value="all">
|
||||||
|
<button type="submit" class="btn btn-warning">Alles purgen (*)</button>
|
||||||
|
<small class="text-muted"> — nötig nach Menü-/Footer-Änderungen bei abgeschaltetem Auto-Purge</small>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Purge-Protokoll <small>(letzte 30)</small></h3>
|
||||||
|
<table class="table table-striped table-sm">
|
||||||
|
<thead><tr><th>Zeit</th><th>Auslöser</th><th>Pfade</th><th>Ergebnis</th><th>Dauer</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<f:for each="{purgeLog}" as="row">
|
||||||
|
<tr>
|
||||||
|
<td><f:format.date format="d.m.Y H:i:s">@{row.crdate}</f:format.date></td>
|
||||||
|
<td>{row.trigger_source}</td>
|
||||||
|
<td><code>{row.paths}</code></td>
|
||||||
|
<td>
|
||||||
|
<f:if condition="{row.success}">
|
||||||
|
<f:then><span class="badge badge-success">ok</span></f:then>
|
||||||
|
<f:else><span class="badge badge-danger">HTTP {row.http_code}</span> {row.message}</f:else>
|
||||||
|
</f:if>
|
||||||
|
</td>
|
||||||
|
<td>{row.duration_ms} ms</td>
|
||||||
|
</tr>
|
||||||
|
</f:for>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Gesundheits-Messungen <small>(letzte 10 — Historie via Scheduler-Task <code>vitec:ssr-health</code>)</small></h3>
|
||||||
|
<table class="table table-striped table-sm">
|
||||||
|
<thead><tr><th>Zeit</th><th>Status</th><th>Release</th><th>Cache</th><th>Antwortzeit</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<f:for each="{healthLog}" as="row">
|
||||||
|
<tr>
|
||||||
|
<td><f:format.date format="d.m.Y H:i:s">@{row.crdate}</f:format.date></td>
|
||||||
|
<td>
|
||||||
|
<f:if condition="{row.ok}">
|
||||||
|
<f:then><span class="badge badge-success">ok</span></f:then>
|
||||||
|
<f:else><span class="badge badge-danger">down</span></f:else>
|
||||||
|
</f:if>
|
||||||
|
</td>
|
||||||
|
<td>{row.release_id}</td>
|
||||||
|
<td>{row.cached_pages} Seiten / {row.cache_mb} MB</td>
|
||||||
|
<td>{row.response_ms} ms</td>
|
||||||
|
</tr>
|
||||||
|
</f:for>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -260,3 +260,11 @@ $GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'][] = 'page';
|
|||||||
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'][] = 'filter';
|
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'][] = 'filter';
|
||||||
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'][] = 'market';
|
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'][] = 'market';
|
||||||
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'][] = 'category';
|
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'][] = 'category';
|
||||||
|
|
||||||
|
// SSR auto-purge: editors' changes evict the matching pages from the SSR
|
||||||
|
// node's full-page cache (queued per request, one call per save/import).
|
||||||
|
// Active only while the SSR flag is ON and vitec.ssr.autoPurge is enabled.
|
||||||
|
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass']['vitec_ssrpurge']
|
||||||
|
= \Evomedien\Vitec\Hooks\DataHandlerSsrPurge::class;
|
||||||
|
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass']['vitec_ssrpurge']
|
||||||
|
= \Evomedien\Vitec\Hooks\DataHandlerSsrPurge::class;
|
||||||
@@ -460,3 +460,36 @@ CREATE TABLE tx_vitec_domain_model_searchcard (
|
|||||||
link varchar(1024) DEFAULT '' NOT NULL,
|
link varchar(1024) DEFAULT '' NOT NULL,
|
||||||
link_label varchar(80) DEFAULT '' NOT NULL
|
link_label varchar(80) DEFAULT '' NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#
|
||||||
|
# SSR purge protocol (auto-purge webhook + backend module, 2026-11-09)
|
||||||
|
#
|
||||||
|
CREATE TABLE tx_vitec_ssrpurgelog (
|
||||||
|
uid int(11) NOT NULL auto_increment,
|
||||||
|
pid int(11) DEFAULT '0' NOT NULL,
|
||||||
|
crdate int(11) DEFAULT '0' NOT NULL,
|
||||||
|
trigger_source varchar(100) DEFAULT '' NOT NULL,
|
||||||
|
paths text,
|
||||||
|
success tinyint(1) DEFAULT '0' NOT NULL,
|
||||||
|
http_code int(11) DEFAULT '0' NOT NULL,
|
||||||
|
message varchar(255) DEFAULT '' NOT NULL,
|
||||||
|
duration_ms int(11) DEFAULT '0' NOT NULL,
|
||||||
|
PRIMARY KEY (uid),
|
||||||
|
KEY crdate (crdate)
|
||||||
|
);
|
||||||
|
|
||||||
|
#
|
||||||
|
# SSR health history (vitec:ssr-health via scheduler, dashboard widgets)
|
||||||
|
#
|
||||||
|
CREATE TABLE tx_vitec_ssrhealthlog (
|
||||||
|
uid int(11) NOT NULL auto_increment,
|
||||||
|
pid int(11) DEFAULT '0' NOT NULL,
|
||||||
|
crdate int(11) DEFAULT '0' NOT NULL,
|
||||||
|
ok tinyint(1) DEFAULT '0' NOT NULL,
|
||||||
|
release_id varchar(100) DEFAULT '' NOT NULL,
|
||||||
|
cached_pages int(11) DEFAULT '0' NOT NULL,
|
||||||
|
cache_mb int(11) DEFAULT '0' NOT NULL,
|
||||||
|
response_ms int(11) DEFAULT '0' NOT NULL,
|
||||||
|
PRIMARY KEY (uid),
|
||||||
|
KEY crdate (crdate)
|
||||||
|
);
|
||||||
Reference in new issue
Block a user