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,
|
'bypass_container_update' => $bypass_container_update,
|
||||||
]);
|
]);
|
||||||
})->setName('profile');
|
})->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) {
|
$app->get('/login', function (Request $request, Response $response, array $args) use ($container) {
|
||||||
$view = Twig::fromRequest($request);
|
$view = Twig::fromRequest($request);
|
||||||
/** @var \AIO\Docker\DockerActionManager $dockerActionManager */
|
/** @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\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use AIO\Data\ConfigurationManager;
|
use AIO\Data\ConfigurationManager;
|
||||||
|
use AIO\Data\DataConst;
|
||||||
|
|
||||||
readonly class DockerController {
|
readonly class DockerController {
|
||||||
private const string TOP_CONTAINER = 'nextcloud-aio-apache';
|
private const string TOP_CONTAINER = 'nextcloud-aio-apache';
|
||||||
|
|
@ -34,6 +35,15 @@ readonly class DockerController {
|
||||||
return;
|
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->DeleteContainer($container);
|
||||||
$this->dockerActionManager->CreateVolumes($container);
|
$this->dockerActionManager->CreateVolumes($container);
|
||||||
$this->dockerActionManager->PullImage($container, $pullImage);
|
$this->dockerActionManager->PullImage($container, $pullImage);
|
||||||
|
|
@ -261,6 +271,48 @@ readonly class DockerController {
|
||||||
return $response->withStatus(201)->withHeader('Location', '.');
|
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 {
|
public function stopTopContainer() : void {
|
||||||
$id = self::TOP_CONTAINER;
|
$id = self::TOP_CONTAINER;
|
||||||
$this->PerformRecursiveContainerStop($id);
|
$this->PerformRecursiveContainerStop($id);
|
||||||
|
|
@ -307,4 +359,32 @@ readonly class DockerController {
|
||||||
$id = 'nextcloud-aio-domaincheck';
|
$id = 'nextcloud-aio-domaincheck';
|
||||||
$this->PerformRecursiveContainerStop($id);
|
$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 {
|
public static function GetContainersDefinitionPath() : string {
|
||||||
return (string)realpath(__DIR__ . '/../../containers.json');
|
return (string)realpath(__DIR__ . '/../../containers.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function GetContainerEventsFile() : string {
|
||||||
|
return self::GetDataDirectory() . '/container_events.log';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="overlay">
|
<div id="overlay">
|
||||||
<div class="loader"></div>
|
<div class="loader"></div>
|
||||||
|
<div id="overlay-log"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<script src="overlay-log.js"></script>
|
||||||
<button id="theme-toggle" onclick="toggleTheme()">
|
<button id="theme-toggle" onclick="toggleTheme()">
|
||||||
<span id="theme-icon"></span>
|
<span id="theme-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue