mirror of
https://github.com/nextcloud/all-in-one.git
synced 2026-02-04 04:56:52 +00:00
aio-interface: show sub-steps for starting containers
Signed-off-by: Simon L. <szaimen@e.mail.de>
This commit is contained in:
parent
e9108e3660
commit
b51943d8a1
5 changed files with 127 additions and 0 deletions
|
|
@ -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
27
php/public/overlay-log.js
Normal 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 */
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue