aio-interface: show sub-steps for starting containers

Signed-off-by: Simon L. <szaimen@e.mail.de>
This commit is contained in:
Simon L. 2026-01-19 16:26:28 +01:00
parent e9108e3660
commit b51943d8a1
5 changed files with 127 additions and 0 deletions

View file

@ -142,6 +142,20 @@ $app->get('/containers', function (Request $request, Response $response, array $
'bypass_container_update' => $bypass_container_update,
]);
})->setName('profile');
// Server-Sent Events endpoint for container events (container-start)
$app->get('/events/containers', function (Request $request, Response $response, array $args) use ($container) {
// Only allow authenticated sessions to access SSE
$authManager = $container->get(\AIO\Auth\AuthManager::class);
if (!$authManager->IsAuthenticated()) {
return $response->withStatus(401);
}
// Delegate streaming logic to the DockerController
$dockerController = $container->get(\AIO\Controller\DockerController::class);
return $dockerController->StreamContainerEvents($response);
});
$app->get('/login', function (Request $request, Response $response, array $args) use ($container) {
$view = Twig::fromRequest($request);
/** @var \AIO\Docker\DockerActionManager $dockerActionManager */

27
php/public/overlay-log.js Normal file
View file

@ -0,0 +1,27 @@
document.addEventListener("DOMContentLoaded", function(event) {
function displayOverlayLogMessage(message) {
const overlayLogElement = document.getElementById('overlay-log');
if (!overlayLogElement) {
return;
}
overlayLogElement.textContent = message;
}
// Attempt to connect to Server-Sent Events at /events/containers and listen for 'container-start' events
if (typeof EventSource !== 'undefined') {
try {
const serverSentEventSource = new EventSource('events/containers');
serverSentEventSource.addEventListener('container-start', function(serverSentEvent) {
try {
let parsedPayload = JSON.parse(serverSentEvent.data);
displayOverlayLogMessage(parsedPayload.name || serverSentEvent.data);
} catch (parseError) {
displayOverlayLogMessage(serverSentEvent.data);
}
});
serverSentEventSource.onerror = function() { serverSentEventSource.close(); };
} catch (connectionError) {
/* ignore if Server-Sent Events are not available */
}
}
});

View file

@ -8,6 +8,7 @@ use AIO\Docker\DockerActionManager;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use AIO\Data\ConfigurationManager;
use AIO\Data\DataConst;
readonly class DockerController {
private const string TOP_CONTAINER = 'nextcloud-aio-apache';
@ -34,6 +35,15 @@ readonly class DockerController {
return;
}
// Emit a container-start event for frontend clients (one JSON line per event)
try {
$this->pruneEventsFileIfTooLarge();
$this->writeEventsToFile(['event' => 'Starting container', 'name' => $id, 'time' => time()]);
} catch (\Throwable $e) {
// non-fatal, just log
error_log('Could not write container-start event: ' . $e->getMessage());
}
$this->dockerActionManager->DeleteContainer($container);
$this->dockerActionManager->CreateVolumes($container);
$this->dockerActionManager->PullImage($container, $pullImage);
@ -261,6 +271,48 @@ readonly class DockerController {
return $response->withStatus(201)->withHeader('Location', '.');
}
public function StreamContainerEvents(Response $response): Response {
$eventsFile = \AIO\Data\DataConst::GetContainerEventsFile();
if (!file_exists($eventsFile)) {
@touch($eventsFile);
}
$body = $response->getBody();
$response = $response
->withHeader('Content-Type', 'text/event-stream')
->withHeader('Cache-Control', 'no-cache')
->withHeader('Connection', 'keep-alive');
$fileHandle = fopen($eventsFile, 'r');
if ($fileHandle === false) {
$body->write('');
return $response;
}
// Start at end of file so only new events are streamed
fseek($fileHandle, 0, SEEK_END);
while (!connection_aborted()) {
clearstatcache(false, $eventsFile);
$line = fgets($fileHandle);
if ($line !== false) {
$data = trim($line);
// Write SSE event
$body->write("event: container-start\n");
$body->write("data: $data\n\n");
$body->flush();
// Small pause to avoid tight loop
usleep(100000);
} else {
// No new data, wait a moment
usleep(200000);
}
}
fclose($fileHandle);
return $response;
}
public function stopTopContainer() : void {
$id = self::TOP_CONTAINER;
$this->PerformRecursiveContainerStop($id);
@ -307,4 +359,32 @@ readonly class DockerController {
$id = 'nextcloud-aio-domaincheck';
$this->PerformRecursiveContainerStop($id);
}
// Write container event to events file and prune old events
private function writeEventsToFile(array $payload): void {
$eventJson = json_encode($payload);
// Append new event (atomic via LOCK_EX)
file_put_contents($eventsFile, $eventJson . PHP_EOL, FILE_APPEND | LOCK_EX);
}
// Truncate the events file to keep only the last $maxBytes bytes, aligned to a newline boundary.
private function pruneEventsFileIfTooLarge(): void {
$eventsFile = DataConst::GetContainerEventsFile();
$maxBytes = 512 * 1024; // 512 KB
$maxLines = 1000; // keep last 1000 events
if (!file_exists($eventsFile) || filesize($eventsFile) <= $maxBytes) {
return;
}
$lines = file($eventsFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($lines !== false) {
$total = count($lines);
$start = max(0, $total - $maxLines);
$keep = array_slice($lines, $start);
// rewrite file with kept lines
file_put_contents($eventsFile, implode(PHP_EOL, $keep) . PHP_EOL, LOCK_EX);
}
}
}

View file

@ -66,4 +66,8 @@ class DataConst {
public static function GetContainersDefinitionPath() : string {
return (string)realpath(__DIR__ . '/../../containers.json');
}
public static function GetContainerEventsFile() : string {
return self::GetDataDirectory() . '/container_events.log';
}
}

View file

@ -13,7 +13,9 @@
</div>
<div id="overlay">
<div class="loader"></div>
<div id="overlay-log"></div>
</div>
<script src="overlay-log.js"></script>
<button id="theme-toggle" onclick="toggleTheme()">
<span id="theme-icon"></span>
</button>