release: r016 fix Docker update cache matching
This commit is contained in:
@@ -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"}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
BIN
Binary file not shown.
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -234,36 +234,129 @@ 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] ?? '';
|
||||||
case 'system-health': respond(system_health()); break;
|
switch ($action) {
|
||||||
case 'storage-status': respond(storage_status()); break;
|
case 'system-health': respond(system_health()); break;
|
||||||
case 'disk-health': respond(disk_health($argv[2] ?? null)); break;
|
case 'storage-status': respond(storage_status()); break;
|
||||||
case 'notifications': respond(notification_list((int)($argv[2] ?? 20), $argv[3] ?? 'all')); break;
|
case 'disk-health': respond(disk_health($argv[2] ?? null)); break;
|
||||||
case 'shares-list': respond(shares_list(null)); break;
|
case 'notifications': respond(notification_list((int)($argv[2] ?? 20), $argv[3] ?? 'all')); break;
|
||||||
case 'share-inspect': respond(shares_list($argv[2] ?? '')); break;
|
case 'shares-list': respond(shares_list(null)); break;
|
||||||
case 'docker-update-status': respond(docker_update_status()); break;
|
case 'share-inspect': respond(shares_list($argv[2] ?? '')); break;
|
||||||
default: fail_status('Unknown read-only status action');
|
case 'docker-update-status': respond(docker_update_status()); break;
|
||||||
|
default: fail_status('Unknown read-only status action');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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
@@ -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"),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user