Put everything Varaverk persists under one root, state included
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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') ?: [];
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user