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>
151 lines
4.7 KiB
PHP
151 lines
4.7 KiB
PHP
<?php
|
|
/**
|
|
* helpers.php
|
|
*
|
|
* Small, stateless formatting helpers shared by the ajax/*.php endpoints.
|
|
* Kept separate from PodmanClient (which only knows the API) and from the
|
|
* endpoints themselves (which only know their one resource), so the same
|
|
* "format 1610612736 bytes as 1.5 GB" logic isn't duplicated across
|
|
* images.php, volumes.php, and system.php.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Reads /usr/local/share/unraid-podman/installed-versions.env — the same
|
|
* manifest plugin/sbin/podman-verify-packages.sh and
|
|
* podman-update-packages.sh use (see plugin/podman.plg's postinstall step,
|
|
* which generates it) — shared here so settings.php (package version
|
|
* chips) and system.php (Dashboard's plugin version chip) both report
|
|
* exactly what those tools would, not two possibly-diverging readers of
|
|
* the same file.
|
|
*
|
|
* @return array<string,string>
|
|
*/
|
|
function podman_read_installed_versions(): array
|
|
{
|
|
$path = '/usr/local/share/unraid-podman/installed-versions.env';
|
|
if (!is_readable($path)) {
|
|
return [];
|
|
}
|
|
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
|
$out = [];
|
|
foreach ($lines as $line) {
|
|
$line = trim($line);
|
|
if ($line === '' || str_starts_with($line, '#')) {
|
|
continue;
|
|
}
|
|
if (preg_match('/^([A-Z_][A-Z0-9_]*)="?([^"]*)"?$/', $line, $m)) {
|
|
$out[$m[1]] = $m[2];
|
|
}
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
function podman_format_bytes(int $bytes): string
|
|
{
|
|
if ($bytes <= 0) {
|
|
return '0 B';
|
|
}
|
|
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
$i = (int) floor(log($bytes, 1024));
|
|
$i = min($i, count($units) - 1);
|
|
$value = $bytes / (1024 ** $i);
|
|
return sprintf($value >= 100 || $i === 0 ? '%.0f %s' : '%.1f %s', $value, $units[$i]);
|
|
}
|
|
|
|
/** Formats a duration in seconds as a compact "14d 6h" / "6h 12m" / "38m" style string. */
|
|
function podman_format_duration(int $seconds): string
|
|
{
|
|
if ($seconds < 60) {
|
|
return $seconds . 's';
|
|
}
|
|
$days = intdiv($seconds, 86400);
|
|
$hours = intdiv($seconds % 86400, 3600);
|
|
$minutes = intdiv($seconds % 3600, 60);
|
|
|
|
if ($days > 0) {
|
|
return "{$days}d {$hours}h";
|
|
}
|
|
if ($hours > 0) {
|
|
return "{$hours}h {$minutes}m";
|
|
}
|
|
return "{$minutes}m";
|
|
}
|
|
|
|
/**
|
|
* Converts a libpod timestamp to a Unix timestamp, or null if unparsable.
|
|
* Accepts both the RFC3339 strings `inspect`-style endpoints return and
|
|
* the raw Unix-epoch integers `list`-style endpoints return for the same
|
|
* logical field (e.g. containers/json's StartedAt/Created vs. inspect's)
|
|
* — verified live against a real podman system service, not just docs.
|
|
*/
|
|
function podman_parse_time(string|int|null $value): ?int
|
|
{
|
|
if ($value === null || $value === '') {
|
|
return null;
|
|
}
|
|
if (is_int($value)) {
|
|
return $value > 0 ? $value : null;
|
|
}
|
|
if (str_starts_with($value, '0001-01-01')) {
|
|
return null;
|
|
}
|
|
$ts = strtotime($value);
|
|
return $ts === false ? null : $ts;
|
|
}
|
|
|
|
/**
|
|
* Shortens a full image/container ID to the 12-character form Docker/
|
|
* Podman CLIs conventionally display, matching what users expect to see
|
|
* (and copy-paste into `podman inspect <id>`, which accepts short IDs).
|
|
*/
|
|
function podman_short_id(string $id): string
|
|
{
|
|
// Some APIs prefix with "sha256:" for image IDs.
|
|
$id = str_starts_with($id, 'sha256:') ? substr($id, 7) : $id;
|
|
return substr($id, 0, 12);
|
|
}
|
|
|
|
/**
|
|
* Sends a JSON response and terminates the request — every ajax/*.php
|
|
* endpoint's single exit point, so response shape (envelope with "ok" and
|
|
* either "data" or "error") is consistent for the frontend's shared AJAX
|
|
* helper (javascript/app.js's request() function) to rely on.
|
|
*/
|
|
function podman_json_response(mixed $data, int $httpStatus = 200): never
|
|
{
|
|
http_response_code($httpStatus);
|
|
header('Content-Type: application/json');
|
|
echo json_encode(['ok' => $httpStatus < 400, 'data' => $data], JSON_UNESCAPED_SLASHES);
|
|
exit;
|
|
}
|
|
|
|
function podman_json_error(string $message, int $httpStatus = 500): never
|
|
{
|
|
http_response_code($httpStatus);
|
|
header('Content-Type: application/json');
|
|
echo json_encode(['ok' => false, 'error' => $message], JSON_UNESCAPED_SLASHES);
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Reads and JSON-decodes the request body for POST/DELETE actions that
|
|
* take parameters (e.g. {"id": "..."}), with a friendly error on
|
|
* malformed input instead of a fatal error deep inside an endpoint.
|
|
*
|
|
* @return array<string,mixed>
|
|
*/
|
|
function podman_read_json_body(): array
|
|
{
|
|
$raw = file_get_contents('php://input');
|
|
if ($raw === false || trim($raw) === '') {
|
|
return [];
|
|
}
|
|
$decoded = json_decode($raw, true);
|
|
if (!is_array($decoded)) {
|
|
podman_json_error('Request body must be a JSON object', 400);
|
|
}
|
|
return $decoded;
|
|
}
|