release: r016 fix Docker update cache matching

This commit is contained in:
Mikei386
2026-08-23 14:27:46 +02:00
parent c4c04ae7af
commit 9d46a8c4b6
11 changed files with 178 additions and 37 deletions
+3 -3
View File
@@ -76,10 +76,10 @@ Oder manuell:
```bash ```bash
# .txz von Gitea laden # .txz von Gitea laden
curl -O http://192.168.1.2:4000/michael/MUA-Mikes-Unraid-Agent/raw/branch/main/dist/mua-2026.08.22.r015-x86_64-1.txz curl -O http://192.168.1.2:4000/michael/MUA-Mikes-Unraid-Agent/raw/branch/main/dist/mua-2026.08.23.r016-x86_64-1.txz
# Installieren # Installieren
upgradepkg --install-new mua-2026.08.22.r015-x86_64-1.txz upgradepkg --install-new mua-2026.08.23.r016-x86_64-1.txz
``` ```
### 2. Service starten ### 2. Service starten
@@ -94,7 +94,7 @@ Der Service startet automatisch bei jedem Boot (SysVinit).
```bash ```bash
curl http://192.168.1.2:3002/health curl http://192.168.1.2:3002/health
# → {"status":"ok","version":"2026.08.22.r015","auth":"required"} # → {"status":"ok","version":"2026.08.23.r016","auth":"required"}
``` ```
--- ---
Binary file not shown.
+1 -1
View File
@@ -10,7 +10,7 @@
error_reporting(E_ALL); error_reporting(E_ALL);
ini_set('display_errors', 0); // Fehler werden als JSON-RPC-Error zurückgegeben, nicht als HTML ini_set('display_errors', 0); // Fehler werden als JSON-RPC-Error zurückgegeben, nicht als HTML
const MUA_VERSION = '2026.08.22.r015'; const MUA_VERSION = '2026.08.23.r016';
const MUA_SERVER_NAME = 'mua'; const MUA_SERVER_NAME = 'mua';
/** /**
+1 -1
View File
@@ -21,7 +21,7 @@ const MUA_PORT = 3002;
const MUA_BIND = '0.0.0.0'; const MUA_BIND = '0.0.0.0';
const MUA_SESSION_TTL = 3600; // Sekunden const MUA_SESSION_TTL = 3600; // Sekunden
const MUA_SERVER_NAME = 'mua'; const MUA_SERVER_NAME = 'mua';
const MUA_VERSION = '2026.08.22.r015'; const MUA_VERSION = '2026.08.23.r016';
// ── Session-Management (in-memory) ─────────────────────────────────────── // ── Session-Management (in-memory) ───────────────────────────────────────
$sessions = []; // session_id => ['created' => time, 'initialized' => bool] $sessions = []; // session_id => ['created' => time, 'initialized' => bool]
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "mua", "name": "mua",
"version": "2026.08.22.r015", "version": "2026.08.23.r016",
"description": "Mikes Unraid Agent - MCP over HTTP (Streamable HTTP) for Unraid", "description": "Mikes Unraid Agent - MCP over HTTP (Streamable HTTP) for Unraid",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+8 -4
View File
@@ -2,13 +2,13 @@
<!DOCTYPE PLUGIN [ <!DOCTYPE PLUGIN [
<!ENTITY name "mua"> <!ENTITY name "mua">
<!ENTITY author "Michael"> <!ENTITY author "Michael">
<!ENTITY version "2026.08.22.r015"> <!ENTITY version "2026.08.23.r016">
<!ENTITY launch "Settings/mua"> <!ENTITY launch "Settings/mua">
<!ENTITY pluginURL "http://192.168.1.2:4000/michael/MUA-Mikes-Unraid-Agent/raw/branch/main/plugin/mua.plg"> <!ENTITY pluginURL "http://192.168.1.2:4000/michael/MUA-Mikes-Unraid-Agent/raw/branch/main/plugin/mua.plg">
<!ENTITY pluginLOC "/boot/config/plugins/&name;"> <!ENTITY pluginLOC "/boot/config/plugins/&name;">
<!ENTITY emhttpLOC "/usr/local/emhttp/plugins/&name;"> <!ENTITY emhttpLOC "/usr/local/emhttp/plugins/&name;">
<!ENTITY txzURL "http://192.168.1.2:4000/michael/MUA-Mikes-Unraid-Agent/raw/branch/main/dist/mua-2026.08.22.r015-x86_64-1.txz"> <!ENTITY txzURL "http://192.168.1.2:4000/michael/MUA-Mikes-Unraid-Agent/raw/branch/main/dist/mua-2026.08.23.r016-x86_64-1.txz">
<!ENTITY txzSHA256 "4825c67b3082e26ff464d32c59369117aabc4b06131deadd2cff37261a46e344"> <!ENTITY txzSHA256 "2fd0d75e84fcc3b7591ff4373a844f28ea686cdfc6e9fdbd9fa2da6f99654c24">
]> ]>
<PLUGIN name="&name;" <PLUGIN name="&name;"
@@ -23,6 +23,10 @@
> >
<CHANGES> <CHANGES>
### 2026.08.23.r016
- Docker-Update-Status wird primär über Unraids nativen Containernamen-Schlüssel aufgelöst; Image-, `library/`- und `:latest`-Varianten bleiben als kompatible Fallbacks erhalten.
- Alternative Cacheformate und boolesche oder textuelle Statuswerte werden global normalisiert, statt einzelne Container wie Windows11 und WindowsXP als `unknown` zu verlieren.
- Unvollständige Ergebnisse enthalten `unknown_count`, `status_complete` und kennzeichnen `updates_available` ausdrücklich als Mindestzahl.
### 2026.08.22.r015 ### 2026.08.22.r015
- Docker-Liste kann serverseitig nach running/stopped/created/exited filtern und liefert verbindliche Zustandszähler. - Docker-Liste kann serverseitig nach running/stopped/created/exited filtern und liefert verbindliche Zustandszähler.
- Kompakte Antworten ohne Live-Metriken sind nun Standard; CPU/RAM/I/O und Detailfelder werden nur bei Bedarf erzeugt. - Kompakte Antworten ohne Live-Metriken sind nun Standard; CPU/RAM/I/O und Detailfelder werden nur bei Bedarf erzeugt.
@@ -115,7 +119,7 @@ Das .txz enthält:
install/doinst.sh (läuft nach Installation) install/doinst.sh (läuft nach Installation)
=========================================== ===========================================
--> -->
<FILE Name="/boot/config/plugins/&name;/mua-2026.08.22.r015-x86_64-1.txz" Run="upgradepkg --install-new" Mode="755" Min="7.0.0"> <FILE Name="/boot/config/plugins/&name;/mua-2026.08.23.r016-x86_64-1.txz" Run="upgradepkg --install-new" Mode="755" Min="7.0.0">
<URL>&txzURL;</URL> <URL>&txzURL;</URL>
<SHA256>&txzSHA256;</SHA256> <SHA256>&txzSHA256;</SHA256>
</FILE> </FILE>
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "mua", "name": "mua",
"author": "Michael", "author": "Michael",
"version": "2026.08.22.r015", "version": "2026.08.23.r016",
"minver": "7.0.0", "minver": "7.0.0",
"pluginDirectory": "/usr/local/emhttp/plugins/mua", "pluginDirectory": "/usr/local/emhttp/plugins/mua",
"configDirectory": "/boot/config/plugins/mua", "configDirectory": "/boot/config/plugins/mua",
+44
View File
@@ -0,0 +1,44 @@
<?php
require_once __DIR__ . '/unraid-mcp-status-helper.php';
function expect_same(string $label, mixed $expected, mixed $actual): void {
if ($expected === $actual) {
echo "PASS: $label\n";
return;
}
fwrite(STDERR, "FAIL: $label\nExpected: " . json_encode($expected) . "\nActual: " . json_encode($actual) . "\n");
exit(1);
}
$native = [
'Windows11' => 'false',
'WindowsXP' => false,
'AirConnect' => ['status' => 'true'],
];
expect_same('native container-name key', 'update-available', docker_update_state($native, 'Windows11', 'dockurr/windows'));
expect_same('boolean false is an update', 'update-available', docker_update_state($native, 'WindowsXP', 'dockurr/windows'));
expect_same('object status is understood', 'up-to-date', docker_update_state($native, 'AirConnect', '1activegeek/airconnect:latest'));
$legacy = [
'library/redis:latest' => ['status' => 'false'],
'dockurr/windows:latest' => ['updated' => true],
];
expect_same('Docker Hub library alias', 'update-available', docker_update_state($legacy, 'Redis', 'redis'));
expect_same('implicit latest tag', 'up-to-date', docker_update_state($legacy, 'Windows11', 'dockurr/windows'));
$list = [
['container' => 'Virtual-DSM', 'image' => 'vdsm/virtual-dsm:latest', 'updateAvailable' => true],
];
expect_same('list format with updateAvailable', 'update-available', docker_update_state($list, 'Virtual-DSM', 'vdsm/virtual-dsm:latest'));
$summary = docker_update_status_from(
['Windows11' => 'false', 'AirConnect' => 'true'],
['Windows11|dockurr/windows', 'AirConnect|1activegeek/airconnect:latest', 'Mystery|example/mystery'],
);
expect_same('known update count', 1, $summary['updates_available']);
expect_same('unknown count', 1, $summary['unknown_count']);
expect_same('incomplete cache is explicit', false, $summary['status_complete']);
expect_same('reported count marked as minimum', true, $summary['updates_available_is_minimum']);
echo "All Docker update cache tests passed.\n";
+109 -16
View File
@@ -234,30 +234,122 @@ function shares_list(?string $requested): array {
return ['schema_version' => '1.0', 'share_count' => count($items), 'shares' => $items]; return ['schema_version' => '1.0', 'share_count' => count($items), 'shares' => $items];
} }
function docker_update_state_value(mixed $value): ?string {
if (is_array($value)) {
foreach (['update_available', 'updateAvailable'] as $field) {
if (!array_key_exists($field, $value)) continue;
$flag = $value[$field];
if (is_bool($flag)) return $flag ? 'update-available' : 'up-to-date';
$flag = strtolower(trim((string)$flag));
if (in_array($flag, ['true', '1', 'yes'], true)) return 'update-available';
if (in_array($flag, ['false', '0', 'no'], true)) return 'up-to-date';
}
foreach (['status', 'updated', 'update_status', 'updateStatus'] as $field) {
if (array_key_exists($field, $value)) return docker_update_state_value($value[$field]);
}
return null;
}
if (is_bool($value)) return $value ? 'up-to-date' : 'update-available';
if (!is_string($value) && !is_int($value)) return null;
$state = strtolower(trim((string)$value));
if (in_array($state, ['true', '1', 'up-to-date', 'up_to_date', 'current', 'updated'], true)) {
return 'up-to-date';
}
if (in_array($state, ['false', '0', 'update-available', 'update_available', 'outdated'], true)) {
return 'update-available';
}
return null;
}
function docker_image_aliases(string $image): array {
$image = trim($image);
$without_digest = preg_replace('/@sha256:[a-f0-9]+$/i', '', $image) ?? $image;
$aliases = [$image, $without_digest];
$last_part = basename($without_digest);
if (!str_contains($last_part, ':')) $aliases[] = $without_digest . ':latest';
if (!str_contains($without_digest, '/')) {
$aliases[] = 'library/' . $without_digest;
if (!str_contains($last_part, ':')) $aliases[] = 'library/' . $without_digest . ':latest';
} elseif (str_starts_with($without_digest, 'library/')) {
$aliases[] = substr($without_digest, strlen('library/'));
}
return array_values(array_unique(array_filter($aliases, fn($value) => $value !== '')));
}
function docker_update_state(array $updates, string $name, string $image): ?string {
// Unraid's native Docker manager stores the cache by container name.
// Older/plugin-generated caches may instead use an image reference, so
// keep those forms as compatibility fallbacks.
$cache = isset($updates['containers']) && is_array($updates['containers'])
? $updates['containers'] : $updates;
$keys = array_values(array_unique(array_merge([$name], docker_image_aliases($image))));
$key_lookup = [];
foreach ($cache as $key => $_value) {
if (is_string($key)) $key_lookup[strtolower($key)] = $key;
}
foreach ($keys as $candidate) {
$actual = $key_lookup[strtolower($candidate)] ?? null;
if ($actual === null) continue;
$state = docker_update_state_value($cache[$actual]);
if ($state !== null) return $state;
}
// Also understand list/digest keyed formats that carry their identity in
// the entry itself. This prevents a future Unraid cache format change from
// turning every container into an unexplained `unknown` again.
foreach ($cache as $entry) {
if (!is_array($entry)) continue;
$entry_name = (string)($entry['container'] ?? $entry['container_name'] ?? $entry['name'] ?? '');
$entry_image = (string)($entry['image'] ?? $entry['repository'] ?? '');
$name_matches = $entry_name !== '' && strcasecmp($entry_name, $name) === 0;
$image_matches = $entry_image !== '' && in_array(
strtolower($entry_image),
array_map('strtolower', docker_image_aliases($image)),
true,
);
if (!$name_matches && !$image_matches) continue;
$state = docker_update_state_value($entry);
if ($state !== null) return $state;
}
return null;
}
function docker_update_status_from(array $updates, array $rows): array {
$items = [];
foreach ($rows as $row) {
[$name, $image] = array_pad(explode('|', $row, 2), 2, '');
$state = docker_update_state($updates, $name, $image);
$items[] = [
'container' => $name,
'image' => $image,
'status' => $state ?? 'unknown',
];
}
$available = count(array_filter($items, fn($x) => $x['status'] === 'update-available'));
$unknown = count(array_filter($items, fn($x) => $x['status'] === 'unknown'));
return [
'schema_version' => '1.1',
'container_count' => count($items),
'updates_available' => $available,
'unknown_count' => $unknown,
'status_complete' => $unknown === 0,
'updates_available_is_minimum' => $unknown > 0,
'containers' => $items,
];
}
function docker_update_status(): array { function docker_update_status(): array {
$path = '/var/lib/docker/unraid-update-status.json'; $path = '/var/lib/docker/unraid-update-status.json';
$updates = is_readable($path) ? json_decode((string)file_get_contents($path), true) : []; $updates = is_readable($path) ? json_decode((string)file_get_contents($path), true) : [];
if (!is_array($updates)) $updates = []; if (!is_array($updates)) $updates = [];
$rows = []; $rows = [];
exec('/usr/bin/docker ps -a --format ' . escapeshellarg('{{.Names}}|{{.Image}}'), $rows); exec('/usr/bin/docker ps -a --format ' . escapeshellarg('{{.Names}}|{{.Image}}'), $rows);
$items = []; return docker_update_status_from($updates, $rows);
foreach ($rows as $row) {
[$name, $image] = array_pad(explode('|', $row, 2), 2, '');
$key = $image;
if (!isset($updates[$key]) && !str_contains($key, '/')) $key = 'library/' . $key;
$state = $updates[$key]['status'] ?? null;
$items[] = [
'container' => $name,
'image' => $image,
'status' => $state === 'true' ? 'up-to-date' : ($state === 'false' ? 'update-available' : 'unknown'),
];
}
$available = count(array_filter($items, fn($x) => $x['status'] === 'update-available'));
return ['schema_version' => '1.0', 'container_count' => count($items), 'updates_available' => $available, 'containers' => $items];
} }
$action = $argv[1] ?? ''; if (realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) {
switch ($action) { $action = $argv[1] ?? '';
switch ($action) {
case 'system-health': respond(system_health()); break; case 'system-health': respond(system_health()); break;
case 'storage-status': respond(storage_status()); break; case 'storage-status': respond(storage_status()); break;
case 'disk-health': respond(disk_health($argv[2] ?? null)); break; case 'disk-health': respond(disk_health($argv[2] ?? null)); break;
@@ -266,4 +358,5 @@ switch ($action) {
case 'share-inspect': respond(shares_list($argv[2] ?? '')); break; case 'share-inspect': respond(shares_list($argv[2] ?? '')); break;
case 'docker-update-status': respond(docker_update_status()); break; case 'docker-update-status': respond(docker_update_status()); break;
default: fail_status('Unknown read-only status action'); default: fail_status('Unknown read-only status action');
}
} }
+1 -1
View File
@@ -14,7 +14,7 @@ import { createHash, randomUUID } from "node:crypto";
// ── Konstanten ────────────────────────────────────────────────────────── // ── Konstanten ──────────────────────────────────────────────────────────
export const MUA_SERVER_NAME = "mua"; export const MUA_SERVER_NAME = "mua";
export const MUA_VERSION = "2026.08.22.r015"; export const MUA_VERSION = "2026.08.23.r016";
export const MUA_PROTOCOL_VERSION = "2025-03-26"; export const MUA_PROTOCOL_VERSION = "2025-03-26";
export const PHP_HELPER = "/usr/local/bin/unraid-docker-mcp-helper.php"; export const PHP_HELPER = "/usr/local/bin/unraid-docker-mcp-helper.php";
export const STATUS_HELPER = "/usr/local/bin/unraid-mcp-status-helper.php"; export const STATUS_HELPER = "/usr/local/bin/unraid-mcp-status-helper.php";
+1 -1
View File
@@ -188,7 +188,7 @@ export const TOOLS: ToolDef[] = [
{ {
name: "unraid_docker_update_status", name: "unraid_docker_update_status",
description: description:
"Read Unraid's cached Docker image update state and return a compact per-container summary. Does not pull or update images.", "Read Unraid's cached Docker image update state and return a compact per-container summary. Does not pull or update images. Treat updates_available as exhaustive only when status_complete is true; otherwise it is a known minimum and every unknown container must be disclosed.",
inputSchema: empty, inputSchema: empty,
handler: () => runStatusHelper("docker-update-status"), handler: () => runStatusHelper("docker-update-status"),
}, },