Files
maggesandClaude Sonnet 5 23898ff62e
Lint / ShellCheck (push) Successful in 11s
Lint / Validate .plg XML (push) Successful in 10s
Lint / EditorConfig (push) Successful in 6s
Rework Dashboard, add toasts, container folders/icons/WebUI links, detail tabs
Dashboard:
- Plain-count stat tiles (Running/Stopped/Pods/Images/Volumes/Networks)
  separated from a single "Resource Usage" card (CPU/Memory/Swap/Storage
  meter rows) instead of forcing both into one tile grid, which produced
  awkward spanning-tile/dead-cell layouts.
- Fixed CPU usage never changing (libpod's own cpuUtilization is computed
  once and never resampled) by computing it from /proc/stat deltas instead.
- Fixed memory usage reading far too high by using /proc/meminfo's
  MemAvailable instead of libpod's raw (non-reclaimable-aware) memFree.
- Added an Autostart Queue table reusing podman-autostart.sh's own
  failure-counter files.
- Dashboard and Containers now auto-refresh every ~2s (paused when the
  tab is hidden or a modal is open).

Toasts:
- Real success/warn/error/info toast notifications replacing every
  alert() used for one-way feedback, across every panel.

Container detail modal:
- 5 new tabs: Resources, Logs, Console, Events, Healthcheck.

Containers panel:
- Folders to group containers (name + icon), stored in the plugin's own
  folders.json — a folder's header always shows an icon+name+status chip
  per member, matching Unraid's own Docker page folders. "Move to
  Folder" becomes "Remove from Folder" once a container is already
  grouped.
- Containers can carry an icon URL and a WebUI URL (small button next to
  the name), both stored as container labels and auto-filled from
  templates where applicable.
- Settings: an "Add container" control for the Autostart order table.

Fixes:
- Context menus now measure their own rendered size and flip above the
  anchor when there isn't room below, instead of running off-screen.
- Containers table now uses table-layout:fixed with explicit column
  widths — auto layout was shifting every column (and the header) on
  every folder expand/collapse, and briefly again when a flex wrapper
  was mistakenly placed directly on a <td>.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 21:32:34 +00:00

205 lines
7.6 KiB
PHP

<?php
/**
* ajax/settings.php
*
* Backs the Settings panel. Unlike every other ajax/*.php file, this one
* is NOT primarily a PodmanClient consumer — plugin settings (podman.cfg,
* the autostart list, installed package versions) are unraid-podman's own
* data on /boot, not a libpod-managed resource, so there is no API to call
* here in the first place. Writes go straight to the same files
* plugin/sbin/podman-config.sh and podman-autostart.sh read, using the
* same paths (see include/Config.php, which mirrors podman-common.sh's
* path constants).
*
* Actions (?action=...):
* get GET -> current settings + autostart list + package versions
* save POST {"storagePath": "...", "storageImageSizeGb": 20,
* "enabled": true, "stopTimeoutSeconds": 10}
* autostart_save POST {"names": ["postgres", "nextcloud", ...]}
* service_status GET -> {"running": bool, "output": "..."}
* service_start POST -> {"running": bool, "output": "..."}
* service_stop POST -> {"running": bool, "output": "..."}
* service_restart POST -> {"running": bool, "output": "..."}
*/
declare(strict_types=1);
require __DIR__ . '/../include/bootstrap.php';
const RC_PODMAN = '/etc/rc.d/rc.podman';
$action = $_GET['action'] ?? '';
switch ($action) {
case 'get':
podman_json_response(settings_get($podmanConfig));
break;
case 'save':
$body = podman_read_json_body();
settings_save($podmanConfig, $body);
podman_json_response(['status' => 'saved']);
break;
case 'autostart_save':
$body = podman_read_json_body();
$names = $body['names'] ?? null;
if (!is_array($names)) {
podman_json_error('Missing names array in request body', 400);
}
autostart_save($podmanConfig, $names);
podman_json_response(['status' => 'saved']);
break;
case 'service_status':
podman_json_response(rc_podman('status', 15));
break;
case 'service_start':
podman_json_response(rc_podman('start', 120));
break;
case 'service_stop':
podman_json_response(rc_podman('stop', 120));
break;
case 'service_restart':
podman_json_response(rc_podman('restart', 120));
break;
default:
podman_json_error("Unknown action '{$action}'", 400);
}
/**
* Shells out to /etc/rc.d/rc.podman <verb> — the plugin's own real
* start/stop/status script, the SAME one the array-start event hook and
* a terminal `rc.podman status` use (see plugin/rc.d/rc.podman's header
* comment). This exists specifically so a fresh install where podman
* failed to start (e.g. no cache pool configured yet, see
* podman-storage.sh's "does not exist or is not mounted" error) can be
* diagnosed and retried from the WebUI itself — no SSH/terminal access
* needed, which is exactly what was missing when this was first needed
* live (a fresh install on a different Unraid box with nobody able to
* reach a terminal to run `rc.podman start` by hand).
*
* @return array{running: bool, output: string}
*/
function rc_podman(string $verb, int $timeoutSeconds): array
{
$output = run_rc_podman($verb, $timeoutSeconds);
// Only `rc.podman status` prints the "service: running
// (pid ..., socket ...)" line this regex looks for — start/stop/
// restart's OWN messages are worded differently ("start: already
// running (pid ...)", "stop: stopped", ...), so relying on THIS same
// regex against THEIR output silently reported "not running" right
// after a successful start (found live: a start that printed "start:
// already running" turned the status chip red). Always running a
// fresh `status` afterward — regardless of which verb was actually
// requested — is the one output format this check can trust.
$statusOutput = $verb === 'status' ? $output : run_rc_podman('status', 15);
$running = (bool) preg_match('/service:\s+running/', $statusOutput);
return ['running' => $running, 'output' => $output];
}
function run_rc_podman(string $verb, int $timeoutSeconds): string
{
$descriptors = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
$process = proc_open([RC_PODMAN, $verb], $descriptors, $pipes);
if (!is_resource($process)) {
podman_json_error("Could not run rc.podman {$verb}", 500);
}
stream_set_timeout($pipes[1], $timeoutSeconds);
$stdout = stream_get_contents($pipes[1]) ?: '';
$stderr = stream_get_contents($pipes[2]) ?: '';
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
return trim($stdout . $stderr);
}
/** @return array<string,mixed> */
function settings_get(PodmanConfig $config): array
{
return [
'storagePath' => $config->storagePath,
'storageImageSizeGb' => $config->storageImageSizeGb,
'enabled' => $config->enabled,
'stopTimeoutSeconds' => $config->stopTimeoutSeconds,
'autostart' => autostart_read($config),
'packageVersions' => podman_read_installed_versions(),
];
}
/** @param array<string,mixed> $input */
function settings_save(PodmanConfig $config, array $input): void
{
$storagePath = isset($input['storagePath']) ? (string) $input['storagePath'] : $config->storagePath;
if (!storage_path_is_safe($storagePath)) {
podman_json_error(
"storagePath ({$storagePath}) is under /mnt/user (FUSE/shfs). " .
'The overlay storage driver needs a real mounted filesystem — use a cache pool or a specific disk path instead.',
400
);
}
$lines = [
'# Rewritten by the unraid-podman WebUI (ajax/settings.php).',
'# Applies on the next "rc.podman restart" — see plugin/rc.d/rc.podman.',
'STORAGE_PATH="' . $storagePath . '"',
'STORAGE_IMAGE_SIZE_GB="' . (int) ($input['storageImageSizeGb'] ?? $config->storageImageSizeGb) . '"',
'PODMAN_ENABLED="' . ((bool) ($input['enabled'] ?? $config->enabled) ? 'yes' : 'no') . '"',
'STOP_TIMEOUT="' . (int) ($input['stopTimeoutSeconds'] ?? $config->stopTimeoutSeconds) . '"',
'CONFIG_SCHEMA_VERSION="1"',
'',
];
$target = $config->bootDir . '/podman.cfg';
if (file_put_contents($target, implode("\n", $lines), LOCK_EX) === false) {
podman_json_error("Could not write {$target} — check permissions on /boot/config/plugins/podman/", 500);
}
}
/**
* Mirrors plugin/sbin/podman-common.sh's podman_storage_path_is_safe() —
* kept in sync deliberately (both reject the same /mnt/user prefix, for
* the same reason) rather than shelling out to the bash version, since
* this is a one-line string check, not worth a process spawn for.
*/
function storage_path_is_safe(string $path): bool
{
return $path !== '/mnt/user' && !str_starts_with($path, '/mnt/user/');
}
/** @return array<int,string> */
function autostart_read(PodmanConfig $config): array
{
if (!is_readable($config->autostartFile)) {
return [];
}
$lines = file($config->autostartFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
$names = [];
foreach ($lines as $line) {
$line = trim(preg_replace('/#.*$/', '', $line) ?? '');
if ($line !== '') {
$names[] = $line;
}
}
return $names;
}
/** @param array<int,mixed> $names */
function autostart_save(PodmanConfig $config, array $names): void
{
$lines = array_map(static fn($n) => (string) $n, $names);
$content = implode("\n", $lines) . (count($lines) > 0 ? "\n" : '');
if (file_put_contents($config->autostartFile, $content, LOCK_EX) === false) {
podman_json_error("Could not write {$config->autostartFile}", 500);
}
}