Put everything Varaverk persists under one root, state included

This commit is contained in:
Gmer4Lfe
2026-08-08 23:26:46 -04:00
parent d5c36db531
commit f1603349cc
11 changed files with 420 additions and 77 deletions
+6 -6
View File
@@ -115,7 +115,7 @@ function vv_ai_config(): array {
'url' => rtrim(trim($vars["{$host}_OLLAMA_URL"] ?? ''), '/'),
'model' => trim($vars["{$host}_OLLAMA_MODEL"] ?? ''),
'embed_model' => trim($vars["{$host}_OLLAMA_EMBED_MODEL"] ?? 'nomic-embed-text'),
'db' => trim($vars['AI_INDEX_DB'] ?? '') ?: DATA_DIR . '/ai_index.db',
'db' => trim($vars['AI_INDEX_DB'] ?? '') ?: AI_DATA_DIR . '/ai_index.db',
'k' => (int)($vars['AI_SEARCH_K'] ?? 8),
'per_file' => (int)($vars['AI_SEARCH_PER_FILE'] ?? 3),
'timeout' => (int)($vars['AI_REQUEST_TIMEOUT'] ?? 240),
@@ -552,7 +552,7 @@ function vv_ai_memory_path(): string {
$vars = vv_conf_vars();
$p = trim($vars['AI_MEMORY_FILE'] ?? '');
$p = str_replace(['$DATA_DIR', '${DATA_DIR}'], DATA_DIR, $p);
return $p !== '' ? $p : DATA_DIR . '/ai_memory.md';
return $p !== '' ? $p : AI_DATA_DIR . '/ai_memory.md';
}
function vv_ai_memory_max(): int {
@@ -624,7 +624,7 @@ function vv_ai_memory_write(string $text): array {
function vv_ai_token_db(): string {
$p = str_replace(['$DATA_DIR', '${DATA_DIR}'], DATA_DIR,
trim(vv_conf_vars()['AI_TOKEN_DB'] ?? ''));
return $p !== '' ? $p : DATA_DIR . '/ai_token_history.db';
return $p !== '' ? $p : AI_DATA_DIR . '/ai_token_history.db';
}
function vv_ai_token_retain(): int {
@@ -966,7 +966,7 @@ function vv_ai_scope_ok(string $scope): bool {
//
// Under data/ and therefore gitignored: these quote this installation's logs.
function vv_ai_bugs_dir(): string {
$d = DATA_DIR . '/ai_bugs';
$d = AI_DATA_DIR . '/ai_bugs';
if (!is_dir($d)) @mkdir($d, 0755, true);
return $d;
}
@@ -1094,7 +1094,7 @@ function vv_ai_bug_set_open(string $id, bool $open): bool {
// Markdown rather than a delimited .db because the useful part is prose — a fix is a sentence,
// not a field — and it stays hand-editable when a note turns out to be wrong.
function vv_ai_incidents_path(): string {
return DATA_DIR . '/ai_incidents.md';
return AI_DATA_DIR . '/ai_incidents.md';
}
// One entry, appended. The symptom is captured from what was being asked; the fix is written by
@@ -1159,7 +1159,7 @@ function vv_ai_incidents_for(string $scope, int $max = 4): array {
// runs often enough to be trusted with it, and an unbounded directory here would quietly grow
// for as long as the operator keeps talking to the assistant.
function vv_ai_chats_dir(): string {
$d = DATA_DIR . '/ai_chats';
$d = AI_DATA_DIR . '/ai_chats';
if (!is_dir($d)) @mkdir($d, 0755, true);
return $d;
}
+3 -3
View File
@@ -212,7 +212,7 @@ function vv_arr_cleanup_stats(string $type): array {
// Fallback: daily aggregate db — date|arr|orphan_count|orphan_bytes|junk_count|junk_bytes|recent_count|tracked_count
if ($out['last_run'] === null) {
$dbFile = DATA_DIR . '/arr_cleanup_stats.db';
$dbFile = DB_DIR . '/arr_cleanup_stats.db';
if (file_exists($dbFile)) {
$last = null;
foreach (file($dbFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
@@ -255,7 +255,7 @@ function vv_arr_discovery_stats(string $type): array {
// Fallback: per-title history db — status|id|date[|title]
if ($out['last_run'] === null) {
$dbFile = DATA_DIR . '/' . $type . '_discovery_history.db';
$dbFile = DB_DIR . '/' . $type . '_discovery_history.db';
if (file_exists($dbFile)) {
$lastDate = null; $added = 0;
foreach (file($dbFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
@@ -326,7 +326,7 @@ function vv_arr_recovery_stats(): array {
// Fallback: daily aggregate db — date|time|count|bytes
if ($out['last_run'] === null) {
$dbFile = DATA_DIR . '/arr_recovery_stats.db';
$dbFile = DB_DIR . '/arr_recovery_stats.db';
if (file_exists($dbFile)) {
$lines = file($dbFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$last = $lines ? end($lines) : null;
+17
View File
@@ -64,6 +64,23 @@
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/unraid_api.php';
// Timestamp out of the container restart log, whose second field is a formatted local date
// ("2026-08-02 13:15:07") rather than an epoch — docker_watchdog.sh writes it that way because
// its own rolling-window trim compares the strings lexically in awk.
//
// Lives here rather than in watchdog.php because include/monitor.php parses the same file for
// the dashboard card and does not include watchdog.php. Both readers previously cast the field
// with (int), which stops at the first non-digit and returned 2026 for every line ever written —
// below any cutoff, so both restart lists were permanently empty and looked exactly like
// "nothing has restarted".
//
// Accepts an epoch too, so that changing the writer later does not require changing the readers.
function vv_wd_restart_ts(string $raw): int {
$raw = trim($raw);
if (ctype_digit($raw)) return (int)$raw;
return (int)(strtotime($raw) ?: 0);
}
function vv_system_info(): array {
// ── Shared local reads (always needed regardless of API) ──────────────────
$ident = @parse_ini_file('/boot/config/ident.cfg') ?: [];
+40 -10
View File
@@ -103,8 +103,20 @@ $_vv_cfg = @parse_ini_file(PLUGIN_CFG) ?: [];
define('SCRIPTS_DIR', $_vv_cfg['SCRIPTS_DIR'] ?? '/boot/config/plugins/varaverk');
define('CONF_DIR', SCRIPTS_DIR . '/Configurations');
define('DEPLOY_DIR', SCRIPTS_DIR . '/Deployment');
define('DATA_DIR', SCRIPTS_DIR . '/data');
define('STATE_DIR', SCRIPTS_DIR . '/State_Files');
// DATA_DIR is the one on-disk root; everything Varaverk persists lives under it in a
// subdirectory named for what the files are. Mirrors the same block in master.conf, which is
// where the shell layer reads them from — these are derived from SCRIPTS_DIR rather than parsed
// so that a conf that has not upgraded yet still resolves, and so this file keeps working when
// master.conf is missing entirely (setup, first boot, a botched pull).
//
// STATE_DIR moved from SCRIPTS_DIR/State_Files to DATA_DIR/state and kept its name, which is why
// the 23 call sites in this layer that build on it needed no edits at all.
define('DATA_DIR', SCRIPTS_DIR . '/data');
define('DB_DIR', DATA_DIR . '/db');
define('STATE_DIR', DATA_DIR . '/state');
define('AI_DATA_DIR', DATA_DIR . '/ai');
define('CACHE_BACKUP_DIR', DATA_DIR . '/cache');
define('LOG_ARCHIVE_DIR', DATA_DIR . '/logs');
define('LOG_DIR', '/var/log/varaverk');
// User-authored custom scripts (scheduler page "+ Create Script") — kept outside the git
// repo entirely, alongside the User Scripts plugin's own storage. Any *.sh file placed
@@ -204,12 +216,21 @@ function vv_push_setup_state(): void {
break;
}
}
$remoteStatePath = $remoteSD . '/State_Files/varaverk_setup.db';
shell_exec($sshBase . ' "mkdir -p ' . escapeshellarg(dirname($remoteStatePath)) . '" 2>/dev/null');
$dest = escapeshellarg('root@' . $ip . ':' . $remoteStatePath);
exec('scp -i ' . escapeshellarg($sshKey)
. ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no'
. ' ' . escapeshellarg(VV_SETUP_STATE_FILE) . ' ' . $dest . ' 2>&1');
// Which layout the partner uses is decided ON the partner, not assumed here. State moved
// from SCRIPTS_DIR/State_Files to DATA_DIR/state, and this file is written to whichever
// one that host will actually read — a setup state pushed to the directory the partner
// does not read is worse than not pushing it, because the push reports success.
//
// The order matters: prefer the new path, fall back to the old ONLY if it already exists.
// A partner that has neither is a fresh install on current code, which reads the new one.
// Piped over ssh rather than scp'd so the resolution and the write are the same call —
// scp needs the path decided here, which is the thing that cannot be known here.
$remoteResolve = 'sf="' . $remoteSD . '/data/state"; '
. '[ -d "$sf" ] || { [ -d "' . $remoteSD . '/State_Files" ] '
. '&& sf="' . $remoteSD . '/State_Files"; }; '
. 'mkdir -p "$sf" && cat > "$sf/varaverk_setup.db"';
exec('cat ' . escapeshellarg(VV_SETUP_STATE_FILE) . ' | '
. $sshBase . ' ' . escapeshellarg($remoteResolve) . ' 2>&1');
}
}
@@ -612,13 +633,22 @@ function vv_auto_create_api_key(string $hostId, string $confFile): array {
return ['ok' => true, 'key_preview' => $key ? substr($key, 0, 8) . '...' . substr($key, -4) : 'registered'];
}
// Build a bash command that reads a state file from the REMOTE host's State_Files/.
// Build a bash command that reads a state file from the REMOTE host's state directory.
// Reads the remote's varaverk.cfg to resolve their SCRIPTS_DIR (may differ from ours
// when the remote is in appdata mode). Falls back to the internal plugin path.
//
// The state directory is probed on the far side rather than assumed, because it moved:
// SCRIPTS_DIR/State_Files became DATA_DIR/state, and a partner may not have pulled that yet.
// This is the call that reads the partner's fallback_state.db, and a miss returns an empty
// string — which the callers cannot distinguish from "partner is in NORMAL state". Reading the
// wrong directory would therefore not look like an error, it would look like an answer. Probing
// also means the two hosts can be upgraded in either order.
function vv_remote_state_cmd(string $filename): string {
$fn = basename($filename);
return 'sd=$(grep -m1 SCRIPTS_DIR= /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null'
. ' | cut -d\'"\' -f2); cat "${sd:-/boot/config/plugins/varaverk}/State_Files/' . $fn . '" 2>/dev/null';
. ' | cut -d\'"\' -f2); sd="${sd:-/boot/config/plugins/varaverk}"; '
. 'sf="$sd/data/state"; [ -d "$sf" ] || sf="$sd/State_Files"; '
. 'cat "$sf/' . $fn . '" 2>/dev/null';
}
// Local LAN IP via routing table — static-cached per request.
+8 -4
View File
@@ -200,14 +200,18 @@ function vv_watchdog_summary(): array {
}
// Recent restarts (24 h)
$restartLog = DATA_DIR . '/container_restart_history.db';
$restartLog = DB_DIR . '/container_restart_history.db';
$restartRaw = @file_get_contents($restartLog) ?: '';
$cutoff = time() - 86400;
$restarts = [];
foreach (explode("\n", trim($restartRaw)) as $line) {
if (!$line || !str_contains($line, '|')) continue;
[$name, $ts] = explode('|', $line, 2);
if ((int)$ts >= $cutoff) $restarts[] = ['name' => trim($name), 'ts' => (int)$ts];
// The second field is a formatted local date, not an epoch — see
// vv_wd_restart_ts() in watchdog.php for why, and for what casting it with (int)
// silently did to this list for as long as it has existed.
[$name, $raw] = explode('|', $line, 2);
$ts = vv_wd_restart_ts(trim($raw));
if ($ts >= $cutoff) $restarts[] = ['name' => trim($name), 'ts' => $ts];
}
usort($restarts, fn($a, $b) => $b['ts'] - $a['ts']);
@@ -422,7 +426,7 @@ function vv_rsync_status(): array {
}
// Profile activity — last 7 days, aggregated per profile
$bwLog = DATA_DIR . '/bandwidth_history.db';
$bwLog = DB_DIR . '/bandwidth_history.db';
$cutoff7 = date('Y-m-d', strtotime('-7 days'));
$profiles = [];
if (file_exists($bwLog)) {
+43 -19
View File
@@ -95,22 +95,36 @@ function vv_wd_parse_kv(string $text): array {
return $out;
}
// Restart log: "container|timestamp" one per line
// Restart log: "container|2026-08-02 13:15:07" one per line.
//
// The second field is a formatted local timestamp, not an epoch — docker_watchdog.sh writes it
// with date '+%Y-%m-%d %H:%M:%S' because its own rolling-window trim compares the strings
// lexically in awk, which is correct for that format. This function used to cast it with (int),
// which stops at the first non-digit and yielded 2026 for every line ever written. 2026 is below
// any plausible cutoff, so every entry was discarded and both restart panels — the Monitor card
// and the Watchdog page — were permanently empty. Not visibly broken: an empty list reads
// exactly like "nothing has restarted", which is the answer you least want to be wrong about
// during a restart loop.
//
// Parsed, not reformatted. Changing what the watchdog writes would strand every existing entry
// and every awk comparison in Tools/watchdog_skip_list_manager.sh.
function vv_wd_parse_restart_log(string $text, int $windowSeconds = 86400): array {
$now = time();
$cutoff = $now - $windowSeconds;
$cutoff = time() - $windowSeconds;
$entries = [];
foreach (explode("\n", trim($text)) as $line) {
$line = trim($line);
if (!$line || !str_contains($line, '|')) continue;
[$name, $ts] = explode('|', $line, 2);
$ts = (int)$ts;
[$name, $raw] = explode('|', $line, 2);
$ts = vv_wd_restart_ts(trim($raw));
if ($ts >= $cutoff) $entries[] = ['name' => trim($name), 'ts' => $ts];
}
usort($entries, fn($a, $b) => $b['ts'] - $a['ts']);
return $entries;
}
// vv_wd_restart_ts() lives in common.php — include/monitor.php parses the same file and does not
// include this one, so a helper defined here would be a fatal on the dashboard.
// Skip list: one container name per line
function vv_wd_parse_skiplist(string $text): array {
return array_values(array_filter(array_map('trim', explode("\n", $text))));
@@ -239,28 +253,38 @@ function vv_wd_remote_data(string $ip, string $sshKey, string $restartLogPath):
// /proc/meminfo is passed as a raw section (not awk-parsed) to avoid quoting
// fragility — escapeshellarg() single-quotes the whole command so awk \$2
// inside double-quotes is unreliable across Unraid builds.
// State files live under the REMOTE's own SCRIPTS_DIR/State_Files (may differ
// from ours in flash mode) — resolve it once, same idiom as vv_remote_state_cmd().
// State files live under the REMOTE's own SCRIPTS_DIR (may differ from ours in flash mode),
// so it is resolved on the far side — same idiom as vv_remote_state_cmd().
//
// The layout is probed rather than assumed. State moved from SCRIPTS_DIR/State_Files to
// DATA_DIR/state, and the histories from DATA_DIR's root into DATA_DIR/db, but a partner is
// not guaranteed to have pulled that yet — and this is the call that reports whether the
// partner's watchdogs are healthy. Guessing wrong returns empty strings for every state
// file, which reads as "partner has no strikes" rather than as an error. Probing costs one
// directory test and makes the answer correct in both directions, which also means the two
// hosts can be upgraded in either order.
$restartLogName = basename($restartLogPath);
$cmd = 'sd=$(grep -m1 SCRIPTS_DIR= /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null'
. ' | cut -d\'"\' -f2); sd="${sd:-/boot/config/plugins/varaverk}"; '
. 'sf="$sd/data/state"; [ -d "$sf" ] || sf="$sd/State_Files"; '
. 'db="$sd/data/db"; [ -d "$db" ] || db="$sd/data"; '
. "printf 'UPTIME:%s\nLOAD:%s\nCORES:%s\nDAEMON:%s\nOOM:%s\nBASELINECOUNT:%s\nBASELINEAGE:%s\n---MEMINFO---\n%s\n---RW---\n%s\n---DOCK---\n%s\n---SKIP---\n%s\n---SYS---\n%s\n---REBOOT---\n%s\n---RESTART---\n%s\n---STORAGE---\n%s\n---NETWORK---\n%s\n' "
. '"$(cat /proc/uptime|cut -d\" \" -f1)" '
. '"$(cat /proc/loadavg|cut -d\" \" -f1)" '
. '"$(nproc)" '
. '"$(docker info >/dev/null 2>&1 && echo ok || echo err)" '
. '"$(cat "$sd/State_Files/system_watchdog_oom.db" 2>/dev/null||echo 0)" '
. '"$(wc -l < "$sd/State_Files/watchdog_appdata_growth.db" 2>/dev/null||echo 0)" '
. '"$(stat -c %Y "$sd/State_Files/watchdog_appdata_growth.db" 2>/dev/null||echo 0)" '
. '"$(cat "$sf/system_watchdog_oom.db" 2>/dev/null||echo 0)" '
. '"$(wc -l < "$sf/watchdog_appdata_growth.db" 2>/dev/null||echo 0)" '
. '"$(stat -c %Y "$sf/watchdog_appdata_growth.db" 2>/dev/null||echo 0)" '
. '"$(cat /proc/meminfo 2>/dev/null)" '
. '"$(cat "$sd/State_Files/resource_watchdog_state.db" 2>/dev/null)" '
. '"$(cat "$sd/State_Files/container_watchdog_state.db" 2>/dev/null)" '
. '"$(cat "$sd/State_Files/docker_watchdog_failed.db" 2>/dev/null)" '
. '"$(cat "$sd/State_Files/system_watchdog_state.db" 2>/dev/null)" '
. '"$(cat "$sd/State_Files/system_watchdog_reboots.db" 2>/dev/null)" '
. '"$(cat "$sd/data/' . $restartLogName . '" 2>/dev/null)" '
. '"$(cat "$sd/State_Files/storage_watchdog_state.db" 2>/dev/null)" '
. '"$(cat "$sd/State_Files/network_watchdog_state.db" 2>/dev/null)"';
. '"$(cat "$sf/resource_watchdog_state.db" 2>/dev/null)" '
. '"$(cat "$sf/container_watchdog_state.db" 2>/dev/null)" '
. '"$(cat "$sf/docker_watchdog_failed.db" 2>/dev/null)" '
. '"$(cat "$sf/system_watchdog_state.db" 2>/dev/null)" '
. '"$(cat "$sf/system_watchdog_reboots.db" 2>/dev/null)" '
. '"$(cat "$db/' . $restartLogName . '" 2>/dev/null)" '
. '"$(cat "$sf/storage_watchdog_state.db" 2>/dev/null)" '
. '"$(cat "$sf/network_watchdog_state.db" 2>/dev/null)"';
$out = vv_pt_ssh($ip, $sshKey, $cmd, 8);
if (!$out) return null;
@@ -366,7 +390,7 @@ function vv_wd_all(): array {
$currentHost = vv_detect_host();
$tsPeers = vv_pt_ts_peers();
$masterRaw = vv_read_conf_raw('master.conf');
$restartLog = DATA_DIR . '/container_restart_history.db';
$restartLog = DB_DIR . '/container_restart_history.db';
// Config thresholds from master.conf
$cfg = [