diff --git a/Plugin/unraid/api/storage.php b/Plugin/unraid/api/storage.php
index 45248e1..360706b 100644
--- a/Plugin/unraid/api/storage.php
+++ b/Plugin/unraid/api/storage.php
@@ -95,35 +95,56 @@ if ($action === 'detect' && $_SERVER['REQUEST_METHOD'] === 'POST') {
exit;
}
-// ── Unraid API key status ─────────────────────────────────────────────────────
+// ── Unraid API key status — all hosts ────────────────────────────────────────
if ($action === 'api_status') {
require_once dirname(__DIR__) . '/include/unraid_api.php';
- $data = vv_api_data();
- $status = vv_api_get_status();
- $vars = vv_conf_vars();
- $myHost = vv_detect_host();
- $keyVar = strtoupper($myHost) . '_UNRAID_API_KEY';
- $key = $vars[$keyVar] ?? '';
+ $localStatus = vv_api_get_status();
+ $vars = vv_conf_vars();
+ $myHost = vv_detect_host();
+ $myId = strtoupper($myHost);
+
+ // Collect status for every configured host
+ $hosts = [];
+ foreach ($vars as $k => $v) {
+ if (!preg_match('/^HOST(\d+)$/', $k, $m) || !$v) continue;
+ $id = 'HOST' . $m[1];
+ $keyVar = $id . '_UNRAID_API_KEY';
+ $key = $vars[$keyVar] ?? '';
+ $isLocal = ($id === $myId);
+ $hosts[] = [
+ 'host_id' => $id,
+ 'hostname' => $v,
+ 'is_local' => $isLocal,
+ 'key_var' => $keyVar,
+ 'key_present' => !empty($key),
+ 'key_preview' => $key ? substr($key, 0, 8) . '...' . substr($key, -4) : null,
+ 'api_ok' => $isLocal
+ ? (!$localStatus['key_missing'] && $localStatus['available'])
+ : !empty($key),
+ ];
+ }
+ usort($hosts, fn($a,$b) => strcmp($a['host_id'], $b['host_id']));
+
echo json_encode([
- 'ok' => true,
- 'key_present' => !empty($key),
- 'key_preview' => $key ? substr($key, 0, 8) . '...' . substr($key, -4) : null,
- 'api_ok' => !empty($key) && !$status['key_missing'] && $status['available'],
- 'key_missing' => $status['key_missing'],
- 'fallbacks' => $status['fallbacks'],
+ 'ok' => true,
+ 'my_id' => $myId,
+ 'hosts' => $hosts,
+ 'fallbacks' => $localStatus['fallbacks'],
]);
exit;
}
-// ── Renew Unraid API key ──────────────────────────────────────────────────────
-if ($action === 'renew_apikey' && $_SERVER['REQUEST_METHOD'] === 'POST') {
+// ── Setup/renew API keys (local + all partners via SSH) ───────────────────────
+if ($action === 'setup_apikeys' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$script = SCRIPTS_DIR . '/unRAID_Essentials/unraid_api_key_renew.sh';
if (!file_exists($script)) {
echo json_encode(['ok' => false, 'error' => 'unraid_api_key_renew.sh not found']); exit;
}
- set_time_limit(30);
+ $allHosts = ($_POST['all_hosts'] ?? '0') === '1';
+ $flags = $allHosts ? ' --all-hosts' : '';
+ set_time_limit(60);
$output = []; $exit = 0;
- exec('bash ' . escapeshellarg($script) . ' 2>&1', $output, $exit);
+ exec('bash ' . escapeshellarg($script) . $flags . ' 2>&1', $output, $exit);
echo json_encode(['ok' => $exit === 0, 'exit' => $exit, 'output' => implode("\n", $output)]);
exit;
}
diff --git a/Plugin/unraid/pages/settings.php b/Plugin/unraid/pages/settings.php
index 1ccdbc5..ea396cd 100644
--- a/Plugin/unraid/pages/settings.php
+++ b/Plugin/unraid/pages/settings.php
@@ -145,28 +145,23 @@ $_apiPreview = $_apiKey ? substr($_apiKey, 0, 8) . '...' . substr($_apiKey, -4)
- Unraid API Key
- = $_apiKey ? 'present' : 'missing' ?>
+ Unraid API Keys
+ …
-
-
- Key: = htmlspecialchars($_apiPreview) ?>
- · Var: = htmlspecialchars($_apiKeyVar) ?>
-
- No key found in = htmlspecialchars($_myHost) ?>.conf — click Renew to register one.
-
-
+
- The Unraid API key is ephemeral — it clears on OS updates and service restarts.
- unraid_api_key_renew.sh
- runs at array start to auto-recover, but if the monitor page shows API errors, renew here.
+ Each host registers a key named Varaverk_HOST1,
+ Varaverk_HOST2, etc.
+ Keys are stored in master.conf
+ so every host can query every other host's GraphQL API directly — no SSH needed for remote monitoring.
- Check status
- Renew key
+ ↻ Check status
+ Renew local
+ Setup all host keys
@@ -337,46 +332,74 @@ function vvNtfSaveWebhook() {
.catch(() => { btn.disabled = false; btn.textContent = 'Save'; fb.style.color='#ef5350'; fb.textContent='Request failed'; });
}
-// ── Unraid API Key ────────────────────────────────────────────────────────────
+// ── Unraid API Keys ───────────────────────────────────────────────────────────
function vvApiCheck() {
- const btn = document.getElementById('vv-api-check-btn');
- const fb = document.getElementById('vv-api-fb');
- const badge= document.getElementById('vv-api-badge');
- btn.disabled = true; btn.textContent = 'Checking…'; fb.textContent = '';
+ const btn = document.getElementById('vv-api-check-btn');
+ const badge = document.getElementById('vv-api-badge');
+ const hosts = document.getElementById('vv-api-hosts');
+ btn.disabled = true; btn.textContent = 'Checking…';
fetch('/plugins/varaverk/api/storage.php?action=api_status&_=' + Date.now())
.then(r => r.json())
.then(d => {
- btn.disabled = false; btn.textContent = 'Check status';
- if (!d.ok) { fb.style.color='#ef5350'; fb.textContent='Request failed'; return; }
- badge.textContent = d.api_ok ? 'ok' : d.key_missing ? 'missing' : 'error';
- badge.className = 'vv-set-badge ' + (d.api_ok ? 'internal' : 'flash');
- fb.style.color = d.api_ok ? '#4caf50' : '#ef5350';
- fb.textContent = d.api_ok ? 'API responding ✓'
- : d.key_missing ? 'Key missing from conf — click Renew'
- : 'API unreachable — ' + (d.fallbacks?.length ? d.fallbacks.join(', ') + ' using fallback' : 'check Unraid API service');
+ btn.disabled = false; btn.textContent = '↻ Check status';
+ if (!d.ok) return;
+ const allOk = d.hosts.every(h => h.api_ok);
+ const anyMissing = d.hosts.some(h => !h.key_present);
+ badge.textContent = allOk ? 'all ok' : anyMissing ? 'keys missing' : 'partial';
+ badge.className = 'vv-set-badge ' + (allOk ? 'internal' : 'flash');
+
+ if (hosts) hosts.innerHTML = d.hosts.map(h => {
+ const local = h.is_local ? '
local ' : '';
+ const keyBit = h.key_present
+ ? `
${h.key_preview}`
+ : `
not set `;
+ const dot = h.api_ok ? '#4caf50' : '#555';
+ const name = `Varaverk_${h.host_id}`;
+ return `
+
+ ${h.host_id}${local}
+ ${name}
+ ${keyBit}
+
`;
+ }).join('');
})
- .catch(() => { btn.disabled = false; btn.textContent = 'Check status'; fb.style.color='#ef5350'; fb.textContent='Request failed'; });
+ .catch(() => { btn.disabled = false; btn.textContent = '↻ Check status'; });
}
-function vvApiRenew() {
- const btn = document.getElementById('vv-api-renew-btn');
- const out = document.getElementById('vv-api-out');
- const fb = document.getElementById('vv-api-fb');
- const badge= document.getElementById('vv-api-badge');
- btn.disabled = true; btn.textContent = 'Renewing…'; fb.textContent = '';
- out.textContent = ''; out.style.display = '';
- const fd = new FormData(); fd.append('action', 'renew_apikey');
+function vvApiRenew(allHosts) {
+ const localBtn = document.getElementById('vv-api-local-btn');
+ const allBtn = document.getElementById('vv-api-all-btn');
+ const out = document.getElementById('vv-api-out');
+ const fb = document.getElementById('vv-api-fb');
+ const badge = document.getElementById('vv-api-badge');
+ const activeBtn = allHosts ? allBtn : localBtn;
+
+ [localBtn, allBtn].forEach(b => { if(b) b.disabled = true; });
+ activeBtn.textContent = allHosts ? 'Setting up…' : 'Renewing…';
+ fb.textContent = ''; out.textContent = ''; out.style.display = '';
+
+ const fd = new FormData();
+ fd.append('action', 'setup_apikeys');
+ fd.append('all_hosts', allHosts ? '1' : '0');
+
fetch('/plugins/varaverk/api/storage.php', { method: 'POST', body: fd })
.then(r => r.json())
.then(d => {
- btn.disabled = false; btn.textContent = 'Renew key';
- out.textContent = d.output || '(no output)';
- out.scrollTop = out.scrollHeight;
- badge.textContent = d.ok ? 'ok' : 'error';
- badge.className = 'vv-set-badge ' + (d.ok ? 'internal' : 'flash');
+ localBtn.disabled = false; localBtn.textContent = 'Renew local';
+ allBtn.disabled = false; allBtn.textContent = 'Setup all host keys';
+ out.textContent = d.output || '(no output)';
+ out.scrollTop = out.scrollHeight;
fb.style.color = d.ok ? '#4caf50' : '#ef5350';
- fb.textContent = d.ok ? 'Key renewed ✓' : 'Renewal failed — see output';
+ fb.textContent = d.ok ? '✓ Done' : 'Failed — see output';
+ if (d.ok) { badge.textContent = ''; vvApiCheck(); }
})
- .catch(() => { btn.disabled = false; btn.textContent = 'Renew key'; fb.style.color='#ef5350'; fb.textContent='Request failed'; });
+ .catch(() => {
+ localBtn.disabled = false; localBtn.textContent = 'Renew local';
+ allBtn.disabled = false; allBtn.textContent = 'Setup all host keys';
+ fb.style.color = '#ef5350'; fb.textContent = 'Request failed';
+ });
}
+
+// Auto-check on load
+vvApiCheck();
diff --git a/unRAID_Essentials/unraid_api_key_renew.sh b/unRAID_Essentials/unraid_api_key_renew.sh
index 251b8df..a8b93fb 100755
--- a/unRAID_Essentials/unraid_api_key_renew.sh
+++ b/unRAID_Essentials/unraid_api_key_renew.sh
@@ -5,26 +5,31 @@
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
-# Creates/overwrites the Varaverk API key in the unraid-api service registry at
-# array start. The registry is ephemeral — OS updates and service restarts clear
-# it. This script re-registers the key every boot so Varaverk's enhanced
-# monitoring self-heals without manual intervention.
+# Creates/syncs the Varaverk API key in the Unraid registry. The registry is
+# ephemeral — OS updates and service restarts clear it. This script re-registers
+# every boot so monitoring self-heals without manual intervention.
#
-# Also updates HOST*_UNRAID_API_KEY in the local host conf so the partnership
-# page always reflects the live key value.
+# Each host's key is named "Varaverk_HOST1", "Varaverk_HOST2", etc., and stored
+# in master.conf (shared) so all hosts can call each other's GraphQL API directly
+# for real-time monitoring without SSH.
+#
+# --all-hosts also SSHes to each partner, creates their key there, and writes
+# all keys into master.conf. Run once from Settings → API Key → Setup
+# to fully wire cross-host API access.
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# unraid_api_key_renew.sh
-# Renew the key. Silent on success.
+# Renew local key only — runs at array start, fast.
+#
+# unraid_api_key_renew.sh --all-hosts
+# Renew local key AND SSH to each partner to create/sync their key.
+# Writes all keys into master.conf and pushes to partners.
#
# unraid_api_key_renew.sh --dry-run
-# Show what would happen — no changes made.
-#
# unraid_api_key_renew.sh --log
-# Verbose output.
#
# ==============================================================================================
@@ -37,11 +42,20 @@ acquire_lock
detect_hosts
# ──────────────────────────────────────────────────────────────────────────────
-CONF_FILE="$SCRIPT_DIR/../Configurations/${MY_ID,,}.conf"
-VAR_NAME="${MY_ID}_UNRAID_API_KEY"
+# Parse --all-hosts from raw args (parse_args doesn't handle this flag)
+ALL_HOSTS=false
+for _arg in "$@"; do [[ "$_arg" == "--all-hosts" ]] && ALL_HOSTS=true; done
+unset _arg
-log "$ICON_GEAR Conf file: $CONF_FILE"
-log "$ICON_GEAR Key var: $VAR_NAME"
+CONF_FILE="$SCRIPT_DIR/../Configurations/${MY_ID,,}.conf"
+MASTER_CONF="$SCRIPT_DIR/../Configurations/master.conf"
+VAR_NAME="${MY_ID}_UNRAID_API_KEY"
+KEY_NAME="Varaverk_${MY_ID}"
+
+log "$ICON_GEAR Conf file: $CONF_FILE"
+log "$ICON_GEAR Key name: $KEY_NAME"
+log "$ICON_GEAR Key var: $VAR_NAME"
+log "$ICON_GEAR All hosts: $ALL_HOSTS"
if [[ ! -f "$CONF_FILE" ]]; then
error "Conf file not found: $CONF_FILE"
@@ -49,63 +63,161 @@ if [[ ! -f "$CONF_FILE" ]]; then
fi
if [[ "$DRY_RUN" == true ]]; then
- warn "DRY RUN — would check registry, renew only if key missing"
+ warn "DRY RUN — would check registry, renew if missing, write to host conf + master.conf"
+ [[ "$ALL_HOSTS" == true ]] && warn "DRY RUN — would also SSH to all partners and sync their keys"
exit 0
fi
+# ── Helper: write a key variable into a conf file ─────────────────────────────
+_write_key_to_conf() {
+ local conf="$1" var="$2" key="$3"
+ [[ ! -f "$conf" ]] && return 1
+ if grep -q "^\s*${var}\s*=" "$conf"; then
+ sed -i "s|^\(\s*${var}\s*=\s*\)\"[^\"]*\"|\1\"${key}\"|" "$conf"
+ else
+ echo " ${var}=\"${key}\"" >> "$conf"
+ fi
+}
+
+# ── Helper: push master.conf to all partners ──────────────────────────────────
+_push_master() {
+ php -r "
+ require_once '/usr/local/emhttp/plugins/varaverk/include/config.php';
+ require_once '/usr/local/emhttp/plugins/varaverk/include/confform.php';
+ vv_push_master_conf();
+ " 2>/dev/null && log "master.conf pushed to partner(s)" || warn "master.conf push failed (partner offline?)"
+}
+
# ──────────────────────────────────────────────────────────────────────────────
-# Check if key already exists in the unraid-api registry before creating.
-# --overwrite generates a new key value every time, invalidating the old one.
-# Only renew if the registry has lost it.
-log "Checking unraid-api registry for existing Varaverk key..."
-EXISTING=$(timeout 5 /usr/local/sbin/unraid-api apikey --name "Varaverk" --json /dev/null)
+# Step 1: Local key — check registry, create if missing, sync to conf
+# ──────────────────────────────────────────────────────────────────────────────
+echo ""
+echo "━━━ $ICON_GEAR Local key ($KEY_NAME) ━━━"
+
+EXISTING=$(timeout 5 /usr/local/sbin/unraid-api apikey --name "$KEY_NAME" --json /dev/null)
KEY=$(echo "$EXISTING" | jq -r '.key // empty' 2>/dev/null)
if [[ -n "$KEY" ]]; then
PREVIEW="${KEY:0:8}...${KEY: -4}"
- # Always sync registry key → conf — prevents stale key mismatch after reboot/update
- CONF_KEY=$(grep "^\s*${VAR_NAME}\s*=" "$CONF_FILE" 2>/dev/null | \
- sed 's/.*="\(.*\)".*/\1/' | tr -d '[:space:]')
- if [[ "$CONF_KEY" == "$KEY" ]]; then
+ CONF_KEY=$(grep "^\s*${VAR_NAME}\s*=" "$CONF_FILE" 2>/dev/null | sed 's/.*="\(.*\)".*/\1/' | tr -d '[:space:]')
+ MASTER_KEY=$(grep "^\s*${VAR_NAME}\s*=" "$MASTER_CONF" 2>/dev/null | sed 's/.*="\(.*\)".*/\1/' | tr -d '[:space:]')
+
+ if [[ "$CONF_KEY" == "$KEY" && "$MASTER_KEY" == "$KEY" ]]; then
echo "API key valid ✅ — $VAR_NAME = $PREVIEW"
- log "Key in sync — no update needed"
- exit 0
- fi
- log "Registry key differs from conf — syncing..."
- if grep -q "^\s*${VAR_NAME}\s*=" "$CONF_FILE"; then
- sed -i "s|^\(\s*${VAR_NAME}\s*=\s*\)\"[^\"]*\"|\1\"${KEY}\"|" "$CONF_FILE"
+ log "Key in sync across host conf + master.conf"
else
- echo " ${VAR_NAME}=\"${KEY}\"" >> "$CONF_FILE"
+ log "Syncing key to conf files..."
+ _write_key_to_conf "$CONF_FILE" "$VAR_NAME" "$KEY"
+ _write_key_to_conf "$MASTER_CONF" "$VAR_NAME" "$KEY"
+ _push_master
+ warn "API key synced ✅ — $VAR_NAME = $PREVIEW"
fi
- warn "API key synced to conf ✅ — $VAR_NAME = $PREVIEW"
- exit 0
-fi
+else
+ log "Key not found in registry — creating $KEY_NAME..."
+ RAW=$(timeout 10 /usr/local/sbin/unraid-api apikey \
+ --name "$KEY_NAME" --create --overwrite \
+ --description "Varaverk plugin" --roles ADMIN --json &1)
-log "Key not found in registry — creating new key..."
+ KEY=$(echo "$RAW" | jq -r '.key // empty' 2>/dev/null)
+ if [[ -z "$KEY" ]]; then
+ error "unraid-api returned no key: ${RAW:0:200}"
+ exit 1
+ fi
-RAW=$(timeout 10 /usr/local/sbin/unraid-api apikey \
- --name "Varaverk" --create --overwrite \
- --description "Varaverk plugin" --roles ADMIN --json &1)
-
-if [[ -z "$RAW" ]]; then
- error "unraid-api returned no output"
- exit 1
-fi
-
-KEY=$(echo "$RAW" | jq -r '.key // empty' 2>/dev/null)
-if [[ -z "$KEY" ]]; then
- error "No key in unraid-api response: ${RAW:0:200}"
- exit 1
+ _write_key_to_conf "$CONF_FILE" "$VAR_NAME" "$KEY"
+ _write_key_to_conf "$MASTER_CONF" "$VAR_NAME" "$KEY"
+ _push_master
+ PREVIEW="${KEY:0:8}...${KEY: -4}"
+ warn "API key created ✅ — $VAR_NAME = $PREVIEW"
fi
# ──────────────────────────────────────────────────────────────────────────────
-if grep -q "^\s*${VAR_NAME}\s*=" "$CONF_FILE"; then
- sed -i "s|^\(\s*${VAR_NAME}\s*=\s*\)\"[^\"]*\"|\1\"${KEY}\"|" "$CONF_FILE"
-else
- # Field missing from conf — append it
- echo " ${VAR_NAME}=\"${KEY}\"" >> "$CONF_FILE"
+# Step 2 (--all-hosts): SSH to each partner, create their key, write to master.conf
+# ──────────────────────────────────────────────────────────────────────────────
+[[ "$ALL_HOSTS" != true ]] && exit 0
+
+echo ""
+echo "━━━ $ICON_SYNC Partner keys ━━━"
+
+PARTNER_OK=0
+PARTNER_FAIL=0
+
+for host_var in HOST1 HOST2 HOST3 HOST4 HOST5 HOST6 HOST7 HOST8; do
+ [[ "$host_var" == "$MY_ID" ]] && continue
+ hostname="${!host_var:-}"
+ [[ -z "$hostname" ]] && continue
+
+ r_var_name="${host_var}_UNRAID_API_KEY"
+ r_key_name="Varaverk_${host_var}"
+ r_conf_path="/boot/config/plugins/varaverk/Configurations/${host_var,,}.conf"
+
+ echo " $host_var ($hostname)…"
+
+ REMOTE_IP=$(resolve_tailscale_ip "$hostname")
+ if [[ -z "$REMOTE_IP" ]]; then
+ warn " $host_var: cannot resolve Tailscale IP — skipping"
+ (( PARTNER_FAIL++ ))
+ continue
+ fi
+
+ # SSH: check for existing key, create if missing, return the key value
+ REMOTE_KEY=$(ssh -i "$SSH_KEY" \
+ -o ConnectTimeout=10 \
+ -o StrictHostKeyChecking=no \
+ -o BatchMode=yes \
+ "root@${REMOTE_IP}" "
+ EXISTING=\$(timeout 5 /usr/local/sbin/unraid-api apikey --name '${r_key_name}' --json /dev/null)
+ KEY=\$(echo \"\$EXISTING\" | jq -r '.key // empty' 2>/dev/null)
+ if [[ -n \"\$KEY\" ]]; then
+ echo \"\$KEY\"
+ else
+ timeout 10 /usr/local/sbin/unraid-api apikey \\
+ --name '${r_key_name}' --create --overwrite \\
+ --description 'Varaverk plugin' --roles ADMIN --json /dev/null \\
+ | jq -r '.key // empty' 2>/dev/null
+ fi
+ " 2>/dev/null | tr -d '[:space:]')
+
+ if [[ -z "$REMOTE_KEY" ]]; then
+ warn " $host_var: could not get key from $hostname — skipping"
+ (( PARTNER_FAIL++ ))
+ continue
+ fi
+
+ R_PREVIEW="${REMOTE_KEY:0:8}...${REMOTE_KEY: -4}"
+
+ # Write remote key to master.conf locally
+ _write_key_to_conf "$MASTER_CONF" "$r_var_name" "$REMOTE_KEY"
+
+ # Also write to remote's host*.conf so they have it locally
+ ssh -i "$SSH_KEY" \
+ -o ConnectTimeout=10 \
+ -o StrictHostKeyChecking=no \
+ -o BatchMode=yes \
+ "root@${REMOTE_IP}" "
+ CONF='${r_conf_path}'
+ if [[ -f \"\$CONF\" ]]; then
+ if grep -q '^\s*${r_var_name}\s*=' \"\$CONF\"; then
+ sed -i \"s|^\(\s*${r_var_name}\s*=\s*\)\\\"[^\\\"]*\\\"|\1\\\"${REMOTE_KEY}\\\"|\" \"\$CONF\"
+ else
+ echo ' ${r_var_name}=\\\"${REMOTE_KEY}\\\"' >> \"\$CONF\"
+ fi
+ fi
+ " 2>/dev/null
+
+ echo " $host_var: $r_key_name = $R_PREVIEW ✅"
+ (( PARTNER_OK++ ))
+done
+
+# Push master.conf with all updated keys to all partners
+if (( PARTNER_OK > 0 )); then
+ echo ""
+ echo " Pushing master.conf with all keys…"
+ _push_master
fi
-PREVIEW="${KEY:0:8}...${KEY: -4}"
-log "Writing new key to: $CONF_FILE"
-warn "API key renewed ✅ — $VAR_NAME = $PREVIEW (registry had lost it)"
+echo ""
+echo "━━━━━ $ICON_SUMMARY Key Setup Summary ━━━━━"
+echo " Local: ✅ $VAR_NAME"
+echo " Partners: $PARTNER_OK updated · $PARTNER_FAIL failed"
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"