Merge claude/1
This commit is contained in:
commit
0d10284203
@ -11,9 +11,11 @@
|
||||
// directly over the unix socket. That means no extra system deps and
|
||||
// no group-level privilege grants — node only sees what the mounted
|
||||
// socket lets it see.
|
||||
// - Restart still goes through the existing task system. The bash task
|
||||
// processor runs on the host (where `docker` IS available) so its
|
||||
// `docker compose restart …` command works fine.
|
||||
// - This file is READ-ONLY surface (status + log tails). Restarting a
|
||||
// service is a mutation, so it goes through the task system like every
|
||||
// other mutation: the Services tab dispatches a `service_restart` task
|
||||
// (core/tasks) that runs `libreportal app restart <app> <service>` on
|
||||
// the host, where `docker` IS available and runs as the right user.
|
||||
// - URLs / port chips for each service are read client-side from the
|
||||
// existing /data/apps/generated/apps-services.json — no backend
|
||||
// surface needed for that.
|
||||
@ -25,18 +27,10 @@ const path = require('path');
|
||||
const http = require('http');
|
||||
const { spawn } = require('child_process');
|
||||
const { requireAuth } = require('../utils/middleware.js');
|
||||
const { pokeFifo } = require('../utils/fifo.js');
|
||||
const { fileConfig } = require('../utils/config.js');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const TASKS_DIR = path.join(__dirname, '..', '..', 'frontend', 'data', 'tasks');
|
||||
const FIFO_PATH = path.join(TASKS_DIR, '.queue.fifo');
|
||||
// Host live-app-data root. Provided by the compose env (LP_CONTAINERS_DIR, filled
|
||||
// from the host's containers root at generation — see scripts/source/paths.sh).
|
||||
// Falls back to the legacy /docker path so a container that hasn't been recreated
|
||||
// since the split-layout change keeps working until it is.
|
||||
const CONTAINERS_DIR = process.env.LP_CONTAINERS_DIR || '/docker/containers';
|
||||
const APPS_SERVICES_JSON = path.join(__dirname, '..', '..', 'frontend', 'data', 'apps', 'generated', 'apps-services.json');
|
||||
|
||||
// =====================================================================
|
||||
@ -309,10 +303,6 @@ async function lookupServiceTransport(appName, serviceName) {
|
||||
return { transport: 'docker' };
|
||||
}
|
||||
|
||||
function appComposeFile(appName) {
|
||||
return path.join(CONTAINERS_DIR, appName, 'docker-compose.yml');
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// GET /api/apps/:appName/services/status
|
||||
// → [{ serviceName, state, statusText, containerName, containerId }]
|
||||
@ -372,50 +362,6 @@ router.get('/:appName/services/status', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// =====================================================================
|
||||
// POST /api/apps/:appName/services/:serviceName/restart
|
||||
// Creates a task that runs `docker compose restart <service>` on the
|
||||
// host. The host has `docker` available; this container does not.
|
||||
// =====================================================================
|
||||
router.post('/:appName/services/:serviceName/restart', requireAuth, async (req, res) => {
|
||||
const { appName, serviceName } = req.params;
|
||||
if (!safeName(appName) || !safeName(serviceName)) {
|
||||
return res.status(400).json({ error: 'Invalid app or service name' });
|
||||
}
|
||||
const compose = appComposeFile(appName);
|
||||
if (!fs.existsSync(compose)) {
|
||||
return res.status(404).json({ error: `Compose file not found: ${compose}` });
|
||||
}
|
||||
|
||||
const id = `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
const task = {
|
||||
id,
|
||||
command: `docker compose -f "${compose}" restart "${serviceName}"`,
|
||||
type: 'service-restart',
|
||||
app: appName,
|
||||
config: serviceName,
|
||||
status: 'queued',
|
||||
createdAt: new Date().toISOString(),
|
||||
startedAt: null,
|
||||
completedAt: null,
|
||||
heartbeatAt: null,
|
||||
exitCode: null,
|
||||
errorMessage: null
|
||||
};
|
||||
|
||||
try {
|
||||
await fsp.mkdir(TASKS_DIR, { recursive: true });
|
||||
const taskPath = path.join(TASKS_DIR, `${id}.json`);
|
||||
const tmp = `${taskPath}.tmp`;
|
||||
await fsp.writeFile(tmp, JSON.stringify(task, null, 2));
|
||||
await fsp.rename(tmp, taskPath);
|
||||
pokeFifo(FIFO_PATH, id);
|
||||
res.status(201).json(task);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// =====================================================================
|
||||
// GET /api/apps/:appName/services/:serviceName/logs
|
||||
// SSE-wraps the Docker /containers/<id>/logs?follow=1 stream.
|
||||
|
||||
@ -575,24 +575,33 @@ class ServicesManager {
|
||||
});
|
||||
}
|
||||
|
||||
// Restart one compose service as a background task: routed through the task
|
||||
// system to the locked-down CLI (libreportal app restart <app> <service>) —
|
||||
// same path as the whole-app restart, scoped to one service. The task runs
|
||||
// host-side, so this works even when the target is the WebUI's own service.
|
||||
// No page switch; the task toast (with a link) comes from the task system,
|
||||
// and the rows below re-poll their live status.
|
||||
async _restartService(serviceName, btn) {
|
||||
if (!this.currentApp) return;
|
||||
btn.disabled = true;
|
||||
btn.classList.add('is-running');
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`/api/apps/${encodeURIComponent(this.currentApp)}/services/${encodeURIComponent(serviceName)}/restart`,
|
||||
{ method: 'POST', headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.error || `HTTP ${resp.status}`);
|
||||
const route = window.LP?.services?.tasks?.route || window.tasksManager?.router;
|
||||
if (route && typeof route.routeAction === 'function') {
|
||||
await route.routeAction('service_restart', { appName: this.currentApp, service: serviceName });
|
||||
} else if (typeof route === 'function') {
|
||||
await route('service_restart', { appName: this.currentApp, service: serviceName });
|
||||
} else {
|
||||
throw new Error('Task system not ready — try again in a moment.');
|
||||
}
|
||||
// Background task processor picks it up — refresh status shortly.
|
||||
// The restart runs in the background — refresh status as it lands.
|
||||
setTimeout(() => this._refreshStatusOnly(), 2500);
|
||||
setTimeout(() => this._refreshStatusOnly(), 7000);
|
||||
setTimeout(() => this._refreshStatusOnly(), 15000);
|
||||
} catch (e) {
|
||||
alert(`Restart failed: ${e.message}`);
|
||||
if (window.notificationSystem?.show) {
|
||||
window.notificationSystem.show(`Restart of ${serviceName} failed: ${e.message}`, 'error');
|
||||
}
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
btn.disabled = false;
|
||||
|
||||
@ -98,6 +98,25 @@ class TaskActions {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart a single compose service of an application (Services tab row
|
||||
* button). Same locked-down CLI as the whole-app restart, scoped to one
|
||||
* service — runs as a background task, no page switch.
|
||||
*/
|
||||
async restartService(appName, serviceName) {
|
||||
try {
|
||||
this.commands.validateCommand('service_restart', { appName, service: serviceName });
|
||||
return await this.executeTask(
|
||||
'service_restart',
|
||||
appName,
|
||||
`libreportal app restart ${appName} ${serviceName}`,
|
||||
`Restart ${serviceName}`
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to restart ${serviceName}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start an application
|
||||
*/
|
||||
|
||||
@ -10,6 +10,8 @@ class TaskCommands {
|
||||
install: 'libreportal app install {appName} {config}',
|
||||
uninstall: 'libreportal app uninstall {appName}',
|
||||
restart: 'libreportal app restart {appName}',
|
||||
// Restart a single compose service of an app (Services tab row button).
|
||||
service_restart: 'libreportal app restart {appName} {service}',
|
||||
start: 'libreportal app start {appName}',
|
||||
stop: 'libreportal app stop {appName}',
|
||||
backup: 'libreportal app backup {appName}',
|
||||
@ -45,6 +47,7 @@ class TaskCommands {
|
||||
install: 'implemented',
|
||||
uninstall: 'implemented',
|
||||
restart: 'implemented',
|
||||
service_restart: 'implemented',
|
||||
start: 'implemented',
|
||||
stop: 'implemented',
|
||||
backup: 'implemented',
|
||||
@ -139,6 +142,7 @@ class TaskCommands {
|
||||
install: ['appName'], // config is optional
|
||||
uninstall: ['appName'],
|
||||
restart: ['appName'],
|
||||
service_restart: ['appName', 'service'],
|
||||
start: ['appName'],
|
||||
stop: ['appName'],
|
||||
backup: ['appName'],
|
||||
|
||||
@ -33,6 +33,9 @@ class TaskRouter {
|
||||
case 'restart':
|
||||
return await this.actions.restartApp(params.appName);
|
||||
|
||||
case 'service_restart':
|
||||
return await this.actions.restartService(params.appName, params.service);
|
||||
|
||||
case 'start':
|
||||
return await this.actions.startApp(params.appName);
|
||||
|
||||
|
||||
@ -95,10 +95,13 @@ cliHandleAppCommands()
|
||||
;;
|
||||
|
||||
"restart")
|
||||
# Optional 4th arg = one compose service to restart instead of the
|
||||
# whole app (the WebUI Services tab routes per-service restarts
|
||||
# here): libreportal app restart <app> [service]
|
||||
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
|
||||
dockerRestartApp "$app_name"
|
||||
dockerRestartApp "$app_name" "$config"
|
||||
else
|
||||
cliTaskRun "libreportal app restart $app_name" "restart" "$app_name"
|
||||
cliTaskRun "libreportal app restart $app_name${config:+ $config}" "restart" "$app_name"
|
||||
fi
|
||||
;;
|
||||
|
||||
|
||||
@ -20,7 +20,8 @@ cliShowAppHelp()
|
||||
echo ""
|
||||
echo " libreportal app start [name*] - Start the specified app (Must be installed)"
|
||||
echo " libreportal app stop [name*] - Stop the specified app (Must be installed)"
|
||||
echo " libreportal app restart [name*] - Restart the specified app (Must be installed)"
|
||||
echo " libreportal app restart [name*] [service] - Restart the specified app, or just one of its"
|
||||
echo " compose services when [service] is given"
|
||||
echo ""
|
||||
echo " libreportal app up [name*] - Docker-Compose up (Rebuild app)"
|
||||
echo " libreportal app down [name*] - Docker-Compose up (Uninstall app)"
|
||||
|
||||
@ -3,9 +3,32 @@
|
||||
dockerRestartApp()
|
||||
{
|
||||
local app_name="$1"
|
||||
local service_name="$2" # optional: restart just this one compose service
|
||||
|
||||
if [[ -z "$app_name" ]]; then
|
||||
isNotice "No app name provided. Unable to restart containers."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Single-service restart — the WebUI Services tab's per-row button routes
|
||||
# here as a task (libreportal app restart <app> <service>). Uses compose so
|
||||
# the restart respects the deployed service definition, and dockerCommandRun
|
||||
# so it runs as the right user in rootless mode.
|
||||
if [[ -n "$service_name" ]]; then
|
||||
# The name reaches a shell line — accept only compose-legal names.
|
||||
if [[ ! "$service_name" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then
|
||||
isError "Invalid service name: $service_name"
|
||||
return 1
|
||||
fi
|
||||
local app_dir="${containers_dir%/}/$app_name"
|
||||
if [[ ! -f "$app_dir/docker-compose.yml" ]]; then
|
||||
isError "No compose file for '$app_name' at $app_dir/docker-compose.yml"
|
||||
return 1
|
||||
fi
|
||||
isNotice "Restarting service '$service_name' of '$app_name'. Please wait..."
|
||||
local result; result=$(dockerCommandRun "cd $app_dir && docker compose restart $service_name" 2>&1)
|
||||
checkSuccess "Restarted service '$service_name' of '$app_name'"
|
||||
return
|
||||
fi
|
||||
|
||||
isNotice "Restarting Docker containers for '$app_name'. Please wait..."
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user