35 lines
548 KiB
JSON
35 lines
548 KiB
JSON
{"parentUuid":null,"isSidechain":true,"promptId":"3c1ac879-b13a-4ebc-9aeb-ea73615e9af3","agentId":"accc3589951fac88e","type":"user","message":{"role":"user","content":"Do a thorough audit of all 11 PHP pages in the Varaverk Unraid plugin at /boot/config/plugins/varaverk/Plugin/unraid/pages/. Also read the shared CSS at /boot/config/plugins/varaverk/Plugin/unraid/css/varaverk.css.\n\nI need a page-by-page catalog of:\n\n1. **Visual inconsistencies** — buttons, cards, badges, spacing, colors, fonts that don't match the design language used on other pages. Note which page does it \"right\" as the reference.\n\n2. **UX gaps** — missing loading states, no empty state messages, missing error feedback, no confirmation on destructive actions, things that silently do nothing.\n\n3. **Inline styles vs CSS** — how much styling is inline vs in varaverk.css? Which pages dump large <style> blocks inline vs sharing from the CSS file? Is the CSS file well-organized?\n\n4. **Broken or incomplete flows** — pages or sections that appear unfinished, have placeholder text, dead buttons, or flows that don't complete correctly.\n\n5. **Cross-page duplication** — identical JS functions, CSS class definitions, or HTML patterns repeated across multiple page files.\n\nFor each finding: page filename, what the issue is, and (where relevant) which other page has the \"correct\" version to copy from.\n\nBe thorough — read each file fully. Report findings grouped by page. This audit will be used to plan a polishing pass, so concrete specifics matter more than summaries."},"uuid":"2f3ff849-8384-492f-9e8f-2bb973265c22","timestamp":"2026-06-06T14:29:32.226Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"2f3ff849-8384-492f-9e8f-2bb973265c22","isSidechain":true,"agentId":"accc3589951fac88e","attachment":{"type":"deferred_tools_delta","addedNames":["CronCreate","CronDelete","CronList","EnterWorktree","ExitWorktree","Monitor","PushNotification","RemoteTrigger","TaskStop","WebFetch","WebSearch","mcp__claude_ai_Google_Drive__authenticate","mcp__claude_ai_Google_Drive__complete_authentication"],"addedLines":["CronCreate","CronDelete","CronList","EnterWorktree","ExitWorktree","Monitor","PushNotification","RemoteTrigger","TaskStop","WebFetch","WebSearch","mcp__claude_ai_Google_Drive__authenticate","mcp__claude_ai_Google_Drive__complete_authentication"],"removedNames":[],"readdedNames":[]},"type":"attachment","uuid":"26408135-898f-4128-948c-8fddeee86f7d","timestamp":"2026-06-06T14:29:32.229Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"26408135-898f-4128-948c-8fddeee86f7d","isSidechain":true,"agentId":"accc3589951fac88e","attachment":{"type":"skill_listing","content":"- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors (\"from now on when X\", \"each time X\", \"whenever X\", \"before/after X\") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions (\"allow X\", \"add permission\", \"move permission to\"), env vars (\"set X=Y\"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: \"allow npm commands\", \"add bq permission to global settings\", \"move permission to user settings\", \"set DEBUG=true\", \"when claude stops show X\". For simple settings like theme/model, suggest the /config command.\n- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: \"rebind ctrl+s\", \"add a chord shortcut\", \"change the submit key\", \"customize keybindings\".\n- verify: Verify that a code change actually does what it's supposed to by running the app and observing behavior. Use when asked to verify a PR, confirm a fix works, test a change manually, check that a feature works, or validate local changes before pushing.\n- code-review: Review the current diff for correctness bugs and reuse/simplification/efficiency cleanups at the given effort level (low/medium: fewer, high-confidence findings; high→max: broader coverage, may include uncertain findings; ultra: deep multi-agent review in the cloud). Pass --comment to post findings as inline PR comments, or --fix to apply the findings to the working tree after the review.\n- simplify: Review the changed code for reuse, simplification, efficiency, and altitude cleanups, then apply the fixes. Quality only — it does not hunt for bugs; use /code-review for that.\n- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.\n- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. \"check the deploy every 5 minutes\", \"keep running /babysit-prs\"). Do NOT invoke for one-off tasks.\n- schedule: Create, update, list, or run scheduled remote agents (routines) that execute on a cron schedule. - When the user wants to schedule a recurring remote agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/routines. Also use when the user wants a one-time scheduled run (\"run this once at 3pm\", \"remind me to check X tomorrow\").\n- claude-api: Reference for the Claude API / Anthropic SDK — model ids, pricing, params, streaming, tool use, MCP, agents, caching, token counting, model migration.\nTRIGGER — read BEFORE opening the target file; don't skip because it \"looks like a one-liner\" — whenever: the prompt names Claude/Anthropic in any form (Claude, Anthropic, Opus, Sonnet, Haiku, `anthropic`, `@anthropic-ai`, `claude-*`, `us.anthropic.*`, `[1m]`); the user asks about an LLM (pricing/model choice/limits/caching) — never answer from memory; OR the task is LLM-shaped with provider unstated (agent/MCP/tool-definition/multi-agent/RAG/LLM-judge/computer-use; generate/summarize/extract/classify/rewrite/converse over NL; debugging refusals/cutoffs/streaming/tool-calls/tokens).\nSKIP only when another provider is being worked on (overrides all triggers): OpenAI/GPT/Gemini/Llama/Mistral/Cohere/Ollama named in the query; OR `grep -rE 'openai|langchain_openai|google.generativeai|genai|mistralai|cohere|ollama'` over the project hits (run this grep FIRST if no provider named — don't Read the file).\n- run: Launch and drive this project's app to see a change working. Use when asked to run, start, or screenshot the app, or to confirm a change works in the real app (not just tests). First looks for a project skill that already covers launching the app; otherwise falls back to built-in patterns per project type (CLI, server, TUI, Electron, browser-driven, library).\n- init: Initialize a new CLAUDE.md file with codebase documentation\n- review: Review a pull request\n- security-review: Complete a security review of the pending changes on the current branch","skillCount":13,"isInitial":true,"names":["update-config","keybindings-help","verify","code-review","simplify","fewer-permission-prompts","loop","schedule","claude-api","run","init","review","security-review"]},"type":"attachment","uuid":"e3ae9665-2ce8-4daa-b597-bd235041f150","timestamp":"2026-06-06T14:29:32.230Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"e3ae9665-2ce8-4daa-b597-bd235041f150","isSidechain":true,"agentId":"accc3589951fac88e","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01VRSFJ4XdcN69vBqD7fcmGk","type":"message","role":"assistant","content":[{"type":"text","text":"I'll conduct a thorough audit of the Varaverk Unraid plugin pages. Let me start by exploring the structure and reading all the files."}],"stop_reason":"tool_use","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":7775,"cache_read_input_tokens":0,"output_tokens":113,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":7775},"inference_geo":"not_available","iterations":[{"input_tokens":3,"output_tokens":113,"cache_read_input_tokens":0,"cache_creation_input_tokens":7775,"cache_creation":{"ephemeral_5m_input_tokens":7775,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"diagnostics":null},"requestId":"req_011CbnAYqP9wSfg7unKaEoGc","attributionAgent":"Explore","type":"assistant","uuid":"487889be-3303-4b35-aa61-1d8440db45a6","timestamp":"2026-06-06T14:29:33.529Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"487889be-3303-4b35-aa61-1d8440db45a6","isSidechain":true,"agentId":"accc3589951fac88e","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01VRSFJ4XdcN69vBqD7fcmGk","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_012cBoARG3RxCuKDFtGhYjHH","name":"Bash","input":{"command":"find /boot/config/plugins/varaverk/Plugin/unraid/pages -type f -name \"*.php\" | sort"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":7775,"cache_read_input_tokens":0,"output_tokens":113,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":7775},"inference_geo":"not_available","iterations":[{"input_tokens":3,"output_tokens":113,"cache_read_input_tokens":0,"cache_creation_input_tokens":7775,"cache_creation":{"ephemeral_5m_input_tokens":7775,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"diagnostics":null},"requestId":"req_011CbnAYqP9wSfg7unKaEoGc","attributionAgent":"Explore","type":"assistant","uuid":"9ed7d21d-c5a2-4baf-a2b3-f99534c4a973","timestamp":"2026-06-06T14:29:33.587Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"9ed7d21d-c5a2-4baf-a2b3-f99534c4a973","isSidechain":true,"promptId":"3c1ac879-b13a-4ebc-9aeb-ea73615e9af3","agentId":"accc3589951fac88e","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_012cBoARG3RxCuKDFtGhYjHH","type":"tool_result","content":"/boot/config/plugins/varaverk/Plugin/unraid/pages/arrs.php\n/boot/config/plugins/varaverk/Plugin/unraid/pages/auth.php\n/boot/config/plugins/varaverk/Plugin/unraid/pages/docker.php\n/boot/config/plugins/varaverk/Plugin/unraid/pages/fallback.php\n/boot/config/plugins/varaverk/Plugin/unraid/pages/monitor.php\n/boot/config/plugins/varaverk/Plugin/unraid/pages/partnership.php\n/boot/config/plugins/varaverk/Plugin/unraid/pages/rsync.php\n/boot/config/plugins/varaverk/Plugin/unraid/pages/scheduler.php\n/boot/config/plugins/varaverk/Plugin/unraid/pages/settings.php\n/boot/config/plugins/varaverk/Plugin/unraid/pages/setup.php\n/boot/config/plugins/varaverk/Plugin/unraid/pages/watchdog.php","is_error":false}]},"uuid":"d97ba48f-436c-4d73-ab92-bdf9987cdd84","timestamp":"2026-06-06T14:29:34.138Z","sourceToolAssistantUUID":"9ed7d21d-c5a2-4baf-a2b3-f99534c4a973","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"d97ba48f-436c-4d73-ab92-bdf9987cdd84","isSidechain":true,"agentId":"accc3589951fac88e","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01EHXveD5KL1GDDfpYXYummu","type":"message","role":"assistant","content":[{"type":"text","text":"Perfect! I found all 11 pages. Now let me read each one along with the CSS file. I'll start with the CSS file to understand the design language, then read all PHP pages."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":366,"cache_read_input_tokens":7775,"cache_creation":{"ephemeral_5m_input_tokens":366,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null},"requestId":"req_011CbnAYyu8Br8CfBBXFBkPr","attributionAgent":"Explore","type":"assistant","uuid":"859c0f33-3290-41dc-a0d1-6c2f6521c265","timestamp":"2026-06-06T14:29:36.348Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"859c0f33-3290-41dc-a0d1-6c2f6521c265","isSidechain":true,"agentId":"accc3589951fac88e","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01EHXveD5KL1GDDfpYXYummu","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Y2MLVYVPyFot32JY5pk7hF","name":"Read","input":{"file_path":"/boot/config/plugins/varaverk/Plugin/unraid/css/varaverk.css"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":366,"cache_read_input_tokens":7775,"cache_creation":{"ephemeral_5m_input_tokens":366,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null},"requestId":"req_011CbnAYyu8Br8CfBBXFBkPr","attributionAgent":"Explore","type":"assistant","uuid":"e35a7a4e-fe87-4854-9736-7fb7fdc32f47","timestamp":"2026-06-06T14:29:36.588Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"e35a7a4e-fe87-4854-9736-7fb7fdc32f47","isSidechain":true,"promptId":"3c1ac879-b13a-4ebc-9aeb-ea73615e9af3","agentId":"accc3589951fac88e","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Y2MLVYVPyFot32JY5pk7hF","type":"tool_result","content":"1\t/* Varaverk plugin styles — inherits unRAID theme, adds plugin-specific layout */\n2\t\n3\t#varaverk-wrap { padding: 10px; font-family: inherit; }\n4\t\n5\t/* Tab bar */\n6\t#vv-tabs { display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 2px solid #444; align-items: flex-end; }\n7\t.vv-tab { padding: 6px 16px; text-decoration: none; color: #aaa; border-radius: 4px 4px 0 0; }\n8\t.vv-tab:hover { color: #fff; background: #333; }\n9\t.vv-tab.active { color: #fff; background: #555; border-bottom: 2px solid #fff; }\n10\t\n11\t/* Expand toggle button */\n12\t#vv-expand-btn {\n13\t background: none; border: none; cursor: pointer;\n14\t color: #333; font-size: 15px; padding: 2px 8px 4px;\n15\t line-height: 1; border-radius: 3px; transition: color .15s;\n16\t margin-left: 6px; flex-shrink: 0;\n17\t}\n18\t#vv-expand-btn:hover { color: #888; }\n19\t#vv-expand-btn.active { color: #aaa; }\n20\t\n21\t/* Fullscreen mode — hide Unraid chrome, reclaim the space */\n22\tbody.vv-fullscreen #header { display: none !important; }\n23\tbody.vv-fullscreen #menu { display: none !important; }\n24\tbody.vv-fullscreen #displaybox { padding-left: 1rem !important; padding-top: .5rem !important; }\n25\t\n26\t/* Cards / layout */\n27\t.vv-row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }\n28\t.vv-card { flex: 1; min-width: 200px; background: #1e1e1e; border: 1px solid #444;\n29\t border-radius: 6px; padding: 12px; }\n30\t.vv-wide { flex: 100%; }\n31\t.vv-card h3 { margin: 0 0 10px; font-size: 13px; text-transform: uppercase;\n32\t color: #888; letter-spacing: 0.05em; white-space: normal;\n33\t overflow: hidden; min-width: 0;\n34\t display: flex; align-items: center; justify-content: space-between; }\n35\t\n36\t/* Cog icon — links to related Unraid page. Hidden until card hovered. */\n37\t.vv-card-cog { color: #2a2a2a; font-size: 13px; line-height: 1; text-decoration: none;\n38\t padding: 1px 3px; border-radius: 3px; flex-shrink: 0;\n39\t transition: color 0.15s, background 0.15s;\n40\t font-style: normal; font-weight: normal; text-transform: none;\n41\t letter-spacing: 0; cursor: pointer; }\n42\t.vv-card:hover .vv-card-cog { color: #4a4a4a; }\n43\t.vv-card-cog:hover { color: #aaa !important; background: #333; }\n44\t\n45\t/* Card header icon wrapper */\n46\t.vv-ico { display:inline-flex; align-items:center; opacity:0.38; flex-shrink:0; }\n47\t\n48\t/* Status accent — left border colour for state-driven cards */\n49\t.vv-accent-ok { border-left-color: #2a5a2a !important; }\n50\t.vv-accent-warn { border-left-color: #5a4000 !important; }\n51\t.vv-accent-err { border-left-color: #5a1e1e !important; }\n52\t#vv-monitor .vv-card { transition: border-left-color 0.5s; }\n53\t\n54\t/* Status banner — coloured strip at top of card body */\n55\t.vv-banner { border-radius: 4px; padding: 5px 10px; margin-bottom: 10px;\n56\t display: flex; align-items: center; justify-content: space-between;\n57\t font-size: 12px; font-weight: 600; }\n58\t.vv-banner-ok { background: #061306; border: 1px solid #1a401a; color: #4caf50; }\n59\t.vv-banner-warn { background: #130e00; border: 1px solid #3d2e00; color: #ff9800; }\n60\t.vv-banner-err { background: #140404; border: 1px solid #3d1010; color: #f44336; }\n61\t.vv-banner-off { background: #0d0d0d; border: 1px solid #252525; color: #555; }\n62\t\n63\t/* System card — no h3, no top padding waste */\n64\t#vv-system { padding-top: 14px; }\n65\t\n66\t/* System action buttons */\n67\t.vv-sys-btn { background: #2a2a2a; border: 1px solid #e65100; color: #ff9800; border-radius: 3px;\n68\t padding: 3px 0; font-size: 6px; cursor: pointer; line-height: 1;\n69\t width: 50px; min-width: 0; text-align: center; }\n70\t.vv-sys-btn:hover { background: #3a2000; color: #ffb74d; border-color: #ff9800; }\n71\t\n72\t/* Fallback state badge */\n73\t.vv-state-badge { font-size: 20px; font-weight: bold; padding: 4px 0; margin-bottom: 2px; }\n74\t.vv-state-normal { color: #4caf50; }\n75\t.vv-state-failover { color: #f44336; }\n76\t.vv-state-no_internet { color: #ff9800; }\n77\t.vv-state-dark { color: #9e9e9e; }\n78\t.vv-state-unknown { color: #666; }\n79\t\n80\t/* Docker table */\n81\t#vv-docker-table { width: 100%; border-collapse: collapse; font-size: 13px; }\n82\t#vv-docker-table th { text-align: left; padding: 4px 8px; color: #888;\n83\t border-bottom: 1px solid #444; }\n84\t#vv-docker-table td { padding: 4px 8px; border-bottom: 1px solid #2a2a2a; }\n85\t.vv-status-up { color: #4caf50; }\n86\t.vv-status-down { color: #f44336; }\n87\t\n88\t/* Scheduler */\n89\t.vv-hint { color: #888; font-size: 13px; margin-bottom: 16px; }\n90\t.vv-sched-card { margin-bottom: 10px; }\n91\t.vv-script { background: #161616; border: 1px solid #333; border-radius: 4px;\n92\t padding: 6px 10px; margin: 4px 0; margin-left: 20px; }\n93\t.vv-job-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;\n94\t transition: background 0.1s, box-shadow 0.1s; }\n95\t/* Orchestrator top row — clickable, show pointer + hover feedback */\n96\t.vv-orch-row { cursor: pointer; border-radius: 4px; }\n97\t.vv-orch-row:hover { background: rgba(255,255,255,0.04); }\n98\t.vv-job-label { flex: 1; font-size: 16px; font-weight: bold;\n99\t color: #6fcf97; min-width: 80px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n100\t.vv-job-actions { display: flex; align-items: center; gap: 8px; padding-left: 44px; margin-top: 6px; }\n101\t.vv-job-desc { font-size: 14px; color: #777; margin: 3px 0 2px 0;\n102\t padding-left: 44px; box-sizing: border-box; width: 100%;\n103\t white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n104\t cursor: default; }\n105\t.vv-cron { flex: 0 0 80px; width: 80px; background: #111; border: 1px solid #444; color: #ddd;\n106\t padding: 4px 6px; border-radius: 4px; font-family: monospace; font-size: 11px; }\n107\t.vv-event-badge { flex: 0 0 auto; padding: 3px 8px; border-radius: 4px; font-size: 12px;\n108\t background: #1a3a1a; border: 1px solid #2e6b2e; color: #6fcf6f;\n109\t white-space: nowrap; font-weight: 500; }\n110\t.vv-flag-badge { flex: 0 0 auto; padding: 2px 6px; border-radius: 3px; font-size: 11px;\n111\t background: #2e2200; border: 1px solid #6b4e00; color: #d4a017;\n112\t white-space: nowrap; font-family: monospace; }\n113\t.vv-log-label { display: flex; align-items: center; gap: 4px; font-size: 12px; color: #888;\n114\t cursor: pointer; white-space: nowrap; flex-shrink: 0; }\n115\t.vv-log-label input { cursor: pointer; accent-color: #4caf50; }\n116\t.vv-log-label:has(input:checked) { color: #4caf50; }\n117\t.vv-children { padding-top: 8px; border-top: 1px solid #333; margin-top: 8px; }\n118\t.vv-advanced-toggle { background: none; border: 1px solid #555; color: #aaa;\n119\t padding: 2px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; }\n120\t.vv-advanced-toggle:hover { border-color: #888; color: #fff; }\n121\t.vv-btn-sm { padding: 3px 10px; background: #2a2a2a; border: 1px solid #555; color: #ccc;\n122\t border-radius: 4px; cursor: pointer; font-size: 12px; white-space: nowrap; }\n123\t.vv-btn-sm:hover { border-color: #888; color: #fff; }\n124\t.vv-btn-sm.active { border-color: #4caf50; color: #4caf50; }\n125\t\n126\t/* Save checkmark */\n127\t.vv-save-check { color: #4caf50; font-size: 13px; width: 14px; flex-shrink: 0;\n128\t opacity: 0; text-align: center; }\n129\t@keyframes vv-check-fade { 0%,60% { opacity: 1; } 100% { opacity: 0; } }\n130\t.vv-save-check.vv-check-show { animation: vv-check-fade 2s forwards; }\n131\t\n132\t/* Running dot */\n133\t.vv-job-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex-shrink: 0; }\n134\t.vv-dot-running { background: #4caf50; animation: vv-pulse-dot 1s ease-in-out infinite; }\n135\t#vv-log-dot { animation: vv-pulse-dot 1s ease-in-out infinite; }\n136\t@keyframes vv-pulse-dot { 0%, 100% { opacity: 1; } 50% { opacity: 0.2; } }\n137\t\n138\t/* Selected job row — editor-style left accent + blue tint */\n139\t.vv-row-selected { background: rgba(100,149,237,0.1); border-radius: 4px;\n140\t outline: 1px solid rgba(100,149,237,0.35);\n141\t box-shadow: inset 3px 0 0 #6495ed; }\n142\t\n143\t/* Two-panel layout */\n144\t#vv-sched-layout { display: flex; gap: 16px; align-items: stretch; }\n145\t#vv-sched-left { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; align-self: flex-start; }\n146\t#vv-sched-cards { flex: 1; }\n147\t#vv-sched-right { display: none; flex: 1 1 0; min-width: 0; flex-direction: column; overflow: hidden; align-self: flex-start; }\n148\t#vv-sched-right.vv-panel-visible { display: flex; }\n149\t.vv-log-card { flex: 1; display: flex; flex-direction: column; padding-bottom: 0; }\n150\t.vv-log-right-pre { max-height: none; overflow-y: auto; }\n151\t\n152\t/* Scheduler stacked layout (narrow viewport) */\n153\t@media (max-width: 900px) {\n154\t #vv-sched-layout { flex-direction: column; align-items: stretch; }\n155\t #vv-sched-left { flex: none; width: 100%; }\n156\t #vv-sched-right { flex-direction: column; width: 100%; overflow: visible; }\n157\t .vv-log-right-pre { min-height: 520px; max-height: 680px; }\n158\t /* Toolbar: stack title row above buttons row, let buttons wrap */\n159\t .vv-log-toolbar { flex-direction: column; align-items: flex-start; gap: 6px; }\n160\t .vv-log-toolbar > div { flex-wrap: wrap; gap: 6px !important; }\n161\t /* Array-event rows: badge already shows the event, cron input is redundant */\n162\t .vv-job-row:has(.vv-event-badge) .vv-cron { display: none; }\n163\t}\n164\t\n165\t/* Plugin settings row (Advanced mode) */\n166\t.vv-nb-settings { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;\n167\t padding: 5px 10px; background: #101010; border-bottom: 1px solid #1a1a1a; }\n168\t\n169\t/* ── Monitor: locked row height + scrollable cards ────────────────────────── */\n170\t\n171\t/* Cards on the monitor grid are flex columns — h3 pins, body scrolls */\n172\t#vv-monitor .vv-card {\n173\t display: flex;\n174\t flex-direction: column;\n175\t overflow: hidden;\n176\t}\n177\t#vv-monitor .vv-card h3 { flex-shrink: 0; }\n178\t#vv-monitor .vv-card > div {\n179\t flex: 1;\n180\t min-height: 0;\n181\t overflow-y: auto;\n182\t -ms-overflow-style: none;\n183\t scrollbar-width: none;\n184\t}\n185\t#vv-monitor .vv-card > div::-webkit-scrollbar { display: none; }\n186\t/* Hide scrollbars on any nested scrollable div inside monitor cards */\n187\t#vv-monitor .vv-card div::-webkit-scrollbar { display: none; }\n188\t\n189\t/* Dynamic row heights capped per screen tier — rows size to content, never exceed the cap.\n190\t minmax(0, Xpx): track is content-driven but capped; align-items:stretch makes all cards\n191\t in a row fill the track, so short cards (Pools, Watchdog) match tall ones (Array).\n192\t Breakpoints are viewport height (after browser chrome), not screen height. */\n193\t\n194\t/* ~720p (viewport ≤ 700px) */\n195\t@media (min-width: 481px) and (max-height: 700px) {\n196\t #vv-monitor { grid-auto-rows: minmax(0, calc((100vh - 160px) / 4)); }\n197\t}\n198\t/* ~1080p (viewport 701–1100px) */\n199\t@media (min-width: 481px) and (min-height: 701px) and (max-height: 1100px) {\n200\t #vv-monitor { grid-auto-rows: minmax(0, calc((100vh - 240px) / 4)); }\n201\t}\n202\t/* ~1440p (viewport 1101–1450px) — calibrated on 15\" 1440p display */\n203\t@media (min-width: 481px) and (min-height: 1101px) and (max-height: 1450px) {\n204\t #vv-monitor { grid-auto-rows: minmax(0, calc((100vh - 335px) / 4)); }\n205\t}\n206\t/* ~4K (viewport > 1450px) */\n207\t@media (min-width: 481px) and (min-height: 1451px) {\n208\t #vv-monitor { grid-auto-rows: minmax(0, calc((100vh - 500px) / 4)); }\n209\t}\n210\t\n211\t/* Mobile: natural heights, let page scroll */\n212\t@media (max-width: 480px) {\n213\t #vv-monitor { grid-auto-rows: auto !important; }\n214\t #vv-monitor .vv-card { overflow: visible !important; }\n215\t #vv-monitor .vv-card > div { overflow-y: visible; min-height: auto; }\n216\t}\n217\t\n218\t/* Monitor responsive — 4-column grid at medium width */\n219\t@media (max-width: 1400px) {\n220\t /* CPU core bars — reduce gap/min-width at intermediate widths before cores get clipped */\n221\t .vv-cpu-cores { gap: 2px !important; }\n222\t .vv-cpu-core { min-width: 7px !important; }\n223\t}\n224\t\n225\t@media (max-width: 1024px) {\n226\t #vv-monitor { grid-template-columns: repeat(4, 1fr) !important; }\n227\t #vv-docker { grid-column: span 4 !important; }\n228\t /* Reset explicit placements so cards reflow in the 4-col grid */\n229\t #vv-docker-folders { grid-column: span 4 !important; }\n230\t #vv-parity-card { grid-column: auto !important; }\n231\t #vv-storage-card { grid-column: auto !important; }\n232\t #vv-array-card { grid-column: auto !important; }\n233\t /* Streams header: hide right chip group entirely, keep server badges + media type */\n234\t .vv-stream-right { display: none; }\n235\t}\n236\t\n237\t/* Containers+VMs: single column when viewport is narrow */\n238\t@media (max-width: 900px) {\n239\t .vv-df-cols { flex-direction: column; align-items: stretch; }\n240\t}\n241\t\n242\t/* Phone layout — scheduler row/actions fixes + monitor single-column */\n243\t@media (max-width: 480px) {\n244\t /* Phone portrait — hint gone, cron stays inline but compact */\n245\t .vv-cron-hint { display: none !important; }\n246\t .vv-job-row .vv-cron { flex: 0 0 68px; width: 68px; font-size: 10px; margin-left: 30px; }\n247\t}\n248\t\n249\t@media (max-width: 600px) {\n250\t /* Allow action buttons to wrap rather than overflow the card */\n251\t .vv-job-actions { flex-wrap: wrap; padding-left: 0; }\n252\t /* Kill the inline margin-left:auto that pushes Advanced off-screen */\n253\t .vv-advanced-toggle { margin-left: 0 !important; width: auto !important; }\n254\t /* Keep cron from growing — label owns remaining row space */\n255\t .vv-cron { flex: 0 0 75px; width: 75px; }\n256\t /* Slightly tighter label on narrow screens */\n257\t .vv-job-label { font-size: 14px; }\n258\t /* Footer buttons wrap instead of overflowing */\n259\t .vv-sched-footer { flex-wrap: wrap; }\n260\t /* Log toolbar search — narrow on small screens */\n261\t #vv-log-search { width: 80px; }\n262\t /* Snapshot footer — smaller on mobile */\n263\t .vv-snap-footer { gap: 10px !important; }\n264\t .vv-snap-item { gap: 4px; }\n265\t .vv-snap-label { font-size: 10px; }\n266\t .vv-snap-bar { width: 44px; height: 5px; }\n267\t .vv-snap-val { font-size: 11px; min-width: 26px; }\n268\t .vv-snap-div { font-size: 11px; }\n269\t .vv-snap-state { font-size: 11px; }\n270\t #vv-snap-partner { font-size: 11px; }\n271\t .vv-snap-media { font-size: 11px; }\n272\t\n273\t /* CPU core bars — shrink gap and min-width so many cores don't overflow */\n274\t .vv-cpu-cores { gap: 2px !important; }\n275\t .vv-cpu-core { min-width: 6px !important; }\n276\t\n277\t /* Monitor single-column — explicit placement cards need override too */\n278\t #vv-monitor { grid-template-columns: 1fr !important; }\n279\t #vv-monitor > .vv-card { grid-column: 1 / -1 !important; }\n280\t #vv-docker-folders { grid-column: 1 / -1 !important; }\n281\t}\n282\t\n283\t/* Shared footer (Save Schedule left, info right) — same min-height so log card ends level with script cards */\n284\t.vv-sched-footer { display: flex; align-items: center; gap: 10px;\n285\t margin-top: 12px; padding: 10px 0; border-top: 1px solid #333;\n286\t flex-shrink: 0; min-height: 72px; box-sizing: border-box; }\n287\t.vv-sched-info { color: #666; font-size: 14px; display: flex; flex-direction: column; gap: 9px; }\n288\t.vv-save-btn { padding: 5px 18px; background: #4caf50; border: none; color: #fff;\n289\t border-radius: 4px; cursor: pointer; font-size: 13px; }\n290\t.vv-save-btn:hover { background: #388e3c; }\n291\t.vv-save-status { font-size: 12px; color: #aaa; }\n292\t.vv-run-btn { border-color: #2196f3 !important; color: #2196f3 !important; }\n293\t.vv-run-btn:hover { background: #0d47a1 !important; color: #fff !important;\n294\t border-color: #2196f3 !important; }\n295\t.vv-dry-btn { border-color: #ff9800 !important; color: #ff9800 !important; }\n296\t.vv-dry-btn:hover { background: #e65100 !important; color: #fff !important;\n297\t border-color: #ff9800 !important; }\n298\t.vv-log-btn { border-color: #555 !important; color: #666 !important; }\n299\t.vv-log-btn:hover { background: #333 !important; color: #aaa !important; border-color: #777 !important; }\n300\t.vv-log-btn.vv-has-log { border-color: #4caf50 !important; color: #4caf50 !important; }\n301\t.vv-log-btn.vv-has-log:hover { background: #1b5e20 !important; color: #fff !important;\n302\t border-color: #4caf50 !important; }\n303\t.vv-edit-btn { border-color: #7b1fa2 !important; color: #ce93d8 !important; }\n304\t.vv-edit-btn:hover { background: #4a148c !important; color: #fff !important; border-color: #7b1fa2 !important; }\n305\t.vv-add-script-btn { background: #1565c0 !important; border: none !important; color: #fff !important; margin-left: 8px; }\n306\t.vv-add-script-btn:hover { background: #0d47a1 !important; }\n307\t.vv-save-script-btn-style { background: #1565c0 !important; border: none !important; color: #fff !important; }\n308\t.vv-save-script-btn-style:hover { background: #0d47a1 !important; }\n309\t.vv-delete-btn { background: #b71c1c !important; border: none !important; color: #fff !important; margin-left: 4px; }\n310\t.vv-delete-btn:hover { background: #7f0000 !important; }\n311\t.vv-conf-btn { border-color: #00838f !important; color: #4dd0e1 !important; }\n312\t.vv-conf-btn:hover { background: #006064 !important; color: #fff !important; border-color: #00838f !important; }\n313\t\n314\t/* Config form */\n315\t#vv-confform { padding: 2px 4px; }\n316\t.vv-cf-group { border-bottom: 1px solid #252525; padding-bottom: 14px; margin-bottom: 14px; }\n317\t.vv-cf-group:last-child { border-bottom: none; margin-bottom: 0; }\n318\t.vv-cf-group-header { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: bold;\n319\t color: #7a9eb5; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 10px; }\n320\t.vv-cf-file { font-size: 10px; color: #555; background: #1a1a1a; border: 1px solid #2e2e2e;\n321\t padding: 1px 6px; border-radius: 3px; font-weight: normal; text-transform: none;\n322\t letter-spacing: 0; }\n323\t.vv-cf-field { margin-bottom: 10px; }\n324\t.vv-cf-key { font-family: monospace; font-size: 12px; color: #ccc; margin-bottom: 3px; }\n325\t.vv-cf-desc { font-size: 11px; color: #666; margin-bottom: 4px; font-style: italic; line-height: 1.4; }\n326\t.vv-cf-scalar { display: block; width: 100%; box-sizing: border-box; background: #111; border: 1px solid #333;\n327\t color: #ddd; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; }\n328\t.vv-cf-scalar:focus { border-color: #555; outline: none; }\n329\t.vv-cf-array { display: block; width: 100%; box-sizing: border-box; background: #0d0d0d; border: 1px solid #333;\n330\t color: #ccc; padding: 8px; border-radius: 4px; font-family: monospace; font-size: 11px;\n331\t line-height: 1.6; resize: vertical; min-height: 60px; }\n332\t.vv-cf-array:focus { border-color: #555; outline: none; }\n333\t.vv-cf-empty { color: #555; font-size: 13px; font-style: italic; padding: 20px 4px; text-align: center; margin: 0; }\n334\t.vv-custom-empty { color: #666; font-size: 13px; padding: 8px 4px; margin: 0; font-style: italic; }\n335\t.vv-custom-count { font-size: 12px; color: #666; margin-left: 8px; flex-shrink: 0; }\n336\t.vv-section-sep { font-size: 11px; font-weight: bold; color: #666; text-transform: uppercase;\n337\t letter-spacing: 0.08em; padding: 10px 4px 4px; border-top: 1px solid #222; margin-top: 8px; }\n338\t#vv-editor { flex-direction: column; gap: 0; }\n339\t.vv-editor-body { font-family: monospace; font-size: 12px; background: #0d0d0d; color: #ccc;\n340\t border: 1px solid #333; border-radius: 4px; padding: 10px 12px; resize: none;\n341\t line-height: 1.5; scroll-behavior: auto; tab-size: 2; width: 100%; box-sizing: border-box;\n342\t white-space: pre; overflow-x: auto; }\n343\t\n344\t/* Editor layout: gutter + inner area — unified bordered block */\n345\t#vv-editor-wrap { display: flex; border: 1px solid #2e2e2e; border-radius: 4px 4px 0 0;\n346\t overflow: hidden; background: #0d0d0d; }\n347\t#vv-ln-gutter { width: 46px; min-width: 46px; flex-shrink: 0;\n348\t background: #0a0a0a; border-right: 1px solid #1c1c1c;\n349\t font-family: monospace; font-size: 12px; line-height: 1.5; tab-size: 2;\n350\t color: #3a3a3a; text-align: right; padding: 10px 8px 10px 0;\n351\t overflow: hidden; user-select: none; white-space: pre; }\n352\t.vv-gln-cur { color: #ccc; font-weight: bold; }\n353\t#vv-editor-inner { position: relative; flex: 1; overflow: hidden; background: #0d0d0d; }\n354\t\n355\t/* Current line highlight — sits behind overlay */\n356\t#vv-cur-line { position: absolute; left: 0; right: 0; height: 18px;\n357\t background: rgba(255,255,255,0.04);\n358\t border-top: 1px solid rgba(255,255,255,0.03);\n359\t border-bottom: 1px solid rgba(255,255,255,0.03);\n360\t pointer-events: none; display: none; }\n361\t\n362\t/* Syntax-highlight overlay — transparent bg so cur-line shows through */\n363\t#vv-hl-overlay { display: none; position: absolute; top: 0; left: 0; right: 0; bottom: 0;\n364\t margin: 0; padding: 10px 12px; box-sizing: border-box;\n365\t font-family: monospace; font-size: 12px; line-height: 1.5; tab-size: 2;\n366\t white-space: pre; overflow: hidden;\n367\t pointer-events: none; user-select: none;\n368\t background: transparent; border: none; border-radius: 0; }\n369\t.vv-editor-hl #vv-hl-overlay { display: block; }\n370\t.vv-editor-hl #vv-editor-body { color: transparent; caret-color: #ddd; background: transparent;\n371\t border: none !important; border-radius: 0 !important; }\n372\t\n373\t/* Visible selection — works even when text is transparent */\n374\t.vv-editor-hl #vv-editor-body::selection { background: rgba(100,149,237,0.35); color: transparent; }\n375\t.vv-editor-hl #vv-editor-body::-moz-selection{ background: rgba(100,149,237,0.35); color: transparent; }\n376\t\n377\t/* Word match and search marks in the overlay */\n378\t.vv-word-mark { background: rgba(255,220,60,0.14); outline: 1px solid rgba(255,220,60,0.35); border-radius: 2px; }\n379\t.vv-search-mark { background: rgba(255,140,20,0.22); outline: 1px solid rgba(255,140,20,0.45); border-radius: 2px; }\n380\t.vv-search-mark-current{ background: rgba(255,90,0,0.40); outline: 2px solid rgba(255,100,0,0.7); border-radius: 2px; }\n381\t\n382\t/* Slim scrollbar for editor */\n383\t#vv-editor-body::-webkit-scrollbar { width: 8px; height: 8px; }\n384\t#vv-editor-body::-webkit-scrollbar-track { background: #0a0a0a; }\n385\t#vv-editor-body::-webkit-scrollbar-thumb { background: #2a2a2a; border-radius: 4px; }\n386\t#vv-editor-body::-webkit-scrollbar-thumb:hover { background: #444; }\n387\t\n388\t/* Editor find bar — two-row (find + optional replace) */\n389\t#vv-editor-find { display: none; flex-direction: column; gap: 0; padding: 5px 8px;\n390\t margin-bottom: 5px; background: #161616; border: 1px solid #333;\n391\t border-radius: 4px; font-size: 12px; }\n392\t#vv-editor-find.vv-find-open { display: flex; }\n393\t.vv-find-row { display: flex; align-items: center; gap: 6px; }\n394\t.vv-replace-row { display: none; align-items: center; gap: 6px;\n395\t margin-top: 4px; padding-top: 4px; border-top: 1px solid #222; }\n396\t.vv-replace-row.vv-repl-open { display: flex; }\n397\t.vv-find-expand { font-size: 10px; color: #555; cursor: pointer; flex-shrink: 0;\n398\t width: 14px; text-align: center; user-select: none;\n399\t transition: transform 0.15s; line-height: 1; }\n400\t.vv-find-expand:hover { color: #aaa; }\n401\t.vv-find-expand.open { transform: rotate(90deg); color: #888; }\n402\t#vv-find-input, #vv-replace-input {\n403\t flex: 1; min-width: 0; background: #0d0d0d; border: 1px solid #333;\n404\t color: #ddd; padding: 4px 8px; border-radius: 3px;\n405\t font-family: monospace; font-size: 12px; }\n406\t#vv-find-input:focus, #vv-replace-input:focus {\n407\t border-color: #6495ed; outline: none; box-shadow: 0 0 0 1px rgba(100,149,237,0.25); }\n408\t#vv-find-input.vv-find-no-match { border-color: #c62828; box-shadow: 0 0 0 1px rgba(198,40,40,0.3); }\n409\t.vv-find-count { font-size: 11px; color: #555; white-space: nowrap; flex-shrink: 0; min-width: 64px; text-align: right; }\n410\t.vv-find-nav-btn{ padding: 2px 8px !important; }\n411\t.vv-repl-btn { border-color: #444 !important; color: #777 !important; }\n412\t.vv-repl-btn:hover { border-color: #777 !important; color: #ddd !important; }\n413\t.vv-find-x { color: #555; cursor: pointer; font-size: 16px; line-height: 1;\n414\t padding: 0 2px; flex-shrink: 0; }\n415\t.vv-find-x:hover { color: #aaa; }\n416\t\n417\t/* Editor status bar */\n418\t#vv-editor-status { display: none; align-items: center; gap: 10px; padding: 3px 10px;\n419\t background: #0e0e0e; border: 1px solid #2e2e2e; border-top: none;\n420\t border-radius: 0 0 4px 4px; font-family: monospace;\n421\t font-size: 10px; color: #444; user-select: none; flex-shrink: 0; }\n422\t#vv-editor-status.vv-ed-active { display: flex; }\n423\t.vv-es-pos { color: #666; }\n424\t.vv-es-sel { color: #555; }\n425\t.vv-es-lang { margin-left: auto; color: #4a6a7a; }\n426\t/* Status bar action buttons */\n427\t.vv-es-btn { background: none; border: none; cursor: pointer; color: #555;\n428\t font-size: 10px; padding: 1px 4px; border-radius: 2px; font-family: monospace; }\n429\t.vv-es-btn:hover { color: #aaa; background: rgba(255,255,255,0.05); }\n430\t.vv-es-btn.active { color: #6495ed; }\n431\t#vv-es-fontsize { color: #444; font-size: 10px; min-width: 26px; text-align: center; }\n432\t\n433\t/* Word wrap mode — applied to #vv-editor-wrap when wrap is enabled */\n434\t#vv-editor-wrap.vv-ed-wrap .vv-editor-body { white-space: pre-wrap !important; overflow-x: hidden !important; }\n435\t#vv-editor-wrap.vv-ed-wrap #vv-hl-overlay { white-space: pre-wrap !important; word-break: break-word !important; }\n436\t#vv-editor-wrap.vv-ed-wrap #vv-ln-gutter { display: none; }\n437\t#vv-editor-wrap.vv-ed-wrap #vv-cur-line { display: none !important; }\n438\t\n439\t/* Indent guides — 1px lines every 2 chars, starting at indent level 1 */\n440\t#vv-editor-inner {\n441\t position: relative; flex: 1; overflow: hidden;\n442\t background-color: #0d0d0d;\n443\t background-image: repeating-linear-gradient(\n444\t 90deg,\n445\t rgba(255,255,255,0.045) 0 1px,\n446\t transparent 1px 2ch\n447\t );\n448\t background-position: calc(12px + 2ch) 0;\n449\t}\n450\t\n451\t/* Go to line bar */\n452\t#vv-goto-bar { display: none; align-items: center; gap: 8px; padding: 5px 8px;\n453\t margin-bottom: 5px; background: #161616; border: 1px solid #333;\n454\t border-radius: 4px; font-size: 12px; }\n455\t#vv-goto-bar.vv-goto-open { display: flex; }\n456\t#vv-goto-input { width: 68px; background: #0d0d0d; border: 1px solid #333; color: #ddd;\n457\t padding: 4px 8px; border-radius: 3px; font-family: monospace; font-size: 12px; }\n458\t#vv-goto-input:focus { border-color: #6495ed; outline: none; }\n459\t.vv-goto-info { font-size: 11px; color: #555; white-space: nowrap; flex-shrink: 0; }\n460\t\n461\t/* Undo / Redo buttons */\n462\t.vv-undo-redo-btn { border-color: #444 !important; color: #666 !important; }\n463\t.vv-undo-redo-btn:not([disabled]):hover { border-color: #777 !important; color: #ccc !important; background: #1e1e1e !important; }\n464\t.vv-undo-redo-btn[disabled] { opacity: 0.28 !important; cursor: default !important; pointer-events: none; }\n465\t.vv-undo-count { font-size: 10px; opacity: 0.7; margin-left: 2px; }\n466\t\n467\t/* Suggestions panel — accordion */\n468\t#vv-suggestions { overflow-y: auto; }\n469\t.vv-sug-block { border-bottom: 1px solid #1e1e1e; }\n470\t.vv-sug-header { display: flex; align-items: center; gap: 8px; padding: 7px 4px;\n471\t cursor: pointer; user-select: none; flex-wrap: wrap; }\n472\t.vv-sug-header:hover { background: rgba(255,255,255,0.03); }\n473\t.vv-sug-chevron { color: #555; font-size: 11px; flex-shrink: 0; width: 10px; }\n474\t.vv-sug-title { color: #7a9eb5; font-weight: bold; font-size: 13px; flex: 1; min-width: 120px; }\n475\t.vv-sug-cron { color: #ddd; background: #111; border: 1px solid #333; padding: 1px 6px;\n476\t border-radius: 3px; font-size: 11px; font-family: monospace; white-space: nowrap; flex-shrink: 0; }\n477\t.vv-sug-label { color: #666; font-size: 12px; flex: 1; min-width: 0; white-space: nowrap;\n478\t overflow: hidden; text-overflow: ellipsis; }\n479\t.vv-sug-status { font-size: 11px; flex-shrink: 0; margin-left: auto; }\n480\t.vv-sug-on { color: #4caf50; }\n481\t.vv-sug-off { color: #777; }\n482\t.vv-sug-none { color: #444; }\n483\t.vv-sug-body { padding: 0 14px 10px; }\n484\t.vv-sug-desc { font-size: 12px; color: #888; white-space: pre-wrap; word-break: break-word;\n485\t background: none; border: none; margin: 4px 0 8px; padding: 0;\n486\t font-family: inherit; line-height: 1.6; }\n487\t.vv-sug-scripts { display: flex; flex-direction: column; gap: 4px; }\n488\t.vv-sug-script-row { display: flex; align-items: center; gap: 8px; font-size: 12px; }\n489\t.vv-sug-inline-cron { color: #aaa; background: #111; padding: 1px 5px; border-radius: 3px;\n490\t font-size: 11px; white-space: nowrap; }\n491\t.vv-sug-path { color: #888; font-family: monospace; }\n492\t.vv-sug-configured { color: #4caf50; font-size: 11px; }\n493\t/* Info sections inside Scheduler Information panel */\n494\t.vv-info-block .vv-sug-title { color: #9ab; }\n495\t.vv-info-body { padding: 2px 10px 10px; }\n496\t.vv-info-cols { margin: 0; padding-left: 16px; columns: 2; column-gap: 20px; column-fill: balance; }\n497\t.vv-info-cols li { font-size: 12px; color: #aaa; margin-bottom: 5px; break-inside: avoid; line-height: 1.5; }\n498\t.vv-info-cols .vv-info-sep { column-span: all; list-style: none; margin: 10px -4px 6px;\n499\t padding: 4px 8px; background: #161a1d; border-left: 2px solid #1e6fa5;\n500\t font-size: 10px; font-weight: bold; text-transform: uppercase;\n501\t letter-spacing: .08em; color: #4a8ab5; break-inside: avoid; }\n502\t.vv-info-cols li strong { color: #ccc; }\n503\t.vv-info-cols code { background: #1a1a1a; padding: 0 4px; border-radius: 2px;\n504\t font-size: 11px; color: #9ab; border: 1px solid #333; }\n505\t.vv-info-divider { font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; color: #444;\n506\t padding: 10px 12px 4px; border-top: 1px solid #2a2a2a; margin-top: 2px; }\n507\t\n508\t/* How do I use this — pinned at top of suggestions panel */\n509\t#vv-how-to-use {\n510\t position: sticky;\n511\t top: 0;\n512\t z-index: 3;\n513\t background: #1e1e1e;\n514\t border-bottom: 1px solid #333;\n515\t}\n516\t\n517\t/* Run-status dot colours (tree + activity list) */\n518\t.vv-stat-ok { color: #4caf50; }\n519\t.vv-stat-warn { color: #ff9800; }\n520\t.vv-stat-error{ color: #f44336; }\n521\t.vv-stat-skip { color: #607d8b; }\n522\t.vv-stat-none { color: #444; }\n523\t\n524\t/* Cron humanizer hint — left of cron input; first to collapse under space pressure */\n525\t.vv-cron-hint { font-size: 10px; color: #505050; white-space: nowrap;\n526\t flex: 0 10 auto; min-width: 0; max-width: 130px;\n527\t overflow: hidden; text-overflow: ellipsis;\n528\t pointer-events: none; user-select: none; }\n529\t\n530\t/* Recent Activity list */\n531\t.vv-activity-list { display: flex; flex-direction: column; }\n532\t.vv-activity-row { display: flex; align-items: center; gap: 6px; padding: 4px 0;\n533\t border-bottom: 1px solid #1a1a1a; font-size: 11px; cursor: pointer; }\n534\t.vv-activity-row:last-child { border-bottom: none; }\n535\t.vv-activity-row:hover .vv-activity-label { color: #fff; }\n536\t.vv-activity-dot { flex-shrink: 0; font-size: 9px; }\n537\t.vv-activity-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;\n538\t white-space: nowrap; color: #ccc; }\n539\t.vv-activity-ago { color: #555; font-size: 10px; white-space: nowrap; flex-shrink: 0; }\n540\t.vv-activity-dur { color: #444; font-size: 10px; white-space: nowrap; flex-shrink: 0;\n541\t min-width: 32px; text-align: right; }\n542\t\n543\t/* Log search */\n544\t#vv-log-search { width: 120px; font-size: 11px; padding: 2px 6px;\n545\t background: #111; border: 1px solid #333; color: #ccc;\n546\t border-radius: 3px; font-family: monospace; }\n547\t#vv-log-search:focus { border-color: #555; outline: none; }\n548\tmark { background: #5d4037; color: #ffcc80; border-radius: 2px; }\n549\t.vv-log-dim { opacity: 0.2; }\n550\t\n551\t/* Cron Calculator */\n552\t.vv-calc-wrap { display: flex; flex-direction: column; gap: 8px; }\n553\t.vv-calc-in { width: 100%; background: #111; border: 1px solid #333; color: #ccc;\n554\t padding: 5px 8px; border-radius: 3px; font-family: monospace; font-size: 12px;\n555\t box-sizing: border-box; }\n556\t.vv-calc-in:focus { border-color: #555; outline: none; }\n557\t.vv-calc-expr { font-family: monospace; font-size: 13px; color: #7cb8e8;\n558\t background: #0d1117; padding: 4px 8px; border-radius: 3px; }\n559\t.vv-calc-desc { font-size: 12px; color: #aaa; padding: 2px 2px 0; }\n560\t.vv-calc-hint { font-size: 11px; color: #555; font-style: italic; }\n561\t.vv-calc-runs-lbl { font-size: 10px; color: #555; text-transform: uppercase;\n562\t letter-spacing: .05em; margin-top: 4px; }\n563\t.vv-calc-runs { display: flex; flex-direction: column; gap: 2px; }\n564\t.vv-calc-run-row { display: flex; gap: 8px; font-size: 11px; }\n565\t.vv-calc-run-in { color: #4caf50; white-space: nowrap; min-width: 56px; }\n566\t.vv-calc-run-at { color: #666; }\n567\t.vv-calc-apply-btn { align-self: flex-start; margin-top: 2px; }\n568\t\n569\t/* Board blocks — Next Runs, Errors, Locks, Partner, Disabled */\n570\t.vv-board-placeholder { color: #555; font-size: 11px; padding: 3px 0; }\n571\t\n572\t.vv-hdr-badge { display: inline-block; padding: 1px 7px; border-radius: 10px;\n573\t font-size: 10px; font-weight: bold; flex-shrink: 0; }\n574\t.vv-hdr-badge-red { background: #7f0000; color: #ef9a9a; }\n575\t.vv-hdr-badge-orange { background: #5d2000; color: #ffcc80; }\n576\t.vv-hdr-badge-gray { background: #2a2a2a; color: #aaa; }\n577\t\n578\t/* Next Runs */\n579\t.vv-nextrun-list { display: flex; flex-direction: column; }\n580\t.vv-nextrun-row { display: flex; align-items: center; gap: 8px; padding: 4px 0;\n581\t border-bottom: 1px solid #1a1a1a; font-size: 12px; }\n582\t.vv-nextrun-row:last-child { border-bottom: none; }\n583\t.vv-nr-label { color: #ccc; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n584\t.vv-nr-cron { color: #555; font-family: monospace; font-size: 10px; flex-shrink: 0; }\n585\t.vv-nr-in { color: #4caf50; font-size: 11px; white-space: nowrap; flex-shrink: 0; }\n586\t.vv-nr-at { color: #666; font-size: 11px; white-space: nowrap; flex-shrink: 0; }\n587\t\n588\t/* Errors */\n589\t.vv-errors-list { display: flex; flex-direction: column; gap: 1px; }\n590\t.vv-err-row { padding: 5px 0; border-bottom: 1px solid #1a1a1a; }\n591\t.vv-err-row:last-child { border-bottom: none; }\n592\t.vv-err-top { display: flex; align-items: baseline; gap: 8px; margin-bottom: 2px; }\n593\t.vv-err-script { color: #e07070; font-size: 11px; font-weight: bold; }\n594\t.vv-err-age { color: #555; font-size: 10px; flex: 1; padding-left: 6px; white-space: nowrap; }\n595\t.vv-ack-btn { margin-left: auto; font-size: 10px; padding: 1px 6px;\n596\t color: #555; border-color: #333; flex-shrink: 0; }\n597\t.vv-ack-btn:hover { color: #aaa; border-color: #555; }\n598\t.vv-err-line { color: #888; font-size: 11px; word-break: break-all; line-height: 1.4; }\n599\t\n600\t/* Locks */\n601\t.vv-locks-list { display: flex; flex-direction: column; }\n602\t.vv-lock-row { display: flex; align-items: center; gap: 8px; padding: 4px 0;\n603\t border-bottom: 1px solid #1a1a1a; font-size: 12px; }\n604\t.vv-lock-row:last-child { border-bottom: none; }\n605\t.vv-lk-name { color: #e8a87c; flex: 1; }\n606\t.vv-lk-age { color: #777; font-size: 11px; white-space: nowrap; }\n607\t.vv-lock-clear { font-size: 11px !important; padding: 1px 7px !important;\n608\t border-color: #b71c1c !important; color: #ef9a9a !important; }\n609\t.vv-lock-clear:hover { background: #7f0000 !important; color: #fff !important; }\n610\t\n611\t/* Partner */\n612\t.vv-partner-row { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 12px; }\n613\t.vv-partner-name { color: #ccc; }\n614\t.vv-partner-detail { color: #666; font-size: 11px; }\n615\t.vv-partner-down { color: #f44336 !important; }\n616\t\n617\t/* Disabled scripts */\n618\t.vv-disabled-list { display: flex; flex-direction: column; gap: 1px; }\n619\t.vv-disabled-row { display: flex; align-items: center; gap: 8px; padding: 3px 0;\n620\t border-bottom: 1px solid #1a1a1a; font-size: 12px; }\n621\t.vv-disabled-row:last-child { border-bottom: none; }\n622\t.vv-disabled-name { color: #888; flex: 1; }\n623\t.vv-disabled-grp { color: #444; font-size: 10px; font-family: monospace; white-space: nowrap; }\n624\t\n625\t/* Notification board */\n626\t.vv-nb-board { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;\n627\t padding: 7px 10px; background: #141414; border-bottom: 1px solid #222;\n628\t font-size: 12px; min-height: 34px; }\n629\t.vv-nb-stat { color: #aaa; }\n630\t.vv-nb-sep { color: #444; }\n631\t.vv-nb-running { color: #f0a040; }\n632\t.vv-nb-conf-btns { display: flex; gap: 6px; margin-left: auto; }\n633\t.vv-nb-conf-btn { font-size: 11px; font-family: monospace; }\n634\t\n635\t/* Advanced mode button */\n636\t.vv-adv-mode-btn { border: 1px solid #555; color: #999; transition: background 0.15s, color 0.15s, border-color 0.15s; }\n637\t.vv-adv-mode-btn:hover { border-color: #888; color: #fff; }\n638\t.vv-adv-mode-btn.vv-adv-mode-on { background: #1565c0; border-color: #1565c0; color: #fff; }\n639\t\n640\t/* Script browser in suggestions panel */\n641\t.vv-sb-desc { font-size: 12px; color: #888; margin: 4px 0 6px; line-height: 1.5; }\n642\t.vv-sb-child-desc { margin-left: 16px; }\n643\t.vv-sb-hdr { font-family: monospace; font-size: 11px; color: #666; white-space: pre-wrap;\n644\t word-break: break-word; background: none; border: none; margin: 4px 0 8px;\n645\t padding: 0; line-height: 1.5; border-left: 2px solid #222; padding-left: 8px; }\n646\t.vv-sb-full { font-family: monospace; font-size: 11px; color: #888; white-space: pre-wrap;\n647\t word-break: break-word; background: #0a0a0a; border: 1px solid #222;\n648\t border-radius: 3px; margin: 6px 0 8px; padding: 8px 10px; line-height: 1.5;\n649\t max-height: 480px; overflow-y: auto; }\n650\t.vv-sb-child-block { border-top: 1px solid #1a1a1a; margin-top: 8px; padding-top: 8px; }\n651\t.vv-sb-child-name { display: flex; align-items: center; gap: 6px; margin-bottom: 3px; flex-wrap: wrap; }\n652\t\n653\t/* README content display */\n654\t.vv-readme-body { font-family: monospace; font-size: 11px; color: #777; white-space: pre-wrap;\n655\t word-break: break-word; background: none; border: none; margin: 0;\n656\t padding: 0 4px; line-height: 1.6; }\n657\t\n658\t/* Script browser tree rows */\n659\t#vv-sb-tree { padding: 0; }\n660\t.vv-sb-entry { }\n661\t.vv-sb-row { display: flex; align-items: center; gap: 6px; padding: 5px 6px;\n662\t cursor: pointer; border-radius: 3px; user-select: none;\n663\t transition: background 0.1s, box-shadow 0.1s; }\n664\t.vv-sb-row:hover { background: rgba(255,255,255,0.04); }\n665\t.vv-sb-selected { background: rgba(100,149,237,0.14) !important;\n666\t outline: 1px solid rgba(100,149,237,0.4);\n667\t box-shadow: inset 3px 0 0 #6495ed; }\n668\t.vv-sb-orch-row { border-bottom: 1px solid #1c1c1c; }\n669\t.vv-sb-child-row { padding-left: 2px; }\n670\t.vv-sb-expand { width: 14px; flex-shrink: 0; color: #555; font-size: 10px; text-align: center; }\n671\t.vv-sb-expand:hover { color: #aaa; }\n672\t.vv-sb-leaf { cursor: default; pointer-events: none; }\n673\t.vv-sb-name { flex: 1; font-size: 13px; color: #b0c4d0; min-width: 0;\n674\t white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n675\t.vv-sb-orch-row .vv-sb-name { font-weight: bold; color: #9ab; }\n676\t.vv-sb-children { padding-left: 10px; }\n677\t.vv-sb-child-indent { width: 14px; flex-shrink: 0; color: #333; font-size: 11px;\n678\t text-align: center; pointer-events: none; }\n679\t.vv-sb-badge { flex-shrink: 0; font-size: 11px; }\n680\t.vv-sb-cron { color: #555; background: #111; border: 1px solid #222;\n681\t padding: 0 4px; border-radius: 2px; font-size: 10px;\n682\t font-family: monospace; flex-shrink: 0; }\n683\t.vv-sb-status { font-size: 10px; flex-shrink: 0; }\n684\t\n685\t/* Script info content area */\n686\t.vv-si-hdr { font-family: monospace; font-size: 12px; color: #4d894d; white-space: pre-wrap;\n687\t word-break: break-word; background: none; border: none; margin: 0;\n688\t padding: 8px 12px; line-height: 1.6; }\n689\t.vv-si-src { font-family: monospace; font-size: 11px; white-space: pre-wrap;\n690\t word-break: break-word; background: none; border: none; margin: 0;\n691\t padding: 8px 12px; line-height: 1.5; }\n692\t\n693\t/* Cog settings button next to script name */\n694\t.vv-cog-btn { cursor:pointer; color:#444; font-size:.85em; margin-right:4px;\n695\t line-height:1; vertical-align:middle; user-select:none; }\n696\t.vv-cog-btn:hover { color:#aaa; }\n697\t\n698\t/* Enriched script info blocks (advanced mode: header + docs + config) */\n699\t.vv-sinfo-block { border-top: 1px solid #222; padding: 0; }\n700\t.vv-sinfo-block:first-child { border-top: none; }\n701\t.vv-sinfo-lbl { font-size: 10px; font-weight: bold; text-transform: uppercase;\n702\t letter-spacing: .06em; color: #555; padding: 6px 12px 2px; }\n703\t\n704\t/* Syntax highlight tokens — dark theme (VSCode-inspired) */\n705\t.vv-hl-sep { color: #2d2d2d; }\n706\t.vv-hl-shebang { color: #4a4a4a; }\n707\t.vv-hl-hash { color: #4a7340; }\n708\t.vv-hl-comment { color: #6a9955; }\n709\t.vv-hl-section { color: #9cdcfe; font-weight: bold; letter-spacing: 0.04em; }\n710\t.vv-hl-key { color: #9cdcfe; }\n711\t.vv-hl-value { color: #ce9178; }\n712\t.vv-hl-cron { color: #d7ba7d; font-weight: bold; }\n713\t.vv-hl-text { color: #4d894d; }\n714\t.vv-hl-keyword { color: #569cd6; }\n715\t.vv-hl-builtin { color: #4ec9b0; }\n716\t.vv-hl-string { color: #ce9178; }\n717\t.vv-hl-var { color: #d7ba7d; }\n718\t.vv-hl-number { color: #b5cea8; }\n719\t.vv-hl-op { color: #808080; }\n720\t\n721\t/* Log panel */\n722\t.vv-log-panel { margin-top: 10px; border-top: 1px solid #333; padding-top: 8px; }\n723\t.vv-log-toolbar { display: flex; justify-content: space-between; align-items: center;\n724\t flex-wrap: wrap; gap: 4px; margin-bottom: 4px; }\n725\t.vv-log-ts { font-size: 11px; color: #666; }\n726\t.vv-log-pre { background: #0d0d0d; border: 1px solid #333; border-radius: 4px;\n727\t padding: 10px 12px; margin: 0; font-family: monospace; font-size: 12px;\n728\t color: #ccc; white-space: pre-wrap; word-break: break-all;\n729\t max-height: 340px; overflow-y: auto; line-height: 1.5;\n730\t scroll-behavior: auto; }\n731\t\n732\t/* Toggle switch */\n733\t.vv-toggle { position: relative; display: inline-block; width: 36px; height: 20px; flex-shrink: 0; }\n734\t.vv-toggle input { opacity: 0; width: 0; height: 0; }\n735\t.vv-slider { position: absolute; inset: 0; background: #444; border-radius: 20px; cursor: pointer;\n736\t transition: 0.2s; }\n737\t.vv-slider:before { content: ''; position: absolute; width: 14px; height: 14px; left: 3px; bottom: 3px;\n738\t background: #fff; border-radius: 50%; transition: 0.2s; }\n739\t.vv-toggle input:checked + .vv-slider { background: #4caf50; }\n740\t.vv-toggle input:checked + .vv-slider:before { transform: translateX(16px); }\n741\t\n742\t/* Config editor */\n743\t#vv-conf-tabs { display: flex; gap: 4px; margin-bottom: 8px; }\n744\t.vv-conf-tab { padding: 4px 12px; text-decoration: none; color: #aaa;\n745\t border: 1px solid #444; border-radius: 4px; font-size: 13px; }\n746\t.vv-conf-tab.active { color: #fff; background: #333; border-color: #666; }\n747\t#vv-conf-editor { width: 100%; min-height: 500px; background: #111; color: #ddd;\n748\t border: 1px solid #444; padding: 12px; font-family: monospace;\n749\t font-size: 13px; line-height: 1.5; border-radius: 4px; box-sizing: border-box; resize: vertical; }\n750\t#vv-conf-actions { margin-top: 8px; display: flex; align-items: center; gap: 10px; }\n751\t#vv-conf-actions button { padding: 6px 18px; background: #4caf50; border: none;\n752\t color: #fff; border-radius: 4px; cursor: pointer; font-size: 14px; }\n753\t#vv-conf-actions button:hover { background: #388e3c; }\n754\t#vv-conf-status { font-size: 13px; color: #aaa; }\n755\t\n756\t/* Docs */\n757\t#vv-docs { display: flex; gap: 16px; }\n758\t#vv-docs-sidebar { width: 220px; flex-shrink: 0; }\n759\t#vv-docs-sidebar h3 { font-size: 12px; text-transform: uppercase; color: #888; margin: 0 0 8px; }\n760\t#vv-docs-sidebar ul { list-style: none; padding: 0; margin: 0; }\n761\t#vv-docs-sidebar li { margin: 2px 0; }\n762\t#vv-docs-sidebar a { display: block; padding: 3px 8px; font-size: 12px; color: #aaa;\n763\t text-decoration: none; border-radius: 3px; }\n764\t#vv-docs-sidebar a:hover { background: #222; color: #fff; }\n765\t#vv-docs-sidebar a.active { background: #333; color: #fff; }\n766\t#vv-docs-content { flex: 1; min-width: 0; }\n767\t.vv-doc-body { background: #1a1a1a; border: 1px solid #444; border-radius: 6px;\n768\t padding: 20px; line-height: 1.7; }\n769\t.vv-doc-body h1, .vv-doc-body h2, .vv-doc-body h3 { color: #ddd; }\n770\t.vv-doc-body code { background: #111; padding: 1px 5px; border-radius: 3px; font-size: 12px; }\n771\t.vv-doc-body pre { background: #111; padding: 12px; border-radius: 4px; overflow-x: auto; }\n772\t.vv-doc-hint { font-size: 12px; color: #666; margin-top: 8px; }\n773\t\n774\t/* Media stream sessions */\n775\t.vv-stream-servers { display: flex; justify-content: space-between; align-items: center;\n776\t flex-wrap: nowrap; gap: 8px; margin-bottom: 10px; }\n777\t/* Left side: server badges + media type */\n778\t.vv-stream-left { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; flex-shrink: 0; }\n779\t/* Right side: device / resolution / codec chip groups */\n780\t.vv-stream-right { display: flex; align-items: center; gap: 0; flex-wrap: nowrap; flex-shrink: 0; }\n781\t.vv-server-badge { background: #333; border: 1px solid #555; color: #aaa;\n782\t font-size: 10px; padding: 1px 7px; border-radius: 10px; }\n783\t.vv-server-badge-sm { font-size: 9px; padding: 1px 6px; flex-shrink: 0; }\n784\t/* Chip groups — server badge row */\n785\t.vv-device-sep { color: #2a2a2a; font-size: 12px; margin: 0 2px; user-select: none; }\n786\t/* shared chip geometry */\n787\t.vv-device-chip,\n788\t.vv-chip-device, .vv-chip-res, .vv-chip-codec, .vv-chip-mtype {\n789\t font-size: 10px; padding: 1px 7px; border-radius: 10px;\n790\t white-space: nowrap; border: 1px solid; }\n791\t/* media type — purple */\n792\t.vv-chip-mtype { color: #9a6abf; background: #1a0d2e; border-color: #3a1e55; }\n793\t/* device — blue */\n794\t.vv-chip-device { color: #5a9ec8; background: #0d1e2e; border-color: #1c3f5c; }\n795\t/* resolution — teal */\n796\t.vv-chip-res { color: #4aaa9a; background: #0b1e1c; border-color: #1a4540; }\n797\t/* codec — amber */\n798\t.vv-chip-codec { color: #b38820; background: #201500; border-color: #483300; }\n799\t/* legacy fallback */\n800\t.vv-device-chip { color: #666; background: #181818; border-color: #252525; }\n801\t/* chip group wrappers — inline so they flow with the badge row */\n802\t.vv-chip-group-device, .vv-chip-group-res, .vv-chip-group-codec { display: contents; }\n803\t.vv-stream-empty { color: #555; font-style: italic; font-size: 12px; margin: 4px 0; }\n804\t.vv-stream-empty span { font-size: 11px; color: #444; }\n805\t.vv-stream-row { margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #282828; }\n806\t.vv-stream-row:last-child { margin-bottom: 0; padding-bottom: 0; border-bottom: none; }\n807\t.vv-stream-top { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }\n808\t.vv-stream-icon { font-size: 10px; color: #888; flex-shrink: 0; }\n809\t.vv-stream-title { flex: 1; font-size: 12px; color: #ddd; font-weight: 500;\n810\t white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n811\t.vv-stream-meta { display: flex; gap: 10px; flex-wrap: wrap; font-size: 10px;\n812\t color: #666; margin-bottom: 5px; }\n813\t.vv-stream-user { color: #888; }\n814\t.vv-stream-client { color: #555; }\n815\t.vv-stream-method { font-weight: 500; }\n816\t.vv-stream-time { color: #555; margin-left: auto; }\n817\t.vv-stream-bar { height: 3px; background: #1a1a1a; border-radius: 2px; overflow: hidden; }\n818\t.vv-stream-bar div { height: 100%; border-radius: 2px; transition: width 0.4s; }\n819\t\n820\t/* Live var substitution colours */\n821\tcode.vv-live-var { color: #4caf50; background: #0d1f0d; }\n822\tcode.vv-unknown-var { color: #ff9800; background: #1f130d; }\n823\t\n824\t/* ── Arrange mode ─────────────────────────────────────────────────────────── */\n825\t.vv-drag-handle { cursor: grab; color: #555; font-size: 14px; padding: 0 5px 0 0; user-select: none; flex-shrink: 0; }\n826\t.vv-drag-handle:hover { color: #888; }\n827\t.vv-drag-handle:active { cursor: grabbing; color: #aaa; }\n828\t/* Dragged item: semi-transparent with a dashed ring so you still see what you grabbed */\n829\t.vv-drag-ghost { opacity: 0.4; outline: 1px dashed #555; border-radius: 4px; }\n830\t/* Drop insertion line: blue glow to match selection accent */\n831\t.vv-drop-line { height: 2px; background: #6495ed; border-radius: 2px; margin: 2px 0;\n832\t pointer-events: none; box-shadow: 0 0 6px rgba(100,149,237,0.7); }\n833\t.vv-arrange-active .vv-children { min-height: 28px; border: 1px dashed #2a2a2a; border-radius: 4px;\n834\t padding: 4px 2px; transition: border-color 0.12s, background 0.12s; }\n835\t/* Drop targets use blue to match selection theme */\n836\t.vv-arrange-active .vv-children.vv-drop-target { border-color: #6495ed; background: rgba(100,149,237,0.07); }\n837\t.vv-arrange-btn-active { background: #1a3a1e !important; color: #4caf50 !important; border-color: #2d5c33 !important; }\n838\t/* Grabbing cursor while dragging anything */\n839\t.vv-is-dragging, .vv-is-dragging * { cursor: grabbing !important; }\n840\t.vv-arrange-save-btn { background: #1a3a1e; border-color: #2d5c33; color: #4caf50; }\n841\t.vv-arrange-save-btn:hover { background: #22502a; }\n842\t#vv-arrange-btn { background: #7b1fa2; border-color: #7b1fa2; }\n843\t#vv-arrange-btn:hover { background: #4a148c; border-color: #4a148c; }\n844\t\n845\t/* ── Arrange workspace panel ─────────────────────────────────────────────── */\n846\t.vv-arrange-ws-hdr { font-size: 11px; font-weight: bold; color: #888; text-transform: uppercase;\n847\t letter-spacing: 0.6px; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }\n848\t#vv-pending-badge { background: #ff9800; color: #000; font-size: 10px; padding: 1px 7px;\n849\t border-radius: 10px; font-weight: bold; }\n850\t.vv-arrange-pending-hdr { font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 0.5px;\n851\t margin-bottom: 5px; }\n852\t.vv-pending-row { display: flex; align-items: center; gap: 8px; padding: 3px 0;\n853\t font-size: 11px; border-bottom: 1px solid #1e1e1e; }\n854\t.vv-pending-script { color: #ddd; font-weight: 500; }\n855\t.vv-pending-arrow { color: #555; font-size: 10px; }\n856\t.vv-library-zone { border: 1px dashed #2a2a2a; border-radius: 4px; padding: 6px;\n857\t min-height: 60px; transition: border-color 0.12s, background 0.12s; }\n858\t.vv-library-zone.vv-drop-target { border-color: #ef5350; background: rgba(239,83,80,0.09);\n859\t box-shadow: inset 0 0 8px rgba(239,83,80,0.08); }\n860\t.vv-arrange-drop-hint { font-size: 10px; color: #444; text-align: center; padding: 2px 0 7px; }\n861\t.vv-lib-card { background: #1c1c1c; border: 1px solid #2e2e2e; border-radius: 3px;\n862\t padding: 4px 8px; margin-bottom: 4px; cursor: grab; display: flex;\n863\t align-items: center; justify-content: space-between; gap: 8px; }\n864\t.vv-lib-card:hover { border-color: #444; }\n865\t.vv-lib-card.vv-drag-ghost { opacity: 0.35; }\n866\t.vv-lib-card-name { font-size: 16px; color: #ccc; font-weight: bold; }\n867\t.vv-lib-card-path { font-size: 10px; color: #444; font-family: monospace; }\n868\t\n869\t/* ── Custom script folders ────────────────────────────────────────────────── */\n870\t.vv-folder-group { margin-bottom: 1px; }\n871\t.vv-folder-row { display: flex; align-items: center; gap: 6px; padding: 3px 6px;\n872\t cursor: pointer; border-radius: 3px; color: #888; font-size: 12px; user-select: none; }\n873\t.vv-folder-row:hover { background: #1e1e1e; }\n874\t.vv-folder-chevron { font-size: 10px; color: #555; width: 10px; flex-shrink: 0; }\n875\t.vv-folder-name { flex: 1; font-weight: 500; color: #aaa; }\n876\t.vv-folder-count { font-size: 10px; color: #555; background: #1c1c1c;\n877\t padding: 0 5px; border-radius: 8px; border: 1px solid #2a2a2a; }\n878\t.vv-folder-children { padding-left: 12px; min-height: 4px; }\n879\t.vv-folder-children.vv-drop-target { background: rgba(100,149,237,0.07); border-radius: 4px;\n880\t outline: 1px dashed rgba(100,149,237,0.4); }\n881\t.vv-folder-new-row { display: flex; align-items: center; gap: 6px; padding: 4px 6px; }\n882\t.vv-new-folder-btn { background: #0277bd !important; border: none !important; color: #fff !important; }\n883\t.vv-new-folder-btn:hover { background: #01579b !important; }\n884\t\n885\t/* ── Snapshot footer (right panel status bar) ────────────────────────────── */\n886\t.vv-snap-footer { display: flex !important; flex-direction: row !important;\n887\t align-items: center !important; justify-content: center; gap: 20px; flex-wrap: wrap; }\n888\t.vv-snap-item { display: flex; align-items: center; gap: 7px; }\n889\t.vv-snap-label { font-size: 12px; color: #555; text-transform: uppercase; letter-spacing: 0.05em; }\n890\t.vv-snap-bar { width: 90px; height: 8px; background: #222; border-radius: 4px; overflow: hidden; flex-shrink: 0; }\n891\t.vv-snap-bar span { display: block; height: 100%; border-radius: 4px; width: 0;\n892\t transition: width 0.5s, background-color 0.5s; }\n893\t.vv-snap-val { font-size: 15px; color: #aaa; font-family: monospace; min-width: 36px; }\n894\t.vv-snap-div { color: #333; font-size: 15px; }\n895\t.vv-snap-state { font-size: 15px; font-weight: bold; }\n896\t#vv-snap-partner { font-size: 15px; color: #666; }\n897\t.vv-snap-media { font-size: 15px; color: #666; }\n898\t\n899\t/* ── Containers and VMs card ──────────────────────────────────────────────── */\n900\t.vv-df-section-hdr { font-size: 10px; font-weight: bold; color: #555; text-transform: uppercase;\n901\t letter-spacing: 0.08em; padding: 4px 2px 5px; border-bottom: 1px solid #222;\n902\t margin-bottom: 4px; }\n903\t.vv-df-empty { font-size: 12px; color: #555; font-style: italic; padding: 4px 2px 8px; }\n904\t.vv-df-vm-row { display: flex; align-items: center; gap: 8px; padding: 5px 2px;\n905\t border-bottom: 1px solid #1a1a1a; }\n906\t.vv-df-vm-row:last-of-type { border-bottom: none; }\n907\t.vv-df-vm-icon { font-size: 15px; line-height: 1; flex-shrink: 0; }\n908\t.vv-df-vm-meta { font-size: 10px; color: #555; }\n909\t.vv-df-cols { display: flex; gap: 10px; align-items: flex-start; }\n910\t.vv-df-col { flex: 1; min-width: 0; }\n911\t#vv-docker-folders-body { overflow-x: hidden; }\n912\t.vv-df-folder { border-bottom: 1px solid #1a1a1a; }\n913\t.vv-df-folder:last-child { border-bottom: none; }\n914\t.vv-df-folder-hdr { display: flex; align-items: center; gap: 6px; padding: 5px 4px;\n915\t cursor: pointer; user-select: none; border-radius: 3px; }\n916\t.vv-df-folder-hdr:hover { background: rgba(255,255,255,0.03); }\n917\t.vv-df-chevron { color: #555; font-size: 10px; width: 10px; flex-shrink: 0; }\n918\t.vv-df-fname { flex: 1; font-size: 12px; color: #aaa; font-weight: 500;\n919\t white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n920\t.vv-df-folder-body { padding-left: 10px; padding-bottom: 3px; }\n921\t.vv-df-container { display: flex; align-items: center; gap: 7px; padding: 3px 6px;\n922\t cursor: pointer; border-radius: 3px; user-select: none; }\n923\t.vv-df-container:hover { background: rgba(255,255,255,0.04); }\n924\t.vv-df-active { background: rgba(100,149,237,0.1) !important;\n925\t outline: 1px solid rgba(100,149,237,0.35);\n926\t box-shadow: inset 3px 0 0 #6495ed; }\n927\t.vv-df-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }\n928\t.vv-df-cname { flex: 1; font-size: 12px; color: #ccc;\n929\t white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n930\t.vv-df-status { font-size: 10px; color: #555; flex-shrink: 0; white-space: nowrap; }\n931\t.vv-df-actions { display: flex; gap: 6px; padding: 3px 6px 5px 24px; flex-wrap: wrap; }\n932\t\n933\t/* ── Rsync standalone controls ────────────────────────────────────────────── */\n934\t.vv-rsync-location { width: 120px; flex-shrink: 1; min-width: 60px; font-size: 11px; font-family: monospace;\n935\t background: #111; border: 1px solid #333; color: #aaa;\n936\t padding: 2px 6px; border-radius: 3px; }\n937\t.vv-rsync-location:focus { border-color: #555; outline: none; }\n938\t.vv-rsync-location::placeholder { color: #444; }\n939\t.vv-script-args { flex: 1 1 0; min-width: 60px; max-width: 180px; font-size: 11px; font-family: monospace;\n940\t background: #111; border: 1px solid #2a2a2a; color: #aaa;\n941\t padding: 2px 6px; border-radius: 3px;\n942\t opacity: 0.3; transition: opacity 0.15s; }\n943\t.vv-script-args:hover, .vv-script-args:focus { opacity: 1; border-color: #555; outline: none; }\n944\t.vv-script-args::placeholder { color: #3a3a3a; }\n945\t.vv-rsync-save-btn { background: #1a2e1a; border-color: #2a4a2a; color: #6aaa6a; }\n946\t.vv-rsync-save-btn:hover { background: #22382a; }\n947\t"}]},"uuid":"e1b038cd-ad12-4862-8f35-8e34572cc3a5","timestamp":"2026-06-06T14:29:36.747Z","sourceToolAssistantUUID":"e35a7a4e-fe87-4854-9736-7fb7fdc32f47","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"e1b038cd-ad12-4862-8f35-8e34572cc3a5","isSidechain":true,"agentId":"accc3589951fac88e","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01EHXveD5KL1GDDfpYXYummu","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01HbxKXgAvM9xyAW7Bo6ekAJ","name":"Read","input":{"file_path":"/boot/config/plugins/varaverk/Plugin/unraid/pages/arrs.php"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":366,"cache_read_input_tokens":7775,"cache_creation":{"ephemeral_5m_input_tokens":366,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null},"requestId":"req_011CbnAYyu8Br8CfBBXFBkPr","attributionAgent":"Explore","type":"assistant","uuid":"870d6419-5d02-419b-9fe5-88aa017e2976","timestamp":"2026-06-06T14:29:36.816Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"870d6419-5d02-419b-9fe5-88aa017e2976","isSidechain":true,"promptId":"3c1ac879-b13a-4ebc-9aeb-ea73615e9af3","agentId":"accc3589951fac88e","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01HbxKXgAvM9xyAW7Bo6ekAJ","type":"tool_result","content":"1\t<style>\n2\t/* ── Cards ───────────────────────────────────────────────── */\n3\t.vv-arr-card { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:12px 14px;min-width:0; }\n4\t.vv-arr-sep { border:none;border-top:1px solid #1e1e1e;margin:8px 0; }\n5\t\n6\t/* ── Card header ─────────────────────────────────────────── */\n7\t.vv-arr-hdr { display:flex;justify-content:space-between;align-items:center;margin-bottom:10px; }\n8\t.vv-arr-name { font-size:12px;font-weight:700;text-transform:uppercase;color:#888;letter-spacing:.06em; }\n9\t.vv-arr-dot { width:7px;height:7px;border-radius:50%;flex-shrink:0;margin-right:4px;display:inline-block; }\n10\t.vv-arr-ver { font-size:10px;color:#444; }\n11\t\n12\t/* ── Big stats ───────────────────────────────────────────── */\n13\t.vv-arr-stats { display:flex;gap:0;margin-bottom:10px; }\n14\t.vv-arr-stat { flex:1;text-align:center;padding:0 4px; }\n15\t.vv-arr-stat + .vv-arr-stat { border-left:1px solid #1e1e1e; }\n16\t.vv-arr-stat-n{ font-size:18px;font-weight:700;color:#ccc;line-height:1.1; }\n17\t.vv-arr-stat-l{ font-size:9px;color:#444;text-transform:uppercase;letter-spacing:.06em;margin-top:1px; }\n18\t.vv-arr-stat-n.dim { color:#333; }\n19\t\n20\t/* ── Disk bar ────────────────────────────────────────────── */\n21\t.vv-arr-disk { margin-bottom:8px; }\n22\t.vv-arr-disk-bar { height:3px;background:#1a1a1a;border-radius:2px;overflow:hidden;margin-top:3px; }\n23\t.vv-arr-disk-fill{ height:100%;border-radius:2px;transition:width .3s; }\n24\t.vv-arr-disk-lbl { display:flex;justify-content:space-between;align-items:baseline; }\n25\t\n26\t/* ── Queue badges ────────────────────────────────────────── */\n27\t.vv-arr-q { display:flex;gap:6px;align-items:center;flex-wrap:wrap; }\n28\t.vv-arr-qbadge{ font-size:10px;padding:1px 7px;border-radius:3px;font-weight:600; }\n29\t.vv-arr-qbadge.dl { background:#0a1a2a;color:#4a9eff;border:1px solid #1a3a5a; }\n30\t.vv-arr-qbadge.warn { background:#1f1500;color:#ffb74d;border:1px solid #3a2800; }\n31\t.vv-arr-qbadge.err { background:#200d0d;color:#ef5350;border:1px solid #3a1a1a; }\n32\t.vv-arr-qbadge.idle { background:#111;color:#333;border:1px solid #1e1e1e; }\n33\t\n34\t/* ── Health ──────────────────────────────────────────────── */\n35\t.vv-arr-health{ background:#1a0a0a;border-left:2px solid #ef5350;border-radius:0 3px 3px 0;\n36\t padding:5px 8px;margin-top:6px;font-size:10px;color:#c66;line-height:1.5; }\n37\t.vv-arr-health.warn { background:#1a1000;border-color:#ff9800;color:#c96; }\n38\t\n39\t/* ── Cleanup / Discovery rows ────────────────────────────── */\n40\t.vv-arr-meta { display:flex;justify-content:space-between;align-items:baseline;gap:8px;margin:3px 0; }\n41\t.vv-arr-meta-lbl{ font-size:10px;color:#444; }\n42\t.vv-arr-meta-val{ font-size:10px;color:#666;text-align:right; }\n43\t.vv-arr-meta-sub{ font-size:9px;color:#333;margin-top:0px; }\n44\t.vv-arr-meta-sub.warn { color:#8b4; }\n45\t\n46\t/* ── Root path ───────────────────────────────────────────── */\n47\t.vv-arr-root { font-size:10px;color:#2a2a2a;white-space:nowrap;overflow:hidden;\n48\t text-overflow:ellipsis;margin-top:8px;font-family:monospace; }\n49\t\n50\t/* ── Node section ────────────────────────────────────────── */\n51\t.vv-arr-node { grid-column:1/-1; }\n52\t.vv-arr-node-hdr { display:flex;align-items:baseline;gap:10px;margin-bottom:10px;\n53\t padding-bottom:7px;border-bottom:1px solid #1e1e1e; }\n54\t.vv-arr-node-id { font-size:11px;font-weight:700;color:#555;text-transform:uppercase;letter-spacing:.08em; }\n55\t.vv-arr-node-host { font-size:12px;color:#888; }\n56\t.vv-arr-node-tag { font-size:9px;color:#2a3a2a;background:#0d180d;border:1px solid #1a3a1a;\n57\t border-radius:3px;padding:1px 6px;margin-left:2px; }\n58\t.vv-arr-cards { display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px; }\n59\t\n60\t/* ── Sync + Recovery cards ───────────────────────────────── */\n61\t.vv-arr-sr-grid { grid-column:1/-1;display:grid;grid-template-columns:1fr 1fr;gap:10px; }\n62\t@media (max-width:700px) { .vv-arr-sr-grid { grid-template-columns:1fr; } }\n63\t.vv-arr-sr-num { font-size:20px;font-weight:700;color:#ccc;line-height:1; }\n64\t.vv-arr-sr-lbl { font-size:9px;color:#444;text-transform:uppercase;letter-spacing:.06em;margin-top:2px; }\n65\t.vv-arr-sr-row { display:flex;gap:16px;flex-wrap:wrap; }\n66\t.vv-arr-sr-item { display:flex;flex-direction:column;gap:1px; }\n67\t\n68\t/* ── Settings card ───────────────────────────────────────── */\n69\t.vv-arr-set-card { grid-column:1/-1; }\n70\t.vv-arr-tog-wrap { display:inline-flex;align-items:center;gap:8px;cursor:pointer;user-select:none; }\n71\t.vv-arr-tog-track{ width:28px;height:16px;border-radius:8px;background:#1e1e1e;border:1px solid #2a2a2a;\n72\t position:relative;transition:background .15s,border-color .15s;flex-shrink:0; }\n73\t.vv-arr-tog-track.on { background:#1a3a1a;border-color:#2d5a2d; }\n74\t.vv-arr-tog-track::after { content:'';position:absolute;top:2px;left:2px;width:10px;height:10px;\n75\t border-radius:50%;background:#444;transition:left .15s,background .15s; }\n76\t.vv-arr-tog-track.on::after { left:14px;background:#4caf50; }\n77\t.vv-arr-tog-lbl { font-size:11px;color:#666; }\n78\t.vv-arr-set-inp { background:#0d0d0d;border:1px solid #252525;border-radius:3px;color:#888;\n79\t font-size:11px;padding:3px 7px;outline:none;width:60px; }\n80\t.vv-arr-set-inp:focus { border-color:#444; }\n81\t.vv-arr-pill { display:inline-block;font-size:9px;padding:1px 7px;border-radius:3px;font-weight:700; }\n82\t.vv-arr-pill.ok { background:#0d1f0d;color:#4caf50;border:1px solid #1a3a1a; }\n83\t.vv-arr-pill.err { background:#200d0d;color:#ef5350;border:1px solid #3a1a1a; }\n84\t.vv-arr-pill.off { background:#1e1e1e;color:#444;border:1px solid #2a2a2a; }\n85\t</style>\n86\t\n87\t<div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding:0 2px;\">\n88\t <span style=\"font-size:13px;font-weight:bold;color:#888;text-transform:uppercase;letter-spacing:.06em;\">Arrs</span>\n89\t <span style=\"font-size:11px;color:#3a3a3a;\" id=\"vv-arrs-ts\"></span>\n90\t</div>\n91\t\n92\t<div id=\"vv-arrs-grid\" style=\"display:grid;grid-template-columns:repeat(8,1fr);gap:12px;\">\n93\t <div style=\"grid-column:1/-1;color:#444;font-size:12px;padding:24px 0;text-align:center;\">Loading…</div>\n94\t</div>\n95\t\n96\t<script>\n97\t(function() {\n98\t\n99\t// ── Helpers ───────────────────────────────────────────────────────────────────\n100\tfunction _rel(ts) {\n101\t if (!ts) return '—';\n102\t const d = Math.floor(Date.now() / 1000) - ts;\n103\t if (d < 60) return 'just now';\n104\t if (d < 3600) return Math.floor(d/60) + 'm ago';\n105\t if (d < 86400) return Math.floor(d/3600) + 'h ago';\n106\t if (d < 172800)return 'yesterday';\n107\t return Math.floor(d/86400) + 'd ago';\n108\t}\n109\tfunction _n(v) { return v == null ? '—' : Number(v).toLocaleString(); }\n110\tfunction _gb(b) { if (!b) return '—'; return (b / 1073741824).toFixed(1) + ' GB'; }\n111\tfunction _tb(b) { if (!b) return '—'; return (b / 1099511627776).toFixed(2) + ' TB'; }\n112\tfunction _sz(b) {\n113\t if (!b) return '—';\n114\t if (b >= 1099511627776) return (b/1099511627776).toFixed(1) + ' TB';\n115\t if (b >= 1073741824) return (b/1073741824).toFixed(1) + ' GB';\n116\t return (b/1048576).toFixed(0) + ' MB';\n117\t}\n118\t\n119\t// ── Arr card ──────────────────────────────────────────────────────────────────\n120\tfunction _arrCard(arr) {\n121\t const LABELS = { sonarr:'Sonarr', radarr:'Radarr', lidarr:'Lidarr' };\n122\t const name = LABELS[arr.type] || arr.type;\n123\t const online = !!arr.online;\n124\t const dotCol = online ? '#4caf50' : (arr.remote ? '#444' : '#c62828');\n125\t\n126\t // ── Offline / remote state ────────────────────────────────────────────────\n127\t if (!online) {\n128\t const msg = arr.remote ? 'not available from this host' : 'offline';\n129\t return `<div class=\"vv-arr-card\" style=\"opacity:.6;\">\n130\t <div class=\"vv-arr-hdr\">\n131\t <span class=\"vv-arr-name\">${name}</span>\n132\t <span><span class=\"vv-arr-dot\" style=\"background:${dotCol}\"></span>\n133\t <span class=\"vv-arr-ver\">${msg}</span></span>\n134\t </div>\n135\t ${arr.root ? `<div class=\"vv-arr-root\">${arr.root}</div>` : ''}\n136\t </div>`;\n137\t }\n138\t\n139\t // ── Stats ─────────────────────────────────────────────────────────────────\n140\t let stats = '';\n141\t if (arr.type === 'sonarr') {\n142\t stats = `\n143\t <div class=\"vv-arr-stat\"><div class=\"vv-arr-stat-n\">${_n(arr.total)}</div><div class=\"vv-arr-stat-l\">Series</div></div>\n144\t <div class=\"vv-arr-stat\"><div class=\"vv-arr-stat-n\">${_n(arr.monitored)}</div><div class=\"vv-arr-stat-l\">Monitored</div></div>\n145\t <div class=\"vv-arr-stat\"><div class=\"vv-arr-stat-n\">${_n(arr.episodes)}</div><div class=\"vv-arr-stat-l\">Episodes</div></div>`;\n146\t } else if (arr.type === 'radarr') {\n147\t const pct = arr.total ? Math.round((arr.files||0)/arr.total*100) : 0;\n148\t stats = `\n149\t <div class=\"vv-arr-stat\"><div class=\"vv-arr-stat-n\">${_n(arr.total)}</div><div class=\"vv-arr-stat-l\">Movies</div></div>\n150\t <div class=\"vv-arr-stat\"><div class=\"vv-arr-stat-n\">${_n(arr.files)}</div><div class=\"vv-arr-stat-l\">Have file</div></div>\n151\t <div class=\"vv-arr-stat\"><div class=\"vv-arr-stat-n ${pct<50?'dim':''}\">${pct}%</div><div class=\"vv-arr-stat-l\">Complete</div></div>`;\n152\t } else if (arr.type === 'lidarr') {\n153\t stats = `\n154\t <div class=\"vv-arr-stat\"><div class=\"vv-arr-stat-n\">${_n(arr.total)}</div><div class=\"vv-arr-stat-l\">Artists</div></div>\n155\t <div class=\"vv-arr-stat\"><div class=\"vv-arr-stat-n\">${_n(arr.monitored)}</div><div class=\"vv-arr-stat-l\">Monitored</div></div>\n156\t <div class=\"vv-arr-stat\"><div class=\"vv-arr-stat-n\">${_n(arr.albums)}</div><div class=\"vv-arr-stat-l\">Albums</div></div>`;\n157\t }\n158\t\n159\t // ── Disk bar ──────────────────────────────────────────────────────────────\n160\t let disk = '';\n161\t if (arr.disk?.length) {\n162\t const d = arr.disk.reduce((a,b) => (b.totalSpace||0) > (a.totalSpace||0) ? b : a);\n163\t const free = d.freeSpace, tot = d.totalSpace;\n164\t if (free && tot) {\n165\t const usedPct = Math.round((1 - free/tot)*100);\n166\t const col = usedPct > 90 ? '#ef5350' : usedPct > 75 ? '#ffb74d' : '#4caf50';\n167\t disk = `<div class=\"vv-arr-disk\">\n168\t <div class=\"vv-arr-disk-lbl\">\n169\t <span style=\"font-size:10px;color:#555;\">${_sz(free)} free</span>\n170\t <span style=\"font-size:10px;color:#333;\">${_sz(tot)} · ${usedPct}%</span>\n171\t </div>\n172\t <div class=\"vv-arr-disk-bar\">\n173\t <div class=\"vv-arr-disk-fill\" style=\"width:${usedPct}%;background:${col};\"></div>\n174\t </div>\n175\t </div>`;\n176\t }\n177\t }\n178\t\n179\t // ── Queue ─────────────────────────────────────────────────────────────────\n180\t const q = arr.queue || {};\n181\t const qDl = q.dl || 0, qWrn = q.warn || 0, qErr = q.err || 0;\n182\t const qTot = qDl + qWrn + qErr;\n183\t let qHtml;\n184\t if (qTot === 0) {\n185\t qHtml = `<span class=\"vv-arr-qbadge idle\">idle</span>`;\n186\t } else {\n187\t qHtml = '';\n188\t if (qDl) qHtml += `<span class=\"vv-arr-qbadge dl\">${qDl} downloading</span>`;\n189\t if (qWrn) qHtml += `<span class=\"vv-arr-qbadge warn\">${qWrn} warning</span>`;\n190\t if (qErr) qHtml += `<span class=\"vv-arr-qbadge err\">${qErr} error</span>`;\n191\t }\n192\t\n193\t // ── Health ────────────────────────────────────────────────────────────────\n194\t let health = '';\n195\t if (arr.health?.length) {\n196\t const hasErr = arr.health.some(h => h.type === 'error');\n197\t const cls = hasErr ? 'vv-arr-health' : 'vv-arr-health warn';\n198\t const msgs = arr.health.slice(0,3).map(h => h.message || h.type).join('<br>');\n199\t health = `<div class=\"${cls}\">${msgs}</div>`;\n200\t }\n201\t\n202\t // ── Cleanup ───────────────────────────────────────────────────────────────\n203\t let cleanup = '';\n204\t const cl = arr.cleanup || {};\n205\t if (cl.last_run) {\n206\t const st = cl.status === 'ok' ? '<span style=\"color:#4caf50\">✓</span>' : '<span style=\"color:#ef5350\">✗</span>';\n207\t const orph = cl.orphans || 0, junk = cl.junk || 0;\n208\t const orCol = (orph || junk) ? 'color:#8b4a2a' : 'color:#333';\n209\t cleanup = `\n210\t <div class=\"vv-arr-meta\">\n211\t <span class=\"vv-arr-meta-lbl\">Cleanup</span>\n212\t <span class=\"vv-arr-meta-val\">${_rel(cl.last_run)} ${st}</span>\n213\t </div>\n214\t ${cl.tracked != null ? `<div class=\"vv-arr-meta-sub\" style=\"${orCol}\">\n215\t ${_n(cl.tracked)} files · ${orph} orphans${junk ? ' · ' + junk + ' junk' : ''}\n216\t </div>` : ''}`;\n217\t }\n218\t\n219\t // ── Discovery ─────────────────────────────────────────────────────────────\n220\t let disc = '';\n221\t const dsc = arr.discovery || {};\n222\t if (dsc.last_run) {\n223\t const added = dsc.added != null ? ` <span style=\"color:#6fcf97\">+${dsc.added}</span>` : '';\n224\t disc = `<div class=\"vv-arr-meta\">\n225\t <span class=\"vv-arr-meta-lbl\">Discovery</span>\n226\t <span class=\"vv-arr-meta-val\">${_rel(dsc.last_run)}${added}</span>\n227\t </div>`;\n228\t }\n229\t\n230\t const hasMeta = cleanup || disc;\n231\t\n232\t return `<div class=\"vv-arr-card\">\n233\t <div class=\"vv-arr-hdr\">\n234\t <span class=\"vv-arr-name\">${name}</span>\n235\t <span>\n236\t <span class=\"vv-arr-dot\" style=\"background:${dotCol}\"></span>\n237\t <span class=\"vv-arr-ver\">${arr.version || 'online'}</span>\n238\t </span>\n239\t </div>\n240\t <div class=\"vv-arr-stats\">${stats}</div>\n241\t ${disk}\n242\t <hr class=\"vv-arr-sep\" style=\"margin:6px 0 8px;\">\n243\t <div class=\"vv-arr-q\">${qHtml}</div>\n244\t ${health}\n245\t ${hasMeta ? `<hr class=\"vv-arr-sep\" style=\"margin:8px 0 4px;\">${cleanup}${disc}` : ''}\n246\t <div class=\"vv-arr-root\">${arr.root || ''}</div>\n247\t </div>`;\n248\t}\n249\t\n250\t// ── Node section ──────────────────────────────────────────────────────────────\n251\tfunction _nodeSection(node) {\n252\t const isLocal = node.local;\n253\t const isMiss = node.cache_miss;\n254\t\n255\t let tag, body;\n256\t\n257\t if (isLocal) {\n258\t tag = '<span class=\"vv-arr-node-tag\">local</span>';\n259\t body = `<div class=\"vv-arr-cards\">${node.arrs.map(_arrCard).join('')}</div>`;\n260\t } else if (isMiss) {\n261\t tag = `<span class=\"vv-arr-node-tag\" style=\"background:#111;border-color:#222;color:#333;\">no data</span>\n262\t <button id=\"vv-arr-rfsh-${node.host}\" onclick=\"vvArrsRefreshRemote('${node.host}')\"\n263\t style=\"font-size:9px;color:#4a9eff;background:#0a1a2a;border:1px solid #1a3a5a;\n264\t border-radius:3px;padding:1px 8px;cursor:pointer;margin-left:6px;\">↻ Fetch now</button>`;\n265\t body = `<div style=\"color:#333;font-size:11px;padding:10px 0;\">\n266\t No remote data yet — first fetch runs within 2 hours, or click ↻ Fetch now.\n267\t </div>`;\n268\t } else {\n269\t const age = node.cache_age || 0;\n270\t const ageStr = age < 3600 ? Math.floor(age/60) + 'm ago'\n271\t : age < 86400 ? Math.floor(age/3600) + 'h ago'\n272\t : Math.floor(age/86400) + 'd ago';\n273\t tag = `<span class=\"vv-arr-node-tag\" style=\"background:#1a1000;border-color:#3a2800;color:#b87;\">cached ${ageStr}</span>\n274\t <button id=\"vv-arr-rfsh-${node.host}\" onclick=\"vvArrsRefreshRemote('${node.host}')\"\n275\t style=\"font-size:9px;color:#666;background:none;border:1px solid #2a2a2a;\n276\t border-radius:3px;padding:1px 8px;cursor:pointer;margin-left:6px;\">↻</button>`;\n277\t body = `<div class=\"vv-arr-cards\" style=\"opacity:.85;\">${node.arrs.map(_arrCard).join('')}</div>`;\n278\t }\n279\t\n280\t return `<div class=\"vv-arr-node\">\n281\t <div class=\"vv-arr-node-hdr\">\n282\t <span class=\"vv-arr-node-id\">${node.host.toUpperCase()}</span>\n283\t <span class=\"vv-arr-node-host\">${node.name}</span>${tag}\n284\t </div>\n285\t ${body}\n286\t </div>`;\n287\t}\n288\t\n289\t// ── Sync + Recovery row ───────────────────────────────────────────────────────\n290\tfunction _syncSection(sync, recovery, settings) {\n291\t const _card = (title, data, items, extra) => {\n292\t const ok = data.last_run && data.status === 'ok';\n293\t const bad = data.last_run && data.status !== 'ok';\n294\t const pillCls = !data.last_run ? 'off' : ok ? 'ok' : 'err';\n295\t const pillTxt = !data.last_run ? 'never run' : ok ? 'ok' : data.status || 'error';\n296\t let body = '';\n297\t if (data.last_run) {\n298\t body += `<div style=\"font-size:11px;color:#555;margin-bottom:10px;\">${_rel(data.last_run)}</div>`;\n299\t }\n300\t body += `<div class=\"vv-arr-sr-row\">${items}</div>`;\n301\t if (extra) body += extra;\n302\t return `<div class=\"vv-arr-card\">\n303\t <div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;\">\n304\t <span style=\"font-size:11px;font-weight:700;color:#555;text-transform:uppercase;letter-spacing:.07em;\">${title}</span>\n305\t <span class=\"vv-arr-pill ${pillCls}\">${pillTxt}</span>\n306\t </div>\n307\t ${body}\n308\t </div>`;\n309\t };\n310\t\n311\t const _item = (val, lbl) => `<div class=\"vv-arr-sr-item\">\n312\t <div class=\"vv-arr-sr-num\">${val}</div>\n313\t <div class=\"vv-arr-sr-lbl\">${lbl}</div>\n314\t </div>`;\n315\t\n316\t const syncEnabled = settings?.arr_sync_enabled !== false;\n317\t const syncExtra = !syncEnabled\n318\t ? `<div style=\"font-size:10px;color:#8b4a2a;margin-top:8px;\">ARR_SYNC_ENABLED=false — sync disabled</div>` : '';\n319\t\n320\t const syncItems = sync.last_run\n321\t ? _item(`<span style=\"color:#6fcf97\">+${_n(sync.added)}</span>`, 'Added') +\n322\t (sync.nodes != null ? _item(_n(sync.nodes), 'Nodes') : '') +\n323\t (sync.blocklist_count != null ? _item(_n(sync.blocklist_count), 'Blocklist') : '')\n324\t : `<span style=\"color:#333;font-size:11px;\">No run history</span>`;\n325\t\n326\t const recItems = recovery.last_run\n327\t ? _item(_n(recovery.fixed), 'Fixed') + _item(_n(recovery.searched), 'Re-searched')\n328\t : `<span style=\"color:#333;font-size:11px;\">No run history</span>`;\n329\t\n330\t return `<div class=\"vv-arr-sr-grid\">\n331\t ${_card('Arr Sync', sync, syncItems, syncExtra)}\n332\t ${_card('Failed / Stalled Recovery', recovery, recItems, '')}\n333\t </div>`;\n334\t}\n335\t\n336\t// ── Settings card ─────────────────────────────────────────────────────────────\n337\tfunction _settingsCard(s) {\n338\t if (!s) return '';\n339\t const myHost = s.my_host || 'host1';\n340\t const myId = s.my_id || 'HOST1';\n341\t\n342\t const _tog = (id, on, key, file, label) =>\n343\t `<label class=\"vv-arr-tog-wrap\">\n344\t <div class=\"vv-arr-tog-track${on?' on':''}\" id=\"vv-arr-tog-${id}\"\n345\t onclick=\"event.stopPropagation();vvArrToggle(this,'${key}','${file}')\"></div>\n346\t <span class=\"vv-arr-tog-lbl\">${label}</span>\n347\t </label>`;\n348\t\n349\t const syncTog = _tog('sync', s.arr_sync_enabled,\n350\t 'ARR_SYNC_ENABLED', 'master.conf', 'Arr sync enabled');\n351\t\n352\t const recTogs = [\n353\t s.sonarr_recovery !== undefined\n354\t ? _tog('sonarr-rec', s.sonarr_recovery, `${myId}_SONARR_RECOVERY`, `${myHost}.conf`, 'Sonarr') : '',\n355\t s.radarr_recovery !== undefined\n356\t ? _tog('radarr-rec', s.radarr_recovery, `${myId}_RADARR_RECOVERY`, `${myHost}.conf`, 'Radarr') : '',\n357\t s.lidarr_recovery !== undefined\n358\t ? _tog('lidarr-rec', s.lidarr_recovery, `${myId}_LIDARR_RECOVERY`, `${myHost}.conf`, 'Lidarr') : '',\n359\t ].filter(Boolean).join('');\n360\t\n361\t return `<div class=\"vv-arr-card vv-arr-set-card\">\n362\t <div style=\"font-size:11px;font-weight:700;color:#555;text-transform:uppercase;\n363\t letter-spacing:.07em;margin-bottom:12px;\">Settings</div>\n364\t\n365\t <div style=\"display:flex;flex-wrap:wrap;gap:20px;margin-bottom:12px;\">\n366\t ${syncTog}\n367\t </div>\n368\t\n369\t <hr class=\"vv-arr-sep\">\n370\t\n371\t <div style=\"font-size:10px;color:#3a3a3a;text-transform:uppercase;letter-spacing:.07em;margin-bottom:8px;\">\n372\t Recovery — ${myId}\n373\t </div>\n374\t <div style=\"display:flex;flex-wrap:wrap;gap:16px;margin-bottom:10px;\">${recTogs}</div>\n375\t <div style=\"display:flex;align-items:center;gap:8px;\">\n376\t <span style=\"font-size:11px;color:#555;\">Import recovery age</span>\n377\t <input class=\"vv-arr-set-inp\" id=\"vv-arr-rec-age\" type=\"number\" min=\"1\" max=\"72\"\n378\t value=\"${s.recovery_age_hours ?? 6}\">\n379\t <span style=\"font-size:10px;color:#3a3a3a;\">hours — skip items newer than this</span>\n380\t <button onclick=\"vvArrSaveAge()\" id=\"vv-arr-age-btn\"\n381\t style=\"background:#1a1a1a;border:1px solid #333;color:#666;font-size:10px;\n382\t padding:3px 10px;border-radius:3px;cursor:pointer;margin-left:4px;\">Save</button>\n383\t <span id=\"vv-arr-age-fb\" style=\"font-size:10px;\"></span>\n384\t </div>\n385\t </div>`;\n386\t}\n387\t\n388\t// ── Main render ───────────────────────────────────────────────────────────────\n389\tfunction _render(data) {\n390\t const nodes = data.nodes || [];\n391\t if (!nodes.length && !(data.sync?.last_run) && !(data.recovery?.last_run)) {\n392\t document.getElementById('vv-arrs-grid').innerHTML =\n393\t `<div style=\"grid-column:1/-1;color:#444;font-size:12px;padding:24px 0;text-align:center;\">\n394\t No arrs configured — add Sonarr/Radarr/Lidarr URLs and API keys to ${data.settings?.my_host ?? 'host'}.conf\n395\t </div>`;\n396\t return;\n397\t }\n398\t\n399\t let html = '';\n400\t for (const node of nodes) html += _nodeSection(node);\n401\t html += _syncSection(data.sync || {}, data.recovery || {}, data.settings);\n402\t html += _settingsCard(data.settings);\n403\t\n404\t document.getElementById('vv-arrs-grid').innerHTML = html;\n405\t\n406\t const ts = data.ts ? new Date(data.ts * 1000).toLocaleString([],\n407\t {month:'numeric',day:'numeric',year:'numeric',hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';\n408\t document.getElementById('vv-arrs-ts').textContent = ts ? 'Updated: ' + ts : '';\n409\t}\n410\t\n411\tfunction vvArrsLoad() {\n412\t fetch('/plugins/varaverk/api/arrs.php')\n413\t .then(r => r.json()).then(_render).catch(() => {});\n414\t}\n415\t\n416\tvvArrsLoad();\n417\tsetInterval(vvArrsLoad, 60000);\n418\t\n419\t})();\n420\t\n421\t// ── Remote node refresh ───────────────────────────────────────────────────────\n422\tfunction vvArrsRefreshRemote(host) {\n423\t const btn = document.getElementById('vv-arr-rfsh-' + host);\n424\t if (btn) { btn.disabled = true; btn.textContent = '↻…'; }\n425\t fetch(`/plugins/varaverk/api/arrs.php?action=refresh_remote&host=${host}&_=` + Date.now())\n426\t .then(r => r.json())\n427\t .then(d => {\n428\t if (btn) { btn.disabled = false; btn.textContent = d.ok ? '↻' : '✗'; }\n429\t // Main cache was busted — reload to show fresh data\n430\t setTimeout(vvArrsLoad, 500);\n431\t })\n432\t .catch(() => { if (btn) { btn.disabled = false; btn.textContent = '↻'; } });\n433\t}\n434\t\n435\t// ── Toggle handler ────────────────────────────────────────────────────────────\n436\tfunction vvArrToggle(track, key, file) {\n437\t const on = !track.classList.contains('on');\n438\t track.classList.toggle('on', on);\n439\t const fd = new FormData();\n440\t fd.append('id', 'arrs');\n441\t fd.append('changes', JSON.stringify([{ file, key, value: on ? 'true' : 'false', type: 'scalar' }]));\n442\t fetch('/plugins/varaverk/api/confform.php', { method: 'POST', body: fd })\n443\t .then(r => r.json())\n444\t .then(d => { if (!d.ok) track.classList.toggle('on', !on); })\n445\t .catch(() => track.classList.toggle('on', !on));\n446\t}\n447\t\n448\t// ── Recovery age save ─────────────────────────────────────────────────────────\n449\tfunction vvArrSaveAge() {\n450\t const val = document.getElementById('vv-arr-rec-age')?.value;\n451\t const btn = document.getElementById('vv-arr-age-btn');\n452\t const fb = document.getElementById('vv-arr-age-fb');\n453\t if (!val) return;\n454\t btn.disabled = true; btn.textContent = 'Saving…'; fb.textContent = '';\n455\t const fd = new FormData();\n456\t fd.append('id', 'arrs');\n457\t fd.append('changes', JSON.stringify([{\n458\t file: 'master.conf', key: 'ARR_IMPORT_RECOVERY_AGE', value: val, type: 'scalar'\n459\t }]));\n460\t fetch('/plugins/varaverk/api/confform.php', { method: 'POST', body: fd })\n461\t .then(r => r.json())\n462\t .then(d => {\n463\t btn.disabled = false; btn.textContent = 'Save';\n464\t fb.style.color = d.ok ? '#4caf50' : '#ef5350';\n465\t fb.textContent = d.ok ? 'Saved ✓' : (d.error || 'Failed');\n466\t if (d.ok) setTimeout(() => { fb.textContent = ''; }, 3000);\n467\t })\n468\t .catch(() => { btn.disabled = false; btn.textContent = 'Save'; fb.style.color='#ef5350'; fb.textContent='Failed'; });\n469\t}\n470\t</script>\n471\t"}]},"uuid":"48abc3f6-b09b-40c6-b6a2-2b6d32b821fb","timestamp":"2026-06-06T14:29:36.832Z","sourceToolAssistantUUID":"870d6419-5d02-419b-9fe5-88aa017e2976","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"48abc3f6-b09b-40c6-b6a2-2b6d32b821fb","isSidechain":true,"agentId":"accc3589951fac88e","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01EHXveD5KL1GDDfpYXYummu","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_012bmzFfJPHv6hqK6NjpE3b1","name":"Read","input":{"file_path":"/boot/config/plugins/varaverk/Plugin/unraid/pages/auth.php"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":366,"cache_read_input_tokens":7775,"cache_creation":{"ephemeral_5m_input_tokens":366,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null},"requestId":"req_011CbnAYyu8Br8CfBBXFBkPr","attributionAgent":"Explore","type":"assistant","uuid":"644e010b-9a63-4a54-969b-5f9ae5e985fa","timestamp":"2026-06-06T14:29:37.038Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"644e010b-9a63-4a54-969b-5f9ae5e985fa","isSidechain":true,"agentId":"accc3589951fac88e","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01EHXveD5KL1GDDfpYXYummu","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01U7Djvgbt9Z5uErTrvtCU5E","name":"Read","input":{"file_path":"/boot/config/plugins/varaverk/Plugin/unraid/pages/docker.php"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":366,"cache_read_input_tokens":7775,"cache_creation":{"ephemeral_5m_input_tokens":366,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null},"requestId":"req_011CbnAYyu8Br8CfBBXFBkPr","attributionAgent":"Explore","type":"assistant","uuid":"40cc0363-5dcc-49ca-aa67-9670b6c7ea7a","timestamp":"2026-06-06T14:29:37.042Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"40cc0363-5dcc-49ca-aa67-9670b6c7ea7a","isSidechain":true,"promptId":"3c1ac879-b13a-4ebc-9aeb-ea73615e9af3","agentId":"accc3589951fac88e","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01U7Djvgbt9Z5uErTrvtCU5E","type":"tool_result","content":"1\t<style>\n2\t.vv-dk-toolbar { display:flex;align-items:center;gap:8px;margin-bottom:12px;padding:0 2px;flex-wrap:wrap; }\n3\t.vv-dk-title { font-size:13px;font-weight:bold;color:#888;text-transform:uppercase;letter-spacing:.06em;flex:1; }\n4\t.vv-dk-ts { font-size:11px;color:#3a3a3a; }\n5\t.vv-dk-btn { font-size:11px;padding:3px 10px;border-radius:3px;border:1px solid #2a2a2a;background:#1a1a1a;color:#888;cursor:pointer;white-space:nowrap; }\n6\t.vv-dk-btn:hover { background:#222;color:#bbb; }\n7\t.vv-dk-btn.active { background:#1a2a1a;border-color:#2d4a2d;color:#6fcf97; }\n8\t.vv-dk-btn.warn { border-color:#3a2800;background:#1f1500;color:#ffb74d; }\n9\t.vv-dk-btn.prim { border-color:#1a3a5a;background:#0d1f2a;color:#5c9fd4; }\n10\t\n11\t/* Folder card */\n12\t.vv-dk-folder { background:#161616;border:1px solid #222;border-radius:6px;min-width:0;grid-column:span 4;overflow:hidden; }\n13\t.vv-dk-folder.wide{ grid-column:span 8; }\n14\t.vv-dk-folder.edit{ border-color:#2a3a2a; }\n15\t.vv-dk-folder-h { display:flex;align-items:center;gap:8px;padding:9px 12px;border-bottom:1px solid #1e1e1e;background:#111; }\n16\t.vv-dk-folder-name{ font-size:12px;font-weight:bold;color:#777;flex:1;min-width:0; }\n17\t.vv-dk-folder-name input { background:#0d0d0d;border:1px solid #333;border-radius:3px;color:#bbb;font-size:12px;padding:1px 6px;width:100%;box-sizing:border-box; }\n18\t.vv-dk-folder-count{ font-size:10px;color:#333;white-space:nowrap; }\n19\t.vv-dk-folder-del { font-size:13px;color:#444;cursor:pointer;padding:0 2px;line-height:1; }\n20\t.vv-dk-folder-del:hover { color:#ef5350; }\n21\t\n22\t/* Container rows */\n23\t.vv-dk-ctr-list { max-height:480px;overflow-y:auto; }\n24\t.vv-dk-ctr { padding:8px 12px;border-bottom:1px solid #1a1a1a;display:grid;grid-template-columns:auto 1fr;gap:0 10px;align-items:start; }\n25\t.vv-dk-ctr:last-child { border-bottom:none; }\n26\t.vv-dk-ctr:hover { background:#191919; }\n27\t.vv-dk-ctr.stopped { opacity:.6; }\n28\t.vv-dk-ctr.edit-mode { cursor:pointer; }\n29\t.vv-dk-ctr.edit-mode:hover { background:#141f14; }\n30\t.vv-dk-ctr-icon { width:28px;height:28px;border-radius:4px;object-fit:contain;background:#111;grid-row:span 3;align-self:center; }\n31\t.vv-dk-ctr-icon-ph{ width:28px;height:28px;border-radius:4px;background:#1a1a1a;border:1px solid #222;grid-row:span 3;align-self:center;display:flex;align-items:center;justify-content:center;font-size:11px;color:#333; }\n32\t.vv-dk-ctr-name-row{ display:flex;align-items:baseline;gap:8px;flex-wrap:wrap; }\n33\t.vv-dk-ctr-name { font-size:12px;font-weight:bold;color:#bbb; }\n34\t.vv-dk-ctr-name a { color:#bbb;text-decoration:none; }\n35\t.vv-dk-ctr-name a:hover { color:#6fcf97; }\n36\t.vv-dk-ctr-image { font-size:10px;color:#3a3a3a;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:220px; }\n37\t.vv-dk-ctr-status { font-size:10px;font-weight:bold;letter-spacing:.04em; }\n38\t.vv-dk-ctr-status.running { color:#4caf50; }\n39\t.vv-dk-ctr-status.stopped { color:#555; }\n40\t.vv-dk-ctr-status.paused { color:#ffb74d; }\n41\t.vv-dk-ctr-status.exited { color:#ef5350; }\n42\t.vv-dk-meta-row { display:flex;flex-wrap:wrap;align-items:center;gap:5px;margin-top:3px; }\n43\t.vv-dk-net-badge { font-size:10px;padding:1px 6px;border-radius:2px;background:#0d1a2a;color:#5c7cfa;border:1px solid #1a2a3a;white-space:nowrap; }\n44\t.vv-dk-port-badge { font-size:10px;padding:1px 6px;border-radius:2px;background:#1a1a0a;color:#cddc39;border:1px solid #2a2a1a;white-space:nowrap; }\n45\t.vv-dk-paths { margin-top:4px;display:flex;flex-direction:column;gap:1px; }\n46\t.vv-dk-path { font-size:10px;color:#333;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }\n47\t.vv-dk-path .src { color:#3a3a3a; }\n48\t.vv-dk-path .arr { color:#2a2a2a;margin:0 3px; }\n49\t.vv-dk-path .dst { color:#2e3e2e; }\n50\t.vv-dk-paths-more { font-size:10px;color:#2a2a2a;cursor:pointer;margin-top:1px; }\n51\t.vv-dk-paths-more:hover { color:#555; }\n52\t\n53\t/* Ungrouped */\n54\t.vv-dk-ungroup { grid-column:1/-1;background:#111;border:1px solid #1e1e1e;border-radius:6px;overflow:hidden; }\n55\t.vv-dk-ungroup-h { padding:8px 12px;border-bottom:1px solid #1a1a1a;background:#0d0d0d; }\n56\t.vv-dk-ungroup-ht { font-size:11px;color:#3a3a3a;font-weight:bold;text-transform:uppercase;letter-spacing:.05em; }\n57\t\n58\t/* New folder placeholder */\n59\t.vv-dk-new-card { grid-column:span 4;background:#0d0d0d;border:1px dashed #222;border-radius:6px;min-height:80px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#2a2a2a;font-size:12px; }\n60\t.vv-dk-new-card:hover { border-color:#333;color:#555; }\n61\t\n62\t/* Drift */\n63\t.vv-dk-drift { grid-column:1/-1;border:1px solid #3a2800;background:#1a1200;border-radius:6px;padding:10px;margin-bottom:12px; }\n64\t.vv-dk-drift-h { font-size:11px;color:#aa7020;font-weight:bold;margin-bottom:6px; }\n65\t.vv-dk-drift-row { font-size:11px;color:#7a5020;margin:2px 0; }\n66\t\n67\t/* Popover */\n68\t.vv-dk-popover { position:fixed;z-index:9999;background:#1c1c1c;border:1px solid #333;border-radius:5px;padding:5px 0;min-width:170px;box-shadow:0 4px 20px #000c; }\n69\t.vv-dk-pop-item { padding:5px 14px;font-size:12px;color:#888;cursor:pointer;white-space:nowrap; }\n70\t.vv-dk-pop-item:hover { background:#252525;color:#ccc; }\n71\t.vv-dk-pop-item.current { color:#4caf50; }\n72\t.vv-dk-pop-item.sep { border-top:1px solid #222;margin-top:4px;padding-top:8px; }\n73\t.vv-dk-pop-item.blue { color:#5c9fd4; }\n74\t</style>\n75\t\n76\t<div class=\"vv-dk-toolbar\">\n77\t <span class=\"vv-dk-title\">Docker</span>\n78\t <button class=\"vv-dk-btn warn\" id=\"vv-dk-sync-c2j\" title=\"Apply conf desired state to JSON\">Sync conf → JSON</button>\n79\t <button class=\"vv-dk-btn prim\" id=\"vv-dk-sync-j2c\" title=\"Capture JSON state into conf\">Sync JSON → conf</button>\n80\t <button class=\"vv-dk-btn\" id=\"vv-dk-edit-toggle\">Edit folders</button>\n81\t <span style=\"font-size:10px;color:#1a3a1a;\" id=\"vv-dk-fv3\"></span>\n82\t <span class=\"vv-dk-ts\" id=\"vv-dk-ts\"></span>\n83\t</div>\n84\t\n85\t<div id=\"vv-dk-drift-banner\"></div>\n86\t\n87\t<div id=\"vv-dk-grid\" style=\"display:grid;grid-template-columns:repeat(8,1fr);gap:12px;\">\n88\t <div style=\"grid-column:1/-1;color:#444;font-size:12px;padding:16px 0;text-align:center;\">Loading…</div>\n89\t</div>\n90\t\n91\t<div class=\"vv-dk-popover\" id=\"vv-dk-popover\" style=\"display:none;\"></div>\n92\t\n93\t<script>\n94\t(function() {\n95\t\n96\tlet _data = null;\n97\tlet _editMode = false;\n98\tlet _popTarget = null;\n99\t\n100\t// ── API ───────────────────────────────────────────────────────────────────────\n101\t\n102\tfunction _api(params, cb) {\n103\t const fd = new FormData();\n104\t for (const [k,v] of Object.entries(params)) fd.append(k, v);\n105\t fetch('/plugins/varaverk/api/docker.php', {method:'POST', body:fd})\n106\t .then(r => r.json()).then(cb)\n107\t .catch(e => console.error('docker api', e));\n108\t}\n109\t\n110\t// ── Helpers ───────────────────────────────────────────────────────────────────\n111\t\n112\tfunction _esc(s) {\n113\t return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\"/g,'"');\n114\t}\n115\t\n116\tfunction _shortImage(img) {\n117\t // strip registry host if present (e.g. lscr.io/linuxserver/sonarr → linuxserver/sonarr)\n118\t return img.replace(/^[a-z0-9.-]+\\.[a-z]{2,}\\//i, '');\n119\t}\n120\t\n121\t// ── Container row ─────────────────────────────────────────────────────────────\n122\t\n123\tfunction _ctrRow(c, folderId) {\n124\t const statusCls = c.running ? 'running' : (c.status || 'stopped');\n125\t const statusLbl = c.running ? 'RUNNING' : (c.status || 'STOPPED').toUpperCase();\n126\t const rowCls = 'vv-dk-ctr' + (c.running ? '' : ' stopped') + (_editMode ? ' edit-mode' : '');\n127\t const editAttr = _editMode ? `data-ctr=\"${_esc(c.name)}\" data-folder=\"${folderId||''}\" title=\"Move ${c.name}\"` : '';\n128\t\n129\t // Icon or placeholder\n130\t const iconHtml = c.icon\n131\t ? `<img class=\"vv-dk-ctr-icon\" src=\"${_esc(c.icon)}\" alt=\"\" onerror=\"this.style.display='none'\">`\n132\t : `<div class=\"vv-dk-ctr-icon-ph\">◻</div>`;\n133\t\n134\t // Name — link to WebUI if available\n135\t const nameHtml = c.webui\n136\t ? `<a href=\"${_esc(c.webui)}\" target=\"_blank\">${_esc(c.name)}</a>`\n137\t : _esc(c.name);\n138\t\n139\t // Networks + IPs\n140\t const nets = Object.entries(c.networks || {});\n141\t const netBadges = nets.map(([net, ip]) =>\n142\t `<span class=\"vv-dk-net-badge\" title=\"${_esc(net)}\">${_esc(ip)}</span>`).join('');\n143\t\n144\t // Ports (cap at 4)\n145\t const ports = c.ports || [];\n146\t const portBadges = ports.slice(0,4).map(p =>\n147\t `<span class=\"vv-dk-port-badge\">${_esc(p)}</span>`).join('');\n148\t const morePorts = ports.length > 4 ? `<span class=\"vv-dk-port-badge\" style=\"color:#555\">+${ports.length-4}</span>` : '';\n149\t\n150\t // Paths (bind mounts, show 3 with expand)\n151\t const mounts = c.mounts || [];\n152\t const visibleMounts = mounts.slice(0, 3);\n153\t const hiddenCount = mounts.length - visibleMounts.length;\n154\t let pathsHtml = '';\n155\t if (visibleMounts.length) {\n156\t pathsHtml = `<div class=\"vv-dk-paths\">` +\n157\t visibleMounts.map(m =>\n158\t `<div class=\"vv-dk-path\"><span class=\"vv-dk-path src\">${_esc(m.src)}</span><span class=\"vv-dk-path arr\">→</span><span class=\"vv-dk-path dst\">${_esc(m.dst)}</span></div>`\n159\t ).join('') +\n160\t (hiddenCount > 0 ? `<div class=\"vv-dk-paths-more\" data-expand=\"${_esc(c.name)}\">+ ${hiddenCount} more path${hiddenCount>1?'s':''}</div>` : '') +\n161\t `</div>`;\n162\t }\n163\t\n164\t return `<div class=\"${rowCls}\" ${editAttr}>\n165\t ${iconHtml}\n166\t <div>\n167\t <div class=\"vv-dk-ctr-name-row\">\n168\t <span class=\"vv-dk-ctr-name\">${nameHtml}</span>\n169\t <span class=\"vv-dk-ctr-status ${statusCls}\">${statusLbl}</span>\n170\t </div>\n171\t <div class=\"vv-dk-ctr-image\">${_esc(_shortImage(c.image||''))}</div>\n172\t ${(netBadges || portBadges) ? `<div class=\"vv-dk-meta-row\">${netBadges}${portBadges}${morePorts}</div>` : ''}\n173\t ${pathsHtml}\n174\t </div>\n175\t </div>`;\n176\t}\n177\t\n178\t// ── Folder card ───────────────────────────────────────────────────────────────\n179\t\n180\tfunction _folderCard(f) {\n181\t const wide = f.total > 7;\n182\t const cls = 'vv-dk-folder' + (wide ? ' wide' : '') + (_editMode ? ' edit' : '');\n183\t const running = f.running, total = f.total;\n184\t\n185\t let nameHtml;\n186\t if (_editMode) {\n187\t nameHtml = `<span class=\"vv-dk-folder-name\"><input type=\"text\" value=\"${_esc(f.name)}\" data-folder-id=\"${f.id}\" class=\"vv-dk-rename-input\"></span>`;\n188\t } else {\n189\t nameHtml = `<span class=\"vv-dk-folder-name\">${_esc(f.name)}</span>`;\n190\t }\n191\t\n192\t const delBtn = _editMode\n193\t ? `<span class=\"vv-dk-folder-del\" data-delete-folder=\"${f.id}\" title=\"Delete folder\">×</span>` : '';\n194\t\n195\t const rows = (f.containers || []).map(c => _ctrRow(c, f.id)).join('');\n196\t\n197\t return `<div class=\"${cls}\" data-folder-id=\"${f.id}\">\n198\t <div class=\"vv-dk-folder-h\">\n199\t ${nameHtml}\n200\t <span class=\"vv-dk-folder-count\">${running}/${total} running</span>\n201\t ${delBtn}\n202\t </div>\n203\t <div class=\"vv-dk-ctr-list\">${rows || '<div style=\"padding:10px 12px;color:#2a2a2a;font-size:11px;\">Empty</div>'}</div>\n204\t </div>`;\n205\t}\n206\t\n207\t// ── Ungrouped section ─────────────────────────────────────────────────────────\n208\t\n209\tfunction _ungroupedCard(containers) {\n210\t if (!containers.length) return '';\n211\t const rows = containers.map(c => _ctrRow(c, '')).join('');\n212\t return `<div class=\"vv-dk-ungroup\">\n213\t <div class=\"vv-dk-ungroup-h\">\n214\t <span class=\"vv-dk-ungroup-ht\">Ungrouped (${containers.length})</span>\n215\t </div>\n216\t <div class=\"vv-dk-ctr-list\">${rows}</div>\n217\t </div>`;\n218\t}\n219\t\n220\t// ── Drift banner ──────────────────────────────────────────────────────────────\n221\t\n222\tfunction _driftBanner(drift) {\n223\t const el = document.getElementById('vv-dk-drift-banner');\n224\t if (!drift || !drift.length) { el.innerHTML = ''; return; }\n225\t const rows = drift.map(d => {\n226\t const actual = d.actual ? `in <strong>${d.actual}</strong>` : 'ungrouped';\n227\t return `<div class=\"vv-dk-drift-row\"><strong>${d.container}</strong> — conf wants <strong>${d.conf}</strong>, currently ${actual}</div>`;\n228\t }).join('');\n229\t el.innerHTML = `<div class=\"vv-dk-drift\">\n230\t <div class=\"vv-dk-drift-h\">⚠ Conf / JSON drift — ${drift.length} container${drift.length>1?'s':''} out of sync</div>\n231\t ${rows}\n232\t <button class=\"vv-dk-btn warn\" style=\"margin-top:8px;\" id=\"vv-dk-fix-drift\">Apply conf → JSON now</button>\n233\t </div>`;\n234\t document.getElementById('vv-dk-fix-drift')?.addEventListener('click', _syncC2J);\n235\t}\n236\t\n237\t// ── Main render ───────────────────────────────────────────────────────────────\n238\t\n239\tfunction _render(data) {\n240\t _data = data;\n241\t let html = '';\n242\t for (const f of data.folders || []) html += _folderCard(f);\n243\t if (_editMode) html += `<div class=\"vv-dk-new-card\" id=\"vv-dk-add-folder\">+ New folder</div>`;\n244\t html += _ungroupedCard(data.ungrouped || []);\n245\t if (!html) html = '<div style=\"grid-column:1/-1;color:#444;font-size:12px;padding:16px 0;text-align:center;\">No containers found.</div>';\n246\t\n247\t document.getElementById('vv-dk-grid').innerHTML = html;\n248\t _driftBanner(data.drift);\n249\t\n250\t const ts = data.ts ? new Date(data.ts*1000).toLocaleString([],{month:'numeric',day:'numeric',year:'numeric',hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';\n251\t document.getElementById('vv-dk-ts').textContent = ts ? 'Updated: '+ts : '';\n252\t const fv3El = document.getElementById('vv-dk-fv3');\n253\t if (fv3El) fv3El.textContent = data.fv3_synced ? '⇄ folder.view3' : '';\n254\t\n255\t _bindEvents();\n256\t}\n257\t\n258\t// ── Events ────────────────────────────────────────────────────────────────────\n259\t\n260\tfunction _bindEvents() {\n261\t // Container click in edit mode → folder picker\n262\t document.querySelectorAll('.vv-dk-ctr.edit-mode').forEach(el => {\n263\t el.addEventListener('click', e => {\n264\t e.stopPropagation();\n265\t _popTarget = { container: el.dataset.ctr, currentFolderId: el.dataset.folder };\n266\t _showPopover(e.clientX, e.clientY);\n267\t });\n268\t });\n269\t\n270\t // \"show more paths\" expand\n271\t document.querySelectorAll('[data-expand]').forEach(el => {\n272\t el.addEventListener('click', e => {\n273\t e.stopPropagation();\n274\t const cname = el.dataset.expand;\n275\t const allCtrs = [...(_data.folders||[]).flatMap(f=>f.containers), ...(_data.ungrouped||[])];\n276\t const c = allCtrs.find(x => x.name === cname);\n277\t if (!c) return;\n278\t const paths = document.querySelector(`.vv-dk-paths-more[data-expand=\"${CSS.escape(cname)}\"]`)?.closest('.vv-dk-paths');\n279\t if (!paths || !c.mounts) return;\n280\t paths.innerHTML = c.mounts.map(m =>\n281\t `<div class=\"vv-dk-path\"><span class=\"src\">${_esc(m.src)}</span><span class=\"arr\">→</span><span class=\"dst\">${_esc(m.dst)}</span></div>`\n282\t ).join('');\n283\t });\n284\t });\n285\t\n286\t // Rename inputs\n287\t document.querySelectorAll('.vv-dk-rename-input').forEach(el => {\n288\t el.addEventListener('keydown', e => { if (e.key==='Enter') el.blur(); });\n289\t el.addEventListener('blur', () => {\n290\t const fid = el.dataset.folderId, name = el.value.trim();\n291\t if (!fid || !name) return;\n292\t const f = (_data.folders||[]).find(x=>x.id===fid);\n293\t if (f && name===f.name) return;\n294\t _api({action:'rename_folder',folder_id:fid,name}, r => { if(r.ok) _reload(); else alert('Rename failed: '+(r.error||'?')); });\n295\t });\n296\t });\n297\t\n298\t // Delete folder\n299\t document.querySelectorAll('[data-delete-folder]').forEach(el => {\n300\t el.addEventListener('click', e => {\n301\t e.stopPropagation();\n302\t const fid = el.dataset.deleteFolder;\n303\t const f = (_data.folders||[]).find(x=>x.id===fid);\n304\t if (!f || !confirm(`Delete \"${f.name}\"? Containers will be ungrouped.`)) return;\n305\t _api({action:'delete_folder',folder_id:fid}, r => { if(r.ok) _reload(); else alert('Delete failed: '+(r.error||'?')); });\n306\t });\n307\t });\n308\t\n309\t // New folder\n310\t document.getElementById('vv-dk-add-folder')?.addEventListener('click', () => {\n311\t const name = prompt('New folder name:');\n312\t if (name?.trim()) _api({action:'create_folder',name:name.trim()}, r => { if(r.ok) _reload(); else alert(r.error); });\n313\t });\n314\t}\n315\t\n316\t// ── Popover ───────────────────────────────────────────────────────────────────\n317\t\n318\tfunction _showPopover(x, y) {\n319\t if (!_data || !_popTarget) return;\n320\t const pop = document.getElementById('vv-dk-popover');\n321\t const folders = _data.folders || [];\n322\t let html = folders.map(f => {\n323\t const cur = f.id === _popTarget.currentFolderId;\n324\t return `<div class=\"vv-dk-pop-item${cur?' current':''}\" data-fid=\"${f.id}\">${_esc(f.name)}${cur?' ✓':''}</div>`;\n325\t }).join('');\n326\t html += `<div class=\"vv-dk-pop-item sep\" data-fid=\"\">Ungrouped</div>`;\n327\t html += `<div class=\"vv-dk-pop-item blue sep\" data-fid=\"__new__\">+ New folder…</div>`;\n328\t pop.innerHTML = html;\n329\t pop.style.display = 'block';\n330\t const vw=window.innerWidth, vh=window.innerHeight;\n331\t pop.style.left = Math.min(x, vw-180)+'px';\n332\t pop.style.top = Math.min(y+8, vh-180)+'px';\n333\t\n334\t pop.querySelectorAll('[data-fid]').forEach(el => {\n335\t el.addEventListener('click', () => {\n336\t const fid = el.dataset.fid;\n337\t _hidePopover();\n338\t if (fid==='__new__') {\n339\t const name = prompt('New folder name:');\n340\t if (!name?.trim()) return;\n341\t _api({action:'create_folder',name:name.trim()}, r => {\n342\t if (!r.ok) { alert(r.error); return; }\n343\t _api({action:'move_container',container:_popTarget.container,folder_id:r.id}, r2 => { if(r2.ok) _reload(); });\n344\t });\n345\t } else {\n346\t _api({action:'move_container',container:_popTarget.container,folder_id:fid}, r => { if(r.ok) _reload(); else alert(r.error); });\n347\t }\n348\t });\n349\t });\n350\t}\n351\t\n352\tfunction _hidePopover() {\n353\t document.getElementById('vv-dk-popover').style.display='none';\n354\t _popTarget=null;\n355\t}\n356\tdocument.addEventListener('click', e => {\n357\t if (!document.getElementById('vv-dk-popover').contains(e.target)) _hidePopover();\n358\t});\n359\t\n360\t// ── Toolbar ───────────────────────────────────────────────────────────────────\n361\t\n362\tdocument.getElementById('vv-dk-edit-toggle').addEventListener('click', function() {\n363\t _editMode = !_editMode;\n364\t this.textContent = _editMode ? 'Done editing' : 'Edit folders';\n365\t this.classList.toggle('active', _editMode);\n366\t if (_data) _render(_data);\n367\t});\n368\t\n369\tfunction _syncC2J() {\n370\t _api({action:'sync_conf_to_json'}, r => { if(r.ok) _reload(); else alert(r.error); });\n371\t}\n372\tdocument.getElementById('vv-dk-sync-c2j').addEventListener('click', _syncC2J);\n373\tdocument.getElementById('vv-dk-sync-j2c').addEventListener('click', () => {\n374\t if (!confirm('Overwrite conf map with current JSON state?')) return;\n375\t _api({action:'sync_json_to_conf'}, r => { if(r.ok) _reload(); else alert(r.error); });\n376\t});\n377\t\n378\t// ── Load ──────────────────────────────────────────────────────────────────────\n379\t\n380\tfunction _reload() {\n381\t fetch('/plugins/varaverk/api/docker.php')\n382\t .then(r=>r.json()).then(_render).catch(()=>{});\n383\t}\n384\t\n385\t_reload();\n386\tsetInterval(_reload, 60000);\n387\t\n388\t})();\n389\t</script>\n390\t"}]},"uuid":"84a96eb9-e497-4179-ab51-3e50ba078826","timestamp":"2026-06-06T14:29:37.065Z","sourceToolAssistantUUID":"40cc0363-5dcc-49ca-aa67-9670b6c7ea7a","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"84a96eb9-e497-4179-ab51-3e50ba078826","isSidechain":true,"agentId":"accc3589951fac88e","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01EHXveD5KL1GDDfpYXYummu","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Tkiz3ajynC4bcrRfQVaBLP","name":"Read","input":{"file_path":"/boot/config/plugins/varaverk/Plugin/unraid/pages/fallback.php"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":366,"cache_read_input_tokens":7775,"cache_creation":{"ephemeral_5m_input_tokens":366,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null},"requestId":"req_011CbnAYyu8Br8CfBBXFBkPr","attributionAgent":"Explore","type":"assistant","uuid":"834ab38f-9d4b-42eb-bb90-bfa0dd236a92","timestamp":"2026-06-06T14:29:37.248Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"834ab38f-9d4b-42eb-bb90-bfa0dd236a92","isSidechain":true,"promptId":"3c1ac879-b13a-4ebc-9aeb-ea73615e9af3","agentId":"accc3589951fac88e","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Tkiz3ajynC4bcrRfQVaBLP","type":"tool_result","content":"1\t<style>\n2\t.vv-fb-active { background:#1a1200;border:1px solid #5a3800;border-radius:6px;padding:12px 14px; }\n3\t.vv-fb-active-h { display:flex;align-items:baseline;gap:10px;margin-bottom:8px; }\n4\t.vv-fb-badge { font-size:11px;font-weight:bold;letter-spacing:.06em;padding:2px 7px;border-radius:3px;flex-shrink:0; }\n5\t.vv-fb-badge.fb { background:#5a3800;color:#ffb74d; }\n6\t.vv-fb-badge.norm { background:#1a2a1a;color:#4caf50; }\n7\t.vv-fb-badge.dark { background:#2a1a2a;color:#9c27b0; }\n8\t.vv-fb-badge.nonet{ background:#1a1a2a;color:#5c7cfa; }\n9\t.vv-fb-meta { display:flex;gap:18px;flex-wrap:wrap;margin-bottom:10px; }\n10\t.vv-fb-meta-item{ display:flex;flex-direction:column;gap:1px; }\n11\t.vv-fb-meta-val { font-size:17px;font-weight:bold;color:#ffb74d; }\n12\t.vv-fb-meta-lbl { font-size:10px;color:#5a4020; }\n13\t.vv-fb-ctrs { display:flex;gap:6px;flex-wrap:wrap;margin-top:6px; }\n14\t.vv-fb-ctr { font-size:11px;padding:2px 8px;border-radius:3px;background:#2a1e00;color:#ffb74d;border:1px solid #4a2e00; }\n15\t.vv-fb-ctr.running { background:#1a2a1a;color:#6fcf97;border-color:#2d4a2d; }\n16\t.vv-fb-ctr.stopped { background:#2a1a1a;color:#e57;border-color:#4a2020;opacity:.7; }\n17\t.vv-fb-node { grid-column:span 4; }\n18\t.vv-fb-node-h { display:flex;align-items:baseline;gap:8px;margin-bottom:10px; }\n19\t.vv-fb-node-id { font-size:12px;font-weight:bold;color:#666;letter-spacing:.06em;text-transform:uppercase; }\n20\t.vv-fb-node-nm { font-size:11px;color:#3a3a3a; }\n21\t.vv-fb-arrow { font-size:11px;color:#333; }\n22\t.vv-fb-covers { font-size:11px;color:#3a3a3a; }\n23\t.vv-fb-tier { margin-bottom:8px; }\n24\t.vv-fb-tier-h { display:flex;align-items:baseline;gap:6px;margin-bottom:4px; }\n25\t.vv-fb-tier-lbl { font-size:11px;font-weight:bold;color:#555;text-transform:uppercase;letter-spacing:.04em; }\n26\t.vv-fb-tier-delay { font-size:10px;color:#3a3a3a; }\n27\t.vv-fb-tier-pills { display:flex;gap:5px;flex-wrap:wrap; }\n28\t.vv-fb-pill { font-size:11px;padding:2px 7px;border-radius:3px;background:#1e1e1e;color:#777;border:1px solid #2a2a2a; }\n29\t.vv-fb-pill.active-t { background:#1a2010;color:#8bc34a;border-color:#2d3a1d; }\n30\t.vv-fb-pill.empty { color:#333;font-style:italic; }\n31\t.vv-fb-state-dot { width:6px;height:6px;border-radius:50%;flex-shrink:0;margin-top:3px; }\n32\t.vv-fb-sep { border:none;border-top:1px solid #222;margin:8px 0; }\n33\t.vv-fb-disabled { grid-column:1/-1;color:#3a3a3a;font-size:12px;padding:20px 0;text-align:center; }\n34\t</style>\n35\t\n36\t<div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding:0 2px;\">\n37\t <span style=\"font-size:13px;font-weight:bold;color:#888;text-transform:uppercase;letter-spacing:.06em;\">FallBack</span>\n38\t <span style=\"font-size:11px;color:#3a3a3a;\" id=\"vv-fb-ts\"></span>\n39\t</div>\n40\t\n41\t<div id=\"vv-fb-grid\" style=\"display:grid;grid-template-columns:repeat(8,1fr);gap:12px;\">\n42\t <div style=\"grid-column:1/-1;color:#444;font-size:12px;padding:16px 0;text-align:center;\">Loading…</div>\n43\t</div>\n44\t\n45\t<script>\n46\t(function() {\n47\t\n48\tfunction _dur(startTs) {\n49\t if (!startTs || startTs === 0) return '—';\n50\t const s = Math.floor(Date.now() / 1000) - startTs;\n51\t if (s < 60) return s + 's';\n52\t if (s < 3600) return Math.floor(s / 60) + 'm';\n53\t const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60);\n54\t return m ? h + 'h ' + m + 'm' : h + 'h';\n55\t}\n56\t\n57\tfunction _fmtDelay(min) {\n58\t if (!min) return 'immediate';\n59\t if (min < 60) return min + 'min';\n60\t const h = Math.floor(min / 60), m = min % 60;\n61\t return m ? h + 'h ' + m + 'm' : h + 'h';\n62\t}\n63\t\n64\tfunction _activeTier(state) {\n65\t if (!state) return 0;\n66\t if (state.tier4_started) return 4;\n67\t if (state.tier3_started) return 3;\n68\t if (state.tier2_started) return 2;\n69\t return 1;\n70\t}\n71\t\n72\tfunction _stateBadge(st) {\n73\t const map = {\n74\t NORMAL: ['norm', 'NORMAL'],\n75\t FALLBACK: ['fb', 'FALLBACK'],\n76\t NO_INTERNET: ['nonet', 'NO INTERNET'],\n77\t DARK: ['dark', 'DARK'],\n78\t OFFLINE: ['dark', 'OFFLINE'],\n79\t UNREACHABLE: ['dark', 'UNREACHABLE'],\n80\t UNKNOWN: ['dark', 'UNKNOWN'],\n81\t };\n82\t const [cls, label] = map[st] || ['dark', st];\n83\t return `<span class=\"vv-fb-badge ${cls}\">${label}</span>`;\n84\t}\n85\t\n86\tfunction _stateDot(st) {\n87\t const col = {\n88\t NORMAL:'#4caf50', FALLBACK:'#ffb74d',\n89\t NO_INTERNET:'#5c7cfa', DARK:'#9c27b0',\n90\t OFFLINE:'#555', UNREACHABLE:'#555', UNKNOWN:'#333',\n91\t }[st] || '#333';\n92\t return `<span class=\"vv-fb-state-dot\" style=\"background:${col}\"></span>`;\n93\t}\n94\t\n95\tfunction _activeCard(nodes, handbackReq) {\n96\t const active = nodes.filter(n => n.state && n.state.state === 'FALLBACK');\n97\t if (!active.length) return '';\n98\t\n99\t return active.map(covering => {\n100\t const st = covering.state;\n101\t const tier = _activeTier(st);\n102\t const cov = covering.covers;\n103\t const covered = cov ? cov.hostname : '?';\n104\t\n105\t // All containers that should be running at current tier\n106\t let expected = [...(cov?.tier1 || [])];\n107\t if (tier >= 2) expected = expected.concat(cov?.tier2 || []);\n108\t if (tier >= 3) expected = expected.concat(cov?.tier3 || []);\n109\t if (tier >= 4) expected = expected.concat(cov?.tier4 || []);\n110\t const runningSet = new Set(covering.running || []);\n111\t\n112\t const ctrPills = expected.length\n113\t ? expected.map(c => {\n114\t const cls = runningSet.has(c) ? 'running' : 'stopped';\n115\t const sym = runningSet.has(c) ? '▲' : '▼';\n116\t return `<span class=\"vv-fb-ctr ${cls}\">${sym} ${c}</span>`;\n117\t }).join('')\n118\t : '<span style=\"color:#5a4020;font-size:11px;\">No containers configured for this tier</span>';\n119\t\n120\t return `<div class=\"vv-card vv-fb-active\" style=\"grid-column:1/-1;\">\n121\t <div class=\"vv-fb-active-h\">\n122\t ${_stateBadge('FALLBACK')}\n123\t <span style=\"font-size:12px;color:#aa7020;\">${covering.id} covering ${cov?.id || '?'} (${covered})</span>\n124\t </div>\n125\t <div class=\"vv-fb-meta\">\n126\t <div class=\"vv-fb-meta-item\">\n127\t <span class=\"vv-fb-meta-val\">${_dur(st.fallback_start)}</span>\n128\t <span class=\"vv-fb-meta-lbl\">DURATION</span>\n129\t </div>\n130\t <div class=\"vv-fb-meta-item\">\n131\t <span class=\"vv-fb-meta-val\">Tier ${tier}</span>\n132\t <span class=\"vv-fb-meta-lbl\">ACTIVE TIER</span>\n133\t </div>\n134\t <div class=\"vv-fb-meta-item\">\n135\t <span class=\"vv-fb-meta-val\">${st.handback_strikes} / ${handbackReq}</span>\n136\t <span class=\"vv-fb-meta-lbl\">HANDBACK STRIKES</span>\n137\t </div>\n138\t <div class=\"vv-fb-meta-item\">\n139\t <span class=\"vv-fb-meta-val\">${expected.length}</span>\n140\t <span class=\"vv-fb-meta-lbl\">CONTAINERS</span>\n141\t </div>\n142\t </div>\n143\t <div class=\"vv-fb-ctrs\">${ctrPills}</div>\n144\t </div>`;\n145\t }).join('');\n146\t}\n147\t\n148\tfunction _tierSection(tiers, activeTier, delays) {\n149\t const defs = [\n150\t { n: 1, key: 'tier1', label: 'Tier 1', delay: 0 },\n151\t { n: 2, key: 'tier2', label: 'Tier 2', delay: delays?.tier2 },\n152\t { n: 3, key: 'tier3', label: 'Tier 3', delay: delays?.tier3 },\n153\t { n: 4, key: 'tier4', label: 'Tier 4', delay: delays?.tier4 },\n154\t ];\n155\t return defs.map(({ n, key, label, delay }) => {\n156\t const containers = tiers[key] || [];\n157\t const isActive = activeTier >= n;\n158\t const pills = containers.length\n159\t ? containers.map(c => `<span class=\"vv-fb-pill${isActive ? ' active-t' : ''}\">${c}</span>`).join('')\n160\t : `<span class=\"vv-fb-pill empty\">none</span>`;\n161\t const delayStr = n === 1 ? 'immediate' : _fmtDelay(delay);\n162\t return `<div class=\"vv-fb-tier\">\n163\t <div class=\"vv-fb-tier-h\">\n164\t <span class=\"vv-fb-tier-lbl\">${label}</span>\n165\t <span class=\"vv-fb-tier-delay\">${delayStr}</span>\n166\t </div>\n167\t <div class=\"vv-fb-tier-pills\">${pills}</div>\n168\t </div>`;\n169\t }).join('');\n170\t}\n171\t\n172\tfunction _nodeCard(node) {\n173\t const st = node.state || {};\n174\t const state = st.state || 'UNKNOWN';\n175\t const cov = node.covers;\n176\t const active = _activeTier(state === 'FALLBACK' ? st : null);\n177\t\n178\t const covTarget = cov\n179\t ? `<span class=\"vv-fb-arrow\">→</span><span class=\"vv-fb-covers\">covers ${cov.id} (${cov.hostname})</span>`\n180\t : '';\n181\t\n182\t const tierSection = cov\n183\t ? _tierSection(cov, active, cov.delays)\n184\t : '<div style=\"color:#3a3a3a;font-size:11px;\">No coverage configured</div>';\n185\t\n186\t return `<div class=\"vv-card vv-fb-node\">\n187\t <div class=\"vv-fb-node-h\">\n188\t ${_stateDot(state)}\n189\t <span class=\"vv-fb-node-id\">${node.id}</span>\n190\t <span class=\"vv-fb-node-nm\">${node.hostname}</span>\n191\t ${covTarget}\n192\t <span style=\"flex:1\"></span>\n193\t ${_stateBadge(state)}\n194\t </div>\n195\t <hr class=\"vv-fb-sep\">\n196\t ${tierSection}\n197\t </div>`;\n198\t}\n199\t\n200\tfunction _render(data) {\n201\t if (!data.fb_enabled) {\n202\t document.getElementById('vv-fb-grid').innerHTML =\n203\t '<div class=\"vv-fb-disabled\">FALLBACK_ENABLED=false — fallback monitoring is disabled</div>';\n204\t return;\n205\t }\n206\t\n207\t const nodes = data.nodes || [];\n208\t let html = '';\n209\t\n210\t // Top: active fallback card (if any)\n211\t html += _activeCard(nodes, data.handback_req || 3);\n212\t\n213\t // Per-node cards\n214\t for (const node of nodes) {\n215\t html += _nodeCard(node);\n216\t }\n217\t\n218\t if (!html) {\n219\t html = '<div class=\"vv-fb-disabled\">No nodes configured.</div>';\n220\t }\n221\t\n222\t document.getElementById('vv-fb-grid').innerHTML = html;\n223\t\n224\t const ts = data.ts\n225\t ? new Date(data.ts * 1000).toLocaleString([], {\n226\t month:'numeric', day:'numeric', year:'numeric',\n227\t hour:'2-digit', minute:'2-digit', second:'2-digit'})\n228\t : '';\n229\t document.getElementById('vv-fb-ts').textContent = ts ? 'Updated: ' + ts : '';\n230\t}\n231\t\n232\tfunction vvFbLoad() {\n233\t fetch('/plugins/varaverk/api/fallback.php')\n234\t .then(r => r.json())\n235\t .then(_render)\n236\t .catch(() => {});\n237\t}\n238\t\n239\tvvFbLoad();\n240\tsetInterval(vvFbLoad, 30000);\n241\t\n242\t})();\n243\t</script>\n244\t"}]},"uuid":"ebf7195e-d3be-4d25-9af9-7406ae7de8c6","timestamp":"2026-06-06T14:29:37.262Z","sourceToolAssistantUUID":"834ab38f-9d4b-42eb-bb90-bfa0dd236a92","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"ebf7195e-d3be-4d25-9af9-7406ae7de8c6","isSidechain":true,"agentId":"accc3589951fac88e","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01EHXveD5KL1GDDfpYXYummu","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01QtfhM3zG9xMJbZPcyXEzbQ","name":"Read","input":{"file_path":"/boot/config/plugins/varaverk/Plugin/unraid/pages/monitor.php"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":366,"cache_read_input_tokens":7775,"output_tokens":399,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":366},"inference_geo":"not_available","iterations":[{"input_tokens":6,"output_tokens":399,"cache_read_input_tokens":7775,"cache_creation_input_tokens":366,"cache_creation":{"ephemeral_5m_input_tokens":366,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"diagnostics":null},"requestId":"req_011CbnAYyu8Br8CfBBXFBkPr","attributionAgent":"Explore","type":"assistant","uuid":"0fa09355-e005-42b5-8a12-4eb9230ccc27","timestamp":"2026-06-06T14:29:37.428Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"644e010b-9a63-4a54-969b-5f9ae5e985fa","isSidechain":true,"promptId":"3c1ac879-b13a-4ebc-9aeb-ea73615e9af3","agentId":"accc3589951fac88e","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_012bmzFfJPHv6hqK6NjpE3b1","type":"tool_result","content":"1\t<style>\n2\t/* ── Toolbar ─────────────────────────────────────────────────────────────── */\n3\t.vv-au-toolbar { display:flex;align-items:center;gap:8px;margin-bottom:12px;flex-wrap:wrap; }\n4\t.vv-au-title { font-size:13px;font-weight:bold;color:#888;text-transform:uppercase;letter-spacing:.06em;flex:1; }\n5\t.vv-au-tab { font-size:11px;padding:3px 12px;border-radius:3px;border:1px solid #2a2a2a;background:#1a1a1a;color:#666;cursor:pointer;white-space:nowrap; }\n6\t.vv-au-tab:hover{ background:#222;color:#aaa; }\n7\t.vv-au-tab.active { background:#0d1f2a;border-color:#1a3a5a;color:#5c9fd4; }\n8\t.vv-au-btn { font-size:11px;padding:3px 10px;border-radius:3px;border:1px solid #2a2a2a;background:#1a1a1a;color:#888;cursor:pointer;white-space:nowrap; }\n9\t.vv-au-btn:hover{ background:#222;color:#bbb; }\n10\t.vv-au-btn.prim { border-color:#1a3a5a;background:#0d1f2a;color:#5c9fd4; }\n11\t.vv-au-btn.warn { border-color:#3a2000;background:#1f1200;color:#ff9800; }\n12\t.vv-au-btn.danger{border-color:#3a1a1a;background:#200d0d;color:#ef5350; }\n13\t.vv-au-btn.green{ border-color:#1a3a1a;background:#0d1f0d;color:#4caf50; }\n14\t.vv-au-btn:disabled { opacity:.4;cursor:default; }\n15\t\n16\t/* ── Panels ──────────────────────────────────────────────────────────────── */\n17\t.vv-au-panel { display:none; }\n18\t.vv-au-panel.active { display:block; }\n19\t\n20\t/* ── Tables ──────────────────────────────────────────────────────────────── */\n21\t.vv-au-tbl { width:100%;border-collapse:collapse;font-size:11px; }\n22\t.vv-au-tbl th { text-align:left;padding:5px 10px;font-size:10px;color:#444;text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid #1e1e1e;white-space:nowrap; }\n23\t.vv-au-tbl td { padding:7px 10px;border-bottom:1px solid #161616;vertical-align:middle; }\n24\t.vv-au-tbl tr:hover td { background:#141414; }\n25\t.vv-au-tbl tr:last-child td { border-bottom:none; }\n26\t\n27\t/* ── Badges ──────────────────────────────────────────────────────────────── */\n28\t.vv-au-badge { font-size:10px;padding:1px 6px;border-radius:2px;white-space:nowrap;display:inline-block; }\n29\t.vv-au-badge.ssl { background:#0d1f0d;border:1px solid #1a3a1a;color:#4caf50; }\n30\t.vv-au-badge.nossl { background:#111;border:1px solid #222;color:#444; }\n31\t.vv-au-badge.on { background:#0d1f0d;border:1px solid #1a3a1a;color:#4caf50; }\n32\t.vv-au-badge.off { background:#1a0a0a;border:1px solid #2a1a1a;color:#555; }\n33\t.vv-au-badge.bypass { background:#0a1a2a;border:1px solid #1a3a5a;color:#5c9fd4; }\n34\t.vv-au-badge.one_factor { background:#1a1a0a;border:1px solid #3a3a1a;color:#cddc39; }\n35\t.vv-au-badge.two_factor { background:#0d1f0d;border:1px solid #1a3a1a;color:#4caf50; }\n36\t.vv-au-badge.deny { background:#1a0a0a;border:1px solid #3a1a1a;color:#ef5350; }\n37\t.vv-au-badge.grp { background:#1a0d2a;border:1px solid #2a1a4a;color:#9c6ff7;margin:1px 2px; }\n38\t.vv-au-badge.host { background:#111;border:1px solid #1e1e1e;color:#444; }\n39\t\n40\t/* ── Cards ───────────────────────────────────────────────────────────────── */\n41\t.vv-au-card { background:#161616;border:1px solid #222;border-radius:6px;overflow:hidden; }\n42\t.vv-au-card-h { display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid #1e1e1e;background:#111; }\n43\t.vv-au-card-title { font-size:11px;font-weight:bold;color:#666;text-transform:uppercase;letter-spacing:.06em;flex:1; }\n44\t\n45\t/* ── Users / Groups split ────────────────────────────────────────────────── */\n46\t.vv-au-ug-grid { display:grid;grid-template-columns:3fr 2fr;gap:12px; }\n47\t@media (max-width:700px) { .vv-au-ug-grid { grid-template-columns:1fr; } }\n48\t\n49\t.vv-au-user-row { padding:8px 12px;border-bottom:1px solid #1a1a1a;display:flex;align-items:center;gap:8px;flex-wrap:wrap; }\n50\t.vv-au-user-row:last-child { border-bottom:none; }\n51\t.vv-au-user-row:hover { background:#141414; }\n52\t.vv-au-user-name { font-size:12px;color:#bbb;font-weight:bold;min-width:80px; }\n53\t.vv-au-user-email{ font-size:10px;color:#444;flex:1; }\n54\t.vv-au-user-acts { display:flex;gap:4px;margin-left:auto;flex-shrink:0; }\n55\t\n56\t.vv-au-grp-row { padding:8px 12px;border-bottom:1px solid #1a1a1a;cursor:pointer; }\n57\t.vv-au-grp-row:last-child { border-bottom:none; }\n58\t.vv-au-grp-row:hover { background:#141414; }\n59\t.vv-au-grp-name { font-size:12px;color:#bbb;font-weight:bold; }\n60\t.vv-au-grp-cnt { font-size:10px;color:#444;margin-top:2px; }\n61\t.vv-au-grp-acts { float:right;display:flex;gap:4px;margin-top:1px; }\n62\t\n63\t.vv-au-grp-members { padding:6px 12px 10px 20px;background:#0f0f0f;border-bottom:1px solid #1a1a1a; display:none; }\n64\t.vv-au-grp-members.open { display:block; }\n65\t.vv-au-grp-member { font-size:11px;color:#666;padding:2px 0;display:flex;align-items:center;gap:6px; }\n66\t.vv-au-grp-member-rm { font-size:11px;color:#3a1a1a;cursor:pointer;padding:0 2px; }\n67\t.vv-au-grp-member-rm:hover { color:#ef5350; }\n68\t\n69\t/* ── Access Control ──────────────────────────────────────────────────────── */\n70\t.vv-au-ac-defpol { display:flex;align-items:center;gap:10px;padding:8px 12px;background:#111;border:1px solid #222;border-radius:4px;margin-bottom:10px;font-size:11px;color:#555; }\n71\t.vv-au-ac-defpol select { background:#0d0d0d;border:1px solid #2a2a2a;border-radius:3px;color:#888;font-size:11px;padding:2px 6px; }\n72\t.vv-au-rule-num { color:#333;font-size:10px;width:24px;text-align:right;flex-shrink:0; }\n73\t.vv-au-rule-acts { display:flex;gap:3px;white-space:nowrap; }\n74\t.vv-au-icon-btn { font-size:12px;color:#444;cursor:pointer;padding:1px 3px;border-radius:2px;background:none;border:none;line-height:1; }\n75\t.vv-au-icon-btn:hover { color:#bbb;background:#222; }\n76\t.vv-au-icon-btn.del:hover { color:#ef5350;background:#1a0808; }\n77\t\n78\t/* ── Section toolbar (within panel) ─────────────────────────────────────── */\n79\t.vv-au-sec-bar { display:flex;align-items:center;gap:8px;margin-bottom:10px; }\n80\t.vv-au-sec-title{ font-size:11px;color:#555;flex:1; }\n81\t\n82\t/* ── Toggle switch ───────────────────────────────────────────────────────── */\n83\t.vv-au-tog { width:28px;height:16px;border-radius:8px;background:#1e1e1e;border:1px solid #2a2a2a;position:relative;cursor:pointer;display:inline-block;flex-shrink:0; }\n84\t.vv-au-tog.on { background:#1a3a1a;border-color:#2d5a2d; }\n85\t.vv-au-tog::after { content:'';position:absolute;top:2px;left:2px;width:10px;height:10px;border-radius:50%;background:#444;transition:left .12s,background .12s; }\n86\t.vv-au-tog.on::after { left:14px;background:#4caf50; }\n87\t\n88\t/* ── Modal ───────────────────────────────────────────────────────────────── */\n89\t.vv-au-overlay { position:fixed;inset:0;background:#0009;z-index:9000;display:none;align-items:center;justify-content:center; }\n90\t.vv-au-overlay.open { display:flex; }\n91\t.vv-au-modal { background:#181818;border:1px solid #2a2a2a;border-radius:6px;padding:18px 20px;min-width:360px;max-width:520px;width:90vw;max-height:85vh;overflow-y:auto; }\n92\t.vv-au-modal h3 { font-size:12px;font-weight:bold;color:#888;text-transform:uppercase;letter-spacing:.06em;margin:0 0 14px; }\n93\t.vv-au-field { margin-bottom:10px; }\n94\t.vv-au-label { display:block;font-size:10px;color:#555;margin-bottom:3px;text-transform:uppercase;letter-spacing:.04em; }\n95\t.vv-au-input { width:100%;box-sizing:border-box;background:#0d0d0d;border:1px solid #252525;border-radius:3px;color:#bbb;font-size:11px;padding:5px 8px;outline:none; }\n96\t.vv-au-input:focus { border-color:#1a3a5a; }\n97\t.vv-au-select { width:100%;box-sizing:border-box;background:#0d0d0d;border:1px solid #252525;border-radius:3px;color:#bbb;font-size:11px;padding:5px 8px;outline:none; }\n98\t.vv-au-hint { font-size:10px;color:#333;margin-top:2px; }\n99\t.vv-au-modal-acts { display:flex;justify-content:flex-end;gap:8px;margin-top:16px; }\n100\t.vv-au-tog-row { display:flex;align-items:center;gap:8px;margin-bottom:10px; }\n101\t.vv-au-tog-lbl { font-size:11px;color:#666; }\n102\t.vv-au-adv-toggle { font-size:10px;color:#333;cursor:pointer;margin-bottom:8px; }\n103\t.vv-au-adv-toggle:hover { color:#666; }\n104\t.vv-au-adv-section { display:none; }\n105\t.vv-au-adv-section.open { display:block; }\n106\t.vv-au-err { font-size:11px;color:#ef5350;margin-top:8px;display:none; }\n107\t.vv-au-err.show { display:block; }\n108\t\n109\t/* ── Misc ────────────────────────────────────────────────────────────────── */\n110\t.vv-au-empty { padding:24px;text-align:center;font-size:11px;color:#333; }\n111\t.vv-au-domain { font-size:12px;color:#bbb;font-weight:bold; }\n112\t.vv-au-fwd { font-size:10px;color:#444; }\n113\t.vv-au-loading { color:#333;font-size:11px;padding:16px;text-align:center; }\n114\t\n115\t/* ── Certs panel ─────────────────────────────────────────────────────────── */\n116\t.vv-au-cert-grid { display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:10px; }\n117\t.vv-au-cert-days { font-size:28px;font-weight:700;line-height:1;margin:6px 0 2px; }\n118\t.vv-au-cert-bar { height:3px;border-radius:2px;background:#1a1a1a;overflow:hidden;margin-top:8px; }\n119\t.vv-au-cert-fill { height:100%;border-radius:2px;transition:width .3s; }\n120\t</style>\n121\t\n122\t<?php\n123\trequire_once __DIR__ . '/../include/auth.php';\n124\t$isOwner = vv_is_owner();\n125\t?>\n126\t\n127\t<div class=\"vv-au-toolbar\">\n128\t <span class=\"vv-au-title\">Auth Stack</span>\n129\t <button class=\"vv-au-tab active\" data-tab=\"proxies\">Proxies</button>\n130\t <button class=\"vv-au-tab\" data-tab=\"users\">Users & Groups</button>\n131\t <button class=\"vv-au-tab\" data-tab=\"acl\">Access Control</button>\n132\t <button class=\"vv-au-tab\" data-tab=\"certs\">Certs</button>\n133\t <button class=\"vv-au-btn\" id=\"vv-au-refresh\" title=\"Refresh current tab\">↻ Refresh</button>\n134\t</div>\n135\t\n136\t<!-- ── Proxies ─────────────────────────────────────────────────────────────── -->\n137\t<div class=\"vv-au-panel active\" id=\"vv-au-panel-proxies\">\n138\t <div class=\"vv-au-sec-bar\">\n139\t <span class=\"vv-au-sec-title\" id=\"vv-au-proxy-count\"></span>\n140\t <button class=\"vv-au-btn prim\" id=\"vv-au-proxy-add\">+ Add Proxy</button>\n141\t </div>\n142\t <div class=\"vv-au-card\">\n143\t <div class=\"vv-au-loading\" id=\"vv-au-proxy-loading\">Loading…</div>\n144\t <table class=\"vv-au-tbl\" id=\"vv-au-proxy-tbl\" style=\"display:none\">\n145\t <thead><tr>\n146\t <th>Domain</th><th>Forward</th><th>SSL</th><th>Status</th><th></th>\n147\t </tr></thead>\n148\t <tbody id=\"vv-au-proxy-body\"></tbody>\n149\t </table>\n150\t <div class=\"vv-au-empty\" id=\"vv-au-proxy-empty\" style=\"display:none\">No proxy hosts configured.</div>\n151\t </div>\n152\t</div>\n153\t\n154\t<!-- ── Users & Groups ─────────────────────────────────────────────────────── -->\n155\t<div class=\"vv-au-panel\" id=\"vv-au-panel-users\">\n156\t <div class=\"vv-au-ug-grid\">\n157\t\n158\t <div class=\"vv-au-card\">\n159\t <div class=\"vv-au-card-h\">\n160\t <span class=\"vv-au-card-title\">Users</span>\n161\t <button class=\"vv-au-btn prim\" id=\"vv-au-user-add\">+ Add User</button>\n162\t </div>\n163\t <div class=\"vv-au-loading\" id=\"vv-au-users-loading\">Loading…</div>\n164\t <div id=\"vv-au-users-list\"></div>\n165\t <div class=\"vv-au-empty\" id=\"vv-au-users-empty\" style=\"display:none\">No users found.</div>\n166\t </div>\n167\t\n168\t <div class=\"vv-au-card\">\n169\t <div class=\"vv-au-card-h\">\n170\t <span class=\"vv-au-card-title\">Groups</span>\n171\t <button class=\"vv-au-btn prim\" id=\"vv-au-group-add\">+ Add Group</button>\n172\t </div>\n173\t <div class=\"vv-au-loading\" id=\"vv-au-groups-loading\">Loading…</div>\n174\t <div id=\"vv-au-groups-list\"></div>\n175\t <div class=\"vv-au-empty\" id=\"vv-au-groups-empty\" style=\"display:none\">No groups found.</div>\n176\t </div>\n177\t\n178\t </div>\n179\t</div>\n180\t\n181\t<!-- ── Access Control ─────────────────────────────────────────────────────── -->\n182\t<div class=\"vv-au-panel\" id=\"vv-au-panel-acl\">\n183\t <div class=\"vv-au-ac-defpol\" id=\"vv-au-ac-defpol-bar\">\n184\t <span>Default policy:</span>\n185\t <select id=\"vv-au-ac-defpol\" <?= $isOwner ? '' : 'disabled' ?>>\n186\t <option value=\"deny\">deny</option>\n187\t <option value=\"two_factor\">two_factor</option>\n188\t <option value=\"one_factor\">one_factor</option>\n189\t <option value=\"bypass\">bypass</option>\n190\t </select>\n191\t <?php if (!$isOwner): ?>\n192\t <span style=\"font-size:10px;color:#3a2a1a;\">default policy editable on HOST1</span>\n193\t <?php endif; ?>\n194\t <button class=\"vv-au-btn green\" id=\"vv-au-ac-save\" style=\"margin-left:auto\">Save & Restart Authelia</button>\n195\t </div>\n196\t <div class=\"vv-au-sec-bar\">\n197\t <span class=\"vv-au-sec-title\" id=\"vv-au-acl-count\"></span>\n198\t <button class=\"vv-au-btn prim\" id=\"vv-au-rule-add\">+ Add Rule</button>\n199\t </div>\n200\t <div class=\"vv-au-card\">\n201\t <div class=\"vv-au-loading\" id=\"vv-au-acl-loading\">Loading…</div>\n202\t <table class=\"vv-au-tbl\" id=\"vv-au-acl-tbl\" style=\"display:none\">\n203\t <thead><tr>\n204\t <th style=\"width:24px\">#</th>\n205\t <th>Domain</th><th>Policy</th><th>Subject</th><th></th>\n206\t </tr></thead>\n207\t <tbody id=\"vv-au-acl-body\"></tbody>\n208\t </table>\n209\t <div class=\"vv-au-empty\" id=\"vv-au-acl-empty\" style=\"display:none\">No rules configured.</div>\n210\t </div>\n211\t</div>\n212\t\n213\t<!-- ── Certs ───────────────────────────────────────────────────────────────── -->\n214\t<div class=\"vv-au-panel\" id=\"vv-au-panel-certs\">\n215\t <div class=\"vv-au-sec-bar\">\n216\t <span class=\"vv-au-sec-title\" id=\"vv-au-cert-ts\"></span>\n217\t <button class=\"vv-au-btn prim\" id=\"vv-au-cert-run\">Run now</button>\n218\t </div>\n219\t <div class=\"vv-au-cert-grid\" id=\"vv-au-cert-grid\">\n220\t <div class=\"vv-au-loading\">Loading…</div>\n221\t </div>\n222\t <div id=\"vv-au-cert-log\" style=\"display:none;margin-top:12px;background:#0d0d0d;border:1px solid #1e1e1e;\n223\t border-radius:4px;padding:10px 12px;font-size:10px;color:#555;font-family:monospace;\n224\t max-height:140px;overflow-y:auto;white-space:pre-wrap;\"></div>\n225\t <div id=\"vv-au-cert-cfg\" style=\"margin-top:10px;font-size:10px;color:#3a3a3a;\"></div>\n226\t</div>\n227\t\n228\t<!-- ── Modal overlay ──────────────────────────────────────────────────────── -->\n229\t<div class=\"vv-au-overlay\" id=\"vv-au-overlay\">\n230\t <div class=\"vv-au-modal\" id=\"vv-au-modal\"></div>\n231\t</div>\n232\t\n233\t<script>\n234\t(function () {\n235\t'use strict';\n236\t\n237\tconst API = '/plugins/varaverk/api/auth.php';\n238\tconst CERT_API = '/plugins/varaverk/api/cert.php';\n239\tconst IS_OWNER = <?= $isOwner ? 'true' : 'false' ?>;\n240\t\n241\t// ── State ─────────────────────────────────────────────────────────────────────\n242\tlet _proxies = [], _certs = [];\n243\tlet _users = [], _groups = [];\n244\tlet _rules = [], _defaultPolicy = 'deny';\n245\tlet _activeTab = 'proxies';\n246\t\n247\t// ── Helpers ───────────────────────────────────────────────────────────────────\n248\tfunction _esc(s) {\n249\t return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\"/g,'"');\n250\t}\n251\t\n252\tfunction _get(action, cb) {\n253\t fetch(API + '?action=' + action)\n254\t .then(r => r.json()).then(cb)\n255\t .catch(e => cb({ ok: false, error: String(e) }));\n256\t}\n257\t\n258\tfunction _post(params, cb) {\n259\t const fd = new FormData();\n260\t for (const [k, v] of Object.entries(params)) fd.append(k, v);\n261\t fetch(API, { method: 'POST', body: fd })\n262\t .then(r => r.json()).then(cb)\n263\t .catch(e => cb({ ok: false, error: String(e) }));\n264\t}\n265\t\n266\tfunction _modal(html) {\n267\t document.getElementById('vv-au-modal').innerHTML = html;\n268\t document.getElementById('vv-au-overlay').classList.add('open');\n269\t}\n270\tfunction _closeModal() {\n271\t document.getElementById('vv-au-overlay').classList.remove('open');\n272\t}\n273\t\n274\tfunction _togHtml(id, on, title) {\n275\t return `<span class=\"vv-au-tog${on?' on':''}\" data-tog=\"${_esc(id)}\" title=\"${_esc(title)}\"></span>`;\n276\t}\n277\t\n278\tfunction _policyBadge(p) {\n279\t return `<span class=\"vv-au-badge ${_esc(p)}\">${_esc(p)}</span>`;\n280\t}\n281\t\n282\tfunction _normSubject(val) {\n283\t if (!val) return [];\n284\t if (Array.isArray(val)) return val;\n285\t return [val];\n286\t}\n287\t\n288\tfunction _normDomain(val) {\n289\t if (!val) return [];\n290\t if (Array.isArray(val)) return val;\n291\t return [val];\n292\t}\n293\t\n294\t// ── Tab switching ─────────────────────────────────────────────────────────────\n295\tdocument.querySelectorAll('.vv-au-tab').forEach(btn => {\n296\t btn.addEventListener('click', () => {\n297\t const tab = btn.dataset.tab;\n298\t document.querySelectorAll('.vv-au-tab').forEach(b => b.classList.remove('active'));\n299\t document.querySelectorAll('.vv-au-panel').forEach(p => p.classList.remove('active'));\n300\t btn.classList.add('active');\n301\t document.getElementById('vv-au-panel-' + tab).classList.add('active');\n302\t _activeTab = tab;\n303\t _loadTab(tab);\n304\t });\n305\t});\n306\t\n307\tdocument.getElementById('vv-au-refresh').addEventListener('click', () => _loadTab(_activeTab));\n308\t\n309\tfunction _loadTab(tab) {\n310\t if (tab === 'proxies') _loadProxies();\n311\t if (tab === 'users') { _loadUsers(); _loadGroups(); }\n312\t if (tab === 'acl') _loadAcl();\n313\t if (tab === 'certs') _loadCerts();\n314\t}\n315\t\n316\t// ── Proxies ───────────────────────────────────────────────────────────────────\n317\tfunction _loadProxies() {\n318\t const loading = document.getElementById('vv-au-proxy-loading');\n319\t const tbl = document.getElementById('vv-au-proxy-tbl');\n320\t const empty = document.getElementById('vv-au-proxy-empty');\n321\t loading.style.display = 'block';\n322\t tbl.style.display = 'none';\n323\t empty.style.display = 'none';\n324\t\n325\t // Load certs and proxies in parallel\n326\t let certsLoaded = false, proxiesLoaded = false;\n327\t function _check() {\n328\t if (!certsLoaded || !proxiesLoaded) return;\n329\t loading.style.display = 'none';\n330\t if (!_proxies.length) { empty.style.display = 'block'; return; }\n331\t tbl.style.display = 'table';\n332\t _renderProxies();\n333\t }\n334\t\n335\t _get('npm_certs', r => { _certs = r.certs || []; certsLoaded = true; _check(); });\n336\t _get('npm_proxies', r => {\n337\t if (!r.ok) { loading.innerHTML = '<span style=\"color:#ef5350\">'+_esc(r.error)+'</span>'; return; }\n338\t _proxies = r.proxies || [];\n339\t proxiesLoaded = true;\n340\t _check();\n341\t });\n342\t}\n343\t\n344\tfunction _renderProxies() {\n345\t const count = document.getElementById('vv-au-proxy-count');\n346\t count.textContent = _proxies.length + ' host' + (_proxies.length !== 1 ? 's' : '');\n347\t\n348\t const body = document.getElementById('vv-au-proxy-body');\n349\t body.innerHTML = _proxies.map(p => {\n350\t const domains = (p.domain_names || []).join(', ');\n351\t const fwd = p.forward_scheme + '://' + p.forward_host + ':' + p.forward_port;\n352\t const hasSsl = p.certificate_id && p.certificate_id !== '0';\n353\t const sslBadge = hasSsl\n354\t ? `<span class=\"vv-au-badge ssl\">SSL</span>`\n355\t : `<span class=\"vv-au-badge nossl\">none</span>`;\n356\t const enabled = p.enabled;\n357\t return `<tr>\n358\t <td><div class=\"vv-au-domain\">${_esc(domains)}</div></td>\n359\t <td><div class=\"vv-au-fwd\">${_esc(fwd)}</div></td>\n360\t <td>${sslBadge}</td>\n361\t <td>${_togHtml('proxy-' + p.id, enabled, enabled ? 'Enabled — click to disable' : 'Disabled — click to enable')}</td>\n362\t <td style=\"text-align:right\">\n363\t <div class=\"vv-au-rule-acts\">\n364\t <button class=\"vv-au-icon-btn\" data-proxy-edit=\"${p.id}\" title=\"Edit\">✎</button>\n365\t <button class=\"vv-au-icon-btn del\" data-proxy-del=\"${p.id}\" title=\"Delete\">✕</button>\n366\t </div>\n367\t </td>\n368\t </tr>`;\n369\t }).join('');\n370\t}\n371\t\n372\tfunction _proxyModal(id) {\n373\t const p = id ? _proxies.find(x => x.id === id) : null;\n374\t const certOptions = _certs.map(c =>\n375\t `<option value=\"${c.id}\"${p && p.certificate_id == c.id ? ' selected' : ''}>${_esc(c.nice_name || c.domain_names?.join(', '))}</option>`\n376\t ).join('');\n377\t\n378\t _modal(`<h3>${p ? 'Edit Proxy' : 'Add Proxy'}</h3>\n379\t <div class=\"vv-au-field\">\n380\t <label class=\"vv-au-label\">Domain Names</label>\n381\t <input class=\"vv-au-input\" id=\"pm-domains\" value=\"${_esc((p?.domain_names||[]).join(', '))}\" placeholder=\"example.com, *.example.com\">\n382\t <div class=\"vv-au-hint\">Comma-separated</div>\n383\t </div>\n384\t <div style=\"display:grid;grid-template-columns:1fr 2fr 80px;gap:8px\">\n385\t <div class=\"vv-au-field\">\n386\t <label class=\"vv-au-label\">Scheme</label>\n387\t <select class=\"vv-au-select\" id=\"pm-scheme\">\n388\t <option${(!p||p.forward_scheme==='http')?' selected':''}>http</option>\n389\t <option${(p?.forward_scheme==='https')?' selected':''}>https</option>\n390\t </select>\n391\t </div>\n392\t <div class=\"vv-au-field\">\n393\t <label class=\"vv-au-label\">Forward Host</label>\n394\t <input class=\"vv-au-input\" id=\"pm-host\" value=\"${_esc(p?.forward_host||'')}\" placeholder=\"192.168.1.100\">\n395\t </div>\n396\t <div class=\"vv-au-field\">\n397\t <label class=\"vv-au-label\">Port</label>\n398\t <input class=\"vv-au-input\" id=\"pm-port\" type=\"number\" value=\"${_esc(p?.forward_port||80)}\">\n399\t </div>\n400\t </div>\n401\t <div class=\"vv-au-field\">\n402\t <label class=\"vv-au-label\">SSL Certificate</label>\n403\t <select class=\"vv-au-select\" id=\"pm-cert\">\n404\t <option value=\"0\">None</option>\n405\t ${certOptions}\n406\t </select>\n407\t </div>\n408\t <div class=\"vv-au-tog-row\">\n409\t <span class=\"vv-au-tog${p?.ssl_forced?' on':''}\" id=\"pm-ssl-forced\"></span>\n410\t <span class=\"vv-au-tog-lbl\">Force SSL</span>\n411\t </div>\n412\t <div class=\"vv-au-tog-row\">\n413\t <span class=\"vv-au-tog${p?.block_exploits?' on':''}\" id=\"pm-block-exploits\"></span>\n414\t <span class=\"vv-au-tog-lbl\">Block Common Exploits</span>\n415\t </div>\n416\t <div class=\"vv-au-tog-row\">\n417\t <span class=\"vv-au-tog${(!p||p.allow_websocket_upgrade!==false)?' on':''}\" id=\"pm-websocket\"></span>\n418\t <span class=\"vv-au-tog-lbl\">WebSocket Support</span>\n419\t </div>\n420\t <div class=\"vv-au-err\" id=\"pm-err\"></div>\n421\t <div class=\"vv-au-modal-acts\">\n422\t <button class=\"vv-au-btn\" id=\"pm-cancel\">Cancel</button>\n423\t <button class=\"vv-au-btn prim\" id=\"pm-save\">${p ? 'Save' : 'Create'}</button>\n424\t </div>`);\n425\t\n426\t // Wire inline toggles\n427\t document.querySelectorAll('#vv-au-modal .vv-au-tog').forEach(t => {\n428\t t.addEventListener('click', () => t.classList.toggle('on'));\n429\t });\n430\t\n431\t document.getElementById('pm-cancel').onclick = _closeModal;\n432\t document.getElementById('pm-save').onclick = () => {\n433\t const domains = document.getElementById('pm-domains').value.split(',').map(s=>s.trim()).filter(Boolean);\n434\t if (!domains.length) { _showModalErr('pm-err', 'Domain required'); return; }\n435\t const host = document.getElementById('pm-host').value.trim();\n436\t if (!host) { _showModalErr('pm-err', 'Forward host required'); return; }\n437\t\n438\t const data = {\n439\t domain_names: domains,\n440\t forward_scheme: document.getElementById('pm-scheme').value,\n441\t forward_host: host,\n442\t forward_port: parseInt(document.getElementById('pm-port').value) || 80,\n443\t certificate_id: parseInt(document.getElementById('pm-cert').value) || 0,\n444\t ssl_forced: document.getElementById('pm-ssl-forced').classList.contains('on'),\n445\t block_exploits: document.getElementById('pm-block-exploits').classList.contains('on'),\n446\t allow_websocket_upgrade: document.getElementById('pm-websocket').classList.contains('on'),\n447\t http2_support: false,\n448\t hsts_enabled: false,\n449\t hsts_subdomains: false,\n450\t meta: {},\n451\t locations: [],\n452\t advanced_config: '',\n453\t enabled: true,\n454\t };\n455\t\n456\t const btn = document.getElementById('pm-save');\n457\t btn.disabled = true; btn.textContent = 'Saving…';\n458\t\n459\t const done = r => {\n460\t if (!r.ok) { _showModalErr('pm-err', r.error||'Save failed'); btn.disabled=false; btn.textContent = p?'Save':'Create'; return; }\n461\t _closeModal(); _loadProxies();\n462\t };\n463\t if (p) _post({ action:'npm_update', id: p.id, data: JSON.stringify(data) }, done);\n464\t else _post({ action:'npm_create', data: JSON.stringify(data) }, done);\n465\t };\n466\t}\n467\t\n468\tfunction _showModalErr(id, msg) {\n469\t const el = document.getElementById(id);\n470\t if (el) { el.textContent = msg; el.classList.add('show'); }\n471\t}\n472\t\n473\t// Proxy event delegation\n474\tdocument.getElementById('vv-au-panel-proxies').addEventListener('click', e => {\n475\t // Add button\n476\t if (e.target.id === 'vv-au-proxy-add') { _proxyModal(null); return; }\n477\t // Toggle\n478\t const tog = e.target.closest('[data-tog^=\"proxy-\"]');\n479\t if (tog) {\n480\t const id = parseInt(tog.dataset.tog.split('-')[1]);\n481\t const was = tog.classList.contains('on');\n482\t tog.classList.toggle('on');\n483\t _post({ action:'npm_toggle', id, enabled: was?'0':'1' }, r => {\n484\t if (!r.ok) { tog.classList.toggle('on'); }\n485\t });\n486\t return;\n487\t }\n488\t // Edit\n489\t const editBtn = e.target.closest('[data-proxy-edit]');\n490\t if (editBtn) { _proxyModal(parseInt(editBtn.dataset.proxyEdit)); return; }\n491\t // Delete\n492\t const delBtn = e.target.closest('[data-proxy-del]');\n493\t if (delBtn) {\n494\t const id = parseInt(delBtn.dataset.proxyDel);\n495\t const p = _proxies.find(x => x.id === id);\n496\t if (!confirm('Delete proxy for ' + (p?.domain_names||['this host']).join(', ') + '?')) return;\n497\t _post({ action:'npm_delete', id }, r => { if (r.ok) _loadProxies(); });\n498\t }\n499\t});\n500\t\n501\t// ── Users ─────────────────────────────────────────────────────────────────────\n502\tfunction _loadUsers() {\n503\t const loading = document.getElementById('vv-au-users-loading');\n504\t const list = document.getElementById('vv-au-users-list');\n505\t const empty = document.getElementById('vv-au-users-empty');\n506\t loading.style.display = 'block';\n507\t list.innerHTML = '';\n508\t empty.style.display = 'none';\n509\t\n510\t _get('lldap_users', r => {\n511\t loading.style.display = 'none';\n512\t if (!r.ok) { loading.innerHTML = '<span style=\"color:#ef5350;padding:10px;display:block\">'+_esc(r.error)+'</span>'; loading.style.display='block'; return; }\n513\t _users = r.users || [];\n514\t if (!_users.length) { empty.style.display = 'block'; return; }\n515\t list.innerHTML = _users.map(u => {\n516\t const grpBadges = (u.groups||[]).map(g => `<span class=\"vv-au-badge grp\" title=\"Click to remove\" data-rm-from-group=\"${_esc(u.id)}\" data-gid=\"${g.id}\">${_esc(g.displayName)}</span>`).join('');\n517\t return `<div class=\"vv-au-user-row\">\n518\t <span class=\"vv-au-user-name\">${_esc(u.displayName||u.id)}</span>\n519\t <span class=\"vv-au-user-email\">${_esc(u.email||'')}</span>\n520\t <div style=\"display:flex;gap:3px;align-items:center;flex-wrap:wrap\">${grpBadges}</div>\n521\t <div class=\"vv-au-user-acts\">\n522\t <button class=\"vv-au-icon-btn\" data-user-grp=\"${_esc(u.id)}\" title=\"Add to group\">+grp</button>\n523\t <button class=\"vv-au-icon-btn\" data-user-pass=\"${_esc(u.id)}\" title=\"Change password\">🔑</button>\n524\t <button class=\"vv-au-icon-btn\" data-user-edit=\"${_esc(u.id)}\" title=\"Edit\">✎</button>\n525\t <button class=\"vv-au-icon-btn del\" data-user-del=\"${_esc(u.id)}\" title=\"Delete\">✕</button>\n526\t </div>\n527\t </div>`;\n528\t }).join('');\n529\t });\n530\t}\n531\t\n532\tfunction _userModal(uid) {\n533\t const u = uid ? _users.find(x => x.id === uid) : null;\n534\t _modal(`<h3>${u ? 'Edit User' : 'Add User'}</h3>\n535\t <div class=\"vv-au-field\">\n536\t <label class=\"vv-au-label\">Username (ID)</label>\n537\t <input class=\"vv-au-input\" id=\"um-uid\" value=\"${_esc(u?.id||'')}\" ${u?'readonly':''} placeholder=\"johndoe\">\n538\t ${u ? '' : '<div class=\"vv-au-hint\">Lowercase letters, digits, hyphens — cannot be changed later</div>'}\n539\t </div>\n540\t <div class=\"vv-au-field\">\n541\t <label class=\"vv-au-label\">Display Name</label>\n542\t <input class=\"vv-au-input\" id=\"um-name\" value=\"${_esc(u?.displayName||'')}\" placeholder=\"John Doe\">\n543\t </div>\n544\t <div class=\"vv-au-field\">\n545\t <label class=\"vv-au-label\">Email</label>\n546\t <input class=\"vv-au-input\" id=\"um-email\" type=\"email\" value=\"${_esc(u?.email||'')}\" placeholder=\"john@example.com\">\n547\t </div>\n548\t ${!u ? `<div class=\"vv-au-field\">\n549\t <label class=\"vv-au-label\">Password</label>\n550\t <input class=\"vv-au-input\" id=\"um-pass\" type=\"password\" placeholder=\"Initial password\">\n551\t </div>` : ''}\n552\t <div class=\"vv-au-err\" id=\"um-err\"></div>\n553\t <div class=\"vv-au-modal-acts\">\n554\t <button class=\"vv-au-btn\" id=\"um-cancel\">Cancel</button>\n555\t <button class=\"vv-au-btn prim\" id=\"um-save\">${u ? 'Save' : 'Create'}</button>\n556\t </div>`);\n557\t\n558\t document.getElementById('um-cancel').onclick = _closeModal;\n559\t document.getElementById('um-save').onclick = () => {\n560\t const id = (document.getElementById('um-uid').value||'').trim();\n561\t const name = (document.getElementById('um-name').value||'').trim();\n562\t const email= (document.getElementById('um-email').value||'').trim();\n563\t const pass = document.getElementById('um-pass')?.value || '';\n564\t if (!id) { _showModalErr('um-err','Username required'); return; }\n565\t if (!email){ _showModalErr('um-err','Email required'); return; }\n566\t\n567\t const btn = document.getElementById('um-save');\n568\t btn.disabled = true; btn.textContent = 'Saving…';\n569\t\n570\t const done = r => {\n571\t if (!r.ok) { _showModalErr('um-err', r.error||'Save failed'); btn.disabled=false; btn.textContent=u?'Save':'Create'; return; }\n572\t _closeModal(); _loadUsers();\n573\t };\n574\t if (u) _post({ action:'lldap_update_user', uid:id, email, display_name:name }, done);\n575\t else _post({ action:'lldap_create_user', uid:id, email, display_name:name, password:pass }, done);\n576\t };\n577\t}\n578\t\n579\tfunction _passModal(uid) {\n580\t _modal(`<h3>Change Password</h3>\n581\t <div class=\"vv-au-field\">\n582\t <label class=\"vv-au-label\">User</label>\n583\t <input class=\"vv-au-input\" value=\"${_esc(uid)}\" readonly>\n584\t </div>\n585\t <div class=\"vv-au-field\">\n586\t <label class=\"vv-au-label\">New Password</label>\n587\t <input class=\"vv-au-input\" id=\"pw-pass\" type=\"password\" placeholder=\"New password\" autofocus>\n588\t </div>\n589\t <div class=\"vv-au-err\" id=\"pw-err\"></div>\n590\t <div class=\"vv-au-modal-acts\">\n591\t <button class=\"vv-au-btn\" id=\"pw-cancel\">Cancel</button>\n592\t <button class=\"vv-au-btn prim\" id=\"pw-save\">Set Password</button>\n593\t </div>`);\n594\t document.getElementById('pw-cancel').onclick = _closeModal;\n595\t document.getElementById('pw-save').onclick = () => {\n596\t const pass = document.getElementById('pw-pass').value;\n597\t if (!pass) { _showModalErr('pw-err','Password required'); return; }\n598\t const btn = document.getElementById('pw-save');\n599\t btn.disabled = true; btn.textContent = 'Saving…';\n600\t _post({ action:'lldap_set_password', uid, password:pass }, r => {\n601\t if (!r.ok) { _showModalErr('pw-err', r.error||'Failed'); btn.disabled=false; btn.textContent='Set Password'; return; }\n602\t _closeModal();\n603\t });\n604\t };\n605\t}\n606\t\n607\tfunction _addToGroupModal(uid) {\n608\t const user = _users.find(u => u.id === uid);\n609\t const userGids= new Set((user?.groups||[]).map(g => g.id));\n610\t const available = _groups.filter(g => !userGids.has(g.id));\n611\t if (!available.length) { alert('User is already in all groups.'); return; }\n612\t const opts = available.map(g => `<option value=\"${g.id}\">${_esc(g.displayName)}</option>`).join('');\n613\t _modal(`<h3>Add to Group</h3>\n614\t <div class=\"vv-au-field\">\n615\t <label class=\"vv-au-label\">User</label>\n616\t <input class=\"vv-au-input\" value=\"${_esc(user?.displayName||uid)}\" readonly>\n617\t </div>\n618\t <div class=\"vv-au-field\">\n619\t <label class=\"vv-au-label\">Group</label>\n620\t <select class=\"vv-au-select\" id=\"ag-grp\">${opts}</select>\n621\t </div>\n622\t <div class=\"vv-au-err\" id=\"ag-err\"></div>\n623\t <div class=\"vv-au-modal-acts\">\n624\t <button class=\"vv-au-btn\" id=\"ag-cancel\">Cancel</button>\n625\t <button class=\"vv-au-btn prim\" id=\"ag-save\">Add</button>\n626\t </div>`);\n627\t document.getElementById('ag-cancel').onclick = _closeModal;\n628\t document.getElementById('ag-save').onclick = () => {\n629\t const gid = parseInt(document.getElementById('ag-grp').value);\n630\t const btn = document.getElementById('ag-save');\n631\t btn.disabled = true; btn.textContent = 'Adding…';\n632\t _post({ action:'lldap_add_to_group', uid, gid }, r => {\n633\t if (!r.ok) { _showModalErr('ag-err', r.error||'Failed'); btn.disabled=false; btn.textContent='Add'; return; }\n634\t _closeModal(); _loadUsers(); _loadGroups();\n635\t });\n636\t };\n637\t}\n638\t\n639\t// User event delegation\n640\tdocument.getElementById('vv-au-panel-users').addEventListener('click', e => {\n641\t if (e.target.id === 'vv-au-user-add') { _userModal(null); return; }\n642\t\n643\t const editBtn = e.target.closest('[data-user-edit]');\n644\t if (editBtn) { _userModal(editBtn.dataset.userEdit); return; }\n645\t\n646\t const passBtn = e.target.closest('[data-user-pass]');\n647\t if (passBtn) { _passModal(passBtn.dataset.userPass); return; }\n648\t\n649\t const grpBtn = e.target.closest('[data-user-grp]');\n650\t if (grpBtn) { _addToGroupModal(grpBtn.dataset.userGrp); return; }\n651\t\n652\t const delBtn = e.target.closest('[data-user-del]');\n653\t if (delBtn) {\n654\t const uid = delBtn.dataset.userDel;\n655\t const user = _users.find(u => u.id === uid);\n656\t if (!confirm('Delete user \"' + (user?.displayName||uid) + '\"?')) return;\n657\t _post({ action:'lldap_delete_user', uid }, r => { if (r.ok) _loadUsers(); });\n658\t return;\n659\t }\n660\t\n661\t const rmBadge = e.target.closest('[data-rm-from-group]');\n662\t if (rmBadge) {\n663\t const uid = rmBadge.dataset.rmFromGroup;\n664\t const gid = parseInt(rmBadge.dataset.gid);\n665\t const grp = _groups.find(g => g.id === gid);\n666\t if (!confirm('Remove from group \"' + (grp?.displayName||gid) + '\"?')) return;\n667\t _post({ action:'lldap_remove_from_group', uid, gid }, r => { if (r.ok) { _loadUsers(); _loadGroups(); } });\n668\t }\n669\t});\n670\t\n671\t// ── Groups ────────────────────────────────────────────────────────────────────\n672\tfunction _loadGroups() {\n673\t const loading = document.getElementById('vv-au-groups-loading');\n674\t const list = document.getElementById('vv-au-groups-list');\n675\t const empty = document.getElementById('vv-au-groups-empty');\n676\t loading.style.display = 'block';\n677\t list.innerHTML = '';\n678\t empty.style.display = 'none';\n679\t\n680\t _get('lldap_groups', r => {\n681\t loading.style.display = 'none';\n682\t if (!r.ok) { loading.innerHTML = '<span style=\"color:#ef5350;padding:10px;display:block\">'+_esc(r.error)+'</span>'; loading.style.display='block'; return; }\n683\t _groups = r.groups || [];\n684\t if (!_groups.length) { empty.style.display = 'block'; return; }\n685\t list.innerHTML = _groups.map(g => {\n686\t const members = (g.users||[]).map(u =>\n687\t `<div class=\"vv-au-grp-member\">\n688\t <span>${_esc(u.displayName||u.id)}</span>\n689\t <span class=\"vv-au-grp-member-rm\" data-rm-user=\"${_esc(u.id)}\" data-gid=\"${g.id}\" title=\"Remove from group\">✕</span>\n690\t </div>`\n691\t ).join('');\n692\t return `<div class=\"vv-au-grp-row\" data-grp=\"${g.id}\">\n693\t <div class=\"vv-au-grp-acts\">\n694\t <button class=\"vv-au-icon-btn del\" data-group-del=\"${g.id}\" title=\"Delete group\">✕</button>\n695\t </div>\n696\t <div class=\"vv-au-grp-name\">${_esc(g.displayName)}</div>\n697\t <div class=\"vv-au-grp-cnt\">${(g.users||[]).length} member${(g.users||[]).length!==1?'s':''}</div>\n698\t </div>\n699\t <div class=\"vv-au-grp-members\" id=\"gm-${g.id}\">\n700\t ${members || '<div style=\"font-size:11px;color:#333;padding:2px 0\">No members</div>'}\n701\t </div>`;\n702\t }).join('');\n703\t });\n704\t}\n705\t\n706\t// Group event delegation\n707\tdocument.getElementById('vv-au-panel-users').addEventListener('click', e => {\n708\t if (e.target.id === 'vv-au-group-add') {\n709\t _modal(`<h3>Add Group</h3>\n710\t <div class=\"vv-au-field\">\n711\t <label class=\"vv-au-label\">Group Name</label>\n712\t <input class=\"vv-au-input\" id=\"gm-name\" placeholder=\"admins\" autofocus>\n713\t </div>\n714\t <div class=\"vv-au-err\" id=\"gm-err\"></div>\n715\t <div class=\"vv-au-modal-acts\">\n716\t <button class=\"vv-au-btn\" id=\"gm-cancel\">Cancel</button>\n717\t <button class=\"vv-au-btn prim\" id=\"gm-save\">Create</button>\n718\t </div>`);\n719\t document.getElementById('gm-cancel').onclick = _closeModal;\n720\t document.getElementById('gm-save').onclick = () => {\n721\t const name = (document.getElementById('gm-name').value||'').trim();\n722\t if (!name) { _showModalErr('gm-err','Name required'); return; }\n723\t const btn = document.getElementById('gm-save');\n724\t btn.disabled = true; btn.textContent = 'Creating…';\n725\t _post({ action:'lldap_create_group', name }, r => {\n726\t if (!r.ok) { _showModalErr('gm-err', r.error||'Failed'); btn.disabled=false; btn.textContent='Create'; return; }\n727\t _closeModal(); _loadGroups();\n728\t });\n729\t };\n730\t return;\n731\t }\n732\t\n733\t const grpRow = e.target.closest('[data-grp]');\n734\t if (grpRow && !e.target.closest('button') && !e.target.closest('.vv-au-icon-btn')) {\n735\t const panel = document.getElementById('gm-' + grpRow.dataset.grp);\n736\t if (panel) panel.classList.toggle('open');\n737\t return;\n738\t }\n739\t\n740\t const delGrp = e.target.closest('[data-group-del]');\n741\t if (delGrp) {\n742\t e.stopPropagation();\n743\t const id = parseInt(delGrp.dataset.groupDel);\n744\t const grp = _groups.find(g => g.id === id);\n745\t if (!confirm('Delete group \"' + (grp?.displayName||id) + '\"?')) return;\n746\t _post({ action:'lldap_delete_group', id }, r => { if (r.ok) _loadGroups(); });\n747\t return;\n748\t }\n749\t\n750\t const rmUser = e.target.closest('[data-rm-user]');\n751\t if (rmUser) {\n752\t e.stopPropagation();\n753\t const uid = rmUser.dataset.rmUser;\n754\t const gid = parseInt(rmUser.dataset.gid);\n755\t _post({ action:'lldap_remove_from_group', uid, gid }, r => { if (r.ok) { _loadUsers(); _loadGroups(); } });\n756\t }\n757\t});\n758\t\n759\t// ── Access Control ────────────────────────────────────────────────────────────\n760\tfunction _loadAcl() {\n761\t const loading = document.getElementById('vv-au-acl-loading');\n762\t const tbl = document.getElementById('vv-au-acl-tbl');\n763\t const empty = document.getElementById('vv-au-acl-empty');\n764\t loading.style.display = 'block';\n765\t tbl.style.display = 'none';\n766\t empty.style.display = 'none';\n767\t\n768\t _get('authelia_rules', r => {\n769\t loading.style.display = 'none';\n770\t if (!r.ok) { loading.innerHTML = '<span style=\"color:#ef5350;padding:10px;display:block\">'+_esc(r.error)+'</span>'; loading.style.display='block'; return; }\n771\t _rules = r.rules || [];\n772\t _defaultPolicy = r.default_policy || 'deny';\n773\t\n774\t const sel = document.getElementById('vv-au-ac-defpol');\n775\t if (sel) sel.value = _defaultPolicy;\n776\t\n777\t if (!_rules.length) { empty.style.display = 'block'; return; }\n778\t tbl.style.display = 'table';\n779\t _renderAcl();\n780\t });\n781\t}\n782\t\n783\tfunction _renderAcl() {\n784\t const count = document.getElementById('vv-au-acl-count');\n785\t count.textContent = _rules.length + ' rule' + (_rules.length !== 1 ? 's' : '');\n786\t\n787\t const body = document.getElementById('vv-au-acl-body');\n788\t body.innerHTML = _rules.map((rule, i) => {\n789\t const domains = _normDomain(rule.domain).join(', ');\n790\t const subjects = _normSubject(rule.subject).join(', ');\n791\t const upBtn = i === 0 ? '<span style=\"width:18px;display:inline-block\"></span>' :\n792\t `<button class=\"vv-au-icon-btn\" data-rule-up=\"${i}\" title=\"Move up\">↑</button>`;\n793\t const dnBtn = i === _rules.length-1 ? '<span style=\"width:18px;display:inline-block\"></span>' :\n794\t `<button class=\"vv-au-icon-btn\" data-rule-dn=\"${i}\" title=\"Move down\">↓</button>`;\n795\t return `<tr>\n796\t <td class=\"vv-au-rule-num\">${i+1}</td>\n797\t <td><div class=\"vv-au-domain\">${_esc(domains)}</div></td>\n798\t <td>${_policyBadge(rule.policy)}</td>\n799\t <td style=\"font-size:10px;color:#555\">${_esc(subjects||'—')}</td>\n800\t <td>\n801\t <div class=\"vv-au-rule-acts\">\n802\t ${upBtn}${dnBtn}\n803\t <button class=\"vv-au-icon-btn\" data-rule-edit=\"${i}\" title=\"Edit\">✎</button>\n804\t <button class=\"vv-au-icon-btn del\" data-rule-del=\"${i}\" title=\"Delete\">✕</button>\n805\t </div>\n806\t </td>\n807\t </tr>`;\n808\t }).join('');\n809\t}\n810\t\n811\tfunction _ruleModal(idx) {\n812\t const rule = idx !== null ? _rules[idx] : null;\n813\t const domains = _normDomain(rule?.domain).join(', ');\n814\t const subjects = _normSubject(rule?.subject).join(', ');\n815\t const nets = (rule?.networks||[]).join(', ');\n816\t const resources= (rule?.resources||[]).join(', ');\n817\t\n818\t const policyOpts = ['bypass','one_factor','two_factor','deny'].map(p =>\n819\t `<option value=\"${p}\"${(rule?.policy||'two_factor')===p?' selected':''}>${p}</option>`\n820\t ).join('');\n821\t\n822\t _modal(`<h3>${rule ? 'Edit Rule' : 'Add Rule'}</h3>\n823\t <div class=\"vv-au-field\">\n824\t <label class=\"vv-au-label\">Domain</label>\n825\t <input class=\"vv-au-input\" id=\"rm-domain\" value=\"${_esc(domains)}\" placeholder=\"*.example.com, example.com\">\n826\t <div class=\"vv-au-hint\">Comma-separated; wildcards supported</div>\n827\t </div>\n828\t <div class=\"vv-au-field\">\n829\t <label class=\"vv-au-label\">Policy</label>\n830\t <select class=\"vv-au-select\" id=\"rm-policy\">${policyOpts}</select>\n831\t </div>\n832\t <div class=\"vv-au-field\">\n833\t <label class=\"vv-au-label\">Subject <span style=\"color:#2a2a2a\">(optional)</span></label>\n834\t <input class=\"vv-au-input\" id=\"rm-subject\" value=\"${_esc(subjects)}\" placeholder=\"group:admins, user:john\">\n835\t <div class=\"vv-au-hint\">Comma-separated; prefix with group: or user:</div>\n836\t </div>\n837\t <div class=\"vv-au-adv-toggle\" id=\"rm-adv-toggle\">▶ Advanced (networks, resources)</div>\n838\t <div class=\"vv-au-adv-section\" id=\"rm-adv\">\n839\t <div class=\"vv-au-field\">\n840\t <label class=\"vv-au-label\">Networks <span style=\"color:#2a2a2a\">(optional)</span></label>\n841\t <input class=\"vv-au-input\" id=\"rm-networks\" value=\"${_esc(nets)}\" placeholder=\"192.168.1.0/24, 10.0.0.0/8\">\n842\t <div class=\"vv-au-hint\">Comma-separated CIDR ranges</div>\n843\t </div>\n844\t <div class=\"vv-au-field\">\n845\t <label class=\"vv-au-label\">Resources <span style=\"color:#2a2a2a\">(optional)</span></label>\n846\t <input class=\"vv-au-input\" id=\"rm-resources\" value=\"${_esc(resources)}\" placeholder=\"^/api, ^/admin\">\n847\t <div class=\"vv-au-hint\">Comma-separated regex patterns</div>\n848\t </div>\n849\t </div>\n850\t <div class=\"vv-au-err\" id=\"rm-err\"></div>\n851\t <div class=\"vv-au-modal-acts\">\n852\t <button class=\"vv-au-btn\" id=\"rm-cancel\">Cancel</button>\n853\t <button class=\"vv-au-btn prim\" id=\"rm-save\">${rule ? 'Save' : 'Add Rule'}</button>\n854\t </div>`);\n855\t\n856\t document.getElementById('rm-adv-toggle').addEventListener('click', () => {\n857\t const sec = document.getElementById('rm-adv');\n858\t const tog = document.getElementById('rm-adv-toggle');\n859\t const open = sec.classList.toggle('open');\n860\t tog.textContent = (open ? '▼' : '▶') + ' Advanced (networks, resources)';\n861\t });\n862\t\n863\t document.getElementById('rm-cancel').onclick = _closeModal;\n864\t document.getElementById('rm-save').onclick = () => {\n865\t const domains = document.getElementById('rm-domain').value.split(',').map(s=>s.trim()).filter(Boolean);\n866\t if (!domains.length) { _showModalErr('rm-err','Domain required'); return; }\n867\t\n868\t const subjects = document.getElementById('rm-subject').value.split(',').map(s=>s.trim()).filter(Boolean);\n869\t const networks = document.getElementById('rm-networks').value.split(',').map(s=>s.trim()).filter(Boolean);\n870\t const resources = document.getElementById('rm-resources').value.split(',').map(s=>s.trim()).filter(Boolean);\n871\t\n872\t const newRule = {\n873\t domain: domains.length === 1 ? domains[0] : domains,\n874\t policy: document.getElementById('rm-policy').value,\n875\t };\n876\t if (subjects.length) newRule.subject = subjects.length === 1 ? subjects[0] : subjects;\n877\t if (networks.length) newRule.networks = networks;\n878\t if (resources.length) newRule.resources = resources;\n879\t\n880\t if (idx !== null) _rules[idx] = newRule;\n881\t else _rules.push(newRule);\n882\t\n883\t _closeModal();\n884\t _renderAcl();\n885\t };\n886\t}\n887\t\n888\t// ACL event delegation\n889\tdocument.getElementById('vv-au-panel-acl').addEventListener('click', e => {\n890\t if (e.target.id === 'vv-au-rule-add') { _ruleModal(null); return; }\n891\t\n892\t if (e.target.id === 'vv-au-ac-save') {\n893\t const dp = document.getElementById('vv-au-ac-defpol').value;\n894\t const btn = document.getElementById('vv-au-ac-save');\n895\t btn.disabled = true; btn.textContent = 'Saving…';\n896\t _post({ action:'authelia_save', rules: JSON.stringify(_rules), default_policy: dp }, r => {\n897\t btn.disabled = false; btn.textContent = 'Save & Restart Authelia';\n898\t if (!r.ok) { alert('Save failed: ' + (r.error||'unknown error')); return; }\n899\t // brief visual confirmation\n900\t btn.textContent = 'Saved ✓';\n901\t setTimeout(() => { btn.textContent = 'Save & Restart Authelia'; }, 2000);\n902\t });\n903\t return;\n904\t }\n905\t\n906\t const editBtn = e.target.closest('[data-rule-edit]');\n907\t if (editBtn) { _ruleModal(parseInt(editBtn.dataset.ruleEdit)); return; }\n908\t\n909\t const delBtn = e.target.closest('[data-rule-del]');\n910\t if (delBtn) {\n911\t const i = parseInt(delBtn.dataset.ruleDel);\n912\t if (!confirm('Delete this rule?')) return;\n913\t _rules.splice(i, 1);\n914\t _renderAcl();\n915\t return;\n916\t }\n917\t\n918\t const upBtn = e.target.closest('[data-rule-up]');\n919\t if (upBtn) {\n920\t const i = parseInt(upBtn.dataset.ruleUp);\n921\t if (i > 0) { [_rules[i-1], _rules[i]] = [_rules[i], _rules[i-1]]; _renderAcl(); }\n922\t return;\n923\t }\n924\t\n925\t const dnBtn = e.target.closest('[data-rule-dn]');\n926\t if (dnBtn) {\n927\t const i = parseInt(dnBtn.dataset.ruleDn);\n928\t if (i < _rules.length-1) { [_rules[i], _rules[i+1]] = [_rules[i+1], _rules[i]]; _renderAcl(); }\n929\t }\n930\t});\n931\t\n932\t// ── Close modal on overlay click ──────────────────────────────────────────────\n933\tdocument.getElementById('vv-au-overlay').addEventListener('click', e => {\n934\t if (e.target === document.getElementById('vv-au-overlay')) _closeModal();\n935\t});\n936\t\n937\t// ── Certs ─────────────────────────────────────────────────────────────────────\n938\tfunction _certBadgeCls(s) {\n939\t return ({OK:'ssl', WARN:'one_factor', CRIT:'deny', FAIL:'deny'})[s] || 'nossl';\n940\t}\n941\tfunction _certBadgeTxt(s) {\n942\t return ({OK:'healthy', WARN:'warning', CRIT:'critical', FAIL:'failed', UNKN:'not checked'})[s] || s;\n943\t}\n944\tfunction _certDayColor(days, warn, crit) {\n945\t if (days == null) return '#3a3a3a';\n946\t return days <= crit ? '#ef5350' : days <= warn ? '#ffb74d' : '#4caf50';\n947\t}\n948\tfunction _certRel(ts) {\n949\t if (!ts) return '—';\n950\t const d = Math.floor(Date.now()/1000) - ts;\n951\t if (d < 60) return 'just now';\n952\t if (d < 3600) return Math.floor(d/60) + 'm ago';\n953\t if (d < 86400) return Math.floor(d/3600) + 'h ago';\n954\t return Math.floor(d/86400) + 'd ago';\n955\t}\n956\t\n957\tfunction _renderCerts(data) {\n958\t const grid = document.getElementById('vv-au-cert-grid');\n959\t const ts = document.getElementById('vv-au-cert-ts');\n960\t const cfg = document.getElementById('vv-au-cert-cfg');\n961\t const warn = data.warn_days || 30, crit = data.crit_days || 7;\n962\t\n963\t ts.textContent = data.checked_at ? 'Last checked: ' + _certRel(data.checked_at) : 'Not yet checked';\n964\t cfg.textContent = `Warn: ${warn}d · Crit: ${crit}d`;\n965\t\n966\t const domains = data.domains || [];\n967\t if (!domains.length) {\n968\t grid.innerHTML = '<div class=\"vv-au-empty\">No domains configured — add HOST*_CERT_MONITOR_DOMAINS to host.conf</div>';\n969\t return;\n970\t }\n971\t\n972\t grid.innerHTML = domains.map(d => {\n973\t const s = d.status || 'UNKN';\n974\t const days = d.days;\n975\t const col = _certDayColor(days, warn, crit);\n976\t const barPct = days != null ? Math.min(Math.round(days/90*100), 100) : 0;\n977\t const expStr = d.expires ? 'Expires ' + d.expires : (s === 'UNKN' ? 'Not yet checked' : '');\n978\t return `<div class=\"vv-au-card\" style=\"padding:12px 14px;\">\n979\t <div class=\"vv-au-domain\">${_esc(d.domain)}</div>\n980\t <div class=\"vv-au-cert-days\" style=\"color:${col}\">${days != null ? days : '—'}</div>\n981\t <div style=\"font-size:9px;color:#444;margin-bottom:6px;\">${days != null ? 'days remaining' : ''}</div>\n982\t <span class=\"vv-au-badge ${_certBadgeCls(s)}\">${_certBadgeTxt(s)}</span>\n983\t <div style=\"font-size:9px;color:#3a3a3a;margin-top:5px;\">${_esc(expStr)}</div>\n984\t ${days != null ? `<div class=\"vv-au-cert-bar\"><div class=\"vv-au-cert-fill\" style=\"width:${barPct}%;background:${col};\"></div></div>` : ''}\n985\t </div>`;\n986\t }).join('');\n987\t}\n988\t\n989\tfunction _loadCerts() {\n990\t const grid = document.getElementById('vv-au-cert-grid');\n991\t grid.innerHTML = '<div class=\"vv-au-loading\">Loading…</div>';\n992\t fetch(CERT_API)\n993\t .then(r => r.json())\n994\t .then(d => { if (d.ok) _renderCerts(d); })\n995\t .catch(() => { grid.innerHTML = '<div class=\"vv-au-empty\" style=\"color:#ef5350\">Failed to load cert data</div>'; });\n996\t}\n997\t\n998\tdocument.getElementById('vv-au-cert-run').addEventListener('click', function() {\n999\t const btn = this;\n1000\t const log = document.getElementById('vv-au-cert-log');\n1001\t btn.disabled = true; btn.textContent = 'Checking…';\n1002\t log.style.display = 'none'; log.textContent = '';\n1003\t const fd = new FormData(); fd.append('action', 'run');\n1004\t fetch(CERT_API, { method: 'POST', body: fd })\n1005\t .then(r => r.json())\n1006\t .then(d => {\n1007\t btn.disabled = false; btn.textContent = 'Run now';\n1008\t if (d.data) _renderCerts(d.data);\n1009\t if (d.output && d.output.length) {\n1010\t log.style.display = 'block';\n1011\t log.textContent = d.output.join('\\n').replace(/\\x1b\\[[0-9;]*m/g, '');\n1012\t }\n1013\t })\n1014\t .catch(() => { btn.disabled = false; btn.textContent = 'Run now'; });\n1015\t});\n1016\t\n1017\t// ── Boot ──────────────────────────────────────────────────────────────────────\n1018\t_loadProxies();\n1019\t\n1020\t})();\n1021\t</script>\n1022\t"}]},"uuid":"ae656e91-12d3-4722-a02a-dc583b18400f","timestamp":"2026-06-06T14:29:37.472Z","sourceToolAssistantUUID":"644e010b-9a63-4a54-969b-5f9ae5e985fa","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"0fa09355-e005-42b5-8a12-4eb9230ccc27","isSidechain":true,"promptId":"3c1ac879-b13a-4ebc-9aeb-ea73615e9af3","agentId":"accc3589951fac88e","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01QtfhM3zG9xMJbZPcyXEzbQ","type":"tool_result","content":"<system-reminder>[Truncated: PARTIAL view — showing lines 1-1074 of 2212 total (43758 tokens, cap 25000). Call Read with offset=1075 limit=1074 for the next page, or Grep to find a specific section. Do NOT answer from this page alone if the answer may be further in the file.]</system-reminder>\n\n1\t<?php require_once dirname(__DIR__) . '/include/monitor.php'; ?>\n2\t<style>\n3\t@keyframes vvRsPulse {\n4\t 0%,100% { opacity:.5; transform:scaleX(.9); }\n5\t 50% { opacity:1; transform:scaleX(1.05); }\n6\t}\n7\t</style>\n8\t\n9\t<div id=\"vv-api-banner\" style=\"display:none;border-radius:4px;padding:5px 10px;margin-bottom:8px;font-size:11px;\"></div>\n10\t\n11\t<div id=\"vv-monitor\" style=\"display:grid;grid-template-columns:repeat(8,1fr);gap:12px;width:100%;box-sizing:border-box;\">\n12\t\n13\t <!-- Row 1: System | Power | CPU | Memory | Network -->\n14\t <div class=\"vv-card\" id=\"vv-system\" style=\"grid-column:span 1;\">\n15\t <div id=\"vv-system-body\">Loading...</div>\n16\t </div>\n17\t\n18\t <div class=\"vv-card\" id=\"vv-ups-card\" style=\"grid-column:span 1;\">\n19\t <h3>\n20\t <span style=\"display:flex;align-items:center;gap:5px;\">\n21\t <svg width=\"9\" height=\"14\" viewBox=\"0 0 11 18\" fill=\"none\" style=\"opacity:0.4;flex-shrink:0;\">\n22\t <polygon points=\"9,0 2,10 6,10 3,18 11,6 6,6 8,0\" fill=\"#ff9800\"/>\n23\t </svg>Power\n24\t </span>\n25\t <a href=\"/Settings/UPS\" target=\"_blank\" class=\"vv-card-cog\" title=\"UPS Settings\">⚙</a>\n26\t </h3>\n27\t <div id=\"vv-ups-body\">Loading...</div>\n28\t </div>\n29\t\n30\t <div class=\"vv-card\" id=\"vv-cpu\" style=\"grid-column:span 2;\">\n31\t <h3>\n32\t <span style=\"display:flex;align-items:center;gap:5px;\">\n33\t <span class=\"vv-ico\"><svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" stroke=\"#aaa\" stroke-width=\"1.1\" stroke-linecap=\"round\"><rect x=\"3\" y=\"3\" width=\"6\" height=\"6\" rx=\"0.8\"/><line x1=\"4.5\" y1=\"3\" x2=\"4.5\" y2=\"1.2\"/><line x1=\"7.5\" y1=\"3\" x2=\"7.5\" y2=\"1.2\"/><line x1=\"4.5\" y1=\"9\" x2=\"4.5\" y2=\"10.8\"/><line x1=\"7.5\" y1=\"9\" x2=\"7.5\" y2=\"10.8\"/><line x1=\"3\" y1=\"4.5\" x2=\"1.2\" y2=\"4.5\"/><line x1=\"3\" y1=\"7.5\" x2=\"1.2\" y2=\"7.5\"/><line x1=\"9\" y1=\"4.5\" x2=\"10.8\" y2=\"4.5\"/><line x1=\"9\" y1=\"7.5\" x2=\"10.8\" y2=\"7.5\"/></svg></span>\n34\t <span id=\"vv-cpu-title\">CPU</span>\n35\t </span>\n36\t <a href=\"/Settings/CPUset\" target=\"_blank\" class=\"vv-card-cog\" title=\"CPU Settings\">⚙</a>\n37\t </h3>\n38\t <div id=\"vv-cpu-body\">Loading...</div>\n39\t </div>\n40\t\n41\t <div class=\"vv-card\" id=\"vv-memory\" style=\"grid-column:span 2;\">\n42\t <h3>\n43\t <span style=\"display:flex;align-items:center;gap:5px;\">\n44\t <span class=\"vv-ico\"><svg width=\"14\" height=\"8\" viewBox=\"0 0 14 8\" fill=\"none\" stroke=\"#aaa\" stroke-width=\"1.1\" stroke-linecap=\"round\"><rect x=\"0.6\" y=\"2.5\" width=\"12.8\" height=\"3\" rx=\"0.5\"/><line x1=\"3\" y1=\"2.5\" x2=\"3\" y2=\"0.8\"/><line x1=\"5.5\" y1=\"2.5\" x2=\"5.5\" y2=\"0.8\"/><line x1=\"8\" y1=\"2.5\" x2=\"8\" y2=\"0.8\"/><line x1=\"10.5\" y1=\"2.5\" x2=\"10.5\" y2=\"0.8\"/><line x1=\"3\" y1=\"5.5\" x2=\"3\" y2=\"7.2\"/><line x1=\"5.5\" y1=\"5.5\" x2=\"5.5\" y2=\"7.2\"/><line x1=\"8\" y1=\"5.5\" x2=\"8\" y2=\"7.2\"/><line x1=\"10.5\" y1=\"5.5\" x2=\"10.5\" y2=\"7.2\"/></svg></span>\n45\t Memory\n46\t </span>\n47\t <a href=\"/\" target=\"_blank\" class=\"vv-card-cog\" title=\"Dashboard\">⚙</a>\n48\t </h3>\n49\t <div id=\"vv-memory-body\">Loading...</div>\n50\t </div>\n51\t\n52\t <div class=\"vv-card\" id=\"vv-network\" style=\"grid-column:span 2;\">\n53\t <h3>\n54\t <span style=\"display:flex;align-items:center;gap:5px;\">\n55\t <span class=\"vv-ico\"><svg width=\"13\" height=\"12\" viewBox=\"0 0 13 12\" fill=\"none\" stroke=\"#aaa\" stroke-width=\"1.1\" stroke-linecap=\"round\"><rect x=\"4.5\" y=\"0.5\" width=\"4\" height=\"3\" rx=\"0.7\"/><rect x=\"0.5\" y=\"8\" width=\"4\" height=\"3\" rx=\"0.7\"/><rect x=\"8.5\" y=\"8\" width=\"4\" height=\"3\" rx=\"0.7\"/><line x1=\"6.5\" y1=\"3.5\" x2=\"6.5\" y2=\"6\"/><line x1=\"6.5\" y1=\"6\" x2=\"2.5\" y2=\"6\"/><line x1=\"2.5\" y1=\"6\" x2=\"2.5\" y2=\"8\"/><line x1=\"6.5\" y1=\"6\" x2=\"10.5\" y2=\"6\"/><line x1=\"10.5\" y1=\"6\" x2=\"10.5\" y2=\"8\"/></svg></span>\n56\t Network\n57\t </span>\n58\t <a href=\"/Settings/NetworkSettings\" target=\"_blank\" class=\"vv-card-cog\" title=\"Network Settings\">⚙</a>\n59\t </h3>\n60\t <div id=\"vv-network-body\">Loading...</div>\n61\t </div>\n62\t\n63\t <!-- Row 2: Scripts | Fallback | Partner | Containers & VMs -->\n64\t <div class=\"vv-card\" id=\"vv-scripts-card\" style=\"grid-column:span 1;\">\n65\t <h3>\n66\t <span style=\"display:flex;align-items:center;gap:5px;\">\n67\t <span class=\"vv-ico\"><svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" stroke=\"#aaa\" stroke-width=\"1.1\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"1\" y=\"1\" width=\"10\" height=\"10\" rx=\"1.5\"/><line x1=\"1\" y1=\"4.5\" x2=\"11\" y2=\"4.5\"/><polyline points=\"3.5,7 5,8 3.5,9\"/><line x1=\"6\" y1=\"9\" x2=\"8.5\" y2=\"9\"/></svg></span>\n68\t Scripts\n69\t </span>\n70\t <a href=\"/Apps/plugin_userscripts\" target=\"_blank\" class=\"vv-card-cog\" title=\"User Scripts\">⚙</a>\n71\t </h3>\n72\t <div id=\"vv-scripts-body\">Loading...</div>\n73\t </div>\n74\t\n75\t <div class=\"vv-card\" id=\"vv-fallback\" style=\"grid-column:span 1;\">\n76\t <h3>\n77\t <span style=\"display:flex;align-items:center;gap:5px;\">\n78\t <span class=\"vv-ico\"><svg width=\"10\" height=\"12\" viewBox=\"0 0 10 12\" fill=\"none\" stroke=\"#aaa\" stroke-width=\"1.1\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 0.8L9.2 2.8V6.2C9.2 9 5 11.2 5 11.2C5 11.2 0.8 9 0.8 6.2V2.8Z\"/></svg></span>\n79\t Fallback\n80\t </span>\n81\t </h3>\n82\t <div id=\"vv-fallback-body\">Loading...</div>\n83\t </div>\n84\t\n85\t <div class=\"vv-card\" id=\"vv-partner\" style=\"grid-column:span 2;\">\n86\t <h3>\n87\t <span style=\"display:flex;align-items:center;gap:5px;\">\n88\t <span class=\"vv-ico\"><svg width=\"14\" height=\"10\" viewBox=\"0 0 14 10\" fill=\"none\" stroke=\"#aaa\" stroke-width=\"1.1\" stroke-linecap=\"round\"><rect x=\"0.5\" y=\"1.5\" width=\"4\" height=\"7\" rx=\"0.8\"/><rect x=\"9.5\" y=\"1.5\" width=\"4\" height=\"7\" rx=\"0.8\"/><line x1=\"4.5\" y1=\"5\" x2=\"9.5\" y2=\"5\"/><circle cx=\"7\" cy=\"5\" r=\"1\" fill=\"#888\" stroke=\"none\"/></svg></span>\n89\t Partner\n90\t </span>\n91\t <img src=\"/plugins/varaverk/icons/varaverk.png\" style=\"width:16px;height:16px;object-fit:contain;opacity:0.2;flex-shrink:0;\" title=\"Varaverk\">\n92\t </h3>\n93\t <div id=\"vv-partner-body\">Loading...</div>\n94\t </div>\n95\t\n96\t <div class=\"vv-card\" id=\"vv-docker-folders\" style=\"grid-column:span 4;\">\n97\t <h3>\n98\t <span style=\"display:flex;align-items:center;gap:5px;\">\n99\t <span class=\"vv-ico\"><svg width=\"14\" height=\"10\" viewBox=\"0 0 14 10\" fill=\"none\" stroke=\"#aaa\" stroke-width=\"1.1\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"1\" y=\"0.5\" width=\"12\" height=\"2.5\" rx=\"0.5\"/><rect x=\"1\" y=\"3.8\" width=\"12\" height=\"2.5\" rx=\"0.5\"/><rect x=\"1\" y=\"7\" width=\"12\" height=\"2.5\" rx=\"0.5\"/></svg></span>\n100\t Containers & VMs <span id=\"vv-docker-count\" style=\"font-size:10px;color:#4a4a4a;font-weight:400;text-transform:none;letter-spacing:0;margin-left:2px;\"></span>\n101\t </span>\n102\t <a href=\"/Docker\" target=\"_blank\" class=\"vv-card-cog\" title=\"Docker\">⚙</a>\n103\t </h3>\n104\t <div id=\"vv-docker-folders-body\">Loading...</div>\n105\t </div>\n106\t\n107\t <!-- Row 3: Rsync | GPU | Transcode | Streams -->\n108\t <div class=\"vv-card\" id=\"vv-rsync-card\" style=\"grid-column:span 1;\">\n109\t <h3>\n110\t <span style=\"display:flex;align-items:center;gap:5px;\">\n111\t <span class=\"vv-ico\"><svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" stroke=\"#aaa\" stroke-width=\"1.1\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"2,4 10,4 8,2\"/><polyline points=\"10,8 2,8 4,10\"/></svg></span>\n112\t Rsync\n113\t </span>\n114\t </h3>\n115\t <div id=\"vv-rsync-body\">Loading...</div>\n116\t </div>\n117\t\n118\t <div class=\"vv-card\" id=\"vv-gpu-card\" style=\"grid-column:span 1;\">\n119\t <h3>\n120\t <span style=\"display:flex;align-items:center;gap:5px;\">\n121\t <span class=\"vv-ico\"><svg width=\"15\" height=\"10\" viewBox=\"0 0 16 10\" fill=\"none\" stroke=\"#aaa\" stroke-width=\"1.1\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"0.8\" y=\"0.8\" width=\"14.4\" height=\"7\" rx=\"1.2\"/><rect x=\"2.5\" y=\"2.5\" width=\"3.5\" height=\"3.5\" rx=\"0.5\"/><line x1=\"8\" y1=\"3\" x2=\"13\" y2=\"3\"/><line x1=\"8\" y1=\"5\" x2=\"11\" y2=\"5\"/><rect x=\"3\" y=\"7.8\" width=\"2\" height=\"1.8\" rx=\"0.3\" stroke=\"none\" fill=\"#666\"/><rect x=\"6.5\" y=\"7.8\" width=\"2\" height=\"1.8\" rx=\"0.3\" stroke=\"none\" fill=\"#666\"/><rect x=\"10\" y=\"7.8\" width=\"2\" height=\"1.8\" rx=\"0.3\" stroke=\"none\" fill=\"#666\"/></svg></span>\n122\t GPU\n123\t </span>\n124\t <a href=\"/\" target=\"_blank\" class=\"vv-card-cog\" title=\"Dashboard\">⚙</a>\n125\t </h3>\n126\t <div id=\"vv-gpu-body\">Loading...</div>\n127\t </div>\n128\t\n129\t <div class=\"vv-card\" id=\"vv-transcode\" style=\"grid-column:span 2;\">\n130\t <h3>\n131\t <span style=\"display:flex;align-items:center;gap:5px;\">\n132\t <span class=\"vv-ico\"><svg width=\"14\" height=\"12\" viewBox=\"0 0 14 12\" fill=\"none\" stroke=\"#aaa\" stroke-width=\"1.1\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"0.5\" y=\"1\" width=\"4\" height=\"10\" rx=\"0.8\"/><line x1=\"0.5\" y1=\"3.5\" x2=\"2.3\" y2=\"3.5\"/><line x1=\"0.5\" y1=\"8.5\" x2=\"2.3\" y2=\"8.5\"/><line x1=\"2.7\" y1=\"3.5\" x2=\"4.5\" y2=\"3.5\"/><line x1=\"2.7\" y1=\"8.5\" x2=\"4.5\" y2=\"8.5\"/><line x1=\"6\" y1=\"6\" x2=\"9.5\" y2=\"6\"/><polyline points=\"8.2,4.3 10.2,6 8.2,7.7\"/><rect x=\"11\" y=\"1\" width=\"2.5\" height=\"10\" rx=\"0.5\"/></svg></span>\n133\t Transcode\n134\t </span>\n135\t </h3>\n136\t <div id=\"vv-transcode-body\">Loading...</div>\n137\t </div>\n138\t\n139\t <div class=\"vv-card\" id=\"vv-streams\" style=\"grid-column:span 4;\">\n140\t <h3>\n141\t <span style=\"display:flex;align-items:center;gap:5px;\">\n142\t <span class=\"vv-ico\"><svg width=\"11\" height=\"12\" viewBox=\"0 0 11 12\" fill=\"none\" stroke=\"#aaa\" stroke-width=\"1.1\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"1.5,1 1.5,11 10,6\"/></svg></span>\n143\t Streams\n144\t </span>\n145\t </h3>\n146\t <div id=\"vv-streams-body\">Loading...</div>\n147\t </div>\n148\t\n149\t <!-- Row 4: Watchdog | Parity | Pools | Array -->\n150\t <div class=\"vv-card\" id=\"vv-watchdog-card\" style=\"grid-column:span 1;\">\n151\t <h3>\n152\t <span style=\"display:flex;align-items:center;gap:5px;\">\n153\t <span class=\"vv-ico\"><svg width=\"14\" height=\"9\" viewBox=\"0 0 14 8\" fill=\"none\" stroke=\"#aaa\" stroke-width=\"1.1\" stroke-linecap=\"round\"><path d=\"M1 4C3.2 1 5 0.5 7 0.5C9 0.5 10.8 1 13 4C10.8 7 9 7.5 7 7.5C5 7.5 3.2 7 1 4Z\"/><circle cx=\"7\" cy=\"4\" r=\"1.5\"/></svg></span>\n154\t Watchdog\n155\t </span>\n156\t </h3>\n157\t <div id=\"vv-watchdog-body\">Loading...</div>\n158\t </div>\n159\t\n160\t <div class=\"vv-card\" id=\"vv-parity-card\" style=\"grid-column:span 1;\">\n161\t <h3>\n162\t <span style=\"display:flex;align-items:center;gap:5px;\">\n163\t <span class=\"vv-ico\"><svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" stroke=\"#aaa\" stroke-width=\"1.1\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"6\" cy=\"6\" r=\"5\"/><polyline points=\"3.5,6 5.5,8 8.5,4\"/></svg></span>\n164\t Parity\n165\t </span>\n166\t <a href=\"/Main\" target=\"_blank\" class=\"vv-card-cog\" title=\"Array Management\">⚙</a>\n167\t </h3>\n168\t <div id=\"vv-parity-body\">Loading...</div>\n169\t </div>\n170\t\n171\t <div class=\"vv-card\" id=\"vv-storage-card\" style=\"grid-column:3/span 2;\">\n172\t <h3>\n173\t <span style=\"display:flex;align-items:center;gap:5px;\">\n174\t <span class=\"vv-ico\"><svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" stroke=\"#aaa\" stroke-width=\"1.1\" stroke-linecap=\"round\"><ellipse cx=\"6\" cy=\"3\" rx=\"4.5\" ry=\"1.5\"/><line x1=\"1.5\" y1=\"3\" x2=\"1.5\" y2=\"9\"/><line x1=\"10.5\" y1=\"3\" x2=\"10.5\" y2=\"9\"/><ellipse cx=\"6\" cy=\"9\" rx=\"4.5\" ry=\"1.5\"/></svg></span>\n175\t <span id=\"vv-pools-title\">Pools</span>\n176\t </span>\n177\t <span id=\"vv-pools-io-total\" style=\"font-size:9px;font-weight:400;color:#333;text-transform:none;letter-spacing:0;\"></span>\n178\t <a href=\"/Main\" target=\"_blank\" class=\"vv-card-cog\" title=\"Array Management\">⚙</a>\n179\t </h3>\n180\t <div id=\"vv-storage-body\">Loading...</div>\n181\t </div>\n182\t\n183\t <div class=\"vv-card\" id=\"vv-array-card\" style=\"grid-column:5/span 4;\">\n184\t <h3>\n185\t <span style=\"display:flex;align-items:center;gap:5px;\">\n186\t <span class=\"vv-ico\"><svg width=\"14\" height=\"12\" viewBox=\"0 0 14 12\" fill=\"none\" stroke=\"#aaa\" stroke-width=\"1.1\" stroke-linecap=\"round\"><rect x=\"0.7\" y=\"0.7\" width=\"12.6\" height=\"3\" rx=\"0.8\"/><rect x=\"0.7\" y=\"4.5\" width=\"12.6\" height=\"3\" rx=\"0.8\"/><rect x=\"0.7\" y=\"8.3\" width=\"12.6\" height=\"3\" rx=\"0.8\"/><circle cx=\"11.5\" cy=\"2.2\" r=\"0.8\" fill=\"#888\" stroke=\"none\"/><circle cx=\"11.5\" cy=\"6\" r=\"0.8\" fill=\"#888\" stroke=\"none\"/><circle cx=\"11.5\" cy=\"9.8\" r=\"0.8\" fill=\"#888\" stroke=\"none\"/></svg></span>\n187\t <span id=\"vv-array-title\">Array</span>\n188\t </span>\n189\t <span id=\"vv-array-io-total\" style=\"font-size:9px;font-weight:400;color:#333;text-transform:none;letter-spacing:0;\"></span>\n190\t <a href=\"/Main\" target=\"_blank\" class=\"vv-card-cog\" title=\"Array Management\">⚙</a>\n191\t </h3>\n192\t <div id=\"vv-array-body\">Loading...</div>\n193\t </div>\n194\t\n195\t\n196\t</div>\n197\t\n198\t<script>\n199\t// ── Shared helpers ────────────────────────────────────────────────────────────\n200\t\n201\tfunction vvMeter(label, pct, text) {\n202\t const color = `hsl(${Math.round(120 * (1 - pct / 100))},70%,45%)`;\n203\t return `<div style=\"margin-bottom:7px;\">\n204\t <div style=\"display:flex;justify-content:space-between;font-size:11px;color:#666;margin-bottom:3px;\">\n205\t <span>${label}</span><span style=\"color:#999;\">${text}</span>\n206\t </div>\n207\t <div style=\"background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;\">\n208\t <div style=\"width:${pct}%;height:100%;background:${color};border-radius:3px;transition:width 0.4s;\"></div>\n209\t </div>\n210\t </div>`;\n211\t}\n212\t\n213\t// ── Rolling history ───────────────────────────────────────────────────────────\n214\t\n215\tconst VV_HIST_MAX = 24; // 24 × 5 s = 120 s\n216\tlet vvCpuHistory = [];\n217\tlet vvLastSessions = [];\n218\tlet vvLastStreamPollAt = 0;\n219\tlet vvLastStreamNames = [];\n220\tlet vvStreamServerCount = 0;\n221\tlet vvScriptsFilter = null;\n222\tlet vvLastScripts = {};\n223\tlet vvThresholds = {util_warn:70,util_crit:90,hdd_warn:45,hdd_crit:55,ssd_warn:60,ssd_crit:70};\n224\t\n225\tlet vvNetRxHistory = [];\n226\tlet vvNetTxHistory = [];\n227\tlet vvPoolsOpen = {};\n228\tlet vvPoolGroupOpen = {};\n229\tlet vvDiskIo = {}; // {device: [readMBs, writeMBs]}\n230\tlet vvLastStorageDisks = [];\n231\t\n232\t// ── Canvas chart ──────────────────────────────────────────────────────────────\n233\t\n234\tfunction vvDrawChart(canvas, data, lineColor, fillColor, grid = false) {\n235\t if (!canvas) return;\n236\t const W = canvas.offsetWidth || 200;\n237\t const H = canvas.offsetHeight || 36;\n238\t if (canvas.width !== W) canvas.width = W;\n239\t if (canvas.height !== H) canvas.height = H;\n240\t const ctx = canvas.getContext('2d');\n241\t ctx.clearRect(0, 0, W, H);\n242\t\n243\t // grid lines\n244\t if (grid) {\n245\t ctx.save();\n246\t ctx.setLineDash([2, 3]);\n247\t ctx.lineWidth = 0.5;\n248\t [25, 50, 75].forEach(pct => {\n249\t const y = H - (pct / 100) * (H - 2) - 1;\n250\t ctx.strokeStyle = '#2e2e2e';\n251\t ctx.beginPath();\n252\t ctx.moveTo(0, y);\n253\t ctx.lineTo(W, y);\n254\t ctx.stroke();\n255\t ctx.fillStyle = '#3a3a3a';\n256\t ctx.font = '7px monospace';\n257\t ctx.textAlign = 'left';\n258\t ctx.fillText(pct + '%', 2, y - 2);\n259\t });\n260\t ctx.restore();\n261\t }\n262\t\n263\t if (data.length < 2) return;\n264\t\n265\t const step = W / (VV_HIST_MAX - 1);\n266\t const pts = data.map((v, i) => [i * step, H - (v / 100) * (H - 2) - 1]);\n267\t\n268\t // fill\n269\t ctx.beginPath();\n270\t ctx.moveTo(0, H);\n271\t pts.forEach(([x, y]) => ctx.lineTo(x, y));\n272\t ctx.lineTo((data.length - 1) * step, H);\n273\t ctx.closePath();\n274\t ctx.fillStyle = fillColor;\n275\t ctx.fill();\n276\t\n277\t // line\n278\t ctx.beginPath();\n279\t pts.forEach(([x, y], i) => i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y));\n280\t ctx.strokeStyle = lineColor;\n281\t ctx.lineWidth = 1.5;\n282\t ctx.stroke();\n283\t}\n284\t\n285\t// ── Network helpers ───────────────────────────────────────────────────────────\n286\t\n287\tfunction vvFmtBps(bps) {\n288\t if (bps >= 1e9) return (bps / 1e9).toFixed(2) + ' Gb/s';\n289\t if (bps >= 1e6) return (bps / 1e6).toFixed(1) + ' Mb/s';\n290\t if (bps >= 1e3) return (bps / 1e3).toFixed(0) + ' Kb/s';\n291\t return bps + ' B/s';\n292\t}\n293\t\n294\tfunction vvDrawNetChart(canvas, rxData, txData, maxBps) {\n295\t if (!canvas) return;\n296\t const W = canvas.offsetWidth || 200;\n297\t const H = canvas.offsetHeight || 52;\n298\t if (canvas.width !== W) canvas.width = W;\n299\t if (canvas.height !== H) canvas.height = H;\n300\t const ctx = canvas.getContext('2d');\n301\t ctx.clearRect(0, 0, W, H);\n302\t\n303\t const scale = maxBps > 0 ? maxBps : 1;\n304\t const toY = v => H - (v / scale) * (H - 2) - 1;\n305\t const step = W / (VV_HIST_MAX - 1);\n306\t\n307\t // grid — labels show actual bandwidth at each line\n308\t ctx.save();\n309\t ctx.setLineDash([2, 3]);\n310\t ctx.lineWidth = 0.5;\n311\t [0.25, 0.5, 0.75].forEach(frac => {\n312\t const y = H - frac * (H - 2) - 1;\n313\t ctx.strokeStyle = '#2e2e2e';\n314\t ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();\n315\t ctx.fillStyle = '#3a3a3a'; ctx.font = '7px monospace'; ctx.textAlign = 'left';\n316\t ctx.fillText(vvFmtBps(maxBps * frac), 2, y - 2);\n317\t });\n318\t ctx.restore();\n319\t\n320\t const drawLine = (data, lineColor, fillColor) => {\n321\t if (data.length < 2) return;\n322\t const pts = data.map((v, i) => [i * step, toY(v)]);\n323\t ctx.beginPath(); ctx.moveTo(0, H);\n324\t pts.forEach(([x, y]) => ctx.lineTo(x, y));\n325\t ctx.lineTo((data.length - 1) * step, H);\n326\t ctx.closePath(); ctx.fillStyle = fillColor; ctx.fill();\n327\t ctx.beginPath();\n328\t pts.forEach(([x, y], i) => i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y));\n329\t ctx.strokeStyle = lineColor; ctx.lineWidth = 1.5; ctx.stroke();\n330\t };\n331\t\n332\t drawLine(txData, '#ff9800', 'rgba(255,152,0,0.10)');\n333\t drawLine(rxData, '#4caf50', 'rgba(76,175,80,0.12)');\n334\t}\n335\t\n336\t// ── CPU helpers ───────────────────────────────────────────────────────────────\n337\t\n338\tfunction vvCoreColor(freqMhz, maxMhz, minMhz) {\n339\t if (!freqMhz || !maxMhz || maxMhz === minMhz) return '#4caf50';\n340\t const t = Math.max(0, Math.min(1, (freqMhz - minMhz) / (maxMhz - minMhz)));\n341\t // blue(220°) → green(120°) → red(0°) as t goes 0→1\n342\t const hue = Math.round((1 - t) * 220);\n343\t return `hsl(${hue},70%,45%)`;\n344\t}\n345\t\n346\tfunction vvRenderCpu(cpu) {\n347\t const overall = cpu.overall ?? 0;\n348\t const cores = cpu.cores ?? [];\n349\t\n350\t // Overall usage bar\n351\t let html = vvMeter('Overall', overall, overall + '%');\n352\t\n353\t // Per-core vertical bars\n354\t if (cores.length) {\n355\t html += `<div class=\"vv-cpu-cores\" style=\"display:flex;align-items:flex-end;gap:3px;height:54px;margin:10px 0 4px;overflow:hidden;\">`;\n356\t cores.forEach(c => {\n357\t const usePct = c.usage_pct ?? 0;\n358\t const hue = Math.round(120 * (1 - usePct / 100));\n359\t const color = `hsl(${hue},70%,45%)`;\n360\t const barH = Math.max(2, Math.round(usePct * 0.46)); // max ~46px at 100%\n361\t const label = c.freq_mhz ? (c.freq_mhz >= 1000 ? (c.freq_mhz/1000).toFixed(1)+'G' : c.freq_mhz+'M') : '';\n362\t html += `<div class=\"vv-cpu-core\" style=\"flex:1;display:flex;flex-direction:column;align-items:center;gap:1px;min-width:10px;\">\n363\t <div style=\"font-size:8px;color:#555;line-height:1;\">${label}</div>\n364\t <div style=\"width:100%;height:46px;background:#1a1a1a;border-radius:2px;display:flex;align-items:flex-end;overflow:hidden;\">\n365\t <div style=\"width:100%;height:${barH}px;background:${color};border-radius:2px 2px 0 0;transition:height 0.4s;\"></div>\n366\t </div>\n367\t <div style=\"font-size:8px;color:#555;line-height:1;\">${c.core}</div>\n368\t </div>`;\n369\t });\n370\t html += `</div>`;\n371\t\n372\t // Freq legend\n373\t html += `<div style=\"display:flex;justify-content:space-between;font-size:9px;color:#444;margin-bottom:8px;\">\n374\t <span style=\"color:hsl(120,70%,45%)\">Low</span>\n375\t <span style=\"color:hsl(60,70%,45%)\">Med</span>\n376\t <span style=\"color:hsl(0,70%,45%)\">High</span>\n377\t </div>`;\n378\t }\n379\t\n380\t // Canvas chart\n381\t html += `<canvas id=\"vv-cpu-canvas\" style=\"width:100%;height:60px;display:block;\"></canvas>`;\n382\t return html;\n383\t}\n384\t\n385\t// ── Memory helpers ────────────────────────────────────────────────────────────\n386\t\n387\tconst VV_MEM_COLORS = {\n388\t system: '#5c6bc0',\n389\t vm: '#f57c00',\n390\t zfs: '#0097a7',\n391\t docker: '#388e3c',\n392\t free: '#37474f',\n393\t};\n394\t\n395\tfunction vvFmtGib(kb) {\n396\t return (kb / 1048576).toFixed(1) + ' GiB';\n397\t}\n398\t\n399\tfunction vvMemRow(label, kb, totalKb, color) {\n400\t const pct = totalKb > 0 ? Math.round(kb / totalKb * 100) : 0;\n401\t return `<div style=\"margin-bottom:5px;\">\n402\t <div style=\"display:flex;justify-content:space-between;font-size:11px;margin-bottom:2px;\">\n403\t <span style=\"color:${color};font-weight:500;\">${label}</span>\n404\t <span style=\"color:#777;\">${vvFmtGib(kb)}</span>\n405\t </div>\n406\t <div style=\"background:#1a1a1a;border-radius:3px;height:5px;overflow:hidden;\">\n407\t <div style=\"width:${pct}%;height:100%;background:${color};border-radius:3px;transition:width 0.4s;\"></div>\n408\t </div>\n409\t </div>`;\n410\t}\n411\t\n412\tfunction vvRenderMemory(mem) {\n413\t const total = mem.total_kb ?? 1;\n414\t const usedKb = total - (mem.free_kb ?? 0);\n415\t const usedPct = Math.round(usedKb / total * 100);\n416\t const totalColor = usedPct >= 85 ? '#f44336' : usedPct >= 65 ? '#ff9800' : '#ccc';\n417\t\n418\t const procs = mem.top_procs ?? [];\n419\t const procStrip = procs.map(p =>\n420\t `<span style=\"white-space:nowrap;\">${p.name} <span style=\"color:#aaa;font-weight:600;\">${vvFmtGib(p.kb)}</span></span>`\n421\t ).join('<span style=\"color:#333;margin:0 5px;\">·</span>');\n422\t\n423\t let html = `<div style=\"display:flex;justify-content:space-between;align-items:baseline;margin-bottom:10px;\">\n424\t <div style=\"font-size:14px;font-weight:600;color:${totalColor};white-space:nowrap;\">\n425\t ${vvFmtGib(usedKb)} <span style=\"font-size:11px;color:#555;font-weight:400;\">/ ${vvFmtGib(total)}</span>\n426\t </div>\n427\t <div style=\"font-size:10px;color:#666;text-align:right;margin-left:10px;overflow:hidden;min-width:0;white-space:nowrap;text-overflow:ellipsis;\">${procStrip}</div>\n428\t </div>`;\n429\t html += vvMemRow('System', mem.system_kb ?? 0, total, VV_MEM_COLORS.system)\n430\t + vvMemRow('VM', mem.vm_kb ?? 0, total, VV_MEM_COLORS.vm)\n431\t + vvMemRow('ZFS', mem.arc_kb ?? 0, total, VV_MEM_COLORS.zfs)\n432\t + vvMemRow('Docker', mem.docker_kb ?? 0, total, VV_MEM_COLORS.docker)\n433\t + vvMemRow('Free', mem.free_kb ?? 0, total, VV_MEM_COLORS.free);\n434\t\n435\t // Swap — only shown when swap is configured and has some usage\n436\t const swapTotal = mem.swap_total_kb ?? 0;\n437\t const swapUsed = mem.swap_used_kb ?? 0;\n438\t if (swapTotal > 0) {\n439\t const swapPct = Math.round(swapUsed / swapTotal * 100);\n440\t const swapColor = swapPct >= 50 ? '#f44336' : swapPct >= 20 ? '#ff9800' : '#607d8b';\n441\t html += `<div style=\"margin-top:6px;padding-top:6px;border-top:1px solid #1a1a1a;\">\n442\t <div style=\"display:flex;justify-content:space-between;font-size:11px;margin-bottom:2px;\">\n443\t <span style=\"color:${swapColor};font-weight:500;\">Swap</span>\n444\t <span style=\"color:#777;\">${vvFmtGib(swapUsed)} / ${vvFmtGib(swapTotal)}</span>\n445\t </div>\n446\t <div style=\"background:#1a1a1a;border-radius:3px;height:5px;overflow:hidden;\">\n447\t <div style=\"width:${swapPct}%;height:100%;background:${swapColor};border-radius:3px;transition:width 0.4s;\"></div>\n448\t </div>\n449\t </div>`;\n450\t }\n451\t return html;\n452\t}\n453\t\n454\t// ── Scripts render (filter-aware) ─────────────────────────────────────────────\n455\t\n456\tfunction vvRenderScripts() {\n457\t const sc = vvLastScripts;\n458\t const scList = sc.scripts ?? [];\n459\t const running = sc.running_count ?? 0;\n460\t const ok = sc.ok_count ?? 0;\n461\t const warn = sc.warn_count ?? 0;\n462\t const errors = sc.error_count ?? 0;\n463\t const now = Math.floor(Date.now() / 1000);\n464\t\n465\t function vvScriptPill(type, count, color, bg, border, icon) {\n466\t const active = vvScriptsFilter === type;\n467\t return `<span onclick=\"vvScriptsFilterSet('${type}')\"\n468\t style=\"font-size:10px;color:${color};background:${bg};border:1px solid ${active ? color : border};\n469\t padding:1px 7px;border-radius:10px;cursor:pointer;font-weight:${active ? '700' : '400'};\">${icon} ${count} ${type}</span>`;\n470\t }\n471\t\n472\t let html = `<div style=\"display:flex;gap:5px;flex-wrap:wrap;margin-bottom:8px;\">`;\n473\t if (running > 0) html += vvScriptPill('running', running, '#4fc3f7', '#0a2233', '#1e4060', '●');\n474\t if (ok > 0) html += vvScriptPill('ok', ok, '#4caf50', '#0a1f0a', '#1a3a1a', '✓');\n475\t if (warn > 0) html += vvScriptPill('warn', warn, '#ff9800', '#1f1200', '#3a2200', '!');\n476\t if (errors > 0) html += vvScriptPill('error', errors, '#f44336', '#2a0a0a', '#5a1a1a', '✗');\n477\t if (!running && !ok && !warn && !errors) html += `<span style=\"font-size:10px;color:#555;\">No recent runs</span>`;\n478\t html += `</div>`;\n479\t\n480\t const filtered = vvScriptsFilter ? scList.filter(s => s.status === vvScriptsFilter) : scList;\n481\t let listHtml = '';\n482\t filtered.forEach(s => {\n483\t const diff = now - (s.last_ts ?? now);\n484\t const ago = diff < 60 ? diff + 's' : diff < 3600 ? Math.floor(diff / 60) + 'm' : diff < 86400 ? Math.floor(diff / 3600) + 'h' : Math.floor(diff / 86400) + 'd';\n485\t const icon = s.status === 'running' ? '●' : s.status === 'ok' ? '✓' : s.status === 'warn' ? '!' : s.status === 'error' ? '✗' : '?';\n486\t const color = s.status === 'running' ? '#4fc3f7' : s.status === 'ok' ? '#4caf50' : s.status === 'warn' ? '#ff9800' : s.status === 'error' ? '#f44336' : '#555';\n487\t const name = s.name.length > 24 ? s.name.slice(0, 23) + '…' : s.name;\n488\t const dur = s.duration != null ? ` ${s.duration}s` : '';\n489\t const sid = (s.id || '').replace(/\\\\/g,'\\\\\\\\').replace(/'/g,\"\\\\'\");\n490\t listHtml += `<div onclick=\"vvOpenScript('${sid}')\" title=\"Open ${name} in Scheduler\"\n491\t style=\"display:flex;align-items:center;gap:5px;margin-bottom:4px;font-size:11px;cursor:pointer;\"\n492\t onmouseover=\"this.style.background='#161616'\" onmouseout=\"this.style.background=''\">\n493\t <span style=\"color:${color};flex-shrink:0;width:10px;text-align:center;\">${icon}</span>\n494\t <span style=\"color:#888;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;\">${name}</span>\n495\t <span style=\"color:#444;font-size:10px;flex-shrink:0;\">${ago}${dur}</span>\n496\t </div>`;\n497\t });\n498\t\n499\t if (!filtered.length) listHtml = `<p style=\"color:#555;font-style:italic;font-size:12px;\">${vvScriptsFilter ? 'None in this group' : 'No script logs found'}</p>`;\n500\t html += `<div style=\"max-height:133px;overflow-y:auto;scrollbar-width:none;-ms-overflow-style:none;\">${listHtml}</div>`;\n501\t const el = document.getElementById('vv-scripts-body');\n502\t if (el) el.innerHTML = html;\n503\t}\n504\t\n505\tfunction vvScriptsFilterSet(type) {\n506\t vvScriptsFilter = vvScriptsFilter === type ? null : type;\n507\t vvRenderScripts();\n508\t}\n509\t\n510\t// Deep-link a script row to the Scheduler tab (opens its settings/log panel there).\n511\tfunction vvOpenScript(id) {\n512\t if (!id) { window.location.href = '?tab=scheduler'; return; }\n513\t window.location.href = '?tab=scheduler&vv_script=' + encodeURIComponent(id);\n514\t}\n515\t\n516\t// ── Disk / storage helpers (module-level so vvRenderPools can call them) ─────\n517\t\n518\tfunction vvTempColor(tempC, transport) {\n519\t if (tempC === null) return '#444';\n520\t const isSsd = transport === 'nvme' || transport === 'ssd';\n521\t const warn = isSsd ? vvThresholds.ssd_warn : vvThresholds.hdd_warn;\n522\t const crit = isSsd ? vvThresholds.ssd_crit : vvThresholds.hdd_crit;\n523\t return tempC >= crit ? '#f44336' : tempC >= warn ? '#ff9800' : '#4caf50';\n524\t}\n525\t\n526\tfunction vvFmt(v) { return v >= 1024 ? (v / 1024).toFixed(1) + ' TB' : v + ' GB'; }\n527\t\n528\tfunction vvFmtRate(mbs) {\n529\t if (mbs >= 1000) return (mbs / 1024).toFixed(1) + ' GB/s';\n530\t if (mbs >= 100) return Math.round(mbs) + ' MB/s';\n531\t return mbs.toFixed(1) + ' MB/s';\n532\t}\n533\tfunction vvIoChip(dev) {\n534\t const io = vvDiskIo[dev];\n535\t if (!io) return '';\n536\t const r = io.r ?? 0, w = io.w ?? 0;\n537\t if (r < 0.05 && w < 0.05) return '';\n538\t const parts = [];\n539\t if (r >= 0.05) parts.push(`<span style=\"color:#3a7a3a;\">↓${vvFmtRate(r)}</span>`);\n540\t if (w >= 0.05) parts.push(`<span style=\"color:#7a4a1a;\">↑${vvFmtRate(w)}</span>`);\n541\t return `<span style=\"font-size:9px;margin-left:5px;\">${parts.join(' ')}</span>`;\n542\t}\n543\tfunction vvIoSum(devices) {\n544\t let r = 0, w = 0;\n545\t devices.forEach(dev => { const io = vvDiskIo[dev]; if (io) { r += io.r ?? 0; w += io.w ?? 0; } });\n546\t return [r, w];\n547\t}\n548\tfunction vvIoTotalSum(devices) {\n549\t let tr = 0, tw = 0;\n550\t devices.forEach(dev => { const io = vvDiskIo[dev]; if (io) { tr += io.tr ?? 0; tw += io.tw ?? 0; } });\n551\t return [tr, tw];\n552\t}\n553\tfunction vvFmtGb(gb) {\n554\t if (gb >= 1024) return (gb / 1024).toFixed(1) + ' TB';\n555\t if (gb >= 1) return gb.toFixed(1) + ' GB';\n556\t return (gb * 1024).toFixed(0) + ' MB';\n557\t}\n558\t\n559\tfunction vvDiskRow(disk) {\n560\t const tempC = disk.temp;\n561\t const tempColor = vvTempColor(tempC, disk.transport);\n562\t const tempStr = tempC !== null ? `${tempC}°` : '—';\n563\t const isParity = disk.role === 'parity';\n564\t const nameColor = isParity ? '#6a8faf' : '#aaa';\n565\t const pct = disk.pct ?? 0;\n566\t const barColor = isParity ? '#1e3a5a'\n567\t : pct >= vvThresholds.util_crit ? '#f44336'\n568\t : pct >= vvThresholds.util_warn ? '#ff9800'\n569\t : '#4caf50';\n570\t const barWidth = isParity ? '100' : pct;\n571\t const spinLabel = (!isParity && !disk.mounted) ? `<span style=\"color:#555;font-size:9px;margin-left:4px;\">↓</span>` : '';\n572\t const right = isParity\n573\t ? `<span style=\"color:#444;font-size:10px;\">${vvFmt(disk.size_gb)}</span>`\n574\t : `<span style=\"color:#555;font-size:10px;\">${vvFmt(disk.used_gb)} / ${vvFmt(disk.size_gb)}</span>`;\n575\t return `<div style=\"margin-bottom:7px;\">\n576\t <div style=\"display:flex;justify-content:space-between;align-items:center;font-size:11px;margin-bottom:3px;\">\n577\t <span style=\"color:${nameColor};display:flex;align-items:center;\">${disk.name}${spinLabel}${vvIoChip(disk.device)}</span>\n578\t ${right}\n579\t <span style=\"color:${tempColor};font-size:10px;margin-left:6px;flex-shrink:0;\">${tempStr}</span>\n580\t </div>\n581\t <div style=\"background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;\">\n582\t <div style=\"width:${barWidth}%;height:100%;background:${barColor};border-radius:3px;transition:width 0.4s;\"></div>\n583\t </div>\n584\t </div>`;\n585\t}\n586\t\n587\tfunction vvDiskCol(disks) {\n588\t return `<div style=\"flex:1;min-width:0;\">${disks.map(vvDiskRow).join('')}</div>`;\n589\t}\n590\t\n591\t// ── Poll ──────────────────────────────────────────────────────────────────────\n592\t\n593\tlet _vvFirstPoll = true;\n594\t\n595\tfunction vvPollMonitor() {\n596\t const _url = _vvFirstPoll ? '/plugins/varaverk/api/monitor.php' : '/plugins/varaverk/api/monitor.php?live=1';\n597\t _vvFirstPoll = false;\n598\t fetch(_url)\n599\t .then(r => r.json())\n600\t .then(d => {\n601\t\n602\t // ── API status banner ────────────────────────────────────────────────────\n603\t const apiStatus = d._api_status ?? {};\n604\t const banner = document.getElementById('vv-api-banner');\n605\t if (banner) {\n606\t const fallbacks = apiStatus.fallbacks ?? [];\n607\t if (fallbacks.length === 0) {\n608\t banner.style.display = 'none';\n609\t } else if (apiStatus.key_missing) {\n610\t // Key not configured — informational, not alarming\n611\t Object.assign(banner.style, {display:'', background:'#111', border:'1px solid #2a2a2a', color:'#555'});\n612\t banner.textContent = 'API key not configured — add HOST' +\n613\t (typeof vvLocalHostConf !== 'undefined' ? vvLocalHostConf.replace(/\\D/g,'') : '1') +\n614\t '_UNRAID_API_KEY in Scheduler → host conf to enable enhanced monitoring.';\n615\t } else {\n616\t // Key present but API unreachable — genuine problem\n617\t Object.assign(banner.style, {display:'', background:'#1a1200', border:'1px solid #3a2800', color:'#ff9800'});\n618\t banner.innerHTML = '⚠ Unraid API unreachable — using local reads. Check API key in host conf.' +\n619\t (fallbacks.length ? ' <span style=\"color:#666\">(' + fallbacks.join(', ') + ')</span>' : '');\n620\t }\n621\t }\n622\t\n623\t // ── Thresholds (from dynamix.cfg via backend) ────────────────────────────\n624\t if (d.thresholds) vvThresholds = d.thresholds;\n625\t\n626\t // ── System ──────────────────────────────────────────────────────────────\n627\t const sys = d.system ?? {};\n628\t const now = new Date();\n629\t const timeStr = now.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});\n630\t const dateStr = now.toLocaleDateString([], {weekday:'short', day:'numeric', month:'long', year:'numeric'});\n631\t const tz = Intl.DateTimeFormat().resolvedOptions().timeZone.split('/').pop().replace('_',' ');\n632\t const arrayColor = (sys.array_state === 'STARTED') ? '#4caf50' : '#f44336';\n633\t const ver = (sys.version || '').replace('version=','').replace(/\"/g,'');\n634\t\n635\t // Running container + VM counts for system card\n636\t const _runningCtrs = [...(d.docker_folders?.folders ?? []), {containers: d.docker_folders?.ungrouped ?? []}]\n637\t .flatMap(f => f.containers).filter(c => c.running).length;\n638\t const _runningVMs = (d.vms?.vms ?? []).filter(v => v.state === 'running').length;\n639\t const _threadInfo = sys.cpu_threads ? `${sys.cpu_cores}c / ${sys.cpu_threads}t` : '';\n640\t\n641\t // Load average (1/5/15m) — colour by load[0] vs core count\n642\t const _load = Array.isArray(sys.load_avg) ? sys.load_avg : null;\n643\t const _cores = sys.cpu_cores || 0;\n644\t const _loadColor = _load && _cores\n645\t ? (_load[0] > _cores * 2 ? '#f44336' : _load[0] > _cores ? '#ff9800' : '#888') : '#888';\n646\t const _loadStr = _load\n647\t ? `${_load[0].toFixed(2)} <span style=\"color:#444;\">·</span> ${_load[1].toFixed(2)} <span style=\"color:#444;\">·</span> ${_load[2].toFixed(2)}`\n648\t : '—';\n649\t const _coreMeta = _threadInfo ? ` <span style=\"color:#444;\">(${_threadInfo})</span>` : '';\n650\t\n651\t document.getElementById('vv-system-body').innerHTML =\n652\t `<div style=\"display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;\">\n653\t <div style=\"min-width:0;\">\n654\t <div style=\"font-size:14px;font-weight:700;color:#ddd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;\">${sys.name}</div>\n655\t <div style=\"font-size:10px;color:#555;margin-top:2px;\">${sys.comment || ' '}</div>\n656\t </div>\n657\t <div style=\"display:flex;align-items:flex-start;gap:6px;flex-shrink:0;margin-left:6px;\">\n658\t <svg width=\"32\" height=\"54\" viewBox=\"0 0 38 64\" fill=\"none\" style=\"opacity:0.15;pointer-events:none;\">\n659\t <rect x=\"1\" y=\"1\" width=\"36\" height=\"62\" rx=\"3\" stroke=\"#aaa\" stroke-width=\"1.2\" fill=\"#111\"/>\n660\t <rect x=\"1\" y=\"1\" width=\"36\" height=\"10\" rx=\"3\" fill=\"#1c1c1c\" stroke=\"#aaa\" stroke-width=\"1.2\"/>\n661\t <circle cx=\"19\" cy=\"6\" r=\"2.5\" stroke=\"#ff9800\" stroke-width=\"1\" fill=\"none\"/>\n662\t <line x1=\"19\" y1=\"3.8\" x2=\"19\" y2=\"2.2\" stroke=\"#ff9800\" stroke-width=\"1\"/>\n663\t <rect x=\"26\" y=\"4\" width=\"4\" height=\"2\" rx=\"0.5\" fill=\"#555\"/>\n664\t <rect x=\"3\" y=\"14\" width=\"16\" height=\"44\" rx=\"1\" fill=\"#0d0d0d\" stroke=\"#444\" stroke-width=\"0.6\"/>\n665\t <line x1=\"3\" y1=\"18\" x2=\"19\" y2=\"18\" stroke=\"#333\" stroke-width=\"0.6\"/>\n666\t <line x1=\"3\" y1=\"22\" x2=\"19\" y2=\"22\" stroke=\"#333\" stroke-width=\"0.6\"/>\n667\t <line x1=\"3\" y1=\"26\" x2=\"19\" y2=\"26\" stroke=\"#333\" stroke-width=\"0.6\"/>\n668\t <line x1=\"3\" y1=\"30\" x2=\"19\" y2=\"30\" stroke=\"#333\" stroke-width=\"0.6\"/>\n669\t <line x1=\"3\" y1=\"34\" x2=\"19\" y2=\"34\" stroke=\"#333\" stroke-width=\"0.6\"/>\n670\t <line x1=\"3\" y1=\"38\" x2=\"19\" y2=\"38\" stroke=\"#333\" stroke-width=\"0.6\"/>\n671\t <line x1=\"3\" y1=\"42\" x2=\"19\" y2=\"42\" stroke=\"#333\" stroke-width=\"0.6\"/>\n672\t <line x1=\"3\" y1=\"46\" x2=\"19\" y2=\"46\" stroke=\"#333\" stroke-width=\"0.6\"/>\n673\t <line x1=\"3\" y1=\"50\" x2=\"19\" y2=\"50\" stroke=\"#333\" stroke-width=\"0.6\"/>\n674\t <rect x=\"21\" y=\"14\" width=\"14\" height=\"44\" rx=\"1\" fill=\"#08080f\" stroke=\"#444\" stroke-width=\"0.6\" opacity=\"0.7\"/>\n675\t <rect x=\"5\" y=\"60\" width=\"5\" height=\"2\" rx=\"1\" fill=\"#333\"/>\n676\t <rect x=\"28\" y=\"60\" width=\"5\" height=\"2\" rx=\"1\" fill=\"#333\"/>\n677\t </svg>\n678\t <a href=\"/\" target=\"_blank\" class=\"vv-card-cog\" title=\"Dashboard\" style=\"margin-top:1px;\">⚙</a>\n679\t </div>\n680\t </div>\n681\t <div style=\"font-size:20px;font-weight:300;color:#ccc;line-height:1;\">${timeStr}</div>\n682\t <div style=\"font-size:10px;color:#555;margin-bottom:10px;\">${dateStr} · ${tz}</div>\n683\t <div style=\"display:grid;grid-template-columns:auto 1fr;gap:3px 8px;font-size:11px;\">\n684\t <span style=\"color:#444;\">Model</span> <span style=\"color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;\">${sys.cpu_model}${_coreMeta}</span>\n685\t <span style=\"color:#444;\">Array</span> <span style=\"color:${arrayColor};font-weight:600;\">${sys.array_state}</span>\n686\t <span style=\"color:#444;\">Uptime</span> <span style=\"color:#888;\">${sys.uptime}</span>\n687\t <span style=\"color:#444;\">Load</span> <span style=\"color:${_loadColor};\">${_loadStr}</span>\n688\t <span style=\"color:#444;\">Running</span> <span style=\"color:#888;\">${_runningCtrs} ctr${_runningCtrs !== 1 ? 's' : ''}${_runningVMs > 0 ? ` · ${_runningVMs} VM` : ''}</span>\n689\t <span style=\"color:#444;\">Version</span> <span style=\"color:#3a3a3a;\">${ver}</span>\n690\t </div>`;\n691\t\n692\t // ── Partner ──────────────────────────────────────────────────────────────\n693\t const pt = d.partner ?? {};\n694\t const ptHosts = pt.hosts ?? [];\n695\t const ptRemote = d.remote_hosts ?? {};\n696\t const ptStatus = pt.enabled\n697\t ? `<span style=\"color:#4caf50;\">enabled</span> · sync every ${pt.sync_min}min`\n698\t : `<span style=\"color:#555;\">disabled</span>`;\n699\t\n700\t let ptHtml = `<div style=\"font-size:10px;color:#666;margin-bottom:8px;\">${ptStatus}</div>`;\n701\t ptHosts.forEach(h => {\n702\t const dot = h.online === null ? '#555' : h.online ? '#4caf50' : '#f44336';\n703\t const label = h.online === null ? 'unknown' : h.online ? 'online' : 'offline';\n704\t const tags = [\n705\t h.is_me ? `<span style=\"background:#1a3a1a;color:#4caf50;font-size:8px;padding:1px 5px;border-radius:3px;margin-left:4px;\">US</span>` : '',\n706\t h.is_owner ? `<span style=\"background:#1a2a3a;color:#4a9eff;font-size:8px;padding:1px 5px;border-radius:3px;margin-left:4px;\">OWNER</span>` : '',\n707\t ].join('');\n708\t\n709\t // Onboard phase badge for remote hosts not yet fully onboarded\n710\t const phase = h.onboard_phase ?? null;\n711\t const onboardBadge = (!h.is_me && phase !== null && phase < 2)\n712\t ? (phase === 1\n713\t ? `<div style=\"font-size:9px;color:#ff9800;margin-top:3px;padding:2px 5px;background:#1a1200;border:1px solid #3a2800;border-radius:3px;display:inline-block;\">⏳ Awaiting onboard</div>`\n714\t : `<div style=\"font-size:9px;color:#444;margin-top:3px;padding:2px 5px;background:#111;border:1px solid #222;border-radius:3px;display:inline-block;\">○ Not provisioned</div>`)\n715\t : '';\n716\t\n717\t // Remote stats from Unraid API (only available for non-self hosts with an API key)\n718\t const rs = !h.is_me ? (ptRemote[h.id] ?? null) : null;\n719\t let statsHtml = '';\n720\t if (rs && rs.available) {\n721\t const cpuAvail = rs.cpu_load > 0;\n722\t const ramAvail = rs.mem_used_pct > 0;\n723\t const cpuHue = cpuAvail ? Math.round(120 * (1 - rs.cpu_load / 100)) : 0;\n724\t const memHue = ramAvail ? Math.round(120 * (1 - rs.mem_used_pct / 100)) : 0;\n725\t const cpuStr = cpuAvail ? `<span style=\"color:hsl(${cpuHue},70%,45%);font-weight:600;\">${rs.cpu_load}%${rs.cpu_threads ? `<span style=\"color:#333;font-weight:400;\"> · ${rs.cpu_threads}t</span>` : ''}</span>` : `<span style=\"color:#333;\">—</span>`;\n726\t const ramStr = ramAvail ? `<span style=\"color:hsl(${memHue},70%,45%);font-weight:600;\">${rs.mem_used_pct}%${rs.mem_total_gb ? `<span style=\"color:#333;font-weight:400;\"> · ${rs.mem_total_gb}G</span>` : ''}</span>` : `<span style=\"color:#333;\">—</span>`;\n727\t const arrColor = rs.array_state === 'Started' || rs.array_state === 'STARTED' ? '#4caf50' : '#f44336';\n728\t const uptimeStr = rs.uptime && rs.uptime !== '—' ? rs.uptime : '—';\n729\t\n730\t // Version mismatch warning — scripts will refuse sync operations until versions match\n731\t const myVer = (sys.version || '').replace('version=','').replace(/\"/g,'').trim();\n732\t const remoteVer = (rs.version || '').trim();\n733\t const verMismatch = myVer && remoteVer && myVer !== remoteVer;\n734\t const verWarn = verMismatch\n735\t ? `<div style=\"font-size:10px;color:#ff9800;margin-top:4px;padding:3px 6px;background:#1a1000;border:1px solid #3a2800;border-radius:3px;\">\n736\t ⚠ Version mismatch: local ${myVer} · remote ${remoteVer}<br>\n737\t <span style=\"color:#555;\">Script sync ops are gated until versions match</span>\n738\t </div>` : '';\n739\t\n740\t const verRow = remoteVer ? `<span style=\"color:#444;\">unRAID</span><span style=\"color:#3a3a3a;grid-column:span 3;\">${remoteVer}</span>` : '';\n741\t statsHtml = `<div style=\"display:grid;grid-template-columns:auto 1fr auto 1fr;gap:2px 8px;font-size:10px;margin-top:5px;margin-bottom:2px;\">\n742\t <span style=\"color:#444;\">CPU</span>${cpuStr}\n743\t <span style=\"color:#444;\">RAM</span>${ramStr}\n744\t <span style=\"color:#444;\">Array</span>\n745\t <span style=\"color:${arrColor};font-weight:600;\">${rs.array_state}</span>\n746\t <span style=\"color:#444;\">Uptime</span>\n747\t <span style=\"color:#555;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;\">${uptimeStr}</span>\n748\t ${verRow}\n749\t </div>${verWarn}`;\n750\t } else if (rs && rs.no_api_key) {\n751\t statsHtml = `<div style=\"font-size:10px;color:#444;margin-top:4px;\">API key not configured — complete Onboard to enable</div>`;\n752\t } else if (rs && !rs.available) {\n753\t statsHtml = `<div style=\"font-size:10px;color:#444;margin-top:4px;\">API unreachable</div>`;\n754\t }\n755\t\n756\t ptHtml += `<div style=\"margin-bottom:8px;padding-bottom:8px;border-bottom:1px solid #222;\">\n757\t <div style=\"display:flex;align-items:center;justify-content:space-between;\">\n758\t <div>\n759\t <span style=\"font-size:10px;color:#555;margin-right:4px;\">${h.id}</span>\n760\t <span style=\"font-size:12px;color:#ccc;font-weight:500;\">${h.owner || h.hostname}</span>${tags}\n761\t <div style=\"font-size:10px;color:#555;margin-top:1px;\">${h.hostname}</div>\n762\t ${onboardBadge}\n763\t </div>\n764\t <span style=\"color:${dot};font-size:10px;white-space:nowrap;\">● ${label}</span>\n765\t </div>\n766\t ${statsHtml}\n767\t </div>`;\n768\t });\n769\t document.getElementById('vv-partner-body').innerHTML = ptHtml;\n770\t (function() {\n771\t const c = document.getElementById('vv-partner');\n772\t if (!c) return;\n773\t const remotes = ptHosts.filter(h => !h.is_me);\n774\t c.classList.remove('vv-accent-ok','vv-accent-warn');\n775\t if (remotes.length) {\n776\t if (remotes.some(h => h.online)) c.classList.add('vv-accent-ok');\n777\t else c.classList.add('vv-accent-warn');\n778\t }\n779\t })();\n780\t\n781\t // ── Fallback ──────────────────────────────────────────────────────────────\n782\t (function() {\n783\t const fb = d.fallback ?? {};\n784\t const fbActive = d.fallback_active ?? [];\n785\t const state = (fb.state ?? 'UNKNOWN').toUpperCase();\n786\t const enabled = fb.enabled ?? false;\n787\t const strikes = fb.handback_strikes ?? 0;\n788\t const maxStrikes = fb.handback_strikes_required ?? 3;\n789\t const ptSuspended = fb.partnership_suspended ?? false;\n790\t const partnerLostAt = fb.partner_lost_at ?? 0;\n791\t const suspendAfter = fb.partnership_suspend_after ?? 120;\n792\t const interval = fb.check_interval ?? 30;\n793\t const ptRequired = fb.partnership_required ?? false;\n794\t\n795\t const stateMap = {\n796\t NORMAL: ['#4caf50', '✓ Nominal'],\n797\t FALLBACK: ['#f44336', '⚠ Fallback active'],\n798\t NO_INTERNET: ['#ff9800', '⚡ Internet lost'],\n799\t DARK: ['#9e9e9e', '◌ Dark mode'],\n800\t UNKNOWN: ['#444', '— No state file'],\n801\t };\n802\t const [sColor, sLabel] = stateMap[state] ?? ['#444', state];\n803\t\n804\t // ── Disabled ──────────────────────────────────────────────────────────\n805\t if (!enabled) {\n806\t document.getElementById('vv-fallback-body').innerHTML =\n807\t `<div style=\"font-size:11px;color:#444;margin-bottom:6px;\">○ Disabled</div>\n808\t <div style=\"font-size:10px;color:#333;\">Set FALLBACK_ENABLED=true in master.conf to activate</div>`;\n809\t return;\n810\t }\n811\t\n812\t // ── Suspended (partnership gate) ───────────────────────────────────────\n813\t if (ptSuspended) {\n814\t const lostMin = partnerLostAt > 0 ? Math.floor((Date.now()/1000 - partnerLostAt) / 60) : '?';\n815\t document.getElementById('vv-fallback-body').innerHTML =\n816\t `<div style=\"font-size:11px;color:#555;margin-bottom:6px;\">⊘ Suspended</div>\n817\t <div style=\"font-size:10px;color:#444;margin-bottom:3px;\">Partnership inactive · ${lostMin}m elapsed</div>\n818\t <div style=\"font-size:10px;color:#333;\">Resumes when partnership becomes active</div>`;\n819\t return;\n820\t }\n821\t\n822\t let fbHtml = '';\n823\t\n824\t // ── State label + outage duration ──────────────────────────────────────\n825\t let stateExtra = '';\n826\t if (state === 'FALLBACK' || state === 'DARK') {\n827\t const start = parseInt(fb.failover_start ?? 0);\n828\t if (start > 0) {\n829\t const sec = Math.floor(Date.now() / 1000) - start;\n830\t stateExtra = ` · ${Math.floor(sec/3600)}h ${Math.floor((sec%3600)/60)}m`;\n831\t }\n832\t }\n833\t fbHtml += `<div style=\"font-size:11px;color:${sColor};font-weight:500;margin-bottom:6px;\">${sLabel}${stateExtra}</div>`;\n834\t\n835\t // ── Handback strike progress ───────────────────────────────────────────\n836\t if (state === 'FALLBACK' && strikes > 0) {\n837\t const dots = Array.from({length: maxStrikes}, (_,i) =>\n838\t `<span style=\"color:${i < strikes ? '#4caf50' : '#2a2a2a'};font-size:14px;line-height:1;\">●</span>`\n839\t ).join('');\n840\t fbHtml += `<div style=\"display:flex;align-items:center;gap:6px;margin-bottom:8px;\">\n841\t <span style=\"font-size:10px;color:#555;\">Handback</span>\n842\t <span style=\"display:flex;gap:3px;\">${dots}</span>\n843\t <span style=\"font-size:10px;color:#555;\">${strikes}/${maxStrikes}</span>\n844\t </div>`;\n845\t }\n846\t\n847\t // ── Tier badges (FAILOVER only) ────────────────────────────────────────\n848\t if (state === 'FALLBACK') {\n849\t const t2 = fb.tier2_started === 'true';\n850\t const t3 = fb.tier3_started === 'true';\n851\t const t4 = fb.tier4_started === 'true';\n852\t const tier = (n, on) => `<span style=\"font-size:9px;padding:1px 6px;border-radius:3px;\n853\t background:${on ? '#1a2a1a' : '#1a1a1a'};\n854\t border:1px solid ${on ? '#2a5a2a' : '#252525'};\n855\t color:${on ? '#4caf50' : '#333'};\">T${n}</span>`;\n856\t fbHtml += `<div style=\"display:flex;gap:4px;margin-bottom:8px;\">\n857\t ${tier(1,true)} ${tier(2,t2)} ${tier(3,t3)} ${tier(4,t4)}\n858\t </div>`;\n859\t }\n860\t\n861\t // ── Active containers ──────────────────────────────────────────────────\n862\t if (fbActive.length) {\n863\t fbActive.forEach(group => {\n864\t fbHtml += `<div style=\"font-size:10px;color:#555;margin-bottom:4px;letter-spacing:.03em;\">\n865\t COVERING ${group.hostname}\n866\t </div>`;\n867\t group.containers.forEach(c => {\n868\t const img = c.image.includes('/') ? c.image.split('/').pop() : c.image;\n869\t fbHtml += `<div style=\"display:flex;justify-content:space-between;align-items:center;\n870\t background:#1a1a1a;border-radius:4px;padding:4px 8px;margin-bottom:3px;\">\n871\t <span style=\"color:#ccc;font-size:11px;font-weight:500;\">${c.name}</span>\n872\t <span style=\"color:#444;font-size:9px;margin-left:8px;white-space:nowrap;\">${img}</span>\n873\t </div>`;\n874\t });\n875\t });\n876\t }\n877\t\n878\t // ── NORMAL quiet state ─────────────────────────────────────────────────\n879\t if (state === 'NORMAL' && !fbActive.length) {\n880\t let meta = `monitoring · ${interval}s`;\n881\t if (ptRequired) meta += ' · partnership gated';\n882\t fbHtml += `<div style=\"font-size:10px;color:#2a2a2a;\">${meta}</div>`;\n883\t }\n884\t\n885\t // ── NO_INTERNET / DARK detail ──────────────────────────────────────────\n886\t if (state === 'NO_INTERNET') {\n887\t fbHtml += `<div style=\"font-size:10px;color:#555;\">DDNS stopped · awaiting recovery</div>`;\n888\t } else if (state === 'DARK') {\n889\t fbHtml += `<div style=\"font-size:10px;color:#555;\">Remote + internet both down</div>`;\n890\t }\n891\t\n892\t document.getElementById('vv-fallback-body').innerHTML = fbHtml;\n893\t\n894\t // Status accent border\n895\t const fbCard = document.getElementById('vv-fallback');\n896\t if (fbCard) {\n897\t fbCard.classList.remove('vv-accent-ok','vv-accent-warn','vv-accent-err');\n898\t if (!enabled || ptSuspended) fbCard.classList.add('vv-accent-warn');\n899\t else if (state === 'NORMAL') fbCard.classList.add('vv-accent-ok');\n900\t else if (state === 'FALLBACK') fbCard.classList.add('vv-accent-err');\n901\t else if (state !== 'UNKNOWN') fbCard.classList.add('vv-accent-warn');\n902\t }\n903\t })();\n904\t\n905\t // ── CPU title (core count + temp — watchdog data only comes from full poll) ──\n906\t const _cpuTitleEl = document.getElementById('vv-cpu-title');\n907\t if (_cpuTitleEl) {\n908\t const _cpuTemp = d.watchdog?.stability?.cpu_temp ?? null;\n909\t const _tColor = _cpuTemp == null ? '#888' : _cpuTemp >= 88 ? '#f44336' : _cpuTemp >= 75 ? '#ff9800' : '#4caf50';\n910\t const _coreLbl = sys.cpu_threads ? `CPU · ${sys.cpu_cores}c/${sys.cpu_threads}t` : 'CPU';\n911\t _cpuTitleEl.innerHTML = _coreLbl\n912\t + (_cpuTemp != null ? ` <span style=\"color:${_tColor};font-weight:400;\">${_cpuTemp}°</span>` : '');\n913\t }\n914\t\n915\t // ── UPS / Power ─────────────────────────────────────────────────────────\n916\t const ups = d.ups ?? {};\n917\t if (ups.available) {\n918\t const onBatt = ups.status === 'ONBATT';\n919\t const statCls = onBatt ? 'vv-banner-err' : ups.status === 'ONLINE' ? 'vv-banner-ok' : 'vv-banner-warn';\n920\t const statColor = onBatt ? '#f44336' : ups.status === 'ONLINE' ? '#4caf50' : '#ff9800';\n921\t const loadPct = ups.load_pct ?? 0;\n922\t const loadHue = Math.round(120 * (1 - loadPct / 100));\n923\t const bPct = ups.bcharge ?? 0;\n924\t const bHue = Math.round(120 * (bPct / 100));\n925\t const bColor = onBatt ? '#f44336' : `hsl(${bHue},70%,45%)`;\n926\t const timeLeft = ups.timeleft != null ? ups.timeleft.toFixed(1) + ' min' : '—';\n927\t const watts = ups.watts != null ? ups.watts + ' W' : '—';\n928\t const lineV = ups.line_v != null ? ups.line_v + ' V' : '—';\n929\t const outV = ups.output_v != null ? ups.output_v + ' V' : '—';\n930\t const xfers = ups.num_xfers ?? 0;\n931\t const batIcon = bPct >= 80 ? '▰▰▰▰' : bPct >= 60 ? '▰▰▰▱' : bPct >= 40 ? '▰▰▱▱' : bPct >= 20 ? '▰▱▱▱' : '▱▱▱▱';\n932\t\n933\t document.getElementById('vv-ups-body').innerHTML =\n934\t `<div class=\"vv-banner ${statCls}\" style=\"margin-bottom:8px;\">\n935\t <span>${ups.status}${onBatt ? ' — ON BATTERY' : ''}</span>\n936\t <span style=\"font-size:11px;font-weight:400;opacity:0.8;\">${ups.model}</span>\n937\t </div>\n938\t <div style=\"display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;\">\n939\t <div>\n940\t <div style=\"font-size:10px;color:#555;margin-bottom:3px;\">Load</div>\n941\t <div style=\"font-size:15px;font-weight:600;color:hsl(${loadHue},70%,45%);line-height:1;\">${loadPct.toFixed(0)}<span style=\"font-size:10px;font-weight:400;color:#555;\">%</span></div>\n942\t <div style=\"font-size:10px;color:#444;margin-top:2px;\">${watts}</div>\n943\t </div>\n944\t <div>\n945\t <div style=\"font-size:10px;color:#555;margin-bottom:3px;\">Battery</div>\n946\t <div style=\"font-size:15px;font-weight:600;color:${bColor};line-height:1;\">${bPct.toFixed(0)}<span style=\"font-size:10px;font-weight:400;color:#555;\">%</span></div>\n947\t <div style=\"font-size:10px;color:#444;margin-top:2px;\">${timeLeft}</div>\n948\t </div>\n949\t </div>\n950\t <div style=\"display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-bottom:8px;\">\n951\t <div style=\"background:#1a1a1a;border-radius:3px;height:5px;overflow:hidden;\">\n952\t <div style=\"width:${loadPct}%;height:100%;background:hsl(${loadHue},70%,45%);transition:width 0.4s;\"></div>\n953\t </div>\n954\t <div style=\"background:#1a1a1a;border-radius:3px;height:5px;overflow:hidden;\">\n955\t <div style=\"width:${bPct}%;height:100%;background:${bColor};transition:width 0.4s;\"></div>\n956\t </div>\n957\t </div>\n958\t <div style=\"display:grid;grid-template-columns:auto 1fr auto 1fr;gap:3px 8px;font-size:10px;\">\n959\t <span style=\"color:#444;\">In</span> <span style=\"color:#777;\">${lineV}</span>\n960\t <span style=\"color:#444;\">Out</span> <span style=\"color:#777;\">${outV}</span>\n961\t <span style=\"color:#444;\">Xfers</span> <span style=\"color:${xfers > 0 ? '#ff9800' : '#444'};\">${xfers}</span>\n962\t <span style=\"color:#444;\">Test</span> <span style=\"color:#555;\">${ups.selftest || '—'}</span>\n963\t </div>`;\n964\t } else {\n965\t document.getElementById('vv-ups-body').innerHTML = '<p style=\"color:#555;font-style:italic;font-size:12px;\">No UPS detected</p>';\n966\t }\n967\t\n968\t // ── Parity ──────────────────────────────────────────────────────────────\n969\t const par = d.parity ?? {};\n970\t (function() {\n971\t const valid = par.valid !== false;\n972\t const inProg = par.in_progress;\n973\t const validColor = valid ? '#4caf50' : '#f44336';\n974\t const validLabel = valid ? 'Parity is valid' : 'Parity is INVALID';\n975\t\n976\t function vvRelTime(ts) {\n977\t if (!ts) return '';\n978\t const diff = Math.floor(Date.now() / 1000) - ts;\n979\t const d = Math.floor(diff / 86400), h = Math.floor((diff % 86400) / 3600), m = Math.floor((diff % 3600) / 60);\n980\t const parts = [];\n981\t if (d) parts.push(d + ' day' + (d !== 1 ? 's' : ''));\n982\t if (h) parts.push(h + ' hour' + (h !== 1 ? 's' : ''));\n983\t if (!d && m) parts.push(m + ' minute' + (m !== 1 ? 's' : ''));\n984\t return parts.join(', ') + ' ago';\n985\t }\n986\t function vvDueIn(ts) {\n987\t if (!ts) return '—';\n988\t const diff = ts - Math.floor(Date.now() / 1000);\n989\t if (diff <= 0) return 'overdue';\n990\t const d = Math.floor(diff / 86400), h = Math.floor((diff % 86400) / 3600), m = Math.floor((diff % 3600) / 60);\n991\t const parts = [];\n992\t if (d) parts.push(d + ' day' + (d !== 1 ? 's' : ''));\n993\t if (h) parts.push(h + ' hour' + (h !== 1 ? 's' : ''));\n994\t if (!d && m) parts.push(m + ' minute' + (m !== 1 ? 's' : ''));\n995\t return 'Due in: ' + parts.join(', ');\n996\t }\n997\t function vvFmtDate(ts) {\n998\t if (!ts) return '—';\n999\t return new Date(ts * 1000).toLocaleString([], {weekday:'short',day:'numeric',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit'});\n1000\t }\n1001\t\n1002\t const exitColor = par.exit_label === 'Completed' ? '#4caf50' : par.exit_label === 'Aborted' ? '#ff9800' : '#f44336';\n1003\t const errColor = (par.errors ?? 0) > 0 ? '#f44336' : '#444';\n1004\t const speedStr = par.last_speed_mb ? ` · ${par.last_speed_mb} MB/s` : '';\n1005\t const nextDate = vvFmtDate(par.next_ts);\n1006\t const dueIn = vvDueIn(par.next_ts);\n1007\t const parBannerCls = !valid ? 'vv-banner-err' : (par.errors ?? 0) > 0 ? 'vv-banner-warn' : 'vv-banner-ok';\n1008\t\n1009\t let html = `<div class=\"vv-banner ${parBannerCls}\">\n1010\t <span>${valid ? '✓ Valid' : '✗ INVALID'}</span>\n1011\t ${(par.errors ?? 0) > 0 ? `<span style=\"font-size:11px;\">${par.errors} error${par.errors !== 1 ? 's' : ''}</span>` : ''}\n1012\t </div>`;\n1013\t\n1014\t if (inProg) {\n1015\t const pct = par.resync_pct ?? 0;\n1016\t html += `<div style=\"font-size:11px;color:#aaa;margin-bottom:4px;\">Check in progress — ${pct}%</div>\n1017\t <div style=\"background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;margin-bottom:10px;\">\n1018\t <div style=\"width:${pct}%;height:100%;background:#4caf50;border-radius:3px;transition:width 2s;\"></div>\n1019\t </div>`;\n1020\t }\n1021\t\n1022\t html += `\n1023\t <div style=\"display:grid;grid-template-columns:auto 1fr;gap:3px 10px;font-size:11px;\">\n1024\t <span style=\"color:#555;\">Last check</span>\n1025\t <span style=\"color:#888;\">${par.last_date ?? '—'}${speedStr}</span>\n1026\t <span style=\"color:#555;\">Status</span>\n1027\t <span style=\"color:${exitColor};\">${par.exit_label ?? '—'}</span>\n1028\t <span style=\"color:#555;\">Errors</span>\n1029\t <span style=\"color:${errColor};\">${par.errors ?? 0}</span>\n1030\t <span style=\"color:#555;\">Next check</span>\n1031\t <span style=\"color:#888;\">${nextDate}</span>\n1032\t </div>\n1033\t <div style=\"font-size:10px;color:#555;margin-top:8px;\">${dueIn}</div>`;\n1034\t\n1035\t document.getElementById('vv-parity-body').innerHTML = html;\n1036\t })();\n1037\t\n1038\t // ── Watchdog ─────────────────────────────────────────────────────────────\n1039\t (function() {\n1040\t const wd = d.watchdog ?? {};\n1041\t const el = document.getElementById('vv-watchdog-body');\n1042\t if (!el) return;\n1043\t\n1044\t const healthy = wd.healthy ?? true;\n1045\t const ctrStrikes = wd.ctr_strikes ?? {};\n1046\t const ctrNames = Object.keys(ctrStrikes);\n1047\t const rwLevel = wd.rw_level ?? 0;\n1048\t const rwPaused = wd.rw_paused ?? [];\n1049\t const rwStopped = wd.rw_stopped ?? [];\n1050\t const daemonHit = (wd.daemon_strikes ?? 0) > 0;\n1051\t const oom = wd.oom_count ?? 0;\n1052\t const reboots = wd.reboots_12h ?? 0;\n1053\t const restartCount = wd.restart_count ?? 0;\n1054\t const restarts = wd.restarts_24h ?? [];\n1055\t\n1056\t const stab = wd.stability ?? {};\n1057\t const stabStrikes = stab.strikes ?? {};\n1058\t const stabNames = Object.keys(stabStrikes);\n1059\t\n1060\t const storWd = wd.storage_wd ?? {};\n1061\t const growthStr = storWd.growth_strikes ?? {};\n1062\t const logStr = storWd.log_strikes ?? {};\n1063\t const storIssues = Object.keys(growthStr).length + Object.keys(logStr).length;\n1064\t\n1065\t const netWd = wd.network_wd ?? {};\n1066\t const npmStrikes = netWd.npm_strikes ?? 0;\n1067\t\n1068\t const issueCount = ctrNames.length + stabNames.length + storIssues\n1069\t + (daemonHit ? 1 : 0) + (oom > 0 ? 1 : 0)\n1070\t + (reboots > 0 ? 1 : 0) + (npmStrikes > 0 ? 1 : 0);\n1071\t const bannerCls = healthy ? 'vv-banner-ok' : (reboots || oom || daemonHit ? 'vv-banner-err' : 'vv-banner-warn');\n1072\t const bannerTxt = healthy ? '✓ All clear' : `⚠ ${issueCount} issue${issueCount !== 1 ? 's' : ''}`;\n1073\t\n1074\t let html = `<div class=\"vv-banner ${bannerCls}\">${bannerTxt}`;"}]},"uuid":"c9fb1bba-81a1-49cf-92a9-ec175643f0b6","timestamp":"2026-06-06T14:29:40.818Z","sourceToolAssistantUUID":"0fa09355-e005-42b5-8a12-4eb9230ccc27","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"c9fb1bba-81a1-49cf-92a9-ec175643f0b6","isSidechain":true,"agentId":"accc3589951fac88e","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01Cusyw8FcnVphFqJjcnJin5","type":"message","role":"assistant","content":[{"type":"text","text":"The monitor.php file is very large. Let me continue reading the remaining pages."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":91601,"cache_read_input_tokens":8141,"cache_creation":{"ephemeral_5m_input_tokens":91601,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null},"requestId":"req_011CbnAZUCximcsRa8AmqYUy","attributionAgent":"Explore","type":"assistant","uuid":"4dbe9e07-1c13-4e73-a18a-bdd7a6699e34","timestamp":"2026-06-06T14:29:43.957Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"4dbe9e07-1c13-4e73-a18a-bdd7a6699e34","isSidechain":true,"agentId":"accc3589951fac88e","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01Cusyw8FcnVphFqJjcnJin5","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_017ZsggAtJiwSvPEmkjvTDs7","name":"Read","input":{"file_path":"/boot/config/plugins/varaverk/Plugin/unraid/pages/partnership.php"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":91601,"cache_read_input_tokens":8141,"cache_creation":{"ephemeral_5m_input_tokens":91601,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null},"requestId":"req_011CbnAZUCximcsRa8AmqYUy","attributionAgent":"Explore","type":"assistant","uuid":"be96202e-18c5-4341-b5d1-8e5e0e33297e","timestamp":"2026-06-06T14:29:44.645Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"be96202e-18c5-4341-b5d1-8e5e0e33297e","isSidechain":true,"agentId":"accc3589951fac88e","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01Cusyw8FcnVphFqJjcnJin5","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01VvPt2jWQdSRngUhhqqVDYg","name":"Read","input":{"file_path":"/boot/config/plugins/varaverk/Plugin/unraid/pages/rsync.php"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":91601,"cache_read_input_tokens":8141,"cache_creation":{"ephemeral_5m_input_tokens":91601,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null},"requestId":"req_011CbnAZUCximcsRa8AmqYUy","attributionAgent":"Explore","type":"assistant","uuid":"a5dac394-2ad5-4c96-8ee0-7a7331bfbaa1","timestamp":"2026-06-06T14:29:44.652Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"a5dac394-2ad5-4c96-8ee0-7a7331bfbaa1","isSidechain":true,"promptId":"3c1ac879-b13a-4ebc-9aeb-ea73615e9af3","agentId":"accc3589951fac88e","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01VvPt2jWQdSRngUhhqqVDYg","type":"tool_result","content":"<system-reminder>[Truncated: PARTIAL view — showing lines 1-1133 of 1739 total (32599 tokens, cap 25000). Call Read with offset=1134 limit=1133 for the next page, or Grep to find a specific section. Do NOT answer from this page alone if the answer may be further in the file.]</system-reminder>\n\n1\t<style>\n2\t.vv-ry-card { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:10px 12px;min-width:0; }\n3\t.vv-ry-sec { font-size:10px;font-weight:bold;color:#444;letter-spacing:.07em;text-transform:uppercase;margin-bottom:6px; }\n4\t.vv-ry-row { display:flex;justify-content:space-between;align-items:baseline;gap:6px;margin:2px 0; }\n5\t.vv-ry-lbl { font-size:11px;color:#444;white-space:nowrap; }\n6\t.vv-ry-val { font-size:12px;color:#bbb;text-align:right; }\n7\t.vv-ry-sep { border:none;border-top:1px solid #1e1e1e;margin:7px 0; }\n8\t.vv-ry-pill { display:inline-block;font-size:10px;padding:1px 7px;border-radius:3px;font-weight:bold;letter-spacing:.04em; }\n9\t.vv-ry-pill.on { background:#0d1f0d;color:#4caf50;border:1px solid #1a3a1a; }\n10\t.vv-ry-pill.off { background:#1e1e1e;color:#555;border:1px solid #2a2a2a; }\n11\t.vv-ry-pill.warn { background:#1f1500;color:#ffb74d;border:1px solid #3a2800; }\n12\t.vv-ry-pill.err { background:#200d0d;color:#ef5350;border:1px solid #3a1a1a; }\n13\t.vv-ry-pill.run { background:#0a1a2a;color:#4a9eff;border:1px solid #1a3a5a; }\n14\t.vv-ry-dot { display:inline-block;width:7px;height:7px;border-radius:50%;flex-shrink:0;margin-right:5px;margin-bottom:-1px; }\n15\t\n16\t/* Window cards row */\n17\t.vv-ry-wins { grid-column:1/-1;display:grid;grid-template-columns:repeat(5,1fr);gap:10px; }\n18\t@media (max-width:900px) { .vv-ry-wins { grid-template-columns:repeat(2,1fr); } }\n19\t\n20\t.vv-ry-win { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:10px 12px; }\n21\t.vv-ry-win.enabled { border-color:#1a3a1a; }\n22\t.vv-ry-win.disabled { opacity:.55; }\n23\t.vv-ry-win-lbl { font-size:10px;font-weight:bold;letter-spacing:.08em;text-transform:uppercase;color:#555;margin-bottom:6px; }\n24\t.vv-ry-win-name{ font-size:15px;font-weight:bold;color:#888;margin-bottom:6px; }\n25\t.vv-ry-win-stat{ font-size:11px;color:#3a3a3a;margin-top:4px; }\n26\t.vv-ry-win-dur { font-size:10px;color:#3a3a3a; }\n27\t\n28\t/* Bandwidth section */\n29\t.vv-ry-bw { grid-column:1/-1;display:grid;grid-template-columns:5fr 3fr;gap:10px; }\n30\t@media (max-width:900px) { .vv-ry-bw { grid-template-columns:1fr; } }\n31\t.vv-ry-prof-row{ display:flex;align-items:center;gap:8px;margin:3px 0; }\n32\t.vv-ry-prof-nm { font-size:11px;color:#888;width:120px;flex-shrink:0; }\n33\t.vv-ry-prof-bar{ flex:1;height:5px;background:#1e1e1e;border-radius:2px;overflow:hidden; }\n34\t.vv-ry-prof-fill{ height:100%;border-radius:2px;transition:width .3s; }\n35\t.vv-ry-prof-cnt{ font-size:10px;color:#555;width:60px;text-align:right;flex-shrink:0; }\n36\t.vv-ry-run-row { display:grid;grid-template-columns:80px 90px 1fr 55px 48px;gap:4px;align-items:center;padding:2px 0;border-bottom:1px solid #1a1a1a; }\n37\t.vv-ry-run-row:last-child { border-bottom:none; }\n38\t.vv-ry-runs-scroll { max-height:200px;overflow-y:auto;scrollbar-width:none;-ms-overflow-style:none; }\n39\t.vv-ry-runs-scroll::-webkit-scrollbar { display:none; }\n40\t.vv-ry-run-dt { font-size:10px;color:#3a3a3a; }\n41\t.vv-ry-run-tm { font-size:10px;color:#333; }\n42\t.vv-ry-run-pr { font-size:11px;color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; }\n43\t.vv-ry-run-dur { font-size:10px;color:#555;text-align:right; }\n44\t.vv-ry-run-st { text-align:right; }\n45\t\n46\t/* Settings */\n47\t.vv-ry-set-grid { display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin-top:6px; }\n48\t@media (max-width:900px) { .vv-ry-set-grid { grid-template-columns:repeat(2,1fr); } }\n49\t.vv-ry-toggle { display:inline-flex;align-items:center;gap:8px;cursor:pointer;user-select:none; }\n50\t.vv-ry-toggle-track { width:32px;height:18px;border-radius:9px;background:#222;border:1px solid #333;position:relative;transition:background .15s,border-color .15s;flex-shrink:0; }\n51\t.vv-ry-toggle-track.on { background:#1a3a1a;border-color:#2d5a2d; }\n52\t.vv-ry-toggle-track::after { content:'';position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:#555;transition:left .15s,background .15s; }\n53\t.vv-ry-toggle-track.on::after { left:16px;background:#4caf50; }\n54\t.vv-ry-toggle-lbl { font-size:11px;color:#666; }\n55\t.vv-ry-set-row { display:flex;align-items:center;gap:10px;margin:4px 0; }\n56\t.vv-ry-set-lbl { font-size:11px;color:#555;width:130px;flex-shrink:0; }\n57\t.vv-ry-set-inp { background:#111;border:1px solid #2a2a2a;border-radius:3px;color:#bbb;font-size:12px;padding:3px 7px;width:80px;outline:none; }\n58\t.vv-ry-set-inp:focus { border-color:#444; }\n59\t.vv-ry-set-hint { font-size:10px;color:#3a3a3a; }\n60\t.vv-ry-save-btn { background:#1a2a1a;border:1px solid #2d4a2d;color:#4caf50;font-size:11px;padding:4px 14px;border-radius:3px;cursor:pointer;transition:background .15s; }\n61\t.vv-ry-save-btn:hover { background:#223a22; }\n62\t.vv-ry-save-btn:disabled { opacity:.4;cursor:default; }\n63\t.vv-ry-feedback { font-size:11px;margin-left:8px; }\n64\t\n65\t/* ── Info layer ──────────────────────────────────────────────────── */\n66\t.vv-ry-info-block { border:1px solid #1a1a1a;border-radius:4px;margin-bottom:6px;overflow:hidden; }\n67\t.vv-ry-info-hdr { padding:7px 12px;background:#111;cursor:pointer;font-size:11px;font-weight:600;\n68\t color:#666;display:flex;align-items:center;gap:6px;user-select:none; }\n69\t.vv-ry-info-hdr:hover { color:#999;background:#141414; }\n70\t.vv-ry-info-chev { font-size:10px;color:#333;flex-shrink:0; }\n71\t.vv-ry-info-body { padding:10px 14px;background:#0d0d0d; }\n72\t.vv-ry-info-cols { display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:8px 20px; }\n73\t.vv-ry-info-cols > div { font-size:11px;color:#555;line-height:1.5; }\n74\t.vv-ry-info-cols strong { color:#777; }\n75\t.vv-ry-info-cols code { font-size:10px;color:#4a7a4a;background:#080808;padding:1px 4px;border-radius:2px;font-family:monospace; }\n76\t\n77\t/* ── Profile editor flag checkboxes ─────────────────────────────── */\n78\t.vv-rp-flag { display:inline-flex;align-items:center;gap:4px;cursor:pointer;\n79\t padding:3px 8px;border:1px solid #1e1e1e;border-radius:3px;background:#0d0d0d;\n80\t font-size:10px;color:#555;user-select:none;transition:border-color .1s; }\n81\t.vv-rp-flag:hover { border-color:#2a3a2a;color:#888; }\n82\t.vv-rp-flag input[type=checkbox] { accent-color:#4caf50;width:11px;height:11px;cursor:pointer; }\n83\t.vv-rp-flag input:checked ~ span { color:#4caf50; }\n84\t.vv-rp-flag:has(input:checked) { border-color:#1a3a1a;background:#080f08; }\n85\t.vv-rp-flag em { font-style:normal;color:#2e4e2e; }\n86\t\n87\t/* ── Manual Sync card ──────────────────────────────────────────────── */\n88\t.vv-ms-row { display:flex;gap:10px;margin-bottom:8px;align-items:flex-start;flex-wrap:wrap; }\n89\t.vv-ms-col { display:flex;flex-direction:column;gap:4px;flex:1 1 200px;min-width:0; }\n90\t.vv-ms-lbl { font-size:9px;font-weight:700;color:#444;text-transform:uppercase;letter-spacing:.07em; }\n91\t.vv-ms-inp { background:#0d0d0d;border:1px solid #252525;border-radius:3px;color:#ccc;\n92\t font-size:12px;padding:5px 8px;width:100%;box-sizing:border-box;font-family:monospace;outline:none; }\n93\t.vv-ms-inp:focus { border-color:#3a5a3a; }\n94\t.vv-ms-sel { background:#0d0d0d;border:1px solid #252525;border-radius:3px;color:#ccc;\n95\t font-size:12px;padding:5px 8px;width:100%;box-sizing:border-box;outline:none;cursor:pointer; }\n96\t.vv-ms-sel:focus { border-color:#3a5a3a; }\n97\t.vv-ms-dirs { background:#080808;border:1px solid #1e1e1e;border-radius:3px;\n98\t max-height:160px;overflow-y:auto;margin-top:2px; }\n99\t.vv-ms-dir { padding:4px 10px;font-size:11px;color:#555;font-family:monospace;cursor:pointer;\n100\t white-space:nowrap;overflow:hidden;text-overflow:ellipsis;border-bottom:1px solid #111; }\n101\t.vv-ms-dir:hover { background:#0f1f0f;color:#4caf50; }\n102\t.vv-ms-dir.parent { color:#444;font-style:italic; }\n103\t.vv-ms-dir.parent:hover { color:#888; }\n104\t.vv-ms-optrow { display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin-bottom:8px; }\n105\t.vv-ms-chk { display:flex;align-items:center;gap:5px;cursor:pointer; }\n106\t.vv-ms-chk input { accent-color:#4caf50;width:13px;height:13px; }\n107\t.vv-ms-chk span { font-size:11px;color:#555; }\n108\t.vv-ms-run { background:#1a2a1a;border:1px solid #2d4a2d;color:#4caf50;font-size:12px;\n109\t font-weight:600;padding:6px 18px;border-radius:3px;cursor:pointer;letter-spacing:.03em; }\n110\t.vv-ms-run:hover { background:#223a22; }\n111\t.vv-ms-run:disabled { opacity:.4;cursor:default; }\n112\t.vv-ms-out { background:#080808;border:1px solid #1a1a1a;border-radius:3px;padding:8px 10px;\n113\t font-family:monospace;font-size:10px;color:#555;white-space:pre-wrap;word-break:break-all;\n114\t max-height:220px;overflow-y:auto;margin-top:8px;display:none; }\n115\t.vv-ms-badge { font-size:9px;padding:1px 7px;border-radius:3px;font-weight:700; }\n116\t@media (max-width:700px) { #vv-ms-body > div:first-child { grid-template-columns:1fr !important; } }\n117\t</style>\n118\t\n119\t<div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding:0 2px;\">\n120\t <span style=\"font-size:13px;font-weight:bold;color:#888;text-transform:uppercase;letter-spacing:.06em;\">Rsync</span>\n121\t <span style=\"font-size:11px;color:#3a3a3a;\" id=\"vv-ry-ts\"></span>\n122\t</div>\n123\t\n124\t<!-- Manual Sync card — static, not re-rendered by the poll loop -->\n125\t<div class=\"vv-ry-card\" id=\"vv-ms-card\" style=\"margin-bottom:12px;\">\n126\t <div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;\">\n127\t <div style=\"display:flex;align-items:center;gap:8px;\">\n128\t <span style=\"font-size:12px;font-weight:700;color:#888;text-transform:uppercase;letter-spacing:.06em;\">Manual Rsync</span>\n129\t <span id=\"vv-ms-badge\" class=\"vv-ms-badge\" style=\"display:none;\"></span>\n130\t </div>\n131\t <button onclick=\"vvMsToggle()\" style=\"font-size:10px;color:#4a6a4a;background:#0d1a0d;border:1px solid #1a3a1a;\n132\t border-radius:3px;padding:2px 10px;cursor:pointer;\" id=\"vv-ms-tog\">▾ Show</button>\n133\t </div>\n134\t <div id=\"vv-ms-body\" style=\"display:none;\">\n135\t\n136\t <!-- Two-column browse layout -->\n137\t <div style=\"display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:10px;\">\n138\t\n139\t <!-- ── Left: local source ── -->\n140\t <div>\n141\t <div class=\"vv-ms-lbl\" style=\"margin-bottom:5px;\">Local source</div>\n142\t <div style=\"display:flex;gap:5px;align-items:center;margin-bottom:4px;\">\n143\t <input class=\"vv-ms-inp\" id=\"vv-ms-local\" type=\"text\" placeholder=\"/mnt/user/…\"\n144\t style=\"flex:1;min-width:0;\" onkeydown=\"if(event.key==='Enter')vvMsBrowseLocal()\">\n145\t <button onclick=\"vvMsLocalNavUp()\" title=\"Parent\"\n146\t style=\"background:#111;border:1px solid #222;color:#555;border-radius:3px;\n147\t padding:4px 8px;cursor:pointer;font-size:12px;flex-shrink:0;\">↑</button>\n148\t <button onclick=\"vvMsBrowseLocal()\" id=\"vv-ms-lbrowse-btn\"\n149\t style=\"background:#111;border:1px solid #222;color:#4a9eff;border-radius:3px;\n150\t padding:4px 9px;cursor:pointer;font-size:11px;white-space:nowrap;flex-shrink:0;\">⟳ Browse</button>\n151\t </div>\n152\t <div id=\"vv-ms-local-dirs\" class=\"vv-ms-dirs\" style=\"display:none;\"></div>\n153\t <span id=\"vv-ms-lbrowse-err\" style=\"font-size:9px;color:#ef5350;display:none;margin-top:2px;display:block;\"></span>\n154\t <span style=\"font-size:9px;color:#2a2a2a;margin-top:3px;display:block;\">trailing / = sync contents · without / = sync the folder</span>\n155\t </div>\n156\t\n157\t <!-- ── Right: remote destination ── -->\n158\t <div>\n159\t <div style=\"display:flex;gap:8px;margin-bottom:6px;flex-wrap:wrap;\">\n160\t <div style=\"flex:1 1 140px;min-width:0;\">\n161\t <div class=\"vv-ms-lbl\" style=\"margin-bottom:3px;\">Remote server</div>\n162\t <select class=\"vv-ms-sel\" id=\"vv-ms-host\" onchange=\"vvMsHostChanged()\">\n163\t <option value=\"\">Loading hosts…</option>\n164\t </select>\n165\t <span id=\"vv-ms-host-status\" style=\"font-size:9px;color:#333;margin-top:2px;display:block;\"></span>\n166\t </div>\n167\t <div style=\"flex:0 1 80px;\">\n168\t <div class=\"vv-ms-lbl\" style=\"margin-bottom:3px;\">User</div>\n169\t <select class=\"vv-ms-sel\" id=\"vv-ms-user\">\n170\t <option value=\"root\">root</option>\n171\t </select>\n172\t </div>\n173\t </div>\n174\t <div class=\"vv-ms-lbl\" style=\"margin-bottom:5px;\">Remote path</div>\n175\t <div style=\"display:flex;gap:5px;align-items:center;margin-bottom:4px;\">\n176\t <input class=\"vv-ms-inp\" id=\"vv-ms-rpath\" type=\"text\" placeholder=\"/mnt/user/…\"\n177\t style=\"flex:1;min-width:0;\" onkeydown=\"if(event.key==='Enter')vvMsBrowse()\">\n178\t <button onclick=\"vvMsNavUp()\" title=\"Parent\"\n179\t style=\"background:#111;border:1px solid #222;color:#555;border-radius:3px;\n180\t padding:4px 8px;cursor:pointer;font-size:12px;flex-shrink:0;\">↑</button>\n181\t <button onclick=\"vvMsBrowse()\" id=\"vv-ms-browse-btn\"\n182\t style=\"background:#111;border:1px solid #222;color:#4a9eff;border-radius:3px;\n183\t padding:4px 9px;cursor:pointer;font-size:11px;white-space:nowrap;flex-shrink:0;\">⟳ Browse</button>\n184\t </div>\n185\t <div id=\"vv-ms-dirs\" class=\"vv-ms-dirs\" style=\"display:none;\"></div>\n186\t <span id=\"vv-ms-browse-err\" style=\"font-size:9px;color:#ef5350;display:none;margin-top:2px;display:block;\"></span>\n187\t </div>\n188\t </div>\n189\t\n190\t <!-- Rsync flags -->\n191\t <div style=\"border-top:1px solid #1a1a1a;padding-top:8px;margin-bottom:8px;\">\n192\t <div class=\"vv-ms-lbl\" style=\"margin-bottom:6px;\">Rsync flags\n193\t <span style=\"font-size:9px;font-weight:normal;color:#2a2a2a;margin-left:4px;\">does not inherit defaults — check every desired flag</span>\n194\t </div>\n195\t <div style=\"display:flex;gap:5px;flex-wrap:wrap;\">\n196\t <label class=\"vv-rp-flag\"><input type=\"checkbox\" data-flag=\"-av\" checked onchange=\"vvMsUpdatePreview()\"><span>-av</span></label>\n197\t <label class=\"vv-rp-flag\"><input type=\"checkbox\" data-flag=\"--human-readable\" checked onchange=\"vvMsUpdatePreview()\"><span>--human-readable</span></label>\n198\t <label class=\"vv-rp-flag\"><input type=\"checkbox\" data-flag=\"--stats\" checked onchange=\"vvMsUpdatePreview()\"><span>--stats</span></label>\n199\t <span style=\"border-left:1px solid #1e1e1e;margin:0 3px;\"></span>\n200\t <label class=\"vv-rp-flag\"><input type=\"checkbox\" data-flag=\"--delete\" onchange=\"vvMsUpdatePreview()\"><span>--delete</span></label>\n201\t <label class=\"vv-rp-flag\"><input type=\"checkbox\" data-flag=\"--inplace\" onchange=\"vvMsUpdatePreview()\"><span>--inplace</span></label>\n202\t <label class=\"vv-rp-flag\"><input type=\"checkbox\" data-flag=\"--no-whole-file\" onchange=\"vvMsUpdatePreview()\"><span>--no-whole-file</span></label>\n203\t <label class=\"vv-rp-flag\"><input type=\"checkbox\" data-flag=\"--checksum\" onchange=\"vvMsUpdatePreview()\"><span>--checksum</span></label>\n204\t <label class=\"vv-rp-flag\"><input type=\"checkbox\" data-flag=\"--compress\" onchange=\"vvMsUpdatePreview()\"><span>--compress</span></label>\n205\t <span style=\"border-left:1px solid #1e1e1e;margin:0 3px;\"></span>\n206\t <label class=\"vv-rp-flag\"><input type=\"checkbox\" id=\"vv-ms-dryrun\" data-flag=\"--dry-run\" onchange=\"vvMsUpdatePreview()\"><span>--dry-run</span></label>\n207\t </div>\n208\t <div style=\"margin-top:6px;background:#080808;border:1px solid #181818;border-radius:3px;\n209\t padding:4px 8px;font-family:monospace;font-size:10px;color:#3a5a3a;display:flex;gap:8px;align-items:baseline;\">\n210\t <span style=\"font-size:9px;color:#2a2a2a;flex-shrink:0;\">rsync</span>\n211\t <span id=\"vv-ms-flags-preview\"></span>\n212\t <span id=\"vv-ms-bwlimit-preview\" style=\"color:#2e4e2e;\"></span>\n213\t <span style=\"color:#2a2a2a;\">…src host:dst</span>\n214\t </div>\n215\t </div>\n216\t\n217\t <!-- BW limit + SSH key -->\n218\t <div class=\"vv-ms-optrow\" style=\"margin-bottom:8px;\">\n219\t <div style=\"display:flex;align-items:center;gap:6px;\">\n220\t <span style=\"font-size:11px;color:#444;\">BW limit</span>\n221\t <input class=\"vv-ms-inp\" id=\"vv-ms-bw\" type=\"number\" min=\"0\" value=\"0\"\n222\t style=\"width:72px;padding:3px 6px;\" oninput=\"vvMsUpdatePreview()\">\n223\t <span style=\"font-size:10px;color:#333;\">KB/s (0 = unlimited)</span>\n224\t </div>\n225\t <label class=\"vv-ms-chk\">\n226\t <input type=\"checkbox\" id=\"vv-ms-usekey\" checked>\n227\t <span>SSH key: <code id=\"vv-ms-key-info\"\n228\t style=\"font-size:9px;color:#2a3a2a;margin-left:2px;font-family:monospace;\">loading…</code></span>\n229\t </label>\n230\t </div>\n231\t\n232\t <!-- Run + Show + Stop + status -->\n233\t <div style=\"display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:4px;\">\n234\t <button class=\"vv-ms-run\" id=\"vv-ms-run-btn\" onclick=\"vvMsRun()\">▶ Run Rsync</button>\n235\t <button id=\"vv-ms-show-btn\" onclick=\"vvMsToggleOutput()\"\n236\t style=\"background:#111;border:1px solid #252525;color:#555;font-size:11px;\n237\t padding:5px 14px;border-radius:3px;cursor:pointer;\">▾ Show Rsync</button>\n238\t <button id=\"vv-ms-stop-btn\" onclick=\"vvMsStop()\"\n239\t style=\"display:none;background:#2a1010;border:1px solid #5a2020;color:#ef5350;\n240\t font-size:11px;padding:5px 14px;border-radius:3px;cursor:pointer;font-weight:600;\">■ Stop</button>\n241\t <span id=\"vv-ms-run-status\" style=\"font-size:10px;color:#444;\"></span>\n242\t </div>\n243\t\n244\t <!-- Output (auto-opens on run) -->\n245\t <pre class=\"vv-ms-out\" id=\"vv-ms-out\" style=\"display:none;margin-top:8px;\"></pre>\n246\t </div>\n247\t</div>\n248\t\n249\t<div id=\"vv-ry-grid\" style=\"display:grid;grid-template-columns:repeat(8,1fr);gap:12px;margin-bottom:12px;\"></div>\n250\t\n251\t<!-- ── Info / Profile-editor layer ───────────────────────────────────────── -->\n252\t<div id=\"vv-ry-layer\">\n253\t\n254\t <!-- Info view (default) -->\n255\t <div id=\"vv-ry-info-view\">\n256\t <div style=\"display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;flex-wrap:wrap;gap:8px;\">\n257\t <span style=\"font-size:11px;font-weight:700;color:#666;text-transform:uppercase;letter-spacing:.07em;\">Rsync Reference</span>\n258\t <button onclick=\"vvRpOpen()\"\n259\t style=\"background:#0a1a2a;border:1px solid #1a3a5a;color:#4a9eff;font-size:11px;\n260\t padding:4px 14px;border-radius:3px;cursor:pointer;font-weight:600;\">⚙ Profile Editor</button>\n261\t </div>\n262\t\n263\t <!-- How to use this page -->\n264\t <div class=\"vv-ry-info-block\">\n265\t <div class=\"vv-ry-info-hdr\" onclick=\"vvRyInfoToggle(this)\">\n266\t <span class=\"vv-ry-info-chev\">▾</span> How to use this page\n267\t </div>\n268\t <div class=\"vv-ry-info-body\">\n269\t <div class=\"vv-ry-info-cols\">\n270\t <div><strong>Status card</strong> — shows whether the global rsync gate is open. Red = all syncs blocked regardless of tier toggles.</div>\n271\t <div><strong>Sync windows</strong> — per-tier enable/disable. A tier can be enabled here but still blocked by the global gate.</div>\n272\t <div><strong>Settings → Global gate</strong> — RSYNC_ENABLED in master.conf. Master switch; turn off during rebuilds or maintenance.</div>\n273\t <div><strong>Settings → Tier toggles</strong> — CRITICAL/DAILY/etc._RSYNC_ENABLED. Disable one tier without affecting others.</div>\n274\t <div><strong>Bandwidth history</strong> — last 30 days of per-profile rsync runs. Bar = relative time spent. Click recent runs for detail.</div>\n275\t <div><strong>Manual Rsync</strong> — one-off transfer to any partner host. Browse remote dirs via SSH. Runs in background, output streams below.</div>\n276\t <div><strong>Profile Editor</strong> — create and edit named profiles used by appdata syncs. Each profile controls flags, BW limit, container stop lists, and excludes.</div>\n277\t </div>\n278\t </div>\n279\t </div>\n280\t\n281\t <!-- Sync windows explained -->\n282\t <div class=\"vv-ry-info-block\">\n283\t <div class=\"vv-ry-info-hdr\" onclick=\"vvRyInfoToggle(this)\">\n284\t <span class=\"vv-ry-info-chev\">▸</span> Sync windows\n285\t </div>\n286\t <div class=\"vv-ry-info-body\" style=\"display:none;\">\n287\t <div class=\"vv-ry-info-cols\">\n288\t <div><strong>Critical (30 min)</strong> — appdata (arrs, auth, databases), partnership health check, play state sync. Designed to be fast — uses dirty-sync profiles where possible.</div>\n289\t <div><strong>Intermediate (4 hr)</strong> — arr sync between the nightly runs. Catches new indexer hits without waiting until midnight.</div>\n290\t <div><strong>Daily (nightly)</strong> — media shares, arr maintenance, media cleaner, docker updates, daily container restarts.</div>\n291\t <div><strong>Weekly</strong> — bulk media shares, update checks, weekly restarts, SMART health digest.</div>\n292\t <div><strong>Fallback</strong> — fires when a server comes back online after being the active fallback. Syncs any changes that happened during downtime.</div>\n293\t <div><strong>Two-tier toggle</strong> — global gate (RSYNC_ENABLED) blocks everything. Per-tier flags (e.g. DAILY_RSYNC_ENABLED) disable just that window. Both must be ON for syncs to run.</div>\n294\t </div>\n295\t </div>\n296\t </div>\n297\t\n298\t <!-- Profile system -->\n299\t <div class=\"vv-ry-info-block\">\n300\t <div class=\"vv-ry-info-hdr\" onclick=\"vvRyInfoToggle(this)\">\n301\t <span class=\"vv-ry-info-chev\">▸</span> Profile system\n302\t </div>\n303\t <div class=\"vv-ry-info-body\" style=\"display:none;\">\n304\t <div class=\"vv-ry-info-cols\">\n305\t <div><strong>What is a profile?</strong> — A named set of rsync options applied to a specific appdata directory. Appdata shares match a profile by their directory basename (lowercased).</div>\n306\t <div><strong>PROFILE_RSYNC_OPTS</strong> — Full rsync flag string for this profile. Does NOT inherit DEFAULT_RSYNC_OPTS — list every desired flag explicitly. Use <code>$BW_LIMIT</code> to reference the profile's bandwidth limit.</div>\n307\t <div><strong>PROFILE_BW_LIMIT</strong> — Bandwidth cap in KB/s. Overrides the global BW_LIMIT for this profile.</div>\n308\t <div><strong>PROFILE_RETRY_COUNT / SLEEP</strong> — How many times to retry on failure, and how long to wait between attempts.</div>\n309\t <div><strong>PROFILE_CRITICAL_CONTAINER_NAMES</strong> — Containers stopped on BOTH hosts before rsync. Local stops first (flush DBs), then remote. Only running containers get restarted — stopped ones stay stopped.</div>\n310\t <div><strong>PROFILE_DELAYED_CONTAINERS</strong> — Containers that need a delay after the others start (e.g. NextCloud waits for Postgres).</div>\n311\t <div><strong>PROFILE_CONTAINER_DELAY</strong> — Seconds to wait before starting delayed containers.</div>\n312\t <div><strong>PROFILE_EXCLUDE_DIRS</strong> — Space-separated patterns excluded from the rsync. Supports globs (e.g. <code>logs *.tmp *.db-wal</code>).</div>\n313\t <div><strong>PROFILE_REMOTE_RESTART_CONTAINERS</strong> — Used by dirty-sync profiles. Restarts these containers on the remote after sync so it picks up the new data.</div>\n314\t <div><strong>Clean vs dirty sync</strong> — Clean: stop containers, <code>--delete</code>, full mirror. Dirty: containers keep running, WAL files excluded, <code>--inplace --no-whole-file</code> for safe partial writes. Use dirty for frequent critical syncs.</div>\n315\t <div><strong>App layer</strong> — Scripts add their appdata directory to HOST*_CRITICAL_SYNC_SHARES or HOST*_DAILY_SYNC_SHARES using <code>/path/to/dir|profile-name</code> syntax. Creating the profile here makes it available for that reference.</div>\n316\t </div>\n317\t </div>\n318\t </div>\n319\t\n320\t <!-- Rsync flags reference -->\n321\t <div class=\"vv-ry-info-block\">\n322\t <div class=\"vv-ry-info-hdr\" onclick=\"vvRyInfoToggle(this)\">\n323\t <span class=\"vv-ry-info-chev\">▸</span> Common flag combinations\n324\t </div>\n325\t <div class=\"vv-ry-info-body\" style=\"display:none;\">\n326\t <div class=\"vv-ry-info-cols\">\n327\t <div><strong>Clean sync (weekly appdata)</strong><br><code>-av --human-readable --bwlimit=$BW_LIMIT --delete</code><br>Full mirror — removes files on destination that no longer exist at source. Safe when containers are stopped.</div>\n328\t <div><strong>Dirty sync (every 30 min)</strong><br><code>-av --human-readable --bwlimit=$BW_LIMIT --delete --inplace --no-whole-file</code><br>Containers stay running. <code>--inplace</code> writes directly to destination file (avoids temp-file rename issues). <code>--no-whole-file</code> uses delta transfer. Exclude WAL files to avoid corrupt state.</div>\n329\t <div><strong>Database sync (arrs)</strong><br><code>-av --info=progress2 --human-readable --bwlimit=$BW_LIMIT --delete --inplace</code><br>Stop containers first. <code>--info=progress2</code> gives cleaner progress output in logs. Lower BW limit to not starve other syncs running concurrently.</div>\n330\t <div><strong>Large media (weekly)</strong><br><code>-av --human-readable --bwlimit=$BW_LIMIT --delete --inplace --no-whole-file</code><br>High BW limit, inplace for large files, delete to keep mirror clean. No container stops — media files are read-only.</div>\n331\t <div><code>--delete</code> — remove files at destination that no longer exist at source. Essential for a true mirror.</div>\n332\t <div><code>--inplace</code> — write file data directly into destination file instead of creating a temp copy first. Faster for large files; required for some databases.</div>\n333\t <div><code>--no-whole-file</code> — force delta transfer even over fast LAN. Reduces data sent for files that change incrementally.</div>\n334\t <div><code>--checksum</code> — compare files by checksum rather than mtime+size. Slower but catches any corruption. Use for archive syncs, not frequent syncs.</div>\n335\t </div>\n336\t </div>\n337\t </div>\n338\t </div><!-- /info view -->\n339\t\n340\t <!-- Profile editor view (hidden until button click) -->\n341\t <div id=\"vv-rp-view\" style=\"display:none;\">\n342\t <div style=\"display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap;\">\n343\t <button onclick=\"vvRpClose()\"\n344\t style=\"background:#111;border:1px solid #222;color:#555;font-size:11px;\n345\t padding:4px 12px;border-radius:3px;cursor:pointer;\">← Rsync Info</button>\n346\t <span style=\"font-size:11px;font-weight:700;color:#666;text-transform:uppercase;letter-spacing:.07em;\">Profile Editor</span>\n347\t <div style=\"margin-left:auto;display:flex;align-items:center;gap:6px;\">\n348\t <select id=\"vv-rp-selector\"\n349\t style=\"background:#111;border:1px solid #2a2a2a;color:#bbb;font-size:11px;\n350\t padding:3px 8px;border-radius:3px;outline:none;cursor:pointer;max-width:200px;\"\n351\t onchange=\"vvRpOnSelect(this.value)\">\n352\t <option value=\"\">Loading…</option>\n353\t </select>\n354\t <button onclick=\"vvRpNew()\"\n355\t style=\"background:#0a1a0a;border:1px solid #1a3a1a;color:#4caf50;\n356\t font-size:11px;padding:3px 12px;border-radius:3px;cursor:pointer;font-weight:600;\">+ New</button>\n357\t </div>\n358\t </div>\n359\t\n360\t <!-- Empty state -->\n361\t <div id=\"vv-rp-empty\" class=\"vv-ry-card\" style=\"color:#333;font-size:11px;padding:10px 14px;\">\n362\t Select a profile above to edit, or click + New to create one.\n363\t </div>\n364\t\n365\t <!-- Profile form -->\n366\t <div id=\"vv-rp-form\" style=\"display:none;\">\n367\t <div class=\"vv-ry-card\">\n368\t\n369\t <!-- Name row -->\n370\t <div style=\"display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap;\">\n371\t <span style=\"font-size:10px;font-weight:700;color:#555;text-transform:uppercase;letter-spacing:.06em;width:110px;flex-shrink:0;\">Profile name</span>\n372\t <input id=\"vv-rp-name\" type=\"text\"\n373\t style=\"background:#0d0d0d;border:1px solid #2a2a2a;border-radius:3px;color:#ccc;\n374\t font-size:12px;padding:4px 8px;font-family:monospace;outline:none;width:200px;\"\n375\t placeholder=\"e.g. my-app\">\n376\t <span id=\"vv-rp-name-hint\" style=\"font-size:10px;color:#333;\"></span>\n377\t </div>\n378\t <hr class=\"vv-ry-sep\">\n379\t\n380\t <!-- Rsync opts — checkboxes + preview -->\n381\t <div style=\"margin-bottom:10px;\">\n382\t <div style=\"font-size:10px;font-weight:700;color:#555;text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px;\">\n383\t Rsync opts\n384\t <span style=\"font-size:9px;font-weight:normal;color:#2a2a2a;margin-left:6px;\">does NOT inherit defaults — list every desired flag</span>\n385\t </div>\n386\t <div style=\"display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px;\" id=\"vv-rp-flag-group\">\n387\t <label class=\"vv-rp-flag\"><input type=\"checkbox\" data-flag=\"-av\" onchange=\"vvRpBuildOpts()\"><span>-av</span></label>\n388\t <label class=\"vv-rp-flag\"><input type=\"checkbox\" data-flag=\"--human-readable\" onchange=\"vvRpBuildOpts()\"><span>--human-readable</span></label>\n389\t <label class=\"vv-rp-flag\"><input type=\"checkbox\" data-flag=\"--info=progress2\" onchange=\"vvRpBuildOpts()\"><span>--info=progress2</span></label>\n390\t <span style=\"border-left:1px solid #1e1e1e;margin:0 4px;\"></span>\n391\t <label class=\"vv-rp-flag\"><input type=\"checkbox\" data-flag=\"--delete\" onchange=\"vvRpBuildOpts()\"><span>--delete</span></label>\n392\t <label class=\"vv-rp-flag\"><input type=\"checkbox\" data-flag=\"--inplace\" onchange=\"vvRpBuildOpts()\"><span>--inplace</span></label>\n393\t <label class=\"vv-rp-flag\"><input type=\"checkbox\" data-flag=\"--no-whole-file\" onchange=\"vvRpBuildOpts()\"><span>--no-whole-file</span></label>\n394\t <label class=\"vv-rp-flag\"><input type=\"checkbox\" data-flag=\"--checksum\" onchange=\"vvRpBuildOpts()\"><span>--checksum</span></label>\n395\t <span style=\"border-left:1px solid #1e1e1e;margin:0 4px;\"></span>\n396\t <label class=\"vv-rp-flag\"><input type=\"checkbox\" data-flag=\"--bwlimit=$BW_LIMIT\" onchange=\"vvRpBuildOpts()\"><span>--bwlimit=<em>$BW_LIMIT</em></span></label>\n397\t <label class=\"vv-rp-flag\"><input type=\"checkbox\" data-flag=\"--compress\" onchange=\"vvRpBuildOpts()\"><span>--compress</span></label>\n398\t </div>\n399\t <div style=\"display:flex;align-items:center;gap:8px;margin-bottom:6px;\">\n400\t <span style=\"font-size:10px;color:#444;flex-shrink:0;\">Extra flags</span>\n401\t <input id=\"vv-rp-opts-extra\" type=\"text\"\n402\t style=\"background:#0d0d0d;border:1px solid #1e1e1e;border-radius:3px;color:#888;\n403\t font-size:11px;padding:3px 7px;font-family:monospace;outline:none;flex:1;\"\n404\t placeholder=\"any additional flags\"\n405\t oninput=\"vvRpBuildOpts()\">\n406\t </div>\n407\t <div style=\"background:#080808;border:1px solid #181818;border-radius:3px;padding:5px 9px;\n408\t font-family:monospace;font-size:11px;color:#4a6a4a;display:flex;align-items:baseline;gap:8px;\">\n409\t <span style=\"font-size:9px;color:#2a2a2a;flex-shrink:0;\">rsync_opts =</span>\n410\t <span id=\"vv-rp-opts-preview\" style=\"word-break:break-all;\"></span>\n411\t </div>\n412\t <input type=\"hidden\" id=\"vv-rp-rsync_opts\">\n413\t </div>\n414\t <hr class=\"vv-ry-sep\">\n415\t\n416\t <!-- Numeric fields -->\n417\t <div style=\"display:flex;gap:12px;flex-wrap:wrap;margin-bottom:10px;\">\n418\t <div>\n419\t <div style=\"font-size:9px;color:#444;margin-bottom:3px;\">BW limit <span style=\"color:#2a2a2a;\">KB/s</span></div>\n420\t <input class=\"vv-ry-set-inp\" id=\"vv-rp-bw_limit\" type=\"number\" min=\"0\" style=\"width:80px;\">\n421\t </div>\n422\t <div>\n423\t <div style=\"font-size:9px;color:#444;margin-bottom:3px;\">Retry count</div>\n424\t <input class=\"vv-ry-set-inp\" id=\"vv-rp-retry_count\" type=\"number\" min=\"0\" max=\"20\" style=\"width:60px;\">\n425\t </div>\n426\t <div>\n427\t <div style=\"font-size:9px;color:#444;margin-bottom:3px;\">Retry sleep <span style=\"color:#2a2a2a;\">s</span></div>\n428\t <input class=\"vv-ry-set-inp\" id=\"vv-rp-sleep\" type=\"number\" min=\"0\" style=\"width:70px;\">\n429\t </div>\n430\t <div>\n431\t <div style=\"font-size:9px;color:#444;margin-bottom:3px;\">Container delay <span style=\"color:#2a2a2a;\">s</span></div>\n432\t <input class=\"vv-ry-set-inp\" id=\"vv-rp-container_delay\" type=\"number\" min=\"0\" style=\"width:70px;\">\n433\t </div>\n434\t </div>\n435\t <hr class=\"vv-ry-sep\">\n436\t\n437\t <!-- Container / path fields -->\n438\t <div style=\"display:flex;flex-direction:column;gap:8px;\">\n439\t <div>\n440\t <div style=\"font-size:9px;color:#444;margin-bottom:3px;\">Stop containers <span style=\"color:#2a2a2a;\">space-separated — stopped on both hosts before rsync, restarted after</span></div>\n441\t <input class=\"vv-ry-set-inp\" id=\"vv-rp-critical_containers\" type=\"text\" style=\"width:100%;box-sizing:border-box;font-family:monospace;\">\n442\t </div>\n443\t <div>\n444\t <div style=\"font-size:9px;color:#444;margin-bottom:3px;\">Delayed containers <span style=\"color:#2a2a2a;\">start after delay (e.g. app waits for its database)</span></div>\n445\t <input class=\"vv-ry-set-inp\" id=\"vv-rp-delayed_containers\" type=\"text\" style=\"width:100%;box-sizing:border-box;font-family:monospace;\">\n446\t </div>\n447\t <div>\n448\t <div style=\"font-size:9px;color:#444;margin-bottom:3px;\">Exclude dirs <span style=\"color:#2a2a2a;\">space-separated globs — e.g. <code>logs *.tmp *.db-wal *.db-shm</code></span></div>\n449\t <input class=\"vv-ry-set-inp\" id=\"vv-rp-exclude_dirs\" type=\"text\" style=\"width:100%;box-sizing:border-box;font-family:monospace;\">\n450\t </div>\n451\t <div>\n452\t <div style=\"font-size:9px;color:#444;margin-bottom:3px;\">Remote restart <span style=\"color:#2a2a2a;\">dirty-sync only — restarted on remote after sync so it picks up changes</span></div>\n453\t <input class=\"vv-ry-set-inp\" id=\"vv-rp-remote_restart\" type=\"text\" style=\"width:100%;box-sizing:border-box;font-family:monospace;\">\n454\t </div>\n455\t </div>\n456\t\n457\t <div style=\"margin-top:14px;display:flex;align-items:center;gap:8px;flex-wrap:wrap;\">\n458\t <button class=\"vv-ry-save-btn\" onclick=\"vvRpSave()\">Save Profile</button>\n459\t <button id=\"vv-rp-del-btn\" onclick=\"vvRpDelete()\"\n460\t style=\"background:#1a0a0a;border:1px solid #3a1a1a;color:#ef5350;font-size:11px;\n461\t padding:4px 12px;border-radius:3px;cursor:pointer;display:none;\">Delete</button>\n462\t <span id=\"vv-rp-fb\" class=\"vv-ry-feedback\"></span>\n463\t </div>\n464\t </div>\n465\t </div>\n466\t </div><!-- /profile view -->\n467\t\n468\t</div><!-- /layer -->\n469\t\n470\t<script>\n471\t// Shared state — globals so IIFE internals and onclick handlers can both reach them\n472\tvar _vvRyWinOpen = new Set(); // which window panels are expanded\n473\tvar _vvRyLastData = null; // last successful poll response\n474\tvar _vvRyEditMode = false; // true = skip poll re-renders\n475\tvar _vvRyWinEditing = null; // key of window currently in edit mode\n476\tvar _vvRyWinEState = {}; // {key: {scripts:[{id,enabled}], shares:[{path,profile}]}}\n477\tvar _vvRyScriptLib = null; // cached {Folder:[{id,label}]} — null = not yet loaded\n478\tvar _vvRyProfiles = null; // cached profile name list\n479\t\n480\t// View-mode panel inner HTML — shared by _windowsRow (inside IIFE) and vvRyWinCancel (outside)\n481\tfunction _vvRyViewPanelInner(key, cfg) {\n482\t const scripts = cfg.scripts || [];\n483\t const shares = cfg.shares || [];\n484\t let inner = '';\n485\t\n486\t if (scripts.length) {\n487\t inner += `<div style=\"font-size:9px;font-weight:700;color:#2a2a2a;text-transform:uppercase;letter-spacing:.07em;margin-bottom:4px;\">Scripts</div>`;\n488\t for (const s of scripts) {\n489\t const parts = s.split('/');\n490\t const folder = parts.length > 1 ? parts.slice(0, -1).join('/') : '';\n491\t const name = parts[parts.length - 1].replace(/\\.sh$/, '');\n492\t inner += `<div style=\"font-size:10px;color:#555;padding:1px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;\">\n493\t ${folder ? `<span style=\"color:#2a2a2a;\">${folder}/</span>` : ''}<span>${name}</span>\n494\t </div>`;\n495\t }\n496\t }\n497\t\n498\t if (shares.length) {\n499\t if (scripts.length) inner += `<div style=\"border-top:1px solid #161616;margin:6px 0;\"></div>`;\n500\t inner += `<div style=\"font-size:9px;font-weight:700;color:#2a2a2a;text-transform:uppercase;letter-spacing:.07em;margin-bottom:4px;\">Sync shares</div>`;\n501\t for (const sh of shares) {\n502\t const [rawPath, profile] = sh.split('|');\n503\t const basename = rawPath.trim().split('/').pop();\n504\t inner += `<div style=\"font-size:10px;color:#555;padding:1px 0;display:flex;align-items:center;gap:5px;min-width:0;\">\n505\t <span style=\"overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;\" title=\"${rawPath.trim()}\">${basename}</span>\n506\t ${profile ? `<span style=\"font-size:9px;color:#2a3a2a;flex-shrink:0;font-family:monospace;\">${profile.trim()}</span>` : ''}\n507\t </div>`;\n508\t }\n509\t }\n510\t\n511\t if (scripts.length || shares.length) {\n512\t inner += `<div style=\"margin-top:7px;padding-top:6px;border-top:1px solid #161616;\">\n513\t <button onclick=\"event.stopPropagation();vvRyWinEdit('${key}')\"\n514\t style=\"background:#0a1a2a;border:1px solid #1a3a5a;color:#4a9eff;font-size:10px;\n515\t padding:2px 10px;border-radius:3px;cursor:pointer;\">⚙ Edit</button>\n516\t </div>`;\n517\t }\n518\t\n519\t return inner;\n520\t}\n521\t\n522\t(function() {\n523\t\n524\tconst WIN_META = {\n525\t critical: { label: 'Critical', cadence: '30 min' },\n526\t intermediate: { label: 'Intermediate', cadence: '4 hr' },\n527\t daily: { label: 'Daily', cadence: 'nightly' },\n528\t weekly: { label: 'Weekly', cadence: 'weekly' },\n529\t fallback: { label: 'Fallback', cadence: 'on handback' },\n530\t};\n531\t\n532\tconst PROF_COLORS = [\n533\t '#4caf50','#4a9eff','#ffb74d','#ce93d8','#ef5350',\n534\t '#4dd0e1','#a5d6a7','#ff8a65','#90caf9','#f48fb1',\n535\t];\n536\t\n537\tfunction _rel(ts) {\n538\t if (!ts) return '—';\n539\t const d = Math.floor(Date.now() / 1000) - ts;\n540\t if (d < 60) return 'just now';\n541\t if (d < 3600) return Math.floor(d / 60) + 'm ago';\n542\t if (d < 86400) return Math.floor(d / 3600) + 'h ' + Math.floor((d % 3600) / 60) + 'm ago';\n543\t return Math.floor(d / 86400) + 'd ago';\n544\t}\n545\t\n546\tfunction _dur(s) {\n547\t if (!s) return '—';\n548\t const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;\n549\t if (h) return h + 'h ' + m + 'm';\n550\t if (m) return m + 'm ' + sec + 's';\n551\t return sec + 's';\n552\t}\n553\t\n554\tfunction _fmtBytes(b) {\n555\t if (!b) return '';\n556\t const G = 1073741824, M = 1048576;\n557\t if (b >= G) return (b / G).toFixed(1) + ' GB';\n558\t if (b >= M) return (b / M).toFixed(0) + ' MB';\n559\t return (b / 1024).toFixed(0) + ' KB';\n560\t}\n561\t\n562\t// ── Status + active card ──────────────────────────────────────────────────────\n563\tfunction _statusCard(data) {\n564\t const en = data.enabled;\n565\t const dotC = en ? '#4caf50' : '#ef5350';\n566\t const active = data.active || [];\n567\t\n568\t let activeHtml;\n569\t if (active.length === 0) {\n570\t activeHtml = '<div style=\"color:#333;font-size:11px;margin-top:4px;\">No transfers running</div>';\n571\t } else {\n572\t activeHtml = active.map(a =>\n573\t `<div style=\"display:flex;align-items:center;gap:7px;margin:3px 0;\">\n574\t <span class=\"vv-ry-pill run\">RUNNING</span>\n575\t <span style=\"font-size:12px;color:#bbb;\">${a.profile}</span>\n576\t <span style=\"font-size:11px;color:#555;margin-left:auto;\">${_dur(a.elapsed)}</span>\n577\t </div>`\n578\t ).join('');\n579\t }\n580\t\n581\t return `<div class=\"vv-ry-card\" style=\"grid-column:span 4;\">\n582\t <div style=\"display:flex;align-items:center;gap:8px;margin-bottom:10px;\">\n583\t <span class=\"vv-ry-dot\" style=\"background:${dotC}\"></span>\n584\t <span style=\"font-size:13px;font-weight:bold;color:${en ? '#4caf50' : '#ef5350'};\">\n585\t ${en ? 'RSYNC ENABLED' : 'RSYNC DISABLED'}\n586\t </span>\n587\t <span class=\"vv-ry-pill ${en ? 'on' : 'err'}\" style=\"margin-left:auto;\">\n588\t ${en ? 'global gate open' : 'all syncs blocked'}\n589\t </span>\n590\t </div>\n591\t <hr class=\"vv-ry-sep\">\n592\t <div class=\"vv-ry-sec\">Active transfers</div>\n593\t ${activeHtml}\n594\t </div>`;\n595\t}\n596\t\n597\t// ── Last sync summary card ────────────────────────────────────────────────────\n598\tfunction _lastSyncCard(data) {\n599\t const ls = data.last_sync || {};\n600\t const wins = ['critical','intermediate','daily','weekly'];\n601\t const rows = wins.map(k => {\n602\t const e = ls[k];\n603\t if (!e || !e.ts) return `<div class=\"vv-ry-row\">\n604\t <span class=\"vv-ry-lbl\">${WIN_META[k]?.label || k}</span>\n605\t <span class=\"vv-ry-val\" style=\"color:#333;\">never</span>\n606\t </div>`;\n607\t const cls = e.status === 'success' ? 'color:#4caf50' : 'color:#ef5350';\n608\t return `<div class=\"vv-ry-row\">\n609\t <span class=\"vv-ry-lbl\">${WIN_META[k]?.label || k}</span>\n610\t <span class=\"vv-ry-val\">\n611\t <span style=\"${cls}\">${e.status}</span>\n612\t <span style=\"color:#444;margin-left:6px;\">${_rel(e.ts)}</span>\n613\t ${e.duration ? `<span style=\"color:#3a3a3a;margin-left:4px;\">${_dur(e.duration)}</span>` : ''}\n614\t </span>\n615\t </div>`;\n616\t });\n617\t\n618\t return `<div class=\"vv-ry-card\" style=\"grid-column:span 4;\">\n619\t <div class=\"vv-ry-sec\">Last completed run</div>\n620\t ${rows.join('')}\n621\t </div>`;\n622\t}\n623\t\n624\t// ── Window cards row ──────────────────────────────────────────────────────────\n625\tfunction _windowsRow(data) {\n626\t const windows = data.windows || {};\n627\t const lastSync = data.last_sync || {};\n628\t const winArr = data.win_arrays || {};\n629\t\n630\t const cards = Object.entries(WIN_META).map(([key, meta]) => {\n631\t const enabled = windows[key] !== false;\n632\t const ls = lastSync[key];\n633\t const cls = enabled ? 'enabled' : 'disabled';\n634\t const cfg = winArr[key] || {};\n635\t const scripts = cfg.scripts || [];\n636\t const shares = cfg.shares || [];\n637\t\n638\t let statusHtml = '<div class=\"vv-ry-win-stat\">No run history</div>';\n639\t if (ls && ls.ts) {\n640\t const okCol = ls.status === 'success' ? '#4caf50' : '#ef5350';\n641\t statusHtml = `<div class=\"vv-ry-win-stat\" style=\"color:${okCol};\">${ls.status}</div>\n642\t <div class=\"vv-ry-win-dur\">${_rel(ls.ts)}${ls.duration ? ' · ' + _dur(ls.duration) : ''}</div>`;\n643\t }\n644\t\n645\t const expandable = !!(scripts.length || shares.length);\n646\t const panelHtml = expandable\n647\t ? `<div class=\"vv-ry-win-panel\" style=\"display:none;margin-top:9px;padding-top:8px;border-top:1px solid #1e1e1e;\">${_vvRyViewPanelInner(key, cfg)}</div>`\n648\t : '';\n649\t\n650\t return `<div class=\"vv-ry-win ${cls}\" data-win-key=\"${key}\"\n651\t ${expandable ? 'onclick=\"vvRyWinExpand(this)\" style=\"cursor:pointer;\"' : ''}>\n652\t <div style=\"display:flex;justify-content:space-between;align-items:flex-start;gap:4px;\">\n653\t <div style=\"min-width:0;\">\n654\t <div class=\"vv-ry-win-lbl\">${meta.cadence}</div>\n655\t <div class=\"vv-ry-win-name\">${meta.label}</div>\n656\t <span class=\"vv-ry-pill ${enabled ? 'on' : 'off'}\">${enabled ? 'enabled' : 'disabled'}</span>\n657\t ${statusHtml}\n658\t </div>\n659\t ${expandable ? `<span class=\"vv-ry-win-chev\" style=\"font-size:9px;color:#2a2a2a;flex-shrink:0;padding-top:2px;\">▸</span>` : ''}\n660\t </div>\n661\t ${panelHtml}\n662\t </div>`;\n663\t });\n664\t\n665\t return `<div class=\"vv-ry-wins\">${cards.join('')}</div>`;\n666\t}\n667\t\n668\t// ── Bandwidth section ─────────────────────────────────────────────────────────\n669\tfunction _bwSection(data) {\n670\t const history = data.bw_history || [];\n671\t if (!history.length) {\n672\t return `<div class=\"vv-ry-card\" style=\"grid-column:1/-1;\">\n673\t <div class=\"vv-ry-sec\">Bandwidth history</div>\n674\t <div style=\"color:#333;font-size:11px;padding:8px 0;\">No data in last 30 days</div>\n675\t </div>`;\n676\t }\n677\t\n678\t // Per-profile stats\n679\t const profileMap = {};\n680\t const profileOrder = [];\n681\t for (const r of history) {\n682\t if (!profileMap[r.profile]) {\n683\t profileMap[r.profile] = { runs: 0, success: 0, totalDur: 0, totalBytes: 0 };\n684\t profileOrder.push(r.profile);\n685\t }\n686\t const p = profileMap[r.profile];\n687\t p.runs++;\n688\t if (r.status === 'success') p.success++;\n689\t p.totalDur += r.duration;\n690\t p.totalBytes += r.bytes;\n691\t }\n692\t\n693\t const maxDur = Math.max(...Object.values(profileMap).map(p => p.totalDur), 1);\n694\t\n695\t const profRows = profileOrder.map((name, i) => {\n696\t const p = profileMap[name];\n697\t const col = PROF_COLORS[i % PROF_COLORS.length];\n698\t const pct = Math.round((p.totalDur / maxDur) * 100);\n699\t const rate = Math.round((p.success / p.runs) * 100);\n700\t const byteStr = p.totalBytes > 0 ? _fmtBytes(p.totalBytes) : _dur(p.totalDur) + ' total';\n701\t return `<div class=\"vv-ry-prof-row\">\n702\t <span class=\"vv-ry-prof-nm\">${name}</span>\n703\t <div class=\"vv-ry-prof-bar\"><div class=\"vv-ry-prof-fill\" style=\"width:${pct}%;background:${col}\"></div></div>\n704\t <span class=\"vv-ry-prof-cnt\">${p.runs} runs · ${byteStr}</span>\n705\t </div>`;\n706\t });\n707\t\n708\t // Recent runs (last 20, newest first)\n709\t const recent = [...history].reverse().slice(0, 20);\n710\t const runRows = recent.map(r => {\n711\t const stCls = r.status === 'success' ? 'on' : 'err';\n712\t const durStr = r.duration >= 3600\n713\t ? Math.floor(r.duration / 3600) + 'h ' + Math.floor((r.duration % 3600) / 60) + 'm'\n714\t : r.duration >= 60\n715\t ? Math.floor(r.duration / 60) + 'm'\n716\t : r.duration + 's';\n717\t return `<div class=\"vv-ry-run-row\">\n718\t <span class=\"vv-ry-run-dt\">${r.date}</span>\n719\t <span class=\"vv-ry-run-tm\">${r.time}</span>\n720\t <span class=\"vv-ry-run-pr\">${r.profile}</span>\n721\t <span class=\"vv-ry-run-dur\">${durStr}</span>\n722\t <span class=\"vv-ry-run-st\"><span class=\"vv-ry-pill ${stCls}\" style=\"font-size:9px;\">${r.status}</span></span>\n723\t </div>`;\n724\t });\n725\t\n726\t return `<div class=\"vv-ry-bw\" style=\"grid-column:1/-1;\">\n727\t <div class=\"vv-ry-card\">\n728\t <div class=\"vv-ry-sec\">Profile activity — last 30 days</div>\n729\t ${profRows.join('')}\n730\t </div>\n731\t <div class=\"vv-ry-card\">\n732\t <div class=\"vv-ry-sec\">Recent runs</div>\n733\t <div style=\"font-size:10px;color:#3a3a3a;margin-bottom:4px;display:grid;grid-template-columns:80px 90px 1fr 55px 48px;gap:4px;\">\n734\t <span>Date</span><span>Time</span><span>Profile</span><span style=\"text-align:right;\">Duration</span><span></span>\n735\t </div>\n736\t <div class=\"vv-ry-runs-scroll\">${runRows.join('')}</div>\n737\t </div>\n738\t </div>`;\n739\t}\n740\t\n741\t// ── Settings section ──────────────────────────────────────────────────────────\n742\tfunction _toggle(name, currentVal, label) {\n743\t const id = 'vv-ry-tog-' + name;\n744\t return `<label class=\"vv-ry-toggle\" title=\"${name}\">\n745\t <div class=\"vv-ry-toggle-track ${currentVal ? 'on' : ''}\" id=\"${id}\"\n746\t onclick=\"vvRyToggle(this,'${name}')\"></div>\n747\t <span class=\"vv-ry-toggle-lbl\">${label}</span>\n748\t </label>`;\n749\t}\n750\t\n751\tfunction _settingsSection(data) {\n752\t const w = data.windows || {};\n753\t const s = data.settings || {};\n754\t const en = data.enabled;\n755\t\n756\t const mbit = s.bw_limit ? ' (~' + (s.bw_limit / 125).toFixed(0) + ' Mbit/s)' : '';\n757\t\n758\t return `<div class=\"vv-ry-card\" style=\"grid-column:1/-1;\">\n759\t <div class=\"vv-ry-sec\" style=\"margin-bottom:10px;\">Settings</div>\n760\t\n761\t <div style=\"margin-bottom:12px;\">\n762\t <div style=\"font-size:10px;color:#444;letter-spacing:.05em;text-transform:uppercase;margin-bottom:6px;\">Global gate</div>\n763\t ${_toggle('RSYNC_ENABLED', en, 'RSYNC_ENABLED — blocks all syncs when off')}\n764\t </div>\n765\t\n766\t <hr class=\"vv-ry-sep\">\n767\t\n768\t <div style=\"margin-bottom:12px;\">\n769\t <div style=\"font-size:10px;color:#444;letter-spacing:.05em;text-transform:uppercase;margin-bottom:6px;\">Sync windows</div>\n770\t <div class=\"vv-ry-set-grid\">\n771\t ${_toggle('CRITICAL_RSYNC_ENABLED', w.critical, 'Critical')}\n772\t ${_toggle('INTERMEDIATE_RSYNC_ENABLED', w.intermediate, 'Intermediate')}\n773\t ${_toggle('DAILY_RSYNC_ENABLED', w.daily, 'Daily')}\n774\t ${_toggle('WEEKLY_RSYNC_ENABLED', w.weekly, 'Weekly')}\n775\t ${_toggle('FALLBACK_RSYNC_ENABLED', w.fallback, 'Fallback')}\n776\t </div>\n777\t </div>\n778\t\n779\t <hr class=\"vv-ry-sep\">\n780\t\n781\t <div>\n782\t <div style=\"font-size:10px;color:#444;letter-spacing:.05em;text-transform:uppercase;margin-bottom:8px;\">Defaults</div>\n783\t <div class=\"vv-ry-set-row\">\n784\t <span class=\"vv-ry-set-lbl\">Bandwidth limit</span>\n785\t <input class=\"vv-ry-set-inp\" id=\"vv-ry-bwlimit\" type=\"number\" min=\"0\" value=\"${s.bw_limit || 0}\">\n786\t <span class=\"vv-ry-set-hint\">KB/s${mbit}</span>\n787\t </div>\n788\t <div class=\"vv-ry-set-row\">\n789\t <span class=\"vv-ry-set-lbl\">Retry count</span>\n790\t <input class=\"vv-ry-set-inp\" id=\"vv-ry-retry\" type=\"number\" min=\"0\" max=\"20\" value=\"${s.retry_count ?? 3}\">\n791\t <span class=\"vv-ry-set-hint\">attempts</span>\n792\t </div>\n793\t <div class=\"vv-ry-set-row\">\n794\t <span class=\"vv-ry-set-lbl\">Retry delay</span>\n795\t <input class=\"vv-ry-set-inp\" id=\"vv-ry-sleep\" type=\"number\" min=\"0\" value=\"${s.sleep ?? 300}\">\n796\t <span class=\"vv-ry-set-hint\">seconds between attempts</span>\n797\t </div>\n798\t <div class=\"vv-ry-set-row\">\n799\t <span class=\"vv-ry-set-lbl\">BW warn threshold</span>\n800\t <input class=\"vv-ry-set-inp\" id=\"vv-ry-bwwarn\" type=\"number\" min=\"0\" value=\"${s.bw_warn_gb ?? 50}\">\n801\t <span class=\"vv-ry-set-hint\">GB — flag large transfers in weekly report</span>\n802\t </div>\n803\t <div style=\"margin-top:10px;display:flex;align-items:center;\">\n804\t <button class=\"vv-ry-save-btn\" id=\"vv-ry-save\" onclick=\"vvRySaveDefaults()\">Save</button>\n805\t <span class=\"vv-ry-feedback\" id=\"vv-ry-save-fb\"></span>\n806\t </div>\n807\t </div>\n808\t </div>`;\n809\t}\n810\t\n811\t// ── Main render ───────────────────────────────────────────────────────────────\n812\tfunction _render(data) {\n813\t _vvRyLastData = data;\n814\t if (_vvRyEditMode) return; // don't clobber the edit UI during a poll\n815\t\n816\t let html = '';\n817\t html += _statusCard(data);\n818\t html += _lastSyncCard(data);\n819\t html += _windowsRow(data);\n820\t html += _bwSection(data);\n821\t html += _settingsSection(data);\n822\t document.getElementById('vv-ry-grid').innerHTML = html;\n823\t\n824\t // Restore any window panels that were open before the re-render\n825\t _vvRyWinOpen.forEach(key => {\n826\t const card = document.querySelector(`.vv-ry-win[data-win-key=\"${key}\"]`);\n827\t if (!card) return;\n828\t const panel = card.querySelector('.vv-ry-win-panel');\n829\t const chev = card.querySelector('.vv-ry-win-chev');\n830\t if (panel) panel.style.display = '';\n831\t if (chev) chev.textContent = '▾';\n832\t });\n833\t\n834\t const ts = data.ts\n835\t ? new Date(data.ts * 1000).toLocaleString([], {\n836\t month:'numeric', day:'numeric', year:'numeric',\n837\t hour:'2-digit', minute:'2-digit', second:'2-digit'})\n838\t : '';\n839\t document.getElementById('vv-ry-ts').textContent = ts ? 'Updated: ' + ts : '';\n840\t}\n841\t\n842\tfunction vvRyLoad() {\n843\t fetch('/plugins/varaverk/api/rsync.php')\n844\t .then(r => r.json())\n845\t .then(_render)\n846\t .catch(() => {});\n847\t}\n848\t\n849\tvvRyLoad();\n850\tsetInterval(vvRyLoad, 30000);\n851\t\n852\t})();\n853\t\n854\t// ── Window card expand/collapse ───────────────────────────────────────────────\n855\tfunction vvRyWinExpand(card) {\n856\t const key = card.dataset.winKey;\n857\t const panel = card.querySelector('.vv-ry-win-panel');\n858\t const chev = card.querySelector('.vv-ry-win-chev');\n859\t if (!panel) return;\n860\t const open = panel.style.display !== 'none';\n861\t panel.style.display = open ? 'none' : '';\n862\t if (chev) chev.textContent = open ? '▸' : '▾';\n863\t if (key) open ? _vvRyWinOpen.delete(key) : _vvRyWinOpen.add(key);\n864\t}\n865\t\n866\t// ── Window card edit ──────────────────────────────────────────────────────────\n867\t\n868\tfunction vvRyWinEdit(key) {\n869\t const cfg = (_vvRyLastData?.win_arrays || {})[key] || {};\n870\t _vvRyWinEState[key] = {\n871\t scripts: (cfg.scripts || []).map(s => ({id: s, enabled: true})),\n872\t shares: (cfg.shares || []).map(s => {\n873\t const [p, pr] = s.split('|');\n874\t return {path: p.trim(), profile: (pr || '').trim()};\n875\t }),\n876\t };\n877\t _vvRyEditMode = true;\n878\t _vvRyWinEditing = key;\n879\t\n880\t const go = () => _vvRyBuildEditPanel(key);\n881\t if (!_vvRyScriptLib) {\n882\t fetch('/plugins/varaverk/api/rsync_win_arrays.php?action=list_scripts&_=' + Date.now())\n883\t .then(r => r.json()).then(d => { if (d.ok) _vvRyScriptLib = d.groups; go(); }).catch(go);\n884\t } else { go(); }\n885\t}\n886\t\n887\tfunction vvRyWinCancel(key) {\n888\t _vvRyEditMode = false;\n889\t _vvRyWinEditing = null;\n890\t const cfg = (_vvRyLastData?.win_arrays || {})[key] || {};\n891\t const panel = document.querySelector(`.vv-ry-win[data-win-key=\"${key}\"] .vv-ry-win-panel`);\n892\t if (panel) { panel.innerHTML = _vvRyViewPanelInner(key, cfg); panel.style.display = ''; }\n893\t const chev = document.querySelector(`.vv-ry-win[data-win-key=\"${key}\"] .vv-ry-win-chev`);\n894\t if (chev) chev.textContent = '▾';\n895\t}\n896\t\n897\tfunction vvRyWinSave(key) {\n898\t const state = _vvRyWinEState[key];\n899\t if (!state) return;\n900\t const btn = document.getElementById('vv-ry-esave-' + key);\n901\t const fb = document.getElementById('vv-ry-efb-' + key);\n902\t if (btn) { btn.disabled = true; btn.textContent = 'Saving…'; }\n903\t if (fb) fb.textContent = '';\n904\t\n905\t const fd = new FormData();\n906\t fd.append('action', 'save');\n907\t fd.append('win_key', key);\n908\t fd.append('scripts', JSON.stringify(state.scripts));\n909\t fd.append('shares', JSON.stringify(state.shares));\n910\t\n911\t fetch('/plugins/varaverk/api/rsync_win_arrays.php', {method:'POST', body:fd})\n912\t .then(r => r.json())\n913\t .then(d => {\n914\t if (d.ok) {\n915\t // Update cached data so cancel/re-render shows the new values\n916\t if (_vvRyLastData) {\n917\t _vvRyLastData.win_arrays ??= {};\n918\t _vvRyLastData.win_arrays[key] = {\n919\t scripts: state.scripts.map(s => s.id),\n920\t shares: state.shares.map(s => s.profile ? `${s.path}|${s.profile}` : s.path),\n921\t };\n922\t }\n923\t vvRyWinCancel(key);\n924\t } else {\n925\t if (btn) { btn.disabled = false; btn.textContent = 'Save changes'; }\n926\t if (fb) { fb.style.color = '#ef5350'; fb.textContent = (d.errors || ['Save failed']).join(', '); }\n927\t }\n928\t })\n929\t .catch(e => {\n930\t if (btn) { btn.disabled = false; btn.textContent = 'Save changes'; }\n931\t if (fb) { fb.style.color = '#ef5350'; fb.textContent = 'Request failed'; }\n932\t });\n933\t}\n934\t\n935\t// ── Edit panel builder ────────────────────────────────────────────────────────\n936\tfunction _vvRyBuildEditPanel(key) {\n937\t const panel = document.querySelector(`.vv-ry-win[data-win-key=\"${key}\"] .vv-ry-win-panel`);\n938\t if (!panel) return;\n939\t panel.style.display = '';\n940\t const state = _vvRyWinEState[key] || {scripts:[], shares:[]};\n941\t const scripts = state.scripts;\n942\t const shares = state.shares;\n943\t let h = '';\n944\t\n945\t // ── Scripts list ─────────────────────────────────────────────────────────────\n946\t h += `<div style=\"font-size:9px;font-weight:700;color:#2a2a2a;text-transform:uppercase;letter-spacing:.07em;margin-bottom:5px;\">Scripts</div>`;\n947\t for (let i = 0; i < scripts.length; i++) {\n948\t const s = scripts[i];\n949\t const parts = s.id.split('/');\n950\t const dir = parts.length > 1 ? parts.slice(0,-1).join('/') : '';\n951\t const name = parts[parts.length-1].replace(/\\.sh$/,'');\n952\t h += `<div style=\"display:flex;align-items:center;gap:3px;padding:2px 0;border-bottom:1px solid #111;\">\n953\t <button onclick=\"event.stopPropagation();vvRyERemScript('${key}',${i})\"\n954\t style=\"background:none;border:none;color:#4a1a1a;cursor:pointer;padding:0 3px;font-size:13px;line-height:1;flex-shrink:0;\">×</button>\n955\t <div style=\"display:flex;flex-direction:column;flex-shrink:0;\">\n956\t <button onclick=\"event.stopPropagation();vvRyEMvScript('${key}',${i},-1)\" ${i===0?'disabled':''}\n957\t style=\"background:none;border:none;color:${i===0?'#1e1e1e':'#3a5a3a'};cursor:${i===0?'default':'pointer'};padding:0;font-size:8px;line-height:1.2;\">▲</button>\n958\t <button onclick=\"event.stopPropagation();vvRyEMvScript('${key}',${i},1)\" ${i===scripts.length-1?'disabled':''}\n959\t style=\"background:none;border:none;color:${i===scripts.length-1?'#1e1e1e':'#3a5a3a'};cursor:${i===scripts.length-1?'default':'pointer'};padding:0;font-size:8px;line-height:1.2;\">▼</button>\n960\t </div>\n961\t <span style=\"font-size:10px;color:#666;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;\">\n962\t ${dir?`<span style=\"color:#2a2a2a;\">${dir}/</span>`:''}${name}\n963\t </span>\n964\t </div>`;\n965\t }\n966\t // Script add row\n967\t const existIds = new Set(scripts.map(s => s.id));\n968\t h += `<div style=\"display:flex;gap:4px;margin-top:5px;align-items:center;\" onclick=\"event.stopPropagation()\">\n969\t <select id=\"vv-ry-esel-${key}\"\n970\t style=\"background:#0d0d0d;border:1px solid #1e1e1e;border-radius:3px;color:#666;\n971\t font-size:10px;padding:3px 5px;flex:1;outline:none;min-width:0;\">\n972\t <option value=\"\">— add script —</option>`;\n973\t if (_vvRyScriptLib) {\n974\t for (const [folder, items] of Object.entries(_vvRyScriptLib)) {\n975\t const avail = items.filter(it => !existIds.has(it.id));\n976\t if (!avail.length) continue;\n977\t h += `<optgroup label=\"${folder}\">`;\n978\t for (const it of avail) h += `<option value=\"${it.id}\">${it.label}</option>`;\n979\t h += `</optgroup>`;\n980\t }\n981\t }\n982\t h += `</select>\n983\t <button onclick=\"event.stopPropagation();vvRyEAddScript('${key}')\"\n984\t style=\"background:#0a1a0a;border:1px solid #1a3a1a;color:#4caf50;font-size:10px;\n985\t padding:3px 9px;border-radius:3px;cursor:pointer;flex-shrink:0;\">+ Add</button>\n986\t </div>`;\n987\t\n988\t // ── Shares list ───────────────────────────────────────────────────────────────\n989\t h += `<div style=\"border-top:1px solid #161616;margin:8px 0;\"></div>\n990\t <div style=\"font-size:9px;font-weight:700;color:#2a2a2a;text-transform:uppercase;letter-spacing:.07em;margin-bottom:5px;\">Sync shares</div>`;\n991\t for (let i = 0; i < shares.length; i++) {\n992\t const sh = shares[i];\n993\t const bn = sh.path.split('/').pop();\n994\t h += `<div style=\"display:flex;align-items:center;gap:3px;padding:2px 0;border-bottom:1px solid #111;\">\n995\t <button onclick=\"event.stopPropagation();vvRyERemShare('${key}',${i})\"\n996\t style=\"background:none;border:none;color:#4a1a1a;cursor:pointer;padding:0 3px;font-size:13px;line-height:1;flex-shrink:0;\">×</button>\n997\t <div style=\"display:flex;flex-direction:column;flex-shrink:0;\">\n998\t <button onclick=\"event.stopPropagation();vvRyEMvShare('${key}',${i},-1)\" ${i===0?'disabled':''}\n999\t style=\"background:none;border:none;color:${i===0?'#1e1e1e':'#3a5a3a'};cursor:${i===0?'default':'pointer'};padding:0;font-size:8px;line-height:1.2;\">▲</button>\n1000\t <button onclick=\"event.stopPropagation();vvRyEMvShare('${key}',${i},1)\" ${i===shares.length-1?'disabled':''}\n1001\t style=\"background:none;border:none;color:${i===shares.length-1?'#1e1e1e':'#3a5a3a'};cursor:${i===shares.length-1?'default':'pointer'};padding:0;font-size:8px;line-height:1.2;\">▼</button>\n1002\t </div>\n1003\t <span style=\"font-size:10px;color:#666;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\" title=\"${sh.path}\">${bn}</span>\n1004\t ${sh.profile?`<span style=\"font-size:9px;color:#2a3a2a;flex-shrink:0;font-family:monospace;\">${sh.profile}</span>`:''}\n1005\t </div>`;\n1006\t }\n1007\t // Share add form\n1008\t h += `<div style=\"margin-top:6px;\" onclick=\"event.stopPropagation()\">\n1009\t <div style=\"display:flex;gap:4px;margin-bottom:3px;\">\n1010\t <input id=\"vv-ry-epath-${key}\" type=\"text\" placeholder=\"/mnt/user/…\" autocomplete=\"off\"\n1011\t style=\"background:#0d0d0d;border:1px solid #1e1e1e;border-radius:3px;color:#888;\n1012\t font-size:10px;padding:3px 6px;flex:1;min-width:0;font-family:monospace;outline:none;\"\n1013\t onkeydown=\"if(event.key==='Enter')vvRyEBrowse('${key}')\">\n1014\t <button onclick=\"vvRyEBrowse('${key}')\"\n1015\t style=\"background:#0a1a2a;border:1px solid #1a3a5a;color:#4a9eff;font-size:10px;\n1016\t padding:3px 8px;border-radius:3px;cursor:pointer;flex-shrink:0;white-space:nowrap;\">⟳ Browse</button>\n1017\t </div>\n1018\t <div id=\"vv-ry-ebrowse-${key}\" class=\"vv-ms-dirs\" style=\"display:none;max-height:120px;\" data-last-path=\"\"></div>\n1019\t <div style=\"display:flex;gap:4px;margin-top:4px;align-items:center;\">\n1020\t <select id=\"vv-ry-eprof-${key}\"\n1021\t style=\"background:#0d0d0d;border:1px solid #1e1e1e;border-radius:3px;color:#666;\n1022\t font-size:10px;padding:3px 5px;flex:1;min-width:0;outline:none;\">\n1023\t <option value=\"\">no profile</option>\n1024\t </select>\n1025\t <button onclick=\"vvRyEAddShare('${key}')\"\n1026\t style=\"background:#0a1a0a;border:1px solid #1a3a1a;color:#4caf50;font-size:10px;\n1027\t padding:3px 9px;border-radius:3px;cursor:pointer;flex-shrink:0;white-space:nowrap;\">+ Add</button>\n1028\t </div>\n1029\t </div>`;\n1030\t\n1031\t // ── Save / Cancel ─────────────────────────────────────────────────────────────\n1032\t h += `<div style=\"display:flex;gap:6px;align-items:center;margin-top:10px;padding-top:8px;border-top:1px solid #1e1e1e;\">\n1033\t <button id=\"vv-ry-esave-${key}\" onclick=\"event.stopPropagation();vvRyWinSave('${key}')\"\n1034\t style=\"background:#1a2a1a;border:1px solid #2d4a2d;color:#4caf50;font-size:11px;\n1035\t padding:4px 14px;border-radius:3px;cursor:pointer;\">Save changes</button>\n1036\t <button onclick=\"event.stopPropagation();vvRyWinCancel('${key}')\"\n1037\t style=\"background:#111;border:1px solid #222;color:#555;font-size:11px;\n1038\t padding:4px 12px;border-radius:3px;cursor:pointer;\">Cancel</button>\n1039\t <span id=\"vv-ry-efb-${key}\" style=\"font-size:10px;color:#ef5350;\"></span>\n1040\t </div>`;\n1041\t\n1042\t panel.innerHTML = h;\n1043\t _vvRyLoadProfiles(key);\n1044\t}\n1045\t\n1046\t// ── Script mutations ──────────────────────────────────────────────────────────\n1047\tfunction vvRyEMvScript(key, idx, dir) {\n1048\t const s = _vvRyWinEState[key]?.scripts; if (!s) return;\n1049\t const n = idx + dir; if (n < 0 || n >= s.length) return;\n1050\t [s[idx], s[n]] = [s[n], s[idx]]; _vvRyBuildEditPanel(key);\n1051\t}\n1052\tfunction vvRyERemScript(key, idx) {\n1053\t const s = _vvRyWinEState[key]?.scripts; if (!s) return;\n1054\t s.splice(idx, 1); _vvRyBuildEditPanel(key);\n1055\t}\n1056\tfunction vvRyEAddScript(key) {\n1057\t const sel = document.getElementById('vv-ry-esel-' + key);\n1058\t if (!sel?.value) return;\n1059\t const s = _vvRyWinEState[key]?.scripts; if (!s) return;\n1060\t if (s.find(x => x.id === sel.value)) return;\n1061\t s.push({id: sel.value, enabled: true}); _vvRyBuildEditPanel(key);\n1062\t}\n1063\t\n1064\t// ── Share mutations ───────────────────────────────────────────────────────────\n1065\tfunction vvRyEMvShare(key, idx, dir) {\n1066\t const s = _vvRyWinEState[key]?.shares; if (!s) return;\n1067\t const n = idx + dir; if (n < 0 || n >= s.length) return;\n1068\t [s[idx], s[n]] = [s[n], s[idx]]; _vvRyBuildEditPanel(key);\n1069\t}\n1070\tfunction vvRyERemShare(key, idx) {\n1071\t const s = _vvRyWinEState[key]?.shares; if (!s) return;\n1072\t s.splice(idx, 1); _vvRyBuildEditPanel(key);\n1073\t}\n1074\tfunction vvRyEAddShare(key) {\n1075\t const path = document.getElementById('vv-ry-epath-' + key)?.value.trim();\n1076\t const prof = document.getElementById('vv-ry-eprof-' + key)?.value.trim() || '';\n1077\t if (!path || !path.startsWith('/')) return;\n1078\t const s = _vvRyWinEState[key]?.shares; if (!s) return;\n1079\t s.push({path, profile: prof});\n1080\t document.getElementById('vv-ry-epath-' + key).value = '';\n1081\t const dirsEl = document.getElementById('vv-ry-ebrowse-' + key);\n1082\t if (dirsEl) dirsEl.style.display = 'none';\n1083\t _vvRyBuildEditPanel(key);\n1084\t}\n1085\t\n1086\t// ── Share path browser ────────────────────────────────────────────────────────\n1087\tfunction vvRyEBrowse(key) {\n1088\t const pathEl = document.getElementById('vv-ry-epath-' + key);\n1089\t const dirsEl = document.getElementById('vv-ry-ebrowse-' + key);\n1090\t if (!pathEl || !dirsEl) return;\n1091\t const path = pathEl.value.trim() || '/mnt/user';\n1092\t\n1093\t if (dirsEl.dataset.lastPath === path && dirsEl.style.display !== 'none') {\n1094\t dirsEl.style.display = 'none'; dirsEl.dataset.lastPath = ''; return;\n1095\t }\n1096\t dirsEl.innerHTML = '<div style=\"color:#444;font-size:10px;padding:4px;\">Loading…</div>';\n1097\t dirsEl.style.display = '';\n1098\t\n1099\t fetch(`/plugins/varaverk/api/manual_sync.php?action=browse_local&path=${encodeURIComponent(path)}&_=${Date.now()}`)\n1100\t .then(r => r.json())\n1101\t .then(d => {\n1102\t if (!d.ok) { dirsEl.innerHTML = `<div class=\"vv-ms-dir\" style=\"color:#ef5350;cursor:default;\">${d.error}</div>`; return; }\n1103\t if (pathEl) pathEl.value = d.path;\n1104\t dirsEl.dataset.lastPath = d.path;\n1105\t let h = '';\n1106\t if (d.parent) {\n1107\t const pn = d.parent === '/' ? '/' : (d.parent.replace(/^.*\\//,'') || d.parent) + '/';\n1108\t h += `<div class=\"vv-ms-dir parent\" onclick=\"vvRyENavTo('${key}',${JSON.stringify(d.parent)})\">↑ ${pn}</div>`;\n1109\t }\n1110\t if (!d.dirs.length) {\n1111\t h += '<div class=\"vv-ms-dir\" style=\"color:#2a2a2a;cursor:default;\">— empty —</div>';\n1112\t } else {\n1113\t for (const dir of d.dirs) {\n1114\t const name = dir.replace(/^.*\\//,'') || dir;\n1115\t h += `<div class=\"vv-ms-dir\" onclick=\"vvRyENavTo('${key}',${JSON.stringify(dir)})\" title=\"${dir}\">▶ ${name}</div>`;\n1116\t }\n1117\t }\n1118\t dirsEl.innerHTML = h;\n1119\t })\n1120\t .catch(() => { dirsEl.innerHTML = '<div class=\"vv-ms-dir\" style=\"color:#ef5350;cursor:default;\">Browse failed</div>'; });\n1121\t}\n1122\t\n1123\tfunction vvRyENavTo(key, path) {\n1124\t const pathEl = document.getElementById('vv-ry-epath-' + key);\n1125\t if (pathEl) pathEl.value = path;\n1126\t vvRyEBrowse(key);\n1127\t}\n1128\t\n1129\t// ── Profile dropdown loader ───────────────────────────────────────────────────\n1130\tfunction _vvRyLoadProfiles(key) {\n1131\t const sel = document.getElementById('vv-ry-eprof-' + key);\n1132\t if (!sel) return;\n1133\t const populate = () => {"}]},"uuid":"3f276b0b-9293-47cf-9fd6-981bd9e53b40","timestamp":"2026-06-06T14:29:44.844Z","sourceToolAssistantUUID":"a5dac394-2ad5-4c96-8ee0-7a7331bfbaa1","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"be96202e-18c5-4341-b5d1-8e5e0e33297e","isSidechain":true,"promptId":"3c1ac879-b13a-4ebc-9aeb-ea73615e9af3","agentId":"accc3589951fac88e","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_017ZsggAtJiwSvPEmkjvTDs7","type":"tool_result","content":"1\t<style>\n2\t.vv-pt-grid { display:grid; gap:12px; }\n3\t.vv-pt-node { background:#161616; border:1px solid #2a2a2a; border-radius:6px; padding:12px; min-width:0; }\n4\t.vv-pt-node.me { border-color:#2a3a2a; }\n5\t.vv-pt-node-head { display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:10px; }\n6\t.vv-pt-hostname { font-size:13px; font-weight:bold; color:#ccc; }\n7\t.vv-pt-slot { font-size:10px; color:#444; margin-top:1px; }\n8\t.vv-pt-tags { display:flex; gap:4px; flex-wrap:wrap; justify-content:flex-end; }\n9\t.vv-pt-tag { font-size:8px; padding:1px 5px; border-radius:3px; white-space:nowrap; }\n10\t.vv-pt-tag.me { background:#1a3a1a; color:#4caf50; }\n11\t.vv-pt-tag.owner { background:#1a2a3a; color:#4a9eff; }\n12\t.vv-pt-tag.mirror { background:#2a2a1a; color:#ff9800; }\n13\t.vv-pt-row { display:flex; justify-content:space-between; align-items:baseline; margin:3px 0; }\n14\t.vv-pt-lbl { font-size:11px; color:#555; }\n15\t.vv-pt-val { font-size:11px; color:#bbb; text-align:right; }\n16\t.vv-pt-sep { border:none; border-top:1px solid #222; margin:8px 0; }\n17\t.vv-pt-state { font-size:12px; font-weight:500; }\n18\t.vv-pt-dot { width:8px; height:8px; border-radius:50%; display:inline-block; flex-shrink:0; margin-right:5px; }\n19\t.vv-pt-actions { display:flex; gap:10px; flex-wrap:wrap; margin-top:4px; }\n20\t.vv-pt-action-btn { padding:5px 16px; border:none; border-radius:4px; cursor:pointer;\n21\t font-size:12px; font-weight:500; }\n22\t.vv-pt-action-btn.run { background:#1a3a1a; color:#6fcf97; border:1px solid #2e6b2e; }\n23\t.vv-pt-action-btn.warn { background:#3a1a1a; color:#f88; border:1px solid #6b2e2e; }\n24\t.vv-pt-action-btn.info { background:#1a2a3a; color:#7ab; border:1px solid #2e4a6b; }\n25\t.vv-pt-action-btn:disabled { opacity:0.4; cursor:default; }\n26\t.vv-pt-transfer-note { font-size:10px; color:#555; margin-top:6px; font-family:monospace; }\n27\t.vv-pt-onboard-highlight {\n28\t box-shadow: 0 0 0 2px #4a8, 0 0 12px rgba(100,200,120,.25) !important;\n29\t animation: vvOnboardPulse 1.4s ease-in-out 4;\n30\t}\n31\t@keyframes vvOnboardPulse {\n32\t 0%,100% { box-shadow: 0 0 0 2px #4a8, 0 0 8px rgba(100,200,120,.2); }\n33\t 50% { box-shadow: 0 0 0 4px #4a8, 0 0 18px rgba(100,200,120,.45); }\n34\t}\n35\t\n36\t/* ── Settings panel ─────────────────────────────────────────── */\n37\t.vv-set-file { border:1px solid #222; border-radius:5px; margin-bottom:8px; overflow:hidden; }\n38\t.vv-set-file-hdr { display:flex; justify-content:space-between; align-items:center; padding:8px 12px;\n39\t background:#161616; cursor:pointer; font-size:12px; font-weight:bold; color:#bbb; font-family:monospace; }\n40\t.vv-set-file-hdr:hover { background:#1a1a1a; }\n41\t.vv-set-file-body { padding:6px 10px 10px; }\n42\t.vv-set-sec { border-top:1px solid #1c1c1c; }\n43\t.vv-set-sec:first-child { border-top:none; }\n44\t.vv-set-sec-hdr { display:flex; justify-content:space-between; align-items:center; padding:6px 4px;\n45\t cursor:pointer; font-size:11px; color:#888; text-transform:uppercase; letter-spacing:.04em; }\n46\t.vv-set-sec-hdr:hover { color:#ccc; }\n47\t.vv-set-sec-body { padding:4px 0 8px 8px; }\n48\t.vv-set-chev { color:#444; font-size:10px; }\n49\t.vv-set-field { margin-bottom:10px; }\n50\t.vv-set-key { font-size:11px; color:#7ab; font-family:monospace; margin-bottom:2px; }\n51\t.vv-set-desc { font-size:10px; color:#555; margin-bottom:3px; line-height:1.4; }\n52\t.vv-set-input { width:100%; box-sizing:border-box; background:#0d0d0d; border:1px solid #2a2a2a;\n53\t color:#ddd; padding:5px 8px; border-radius:3px; font-family:monospace; font-size:11px; }\n54\t.vv-set-input:focus { outline:none; border-color:#4a8; }\n55\t.vv-set-input.changed { border-color:#ff9800; }\n56\ttextarea.vv-set-input { resize:vertical; white-space:pre; }\n57\t</style>\n58\t\n59\t<div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding:0 2px;\">\n60\t <span style=\"font-size:13px;font-weight:bold;color:#888;text-transform:uppercase;letter-spacing:.06em;\">Partnership</span>\n61\t <span style=\"font-size:11px;color:#3a3a3a;\" id=\"vv-pt-ts\"></span>\n62\t</div>\n63\t\n64\t<!-- Config bar -->\n65\t<div class=\"vv-card\" style=\"margin-bottom:12px;\" id=\"vv-pt-config-card\">\n66\t <div id=\"vv-pt-config-body\" style=\"color:#444;font-size:12px;\">Loading…</div>\n67\t</div>\n68\t\n69\t<!-- Host Settings -->\n70\t<div class=\"vv-card\" id=\"vv-pt-hosts-card\" style=\"margin-bottom:12px;display:none;\">\n71\t <div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;\">\n72\t <h3 style=\"margin:0;\">Host Settings</h3>\n73\t <button id=\"vv-pt-hosts-save\" class=\"vv-pt-action-btn run\"\n74\t onclick=\"vvPtSaveHosts(this)\" style=\"display:none;\">Save</button>\n75\t </div>\n76\t <div id=\"vv-pt-hosts-body\"></div>\n77\t</div>\n78\t\n79\t<!-- Offline countdown warning (shown only when partner has been unreachable) -->\n80\t<div id=\"vv-pt-offline-warn\" style=\"display:none;margin-bottom:12px;\"></div>\n81\t\n82\t<!-- Node grid -->\n83\t<div id=\"vv-pt-nodes\" class=\"vv-pt-grid\" style=\"margin-bottom:12px;\">\n84\t <div style=\"color:#444;font-size:12px;padding:16px 0;text-align:center;\">Loading…</div>\n85\t</div>\n86\t\n87\t<!-- Array settings (above sync/actions row) -->\n88\t<div id=\"vv-pt-arrays-wrap\" style=\"margin-bottom:12px;display:none;\"></div>\n89\t\n90\t<!-- Mirror sync + Actions side by side -->\n91\t<div style=\"display:flex;gap:12px;margin-bottom:12px;align-items:stretch;flex-wrap:wrap;\">\n92\t <div class=\"vv-card\" id=\"vv-pt-sync-card\" style=\"flex:1 1 220px;min-width:0;display:flex;flex-direction:column;\">\n93\t <h3>Mirror Sync</h3>\n94\t <div id=\"vv-pt-sync-body\" style=\"flex:1;color:#444;font-size:12px;\">Loading…</div>\n95\t </div>\n96\t <div class=\"vv-card\" id=\"vv-pt-actions-card\" style=\"flex:2 1 300px;min-width:0;\">\n97\t <h3>Actions</h3>\n98\t <div id=\"vv-pt-actions-body\" style=\"color:#444;font-size:12px;\">Loading…</div>\n99\t </div>\n100\t</div>\n101\t\n102\t<!-- Settings -->\n103\t<div class=\"vv-card\" id=\"vv-pt-settings-card\" style=\"margin-top:12px;\">\n104\t <div style=\"display:flex;justify-content:space-between;align-items:center;cursor:pointer;\"\n105\t onclick=\"vvPtToggleSettings()\">\n106\t <h3 style=\"margin:0;\">Settings</h3>\n107\t <div style=\"display:flex;align-items:center;gap:10px;\">\n108\t <button id=\"vv-pt-settings-save\" class=\"vv-pt-action-btn run\"\n109\t onclick=\"event.stopPropagation();vvPtSaveSettings(this)\"\n110\t style=\"display:none;\">Save Changes</button>\n111\t <span id=\"vv-pt-settings-toggle\"\n112\t style=\"font-size:11px;color:#7ab;background:#1a2a3a;border:1px solid #2e4a6b;\n113\t border-radius:4px;padding:3px 10px;white-space:nowrap;\">▸ Show settings</span>\n114\t </div>\n115\t </div>\n116\t <div id=\"vv-pt-settings-hint\" style=\"font-size:10px;color:#444;margin-top:4px;\">\n117\t Partnership configuration for this server.\n118\t </div>\n119\t <div id=\"vv-pt-settings-body\" style=\"display:none;margin-top:10px;\"></div>\n120\t</div>\n121\t\n122\t<script>\n123\t// ── Toggle states — persist across 10s poll re-renders ────────────────────────\n124\tconst _vvOnboarding = {}; // hostId → true when onboard steps are expanded\n125\tconst _vvDeleteKeys = {}; // hostId → true when delete-keys panel is expanded\n126\tlet _vvPtReload = null; // set by IIFE so top-level fns can trigger a refresh\n127\t\n128\t// ── Public action functions — top-level so onclick= attributes can reach them ──\n129\tfunction _vvPtRun(id, extraArgs) {\n130\t const params = {id, manual: '1'};\n131\t if (extraArgs) params.extra_args = extraArgs;\n132\t return fetch('/plugins/varaverk/api/run.php', {\n133\t method: 'POST',\n134\t body: new URLSearchParams(params)\n135\t }).then(r => {\n136\t if (!r.ok) throw new Error('HTTP ' + r.status);\n137\t return r.text();\n138\t }).then(text => {\n139\t if (!text.trim()) return {ok: true, _empty_response: true};\n140\t return JSON.parse(text);\n141\t });\n142\t}\n143\t\n144\tfunction vvPtStartOnboard(hostId) {\n145\t _vvOnboarding[hostId] = true;\n146\t delete _vvDeleteKeys[hostId];\n147\t if (_vvPtReload) _vvPtReload();\n148\t}\n149\tfunction vvPtEndOnboard(hostId) {\n150\t delete _vvOnboarding[hostId];\n151\t if (_vvPtReload) _vvPtReload();\n152\t}\n153\tfunction vvPtShowDeleteKeys(hostId) {\n154\t _vvDeleteKeys[hostId] = true;\n155\t delete _vvOnboarding[hostId];\n156\t if (_vvPtReload) _vvPtReload();\n157\t}\n158\tfunction vvPtHideDeleteKeys(hostId) {\n159\t delete _vvDeleteKeys[hostId];\n160\t if (_vvPtReload) _vvPtReload();\n161\t}\n162\t\n163\tfunction vvApiKey(btn) {\n164\t const origText = btn.textContent;\n165\t btn.disabled = true;\n166\t btn.textContent = '⟳ Working…';\n167\t fetch('/plugins/varaverk/api/create_api_key.php?_=' + Date.now())\n168\t .then(r => {\n169\t if (!r.ok || r.status === 0) throw new Error('HTTP ' + r.status + ' ' + r.statusText);\n170\t return r.text();\n171\t })\n172\t .then(text => {\n173\t if (!text.trim()) throw new Error('Empty response from server');\n174\t return JSON.parse(text);\n175\t })\n176\t .then(d => {\n177\t if (d.ok) {\n178\t if (_vvPtReload) _vvPtReload();\n179\t } else {\n180\t alert('Failed: ' + (d.error ?? 'Unknown error'));\n181\t btn.disabled = false; btn.textContent = origText;\n182\t }\n183\t })\n184\t .catch(e => {\n185\t alert('Error: ' + e);\n186\t btn.disabled = false; btn.textContent = origText;\n187\t });\n188\t}\n189\t\n190\tfunction vvPtPhase2(btn, hostId) {\n191\t if (!confirm(`Phase 2: Deploy containers + arr stack + establish partnership on ${hostId}?\\n\\nRequires ${hostId} to have Varaverk installed and SSH keys set up.`)) return;\n192\t btn.disabled = true;\n193\t btn.textContent = '⟳ Starting…';\n194\t _vvPtRun('Partnership/partnership_onboard.sh', '--phase2-only')\n195\t .then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })\n196\t .catch(e => alert('Error: ' + e))\n197\t .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Run Phase 2 Manually'; }, 3000));\n198\t}\n199\t\n200\tfunction vvPtOnboard(btn) {\n201\t if (!confirm('Run full partnership_onboard.sh?\\n\\nRun on the MIRROR first, then on the OWNER.\\n\\nUse Phase 1 + Phase 2 buttons for step-by-step control.')) return;\n202\t btn.disabled = true;\n203\t btn.textContent = '⟳ Starting…';\n204\t _vvPtRun('Partnership/partnership_onboard.sh')\n205\t .then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })\n206\t .catch(e => alert('Error: ' + e))\n207\t .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Onboard (Mirror)'; }, 4000));\n208\t}\n209\t\n210\tfunction vvPtPushConf(btn, hostId) {\n211\t if (!confirm(`Push conf to ${hostId}?\\n\\nAssumes SSH key is already installed on ${hostId}.\\nSkips key generation/install, goes straight to conf push + local setup.`)) return;\n212\t btn.disabled = true;\n213\t btn.textContent = '⟳ Pushing…';\n214\t _vvPtRun('Partnership/partnership_onboard.sh', '--phase1-only --skip-ssh')\n215\t .then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })\n216\t .catch(e => alert('Error: ' + e))\n217\t .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Push Conf (key installed)'; }, 4000));\n218\t}\n219\t\n220\tfunction vvPtLocalSetup(btn) {\n221\t if (!confirm('Complete HOST1 local setup?\\n\\nRuns FolderView3 integration and marks HOST1 as locally ready.\\nDoes not require HOST2 to be online.')) return;\n222\t btn.disabled = true;\n223\t btn.textContent = '⟳ Running…';\n224\t _vvPtRun('Partnership/partnership_manager.sh', '--onboard --local-only')\n225\t .then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })\n226\t .catch(e => alert('Error: ' + e))\n227\t .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Complete HOST1 Setup'; }, 4000));\n228\t}\n229\t\n230\tfunction vvPtCancel(btn, hostId) {\n231\t if (!confirm(`Cancel onboard for ${hostId}?\\n\\nThis will remove keys in both directions and reset all phase state.\\n\\nContinue?`)) return;\n232\t btn.disabled = true;\n233\t btn.textContent = '⟳ Cancelling…';\n234\t _vvPtRun('Partnership/onboard_cancel.sh', '--direction=both')\n235\t .then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })\n236\t .catch(e => alert('Error: ' + e))\n237\t .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '✕ Cancel'; }, 4000));\n238\t}\n239\t\n240\tfunction vvPtDeleteH1(btn, hostId) {\n241\t if (!confirm(`Remove HOST1's key from ${hostId}?\\n\\n• Deletes local SSH key pair\\n• Removes it from ${hostId}'s authorized_keys\\n• Clears phase state`)) return;\n242\t btn.disabled = true; btn.textContent = '⟳ Removing…';\n243\t _vvPtRun('Partnership/onboard_cancel.sh', '--direction=h1')\n244\t .then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })\n245\t .catch(e => alert('Error: ' + e))\n246\t .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '✕ Remove HOST1 key'; }, 4000));\n247\t}\n248\t\n249\tfunction vvPtDeleteH2(btn, hostId) {\n250\t if (!confirm(`Remove ${hostId}'s key from HOST1?\\n\\n• Removes ${hostId}'s public key from HOST1's authorized_keys\\n• ${hostId} will no longer be able to SSH into HOST1`)) return;\n251\t btn.disabled = true; btn.textContent = '⟳ Removing…';\n252\t _vvPtRun('Partnership/onboard_cancel.sh', '--direction=h2')\n253\t .then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })\n254\t .catch(e => alert('Error: ' + e))\n255\t .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = `✕ Remove ${hostId} key`; }, 4000));\n256\t}\n257\t\n258\tfunction vvPtOffboard(btn) {\n259\t if (btn.style.opacity === '0.35' || btn.style.cursor === 'default') return;\n260\t if (!confirm('Run partnership_offboard.sh?\\n\\nThis will end the partnership, reconfigure WebUIs, and revoke SSH access.\\n\\nContinue?')) return;\n261\t btn.disabled = true;\n262\t btn.textContent = '⟳ Starting…';\n263\t _vvPtRun('Partnership/partnership_offboard.sh')\n264\t .then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })\n265\t .catch(e => alert('Error: ' + e))\n266\t .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Offboard'; }, 4000));\n267\t}\n268\t\n269\tfunction vvPtTransfer(btn, token) {\n270\t if (!confirm('Transfer ownership to the mirror?\\n\\nThis promotes the mirror to OWNER and demotes this server. '\n271\t + 'partnership_transfer.sh requires sustained health checks before the switch completes.\\n\\nContinue?')) return;\n272\t if (prompt('Type YES to confirm ownership transfer:') !== 'YES') return;\n273\t btn.disabled = true;\n274\t btn.textContent = '⟳ Transferring…';\n275\t _vvPtRun('Partnership/partnership_transfer.sh', '--confirm=' + token)\n276\t .then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })\n277\t .catch(e => alert('Error: ' + e))\n278\t .finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '⇄ Transfer Ownership'; }, 4000));\n279\t}\n280\t\n281\tfunction vvPtPing(btn, slot, idUpper) {\n282\t const out = document.getElementById('vv-pt-ping-' + idUpper);\n283\t btn.disabled = true;\n284\t btn.textContent = '⟳';\n285\t if (out) { out.textContent = ''; out.style.color = '#444'; }\n286\t fetch('/plugins/varaverk/api/partnership_ping.php?id=' + encodeURIComponent(slot) + '&_=' + Date.now())\n287\t .then(r => r.json())\n288\t .then(d => {\n289\t if (out) {\n290\t if (d.ok) { out.textContent = '✓ ' + d.latency_ms + 'ms'; out.style.color = '#4caf50'; }\n291\t else { out.textContent = '✗ ' + (d.error || 'failed'); out.style.color = '#f44336'; }\n292\t }\n293\t })\n294\t .catch(e => { if (out) { out.textContent = '✗ ' + e; out.style.color = '#f44336'; } })\n295\t .finally(() => { btn.disabled = false; btn.textContent = '⇄ Test'; });\n296\t}\n297\t\n298\tfunction vvPtToggleSync(el, varName, enabled) {\n299\t el.style.pointerEvents = 'none';\n300\t el.style.opacity = '0.5';\n301\t const params = new URLSearchParams({\n302\t csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '',\n303\t name: varName,\n304\t enabled: String(enabled)\n305\t });\n306\t fetch('/plugins/varaverk/api/flag_toggle.php', {\n307\t method: 'POST',\n308\t headers: {'Content-Type': 'application/x-www-form-urlencoded'},\n309\t body: params\n310\t })\n311\t .then(r => r.json())\n312\t .then(d => {\n313\t if (d.ok) { if (_vvPtReload) _vvPtReload(); }\n314\t else { alert('Failed: ' + (d.error ?? 'Unknown error')); el.style.pointerEvents = ''; el.style.opacity = ''; }\n315\t })\n316\t .catch(e => { alert('Error: ' + e); el.style.pointerEvents = ''; el.style.opacity = ''; });\n317\t}\n318\t\n319\t// ── Host Settings ─────────────────────────────────────────────────────────────\n320\t\n321\tfunction _renderHostSettings(nodes, isOwner) {\n322\t let html = '<div style=\"display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;\">';\n323\t for (const n of nodes) {\n324\t const rdonly = isOwner ? '' : 'readonly style=\"opacity:.5;cursor:default;\"';\n325\t html += `<div>\n326\t <div class=\"vv-set-key\" style=\"margin-bottom:4px;\">${_vvSetEsc(n.id)}</div>\n327\t <input type=\"text\" ${rdonly}\n328\t class=\"vv-set-input vv-pt-host-inp\"\n329\t data-key=\"${_vvSetEsc(n.id)}\" data-file=\"master.conf\" data-type=\"scalar\"\n330\t data-orig=\"${_vvSetEsc(n.hostname)}\"\n331\t value=\"${_vvSetEsc(n.hostname)}\"\n332\t oninput=\"vvPtHostChanged(this)\"\n333\t placeholder=\"hostname\">\n334\t </div>`;\n335\t }\n336\t html += '</div>';\n337\t if (!isOwner) html += '<div style=\"font-size:10px;color:#333;margin-top:6px;\">Editing requires host1.</div>';\n338\t return html;\n339\t}\n340\t\n341\tfunction vvPtHostChanged(el) {\n342\t el.classList.toggle('changed', el.value !== el.dataset.orig);\n343\t const any = document.querySelector('.vv-pt-host-inp.changed');\n344\t document.getElementById('vv-pt-hosts-save').style.display = any ? '' : 'none';\n345\t}\n346\t\n347\tfunction vvPtSaveHosts(btn) {\n348\t const changedEls = document.querySelectorAll('.vv-pt-host-inp.changed');\n349\t if (!changedEls.length) return;\n350\t const changes = [];\n351\t changedEls.forEach(el => changes.push({key: el.dataset.key, file: el.dataset.file, type: el.dataset.type, value: el.value}));\n352\t if (!confirm(`Save ${changes.length} host setting(s)?`)) return;\n353\t btn.disabled = true; btn.textContent = '⟳ Saving…';\n354\t fetch('/plugins/varaverk/api/confform.php', {\n355\t method: 'POST',\n356\t headers: {'Content-Type': 'application/x-www-form-urlencoded'},\n357\t body: new URLSearchParams({\n358\t csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '',\n359\t id: '__settings__',\n360\t changes: JSON.stringify(changes)\n361\t })\n362\t })\n363\t .then(r => r.json())\n364\t .then(d => {\n365\t btn.disabled = false;\n366\t if (d.ok) {\n367\t changedEls.forEach(el => { el.dataset.orig = el.value; el.classList.remove('changed'); });\n368\t btn.textContent = '✓ Saved';\n369\t setTimeout(() => { btn.textContent = 'Save'; btn.style.display = 'none'; }, 2000);\n370\t } else {\n371\t btn.textContent = 'Save';\n372\t alert('Save failed: ' + (d.error ?? 'Unknown error'));\n373\t }\n374\t })\n375\t .catch(e => { btn.disabled = false; btn.textContent = 'Save'; alert('Error: ' + e); });\n376\t}\n377\t\n378\t// ── Array settings cards ───────────────────────────────────────────────────────\n379\t\n380\tlet _vvArrFiles = null;\n381\t\n382\tfunction _vvInitArrayCards() {\n383\t if (_vvArrFiles !== null) return;\n384\t _vvArrFiles = false; // loading sentinel\n385\t fetch('/plugins/varaverk/api/partnership_settings.php?_=' + Date.now())\n386\t .then(r => r.json())\n387\t .then(d => {\n388\t _vvArrFiles = (d.ok && d.files) ? d.files : [];\n389\t _vvRenderArrayCards();\n390\t })\n391\t .catch(() => { _vvArrFiles = []; document.getElementById('vv-pt-arrays-wrap').style.display = 'none'; });\n392\t}\n393\t\n394\tfunction _vvRenderArrayCards() {\n395\t const files = _vvArrFiles;\n396\t if (!files || !files.length) { document.getElementById('vv-pt-arrays-wrap').style.display = 'none'; return; }\n397\t\n398\t let html = '';\n399\t let hasArrays = false;\n400\t for (const f of files) {\n401\t for (const g of f.groups) {\n402\t for (const fld of g.fields) {\n403\t if (fld.type === 'scalar') continue;\n404\t hasArrays = true;\n405\t const rows = Math.min(16, (fld.value.match(/\\n/g) || []).length + 2);\n406\t const attrs = `class=\"vv-set-input\" data-key=\"${_vvSetEsc(fld.key)}\" `\n407\t + `data-file=\"${_vvSetEsc(fld.file)}\" data-type=\"${_vvSetEsc(fld.type)}\" `\n408\t + `data-orig=\"${_vvSetEsc(fld.value)}\" oninput=\"vvPtArrChanged(this)\"`;\n409\t html += `<div class=\"vv-set-file\">\n410\t <div class=\"vv-set-file-hdr\" onclick=\"vvPtToggleNode(this)\">\n411\t <span>${_vvSetEsc(fld.key)}</span>\n412\t <div style=\"display:flex;align-items:center;gap:6px;\">\n413\t ${fld.desc ? `<span style=\"font-size:9px;color:#444;font-weight:normal;font-family:inherit;\">${_vvSetEsc(fld.desc)}</span>` : ''}\n414\t <span class=\"vv-set-chev\">▸</span>\n415\t </div>\n416\t </div>\n417\t <div class=\"vv-set-file-body\" style=\"display:none;\">\n418\t <textarea ${attrs} rows=\"${rows}\">${_vvSetEsc(fld.value)}</textarea>\n419\t </div>\n420\t </div>`;\n421\t }\n422\t }\n423\t }\n424\t\n425\t const wrap = document.getElementById('vv-pt-arrays-wrap');\n426\t if (!hasArrays) { wrap.style.display = 'none'; return; }\n427\t\n428\t wrap.style.display = '';\n429\t wrap.innerHTML = `<div class=\"vv-card\">\n430\t <div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;\">\n431\t <h3 style=\"margin:0;\">Array Settings</h3>\n432\t <button id=\"vv-pt-arrays-save\" class=\"vv-pt-action-btn run\"\n433\t onclick=\"vvPtSaveArrays(this)\" style=\"display:none;\">Save Changes</button>\n434\t </div>\n435\t ${html}\n436\t </div>`;\n437\t}\n438\t\n439\tfunction vvPtArrChanged(el) {\n440\t el.classList.toggle('changed', el.value !== el.dataset.orig);\n441\t const any = document.querySelector('#vv-pt-arrays-wrap .vv-set-input.changed');\n442\t const saveBtn = document.getElementById('vv-pt-arrays-save');\n443\t if (saveBtn) saveBtn.style.display = any ? '' : 'none';\n444\t}\n445\t\n446\tfunction vvPtSaveArrays(btn) {\n447\t const changedEls = document.querySelectorAll('#vv-pt-arrays-wrap .vv-set-input.changed');\n448\t if (!changedEls.length) return;\n449\t const changes = [];\n450\t changedEls.forEach(el => changes.push({key: el.dataset.key, file: el.dataset.file, type: el.dataset.type, value: el.value}));\n451\t if (!confirm(`Save ${changes.length} changed setting(s)?`)) return;\n452\t btn.disabled = true; btn.textContent = '⟳ Saving…';\n453\t fetch('/plugins/varaverk/api/confform.php', {\n454\t method: 'POST',\n455\t headers: {'Content-Type': 'application/x-www-form-urlencoded'},\n456\t body: new URLSearchParams({\n457\t csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '',\n458\t id: '__settings__',\n459\t changes: JSON.stringify(changes)\n460\t })\n461\t })\n462\t .then(r => r.json())\n463\t .then(d => {\n464\t btn.disabled = false;\n465\t if (d.ok) {\n466\t changedEls.forEach(el => { el.dataset.orig = el.value; el.classList.remove('changed'); });\n467\t btn.textContent = '✓ Saved';\n468\t _vvArrFiles = null; // invalidate so next init re-fetches\n469\t setTimeout(() => _vvInitArrayCards(), 600);\n470\t } else {\n471\t btn.textContent = 'Save Changes';\n472\t alert('Save failed: ' + (d.error ?? 'Unknown error'));\n473\t }\n474\t })\n475\t .catch(e => { btn.disabled = false; btn.textContent = 'Save Changes'; alert('Error: ' + e); });\n476\t}\n477\t\n478\t// ── Settings panel ─────────────────────────────────────────────────────────────\n479\tlet _vvSetLoaded = false;\n480\t\n481\tfunction _vvSetEsc(s) {\n482\t return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\"/g,'"');\n483\t}\n484\t\n485\tfunction vvPtToggleSettings() {\n486\t const body = document.getElementById('vv-pt-settings-body');\n487\t const toggle = document.getElementById('vv-pt-settings-toggle');\n488\t const open = body.style.display !== 'none';\n489\t body.style.display = open ? 'none' : '';\n490\t toggle.textContent = open ? '▸ Show settings' : '▾ Hide settings';\n491\t if (!open && !_vvSetLoaded) vvPtLoadSettings();\n492\t}\n493\t\n494\tfunction vvPtLoadSettings() {\n495\t const body = document.getElementById('vv-pt-settings-body');\n496\t body.innerHTML = '<div style=\"color:#555;font-size:12px;padding:8px 0;\">Loading…</div>';\n497\t fetch('/plugins/varaverk/api/partnership_settings.php?_=' + Date.now())\n498\t .then(r => r.json())\n499\t .then(d => {\n500\t if (!d.ok || !(d.files || []).length) {\n501\t body.innerHTML = '<div style=\"color:#555;font-size:12px;padding:8px 0;\">No settings available.</div>';\n502\t return;\n503\t }\n504\t // Host-aware hint: master.conf edits propagate; host config stays local.\n505\t const hasMaster = d.files.some(f => f.file === 'master.conf');\n506\t document.getElementById('vv-pt-settings-hint').textContent = hasMaster\n507\t ? 'Shared (master.conf) edits propagate to partners on save · host config stays local to this server.'\n508\t : 'These settings apply to this host only.';\n509\t body.innerHTML = vvPtRenderSettings(d.files);\n510\t _vvSetLoaded = true;\n511\t })\n512\t .catch(e => { body.innerHTML = '<div style=\"color:#f88;font-size:12px;padding:8px 0;\">Error: ' + e + '</div>'; });\n513\t}\n514\t\n515\tfunction vvPtRenderSettings(files) {\n516\t let html = '';\n517\t for (const f of files) {\n518\t const label = f.file === 'master.conf' ? 'master.conf — shared' : f.file + ' — this host';\n519\t const scalarFields = f.groups.flatMap(g => g.fields.filter(fld => fld.type === 'scalar'));\n520\t if (!scalarFields.length) continue;\n521\t html += `<div class=\"vv-set-file\">\n522\t <div class=\"vv-set-file-hdr\" onclick=\"vvPtToggleNode(this)\">\n523\t <span>${_vvSetEsc(label)}</span><span class=\"vv-set-chev\">▾</span>\n524\t </div>\n525\t <div class=\"vv-set-file-body\">`;\n526\t for (const fld of scalarFields) {\n527\t const attrs = `class=\"vv-set-input\" data-key=\"${_vvSetEsc(fld.key)}\" `\n528\t + `data-file=\"${_vvSetEsc(fld.file)}\" data-type=\"${_vvSetEsc(fld.type)}\" `\n529\t + `data-orig=\"${_vvSetEsc(fld.value)}\" oninput=\"vvPtSetChanged(this)\"`;\n530\t html += `<div class=\"vv-set-field\">\n531\t <div class=\"vv-set-key\">${_vvSetEsc(fld.key)}</div>`;\n532\t if (fld.desc) html += `<div class=\"vv-set-desc\">${_vvSetEsc(fld.desc)}</div>`;\n533\t html += `<input type=\"text\" ${attrs} value=\"${_vvSetEsc(fld.value)}\"></div>`;\n534\t }\n535\t html += `</div></div>`;\n536\t }\n537\t return html || '<div style=\"color:#555;font-size:12px;padding:8px 0;\">No scalar settings available.</div>';\n538\t}\n539\t\n540\tfunction vvPtToggleNode(hdr) {\n541\t const body = hdr.nextElementSibling;\n542\t const chev = hdr.querySelector('.vv-set-chev');\n543\t const open = body.style.display !== 'none';\n544\t body.style.display = open ? 'none' : '';\n545\t if (chev) chev.textContent = open ? '▸' : '▾';\n546\t}\n547\t\n548\tfunction vvPtSetChanged(el) {\n549\t const changed = el.value !== el.dataset.orig;\n550\t el.classList.toggle('changed', changed);\n551\t // Show Save button if any field is changed\n552\t const any = document.querySelector('#vv-pt-settings-body .vv-set-input.changed');\n553\t document.getElementById('vv-pt-settings-save').style.display = any ? '' : 'none';\n554\t}\n555\t\n556\tfunction vvPtSaveSettings(btn) {\n557\t const changedEls = document.querySelectorAll('#vv-pt-settings-body .vv-set-input.changed');\n558\t if (!changedEls.length) return;\n559\t const changes = [];\n560\t changedEls.forEach(el => changes.push({\n561\t key: el.dataset.key, file: el.dataset.file, type: el.dataset.type, value: el.value\n562\t }));\n563\t if (!confirm(`Save ${changes.length} changed setting(s)?`)) return;\n564\t btn.disabled = true; btn.textContent = '⟳ Saving…';\n565\t const params = new URLSearchParams({\n566\t csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '',\n567\t id: '__settings__',\n568\t changes: JSON.stringify(changes)\n569\t });\n570\t fetch('/plugins/varaverk/api/confform.php', {\n571\t method: 'POST',\n572\t headers: {'Content-Type': 'application/x-www-form-urlencoded'},\n573\t body: params\n574\t })\n575\t .then(r => r.json())\n576\t .then(d => {\n577\t btn.disabled = false;\n578\t if (d.ok) {\n579\t // Commit new originals, clear highlights\n580\t changedEls.forEach(el => { el.dataset.orig = el.value; el.classList.remove('changed'); });\n581\t btn.textContent = '✓ Saved';\n582\t setTimeout(() => { btn.textContent = 'Save Changes'; btn.style.display = 'none'; }, 2000);\n583\t } else {\n584\t btn.textContent = 'Save Changes';\n585\t alert('Save failed: ' + (d.error ?? 'Unknown error'));\n586\t }\n587\t })\n588\t .catch(e => { btn.disabled = false; btn.textContent = 'Save Changes'; alert('Error: ' + e); });\n589\t}\n590\t\n591\t// ── Private page logic ─────────────────────────────────────────────────────────\n592\t(function() {\n593\t\n594\t// ── Helpers ───────────────────────────────────────────────────────────────────\n595\t\n596\tfunction _relTime(ts) {\n597\t if (!ts) return '—';\n598\t const d = Math.floor(Date.now() / 1000) - ts;\n599\t if (d < 60) return 'just now';\n600\t if (d < 3600) return Math.floor(d / 60) + 'm ago';\n601\t if (d < 86400) return Math.floor(d / 3600) + 'h ago';\n602\t if (d < 172800) return 'yesterday';\n603\t return Math.floor(d / 86400) + 'd ago';\n604\t}\n605\t\n606\tfunction _uptime(sec) {\n607\t if (!sec) return '—';\n608\t const d = Math.floor(sec / 86400);\n609\t const h = Math.floor((sec % 86400) / 3600);\n610\t const m = Math.floor((sec % 3600) / 60);\n611\t return d > 0 ? `${d}d ${h}h ${m}m` : h > 0 ? `${h}h ${m}m` : `${m}m`;\n612\t}\n613\t\n614\tfunction _row(lbl, val) {\n615\t return `<div class=\"vv-pt-row\"><span class=\"vv-pt-lbl\">${lbl}</span><span class=\"vv-pt-val\">${val}</span></div>`;\n616\t}\n617\t\n618\t// ── Offline countdown warning ───────────────────────────────────────────────────\n619\t\n620\tfunction _renderOfflineWarn(cfg) {\n621\t const el = document.getElementById('vv-pt-offline-warn');\n622\t const days = cfg.offline_days;\n623\t const thr = cfg.offline_threshold || 30;\n624\t // Auto-offboard only runs for active partnerships — a stale counter while disabled is meaningless.\n625\t if (!cfg.enabled || !days || days < 1) { el.style.display = 'none'; el.innerHTML = ''; return; }\n626\t\n627\t const left = Math.max(0, thr - days);\n628\t const crit = left <= 5;\n629\t const col = crit ? '#f44336' : '#ff9800';\n630\t const bg = crit ? '#2a1212' : '#1a1200';\n631\t const pct = Math.min(100, Math.round(days / thr * 100));\n632\t el.style.display = '';\n633\t el.innerHTML = `<div class=\"vv-card\" style=\"background:${bg};border-color:${col};\">\n634\t <div style=\"display:flex;justify-content:space-between;align-items:baseline;gap:10px;flex-wrap:wrap;\">\n635\t <span style=\"font-size:12px;color:${col};font-weight:600;\">⚠ Partner unreachable ${days} day${days===1?'':'s'}</span>\n636\t <span style=\"font-size:11px;color:#888;\">Auto-offboard in ${left} day${left===1?'':'s'} (threshold ${thr}d)</span>\n637\t </div>\n638\t <div style=\"margin-top:6px;height:5px;background:#000;border-radius:3px;overflow:hidden;\">\n639\t <div style=\"width:${pct}%;height:100%;background:${col};\"></div>\n640\t </div>\n641\t </div>`;\n642\t}\n643\t\n644\t// ── Mirror sync health ──────────────────────────────────────────────────────────\n645\t\n646\tfunction _syncToggle(gate, dimmed) {\n647\t if (!gate) return '';\n648\t const on = gate.on;\n649\t const col = on ? '#4caf50' : '#f44336';\n650\t const bg = on ? '#0a1a0a' : '#2a1212';\n651\t const op = dimmed ? 'opacity:.4;' : '';\n652\t return `<span onclick=\"vvPtToggleSync(this,'${gate.var}',${on ? 0 : 1})\"\n653\t style=\"cursor:pointer;font-size:9px;font-weight:600;padding:2px 9px;border-radius:10px;\n654\t background:${bg};color:${col};border:1px solid ${col};white-space:nowrap;margin-left:8px;${op}\"\n655\t title=\"${gate.var} — click to ${on ? 'disable' : 'enable'}\">${on ? 'ON' : 'OFF'}</span>`;\n656\t}\n657\t\n658\tfunction _renderSync(sync) {\n659\t const jobs = sync.jobs || {};\n660\t const gates = sync.gates || {};\n661\t const intervalMin = sync.interval_min || 30;\n662\t\n663\t const sCol = s => s === 'ok' ? '#4caf50' : s === 'running' ? '#4a9eff'\n664\t : s === 'warn' ? '#ff9800' : s === 'error' ? '#f44336' : '#555';\n665\t const sLbl = s => s === 'ok' ? '✓' : s === 'running' ? '⟳' : s === 'warn' ? '⚠' : s === 'error' ? '✗' : '—';\n666\t\n667\t function _dur(sec) {\n668\t if (!sec || sec < 0) return '';\n669\t if (sec < 60) return sec + 's';\n670\t if (sec < 3600) return Math.floor(sec / 60) + 'm ' + (sec % 60) + 's';\n671\t return Math.floor(sec / 3600) + 'h ' + Math.floor((sec % 3600) / 60) + 'm';\n672\t }\n673\t\n674\t function _countdown(ts) {\n675\t if (!ts) return null;\n676\t const sec = ts - Math.floor(Date.now() / 1000);\n677\t if (sec <= 0) return 'due';\n678\t if (sec < 60) return 'in ' + sec + 's';\n679\t if (sec < 3600) return 'in ' + Math.floor(sec / 60) + 'm';\n680\t return 'in ' + Math.floor(sec / 3600) + 'h ' + Math.floor((sec % 3600) / 60) + 'm';\n681\t }\n682\t\n683\t const globalOff = gates.global && !gates.global.on;\n684\t\n685\t // Wrap everything in a flex-column so the bottom section anchors naturally\n686\t let html = '<div style=\"display:flex;flex-direction:column;gap:0;height:100%;\">';\n687\t\n688\t // Master toggle row\n689\t if (gates.global) {\n690\t html += `<div class=\"vv-pt-row\" style=\"margin-bottom:8px;padding-bottom:8px;border-bottom:1px solid #1c1c1c;\">\n691\t <span class=\"vv-pt-lbl\" style=\"font-weight:600;color:#999;\">All mirroring</span>\n692\t <span class=\"vv-pt-val\">${_syncToggle(gates.global, false)}</span>\n693\t </div>`;\n694\t }\n695\t\n696\t // Per-tier rows\n697\t const tierMeta = {\n698\t critical: {label: 'Critical', sched: 'every 30 min', desc: 'appdata · partnership · play state'},\n699\t daily: {label: 'Daily', sched: 'nightly', desc: 'media shares · arr sync · docker'},\n700\t weekly: {label: 'Weekly', sched: 'weekly', desc: 'bulk shares · updates · cleanup'},\n701\t };\n702\t\n703\t // Critical next-run estimate\n704\t const critJob = jobs.critical;\n705\t const critNextTs = critJob && critJob.end && critJob.status !== 'running'\n706\t ? critJob.end + intervalMin * 60 : null;\n707\t\n708\t html += '<div style=\"display:flex;flex-direction:column;gap:10px;flex:1;\">';\n709\t\n710\t for (const key of ['critical', 'daily', 'weekly']) {\n711\t const j = jobs[key];\n712\t const meta = tierMeta[key];\n713\t const status = j ? j.status : null;\n714\t const when = j && j.start\n715\t ? (j.status === 'running' ? 'started ' + _relTime(j.start) : _relTime(j.end || j.start))\n716\t : null;\n717\t const dur = j && j.start && j.end && j.status !== 'running' ? _dur(j.end - j.start) : '';\n718\t const summary = j && j.summary ? j.summary : '';\n719\t\n720\t html += `<div style=\"background:#0f0f0f;border:1px solid #1e1e1e;border-radius:4px;padding:7px 9px;\">\n721\t <div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;\">\n722\t <span style=\"font-size:11px;font-weight:500;color:#aaa;\">${meta.label}\n723\t <span style=\"font-size:9px;color:#333;font-weight:normal;margin-left:3px;\">${meta.sched}</span>\n724\t </span>\n725\t <span style=\"display:flex;align-items:center;gap:5px;\">\n726\t <span style=\"font-size:11px;color:${sCol(status)};\">${sLbl(status)}</span>\n727\t ${dur ? `<span style=\"font-size:9px;color:#3a3a3a;\">${dur}</span>` : ''}\n728\t ${when ? `<span style=\"font-size:9px;color:#2e2e2e;\">${when}</span>` : ''}\n729\t ${_syncToggle(gates[key], globalOff)}\n730\t </span>\n731\t </div>\n732\t <div style=\"font-size:9px;color:#2a2a2a;\">${meta.desc}</div>\n733\t ${summary ? `<div style=\"font-size:9px;color:#3a5a3a;margin-top:3px;font-family:monospace;\">${_vvSetEsc(summary)}</div>` : ''}\n734\t </div>`;\n735\t }\n736\t\n737\t html += '</div>'; // flex:1\n738\t\n739\t // Bottom info strip\n740\t const nextLabel = critNextTs ? _countdown(critNextTs) : null;\n741\t html += `<div style=\"margin-top:10px;padding-top:8px;border-top:1px solid #1a1a1a;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:6px;\">\n742\t <span style=\"font-size:10px;color:#2a2a2a;\">interval: ${intervalMin}min</span>\n743\t ${nextLabel ? `<span style=\"font-size:10px;color:#2e3e2e;\">next critical ${nextLabel}</span>` : ''}\n744\t </div>`;\n745\t\n746\t if (globalOff) {\n747\t html += `<div style=\"font-size:10px;color:#f44336;margin-top:6px;\">⚠ All mirroring is off — per-tier switches have no effect until re-enabled.</div>`;\n748\t }\n749\t\n750\t html += '</div>'; // outer flex column\n751\t return html;\n752\t}\n753\t\n754\t// ── Config bar ────────────────────────────────────────────────────────────────\n755\t\n756\tfunction _renderConfig(cfg) {\n757\t const dot = cfg.enabled ? '#4caf50' : '#555';\n758\t const label = cfg.enabled\n759\t ? `<span style=\"color:#4caf50;\">enabled</span>`\n760\t : `<span style=\"color:#555;\">disabled</span>`;\n761\t const owner = cfg.owner_host || '—';\n762\t const items = [\n763\t label,\n764\t `Owner: <span style=\"color:#ccc;\">${owner}</span>`,\n765\t `Sync: ${cfg.sync_min}min`,\n766\t `Grace: ${cfg.grace_hours}h`,\n767\t `Offline threshold: ${cfg.offline_threshold}d`,\n768\t cfg.remove_tailscale ? 'Tailscale removal: on' : 'Tailscale removal: off',\n769\t cfg.tailscale_configured ? '' : '<span style=\"color:#ff9800;\">⚠ Tailscale API key not set</span>',\n770\t ].filter(Boolean);\n771\t\n772\t return `<div style=\"display:flex;align-items:center;gap:6px;flex-wrap:wrap;\">\n773\t <span class=\"vv-pt-dot\" style=\"background:${dot}\"></span>\n774\t ${items.map(i => `<span style=\"font-size:11px;color:#666;\">${i}</span>`)\n775\t .join('<span style=\"color:#333;\">·</span>')}\n776\t </div>`;\n777\t}\n778\t\n779\t// ── Node card ─────────────────────────────────────────────────────────────────\n780\t\n781\tconst fbColors = {\n782\t NORMAL: '#4caf50',\n783\t FAILOVER: '#f44336',\n784\t NO_INTERNET: '#ff9800',\n785\t DARK: '#9e9e9e',\n786\t UNKNOWN: '#444',\n787\t};\n788\tconst fbLabels = {\n789\t NORMAL: '✓ Nominal',\n790\t FAILOVER: '⚠ Failover',\n791\t NO_INTERNET: '⚡ No internet',\n792\t DARK: '◌ Dark mode',\n793\t UNKNOWN: '— Unknown',\n794\t};\n795\tconst ptStates = {\n796\t ACTIVE: ['#4caf50', 'Active'],\n797\t INACTIVE: ['#555', 'Inactive'],\n798\t PENDING: ['#ff9800', 'Pending'],\n799\t};\n800\t\n801\tfunction _nodeCard(node) {\n802\t const tsOnline = node.ts_online;\n803\t const dotCol = tsOnline === null ? '#555' : tsOnline ? '#4caf50' : '#f44336';\n804\t const dotTip = tsOnline === null ? 'unknown' : tsOnline ? (node.ts_active ? 'active' : 'idle') : 'offline';\n805\t\n806\t const tags = [\n807\t node.is_me ? '<span class=\"vv-pt-tag me\">US</span>' : '',\n808\t node.is_owner ? '<span class=\"vv-pt-tag owner\">OWNER</span>' : '<span class=\"vv-pt-tag mirror\">MIRROR</span>',\n809\t ].join('');\n810\t\n811\t // System info\n812\t const sys = node.system || {};\n813\t const ver = sys.unraid_version || '—';\n814\t const uptime = sys.uptime_sec ? _uptime(sys.uptime_sec) : '—';\n815\t\n816\t // Fallback\n817\t const fb = (node.fallback || 'UNKNOWN').toUpperCase();\n818\t const fbCol = fbColors[fb] || '#444';\n819\t const fbLbl = fbLabels[fb] || fb;\n820\t\n821\t // Partnership DB\n822\t const pt = node.partnership || {};\n823\t const ptState = (pt.state || '').toUpperCase();\n824\t const [ptCol, ptLbl] = ptStates[ptState] || ['#444', ptState || '—'];\n825\t const ptUpdated = pt.updated ? _relTime(parseInt(pt.updated)) : null;\n826\t\n827\t // Onboard phase badge (remote nodes) or local-done badge (self)\n828\t const phase = node.onboard_phase;\n829\t const phaseHtml = node.is_me\n830\t ? (node.local_done\n831\t ? `<div style=\"font-size:10px;color:#4caf50;margin-bottom:6px;padding:2px 6px;background:#0a1a0a;border:1px solid #2a5a2a;border-radius:3px;\">✓ Local setup complete · waiting for partner</div>`\n832\t : '')\n833\t : (phase === 2 ? '' :\n834\t phase === 1 ? `<div style=\"font-size:10px;color:#ff9800;margin-bottom:6px;padding:2px 6px;background:#1a1200;border:1px solid #3a2800;border-radius:3px;\">⏳ SSH ready · awaiting onboard</div>` :\n835\t node.key_ready ? `<div style=\"font-size:10px;color:#ff9800;margin-bottom:6px;padding:2px 6px;background:#1a1200;border:1px solid #3a2800;border-radius:3px;\">🔑 Key generated · install on HOST2 then Push Conf</div>` :\n836\t `<div style=\"font-size:10px;color:#555;margin-bottom:6px;padding:2px 6px;background:#111;border:1px solid #222;border-radius:3px;\">○ Not yet provisioned</div>`);\n837\t\n838\t const apiKeyHtml = node.is_me\n839\t ? (node.api_key_set\n840\t ? `<div style=\"font-size:10px;color:#2a4a2a;margin-bottom:6px;padding:2px 6px;background:#0a1a0a;\n841\t border:1px solid #1a3a1a;border-radius:3px;display:flex;justify-content:space-between;align-items:center;\">\n842\t <span style=\"color:#3a7a3a;\">🔑 API key: <span style=\"color:#4caf50;\">${node.api_key_preview}</span></span>\n843\t <button onclick=\"vvApiKey(this)\"\n844\t style=\"font-size:9px;padding:1px 6px;background:#0d0d0d;color:#444;border:1px solid #222;\n845\t border-radius:2px;cursor:pointer;margin-left:6px;white-space:nowrap;\">↻ renew</button>\n846\t </div>`\n847\t : `<div style=\"font-size:10px;color:#ff9800;margin-bottom:6px;padding:2px 6px;background:#1a1200;\n848\t border:1px solid #3a2800;border-radius:3px;display:flex;justify-content:space-between;align-items:center;\">\n849\t <span>⚡ No API key — Monitor stats unavailable</span>\n850\t <button onclick=\"vvApiKey(this)\"\n851\t style=\"font-size:9px;padding:1px 8px;background:#1a3a1a;color:#6fcf97;border:1px solid #2e6b2e;\n852\t border-radius:2px;cursor:pointer;margin-left:6px;white-space:nowrap;\">+ Create</button>\n853\t </div>`)\n854\t : '';\n855\t\n856\t // ── Metrics strip ────────────────────────────────────────────────────────────\n857\t const m = node.metrics || {};\n858\t function _ptCol(v, warn, crit) {\n859\t return v >= crit ? '#f44336' : v >= warn ? '#ff9800' : '#4caf50';\n860\t }\n861\t const metricPairs = [];\n862\t if (m.cpu_pct != null) metricPairs.push(['CPU', `<span style=\"color:${_ptCol(m.cpu_pct,70,90)};\">${m.cpu_pct}%</span>`]);\n863\t if (m.ram_used_gb != null) {\n864\t const ramPct = m.ram_total_gb ? Math.round(m.ram_used_gb / m.ram_total_gb * 100) : 0;\n865\t metricPairs.push(['RAM', `<span style=\"color:${_ptCol(ramPct,75,90)};\">${m.ram_used_gb}<span style=\"color:#444;font-size:9px;\">/${m.ram_total_gb}GB</span></span>`]);\n866\t }\n867\t if (m.load_avg != null) metricPairs.push(['Load', `<span style=\"color:${m.load_avg>8?'#f44336':m.load_avg>4?'#ff9800':'#4caf50'};\">${m.load_avg}</span>`]);\n868\t if (m.containers != null) metricPairs.push(['Conts', `<span style=\"color:#bbb;\">${m.containers}</span>`]);\n869\t if (m.array_used_tb != null) metricPairs.push(['Array', `<span style=\"color:#bbb;\">${m.array_used_tb}<span style=\"color:#444;font-size:9px;\">/${m.array_total_tb}TB</span></span>`]);\n870\t if (m.max_disk_temp != null) metricPairs.push(['Temp', `<span style=\"color:${_ptCol(m.max_disk_temp,55,70)};\">${m.max_disk_temp}°C</span>`]);\n871\t if (m.vm_count != null && m.vm_count > 0) metricPairs.push(['VMs', `<span style=\"color:#bbb;\">${m.vm_count}</span>`]);\n872\t\n873\t let metricsHtml = '';\n874\t if (metricPairs.length) {\n875\t let cells = '';\n876\t for (const [lbl, val] of metricPairs) {\n877\t cells += `<span style=\"color:#555;\">${lbl}</span>${val}`;\n878\t }\n879\t if (metricPairs.length % 2 !== 0) cells += '<span></span><span></span>';\n880\t metricsHtml = `<div style=\"display:grid;grid-template-columns:auto 1fr auto 1fr;gap:2px 8px;font-size:10px;margin-bottom:6px;\">${cells}</div>`;\n881\t }\n882\t\n883\t let body = '';\n884\t\n885\t body += phaseHtml;\n886\t body += apiKeyHtml;\n887\t body += metricsHtml;\n888\t\n889\t // Network\n890\t body += `<div style=\"display:flex;align-items:center;gap:6px;margin-bottom:8px;\">\n891\t <span class=\"vv-pt-dot\" style=\"background:${dotCol}\"></span>\n892\t <span style=\"font-size:11px;color:${dotCol};\">${dotTip}</span>`;\n893\t if (node.ts_ip) body += `<span style=\"font-size:10px;color:#444;margin-left:4px;\">${node.ts_ip}</span>`;\n894\t body += `</div>`;\n895\t\n896\t // System\n897\t body += _row('unRAID', ver);\n898\t body += _row('Uptime', uptime);\n899\t\n900\t body += `<hr class=\"vv-pt-sep\">`;\n901\t\n902\t // States\n903\t body += `<div class=\"vv-pt-row\">\n904\t <span class=\"vv-pt-lbl\">Fallback</span>\n905\t <span class=\"vv-pt-state\" style=\"color:${fbCol};\">${fbLbl}</span>\n906\t </div>`;\n907\t body += `<div class=\"vv-pt-row\">\n908\t <span class=\"vv-pt-lbl\">Partnership</span>\n909\t <span class=\"vv-pt-state\" style=\"color:${ptCol};\">${ptLbl}${ptUpdated ? `<span style=\"font-size:9px;color:#444;margin-left:4px;\">${ptUpdated}</span>` : ''}</span>\n910\t </div>`;\n911\t\n912\t if (pt.reason) {\n913\t body += `<div style=\"font-size:10px;color:#555;margin-top:3px;text-align:right;\">${pt.reason}</div>`;\n914\t }\n915\t\n916\t return `<div class=\"vv-pt-node${node.is_me ? ' me' : ''}\">\n917\t <div class=\"vv-pt-node-head\">\n918\t <div>\n919\t <div class=\"vv-pt-hostname\">${node.hostname}</div>\n920\t <div class=\"vv-pt-slot\">${node.id}</div>\n921\t </div>\n922\t <div class=\"vv-pt-tags\">${tags}</div>\n923\t </div>\n924\t ${body}\n925\t </div>`;\n926\t}\n927\t\n928\t// ── Actions ───────────────────────────────────────────────────────────────────\n929\t\n930\tfunction _renderActions(nodes, cfg) {\n931\t const isOwner = nodes.some(n => n.is_me && n.is_owner);\n932\t const selfNode = nodes.find(n => n.is_me);\n933\t const remotes = nodes.filter(n => !n.is_me);\n934\t const hasPartner = remotes.some(n => n.hostname);\n935\t const termBase = `https://${window.location.hostname}/webterminal/ttyd/`;\n936\t const termCmd = `bash <?= SCRIPTS_DIR ?>/Partnership/partnership_onboard.sh --phase1-only`;\n937\t\n938\t let html = '';\n939\t\n940\t // ── No partner ─────────────────────────────────────────────────────────────\n941\t if (!hasPartner && isOwner) {\n942\t html += `<div style=\"background:#0f0f0f;border:1px solid #1e1e1e;border-radius:4px;padding:14px 16px;\n943\t display:flex;flex-direction:column;align-items:center;gap:6px;text-align:center;\">\n944\t <div style=\"font-size:11px;color:#555;\">No partner host configured</div>\n945\t <div style=\"font-size:10px;color:#333;\">Set HOST2 in master.conf (Host Settings above) to enable partnership.</div>\n946\t </div>`;\n947\t }\n948\t\n949\t // ── Per-remote host section ────────────────────────────────────────────────\n950\t if (isOwner && hasPartner) {\n951\t remotes.forEach((remote, idx) => {\n952\t const phase = remote.onboard_phase ?? 0;\n953\t const isOnboarding = !!_vvOnboarding[remote.id];\n954\t const isDeleting = !!_vvDeleteKeys[remote.id];\n955\t const dotCol = remote.ts_online === null ? '#444' : remote.ts_online ? '#4caf50' : '#f44336';\n956\t const dotLbl = remote.ts_online === null ? 'unknown' : remote.ts_online ? 'online' : 'offline';\n957\t const pt = remote.partnership || {};\n958\t const ptState = (pt.state || '').toUpperCase();\n959\t const ptDate = pt.updated\n960\t ? new Date(parseInt(pt.updated) * 1000).toLocaleDateString([], {month:'short',day:'numeric',year:'numeric'})\n961\t : null;\n962\t const sys = remote.system || {};\n963\t const sysUptime = sys.uptime_sec ? _uptime(sys.uptime_sec) : null;\n964\t const sysVer = sys.unraid_version || null;\n965\t\n966\t // Left border colour by phase\n967\t const borderCol = phase === 2 ? '#2a5a2a'\n968\t : phase === 1 ? '#4a3000'\n969\t : isOnboarding ? '#0e2a4a' : '#222';\n970\t\n971\t if (idx > 0) html += '<div style=\"height:10px;\"></div>';\n972\t\n973\t html += `<div style=\"background:#0d0d0d;border:1px solid #1e1e1e;border-left:3px solid ${borderCol};\n974\t border-radius:4px;overflow:hidden;\">`;\n975\t\n976\t // ── Card header ─────────────────────────────────────────────────────────\n977\t html += `<div style=\"display:flex;justify-content:space-between;align-items:center;\n978\t padding:8px 10px;background:#111;border-bottom:1px solid #1a1a1a;\">\n979\t <div style=\"display:flex;align-items:center;gap:8px;\">\n980\t <span style=\"font-size:9px;color:#444;font-family:monospace;\">${remote.id}</span>\n981\t <span style=\"font-size:12px;color:#ccc;font-weight:600;\">${remote.hostname}</span>\n982\t <span style=\"font-size:9px;color:${dotCol};\">● ${dotLbl}</span>\n983\t </div>\n984\t <div style=\"display:flex;align-items:center;gap:6px;\">\n985\t <span id=\"vv-pt-ping-${remote.id}\" style=\"font-size:9px;color:#444;\"></span>\n986\t <button class=\"vv-pt-action-btn info\" onclick=\"vvPtPing(this,'${remote.slot}','${remote.id}')\"\n987\t style=\"font-size:9px;padding:2px 7px;\" title=\"SSH echo round-trip\">⇄ ping</button>\n988\t </div>\n989\t </div>`;\n990\t\n991\t html += `<div style=\"padding:10px 12px;\">`;\n992\t\n993\t // ── Phase 0: not provisioned ───────────────────────────────────────────\n994\t if (phase === 0) {\n995\t if (isOnboarding) {\n996\t html += `<div style=\"display:flex;flex-direction:column;gap:10px;\">\n997\t <div>\n998\t <div style=\"display:flex;align-items:center;gap:6px;margin-bottom:6px;\">\n999\t <span style=\"font-size:9px;font-weight:700;color:#4a9eff;background:#0a1828;\n1000\t padding:2px 8px;border-radius:10px;border:1px solid #1a3a5a;\">Step 1</span>\n1001\t <span style=\"font-size:11px;color:#888;\">Install SSH key on ${remote.hostname}</span>\n1002\t </div>\n1003\t <div style=\"display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:4px;\">\n1004\t <a href=\"${termBase}\" target=\"_blank\"\n1005\t style=\"padding:3px 10px;background:#0e1a2a;color:#7ab;border:1px solid #1e3a5a;\n1006\t border-radius:3px;text-decoration:none;font-size:10px;white-space:nowrap;\">Open Terminal</a>\n1007\t <code onclick=\"navigator.clipboard.writeText('${termCmd}').then(()=>{this.style.color='#4caf50';setTimeout(()=>this.style.color='#444',1500)})\"\n1008\t style=\"font-size:9px;color:#444;background:#080808;padding:3px 8px;border-radius:3px;\n1009\t border:1px solid #181818;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;\n1010\t white-space:nowrap;cursor:pointer;\" title=\"Click to copy\">${termCmd}</code>\n1011\t </div>\n1012\t <div style=\"font-size:9px;color:#2a2a2a;\">Enter ${remote.hostname} root password when prompted · tab auto-updates on completion</div>\n1013\t </div>\n1014\t <div style=\"border-top:1px solid #1a1a1a;padding-top:10px;\">\n1015\t <div style=\"display:flex;align-items:center;gap:6px;margin-bottom:6px;\">\n1016\t <span style=\"font-size:9px;font-weight:700;color:#4caf50;background:#0a1a0a;\n1017\t padding:2px 8px;border-radius:10px;border:1px solid #1a3a1a;\">Step 2</span>\n1018\t <span style=\"font-size:11px;color:#888;\">Push conf</span>\n1019\t <span style=\"font-size:9px;color:#2a2a2a;\">if key already installed separately</span>\n1020\t </div>\n1021\t <button class=\"vv-pt-action-btn info\" onclick=\"vvPtPushConf(this,'${remote.id}')\"\n1022\t title=\"Runs --phase1-only --skip-ssh\" style=\"font-size:11px;\">▶ Push Conf</button>\n1023\t </div>\n1024\t </div>\n1025\t <div style=\"margin-top:10px;padding-top:8px;border-top:1px solid #181818;\">\n1026\t <button class=\"vv-pt-action-btn warn\" onclick=\"vvPtEndOnboard('${remote.id}')\"\n1027\t style=\"opacity:.5;font-size:11px;\">✕ Close</button>\n1028\t </div>`;\n1029\t } else {\n1030\t html += `<div style=\"display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;\">\n1031\t <div>\n1032\t <div style=\"font-size:11px;color:#555;margin-bottom:2px;\">Not provisioned</div>\n1033\t <div style=\"font-size:9px;color:#2a2a2a;\">SSH keys not installed · conf not pushed</div>\n1034\t </div>\n1035\t <div style=\"display:flex;gap:6px;flex-wrap:wrap;\">\n1036\t <button class=\"vv-pt-action-btn run\" onclick=\"vvPtStartOnboard('${remote.id}')\"\n1037\t style=\"font-size:11px;\">▶ Onboard</button>\n1038\t <button class=\"vv-pt-action-btn warn\" onclick=\"vvPtShowDeleteKeys('${remote.id}')\"\n1039\t style=\"opacity:.45;font-size:11px;\">🗑 Keys</button>\n1040\t </div>\n1041\t </div>`;\n1042\t }\n1043\t\n1044\t // ── Phase 1: SSH done, awaiting HOST2 ─────────────────────────────────\n1045\t } else if (phase === 1) {\n1046\t html += `<div style=\"display:flex;align-items:flex-start;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:10px;\">\n1047\t <div>\n1048\t <div style=\"display:flex;align-items:center;gap:6px;margin-bottom:4px;\">\n1049\t <span style=\"font-size:10px;font-weight:600;color:#ff9800;background:#1a1200;\n1050\t padding:2px 9px;border-radius:10px;border:1px solid #3a2800;\">SSH ready</span>\n1051\t ${remote.ts_ip ? `<span style=\"font-size:9px;color:#333;font-family:monospace;\">${remote.ts_ip}</span>` : ''}\n1052\t </div>\n1053\t <div style=\"font-size:9px;color:#383838;line-height:1.5;\">\n1054\t Waiting for ${remote.id} to install Varaverk and complete onboard.<br>Phase 2 triggers automatically.\n1055\t </div>\n1056\t </div>\n1057\t <div style=\"display:flex;flex-direction:column;gap:5px;align-items:flex-end;\">\n1058\t <button class=\"vv-pt-action-btn info\" onclick=\"vvPtPhase2(this,'${remote.id}')\"\n1059\t style=\"font-size:11px;\" title=\"Manually trigger Phase 2\">▶ Phase 2</button>\n1060\t <button class=\"vv-pt-action-btn warn\" onclick=\"vvPtCancel(this,'${remote.id}')\"\n1061\t style=\"font-size:11px;\">✕ Cancel</button>\n1062\t <button class=\"vv-pt-action-btn warn\" onclick=\"vvPtShowDeleteKeys('${remote.id}')\"\n1063\t style=\"opacity:.45;font-size:10px;\">🗑 Keys</button>\n1064\t </div>\n1065\t </div>`;\n1066\t\n1067\t // ── Phase 2: fully onboarded ───────────────────────────────────────────\n1068\t } else {\n1069\t const ptColor = ptState === 'ACTIVE' ? '#4caf50' : ptState === 'INACTIVE' ? '#f44336' : '#555';\n1070\t html += `<div style=\"display:flex;align-items:flex-start;justify-content:space-between;gap:10px;flex-wrap:wrap;\">\n1071\t <div>\n1072\t <div style=\"display:flex;align-items:center;gap:6px;margin-bottom:5px;\">\n1073\t <span style=\"font-size:10px;font-weight:600;color:#4caf50;background:#0a1a0a;\n1074\t padding:2px 9px;border-radius:10px;border:1px solid #1a4a1a;\">Active</span>\n1075\t ${ptState ? `<span style=\"font-size:9px;color:${ptColor};font-weight:500;\">${ptState}</span>` : ''}\n1076\t ${ptDate ? `<span style=\"font-size:9px;color:#2e2e2e;\">since ${ptDate}</span>` : ''}\n1077\t </div>\n1078\t <div style=\"display:flex;flex-wrap:wrap;gap:10px;font-size:9px;color:#333;font-family:monospace;\">\n1079\t ${remote.ts_ip ? `<span>${remote.ts_ip}</span>` : ''}\n1080\t ${sysVer ? `<span>unRAID ${sysVer}</span>` : ''}\n1081\t ${sysUptime ? `<span>up ${sysUptime}</span>` : ''}\n1082\t </div>\n1083\t </div>\n1084\t <div style=\"display:flex;flex-direction:column;gap:5px;align-items:flex-end;\">\n1085\t <button class=\"vv-pt-action-btn info\" onclick=\"vvPtPhase2(this,'${remote.id}')\"\n1086\t style=\"opacity:.35;font-size:10px;\" title=\"Re-run Phase 2\">↻ Re-run</button>\n1087\t <button class=\"vv-pt-action-btn warn\" onclick=\"vvPtShowDeleteKeys('${remote.id}')\"\n1088\t style=\"opacity:.4;font-size:10px;\">🗑 Keys</button>\n1089\t </div>\n1090\t </div>`;\n1091\t }\n1092\t\n1093\t // ── Delete Keys panel ──────────────────────────────────────────────────\n1094\t if (isDeleting) {\n1095\t html += `<div style=\"margin-top:10px;padding:10px;background:#0a0a0a;border:1px solid #2a1515;\n1096\t border-radius:3px;\">\n1097\t <div style=\"font-size:9px;color:#5a2a2a;font-weight:700;text-transform:uppercase;\n1098\t letter-spacing:.06em;margin-bottom:8px;\">Remove SSH access</div>\n1099\t <div style=\"display:flex;flex-direction:column;gap:7px;\">\n1100\t <div style=\"display:flex;align-items:center;justify-content:space-between;gap:8px;\">\n1101\t <div>\n1102\t <div style=\"font-size:10px;color:#888;\">HOST1 → ${remote.id}</div>\n1103\t <div style=\"font-size:9px;color:#333;\">HOST1's key on ${remote.hostname} · deletes local pair</div>\n1104\t </div>\n1105\t <button class=\"vv-pt-action-btn warn\" onclick=\"vvPtDeleteH1(this,'${remote.id}')\"\n1106\t style=\"white-space:nowrap;font-size:10px;\">✕ HOST1 key</button>\n1107\t </div>\n1108\t <div style=\"display:flex;align-items:center;justify-content:space-between;gap:8px;\">\n1109\t <div>\n1110\t <div style=\"font-size:10px;color:#888;\">${remote.id} → HOST1</div>\n1111\t <div style=\"font-size:9px;color:#333;\">${remote.hostname}'s key from authorized_keys</div>\n1112\t </div>\n1113\t <button class=\"vv-pt-action-btn warn\" onclick=\"vvPtDeleteH2(this,'${remote.id}')\"\n1114\t style=\"white-space:nowrap;font-size:10px;\">✕ ${remote.id} key</button>\n1115\t </div>\n1116\t </div>\n1117\t <div style=\"margin-top:8px;\">\n1118\t <button class=\"vv-pt-action-btn info\" onclick=\"vvPtHideDeleteKeys('${remote.id}')\"\n1119\t style=\"font-size:10px;opacity:.6;\">✓ Done</button>\n1120\t </div>\n1121\t </div>`;\n1122\t }\n1123\t\n1124\t html += `</div></div>`; // padding + card\n1125\t });\n1126\t }\n1127\t\n1128\t // ── HOST1 local setup ──────────────────────────────────────────────────────\n1129\t if (isOwner && selfNode && !selfNode.local_done && remotes.some(r => (r.onboard_phase ?? 0) >= 1)) {\n1130\t html += `<div style=\"margin-top:10px;padding:10px 12px;background:#0d0d0d;\n1131\t border:1px solid #1e1e1e;border-left:3px solid #0e2a4a;border-radius:4px;\n1132\t display:flex;align-items:center;justify-content:space-between;gap:10px;\">\n1133\t <div>\n1134\t <div style=\"font-size:11px;color:#888;margin-bottom:2px;\">${selfNode.id} — ${selfNode.hostname}</div>\n1135\t <div style=\"font-size:9px;color:#333;\">FolderView3 integration · marks HOST1 ready</div>\n1136\t </div>\n1137\t <button class=\"vv-pt-action-btn run\" onclick=\"vvPtLocalSetup(this)\"\n1138\t style=\"font-size:11px;white-space:nowrap;\"\n1139\t title=\"Does not require HOST2 to be online\">▶ Complete Setup</button>\n1140\t </div>`;\n1141\t }\n1142\t\n1143\t // ── Mirror onboard (non-owner) ─────────────────────────────────────────────\n1144\t if (!isOwner) {\n1145\t html += `<div style=\"padding:10px 12px;background:#0d0d0d;border:1px solid #1e1e1e;border-radius:4px;\">\n1146\t <div style=\"display:flex;align-items:center;justify-content:space-between;gap:10px;\">\n1147\t <div>\n1148\t <div style=\"font-size:11px;color:#888;margin-bottom:2px;\">Join partnership</div>\n1149\t <div style=\"font-size:9px;color:#333;\">SSH key setup + notify owner</div>\n1150\t </div>\n1151\t <button class=\"vv-pt-action-btn run\" onclick=\"vvPtOnboard(this)\"\n1152\t ${!hasPartner ? 'disabled style=\"opacity:.35;cursor:default;\"' : ''}\n1153\t style=\"font-size:11px;white-space:nowrap;\">▶ Onboard</button>\n1154\t </div>\n1155\t </div>`;\n1156\t }\n1157\t\n1158\t // ── Offboard + Transfer ────────────────────────────────────────────────────\n1159\t html += `<div style=\"margin-top:12px;padding-top:10px;border-top:1px solid #181818;\">`;\n1160\t\n1161\t html += `<div style=\"display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;\">\n1162\t <div>\n1163\t <div style=\"font-size:11px;color:#666;\">Offboard</div>\n1164\t <div style=\"font-size:9px;color:#2a2a2a;\">End partnership · revoke SSH · restore WebUIs</div>\n1165\t </div>\n1166\t <button class=\"vv-pt-action-btn warn\" onclick=\"vvPtOffboard(this)\"\n1167\t style=\"font-size:11px;white-space:nowrap;\"\n1168\t ${!cfg.enabled ? 'style=\"opacity:.35;cursor:default;\" title=\"No active partnership\"' : ''}>\n1169\t ▶ Offboard\n1170\t </button>\n1171\t </div>`;\n1172\t\n1173\t if (cfg.enabled && isOwner) {\n1174\t const tok = (cfg.transfer_confirm || 'i-understand-this-transfers-ownership').replace(/'/g, \"\\\\'\");\n1175\t html += `<div style=\"margin-top:10px;padding-top:10px;border-top:1px dashed #181818;\n1176\t display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;\">\n1177\t <div>\n1178\t <div style=\"font-size:11px;color:#666;\">Transfer ownership</div>\n1179\t <div style=\"font-size:9px;color:#2a2a2a;\">Promotes mirror to owner · requires sustained health checks</div>\n1180\t </div>\n1181\t <button class=\"vv-pt-action-btn warn\" onclick=\"vvPtTransfer(this, '${tok}')\"\n1182\t style=\"font-size:11px;white-space:nowrap;\"\n1183\t title=\"Runs partnership_transfer.sh — requires sustained health checks before switching\">\n1184\t ⇄ Transfer\n1185\t </button>\n1186\t </div>\n1187\t <div style=\"margin-top:6px;font-size:9px;color:#252525;font-family:monospace;padding:4px 8px;\n1188\t background:#080808;border-radius:3px;\">\n1189\t bash Partnership/partnership_transfer.sh --confirm=${cfg.transfer_confirm || 'i-understand-this-transfers-ownership'}\n1190\t </div>`;\n1191\t }\n1192\t\n1193\t html += '</div>'; // offboard+transfer section\n1194\t\n1195\t return html;\n1196\t}\n1197\t\n1198\t// ── Main render ───────────────────────────────────────────────────────────────\n1199\t\n1200\tfunction _render(data) {\n1201\t const cfg = data.config || {};\n1202\t const nodes = data.nodes || [];\n1203\t const isOwner = nodes.some(n => n.is_me && n.is_owner);\n1204\t\n1205\t // Config bar\n1206\t document.getElementById('vv-pt-config-body').innerHTML = _renderConfig(cfg);\n1207\t\n1208\t // Host settings — always shown upfront\n1209\t const hostsCard = document.getElementById('vv-pt-hosts-card');\n1210\t hostsCard.style.display = nodes.length ? '' : 'none';\n1211\t document.getElementById('vv-pt-hosts-body').innerHTML = _renderHostSettings(nodes, isOwner);\n1212\t document.getElementById('vv-pt-hosts-save').style.display = 'none';\n1213\t\n1214\t // Offline countdown warning\n1215\t _renderOfflineWarn(cfg);\n1216\t\n1217\t // Mirror sync health\n1218\t document.getElementById('vv-pt-sync-body').innerHTML = _renderSync(data.sync || {});\n1219\t\n1220\t // Node grid — columns based on count\n1221\t const cols = nodes.length <= 2 ? nodes.length : nodes.length <= 4 ? 2 : 3;\n1222\t const grid = document.getElementById('vv-pt-nodes');\n1223\t grid.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;\n1224\t grid.innerHTML = nodes.length\n1225\t ? nodes.map(_nodeCard).join('')\n1226\t : '<div style=\"color:#444;font-size:12px;padding:12px 0;\">No hosts found in master.conf.</div>';\n1227\t\n1228\t // Actions\n1229\t document.getElementById('vv-pt-actions-body').innerHTML = _renderActions(nodes, cfg);\n1230\t\n1231\t // Array settings cards above Actions — load once\n1232\t _vvInitArrayCards();\n1233\t\n1234\t // Settings panel — host1 only\n1235\t document.getElementById('vv-pt-settings-card').style.display = isOwner ? '' : 'none';\n1236\t\n1237\t // Highlight Onboard button when arriving from first-run wizard\n1238\t if (new URLSearchParams(location.search).get('vv_onboard') === '1') {\n1239\t const onboardBtn = document.querySelector('#vv-pt-actions-body .vv-pt-action-btn.run');\n1240\t if (onboardBtn) {\n1241\t onboardBtn.classList.add('vv-pt-onboard-highlight');\n1242\t setTimeout(() => onboardBtn.scrollIntoView({behavior: 'smooth', block: 'center'}), 200);\n1243\t }\n1244\t }\n1245\t\n1246\t // Timestamp\n1247\t const ts = data.ts ? new Date(data.ts * 1000).toLocaleString([],\n1248\t {month:'numeric',day:'numeric',year:'numeric',hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';\n1249\t document.getElementById('vv-pt-ts').textContent = ts ? 'Updated: ' + ts : '';\n1250\t}\n1251\t\n1252\t// ── Poll ──────────────────────────────────────────────────────────────────────\n1253\t\n1254\tfunction vvPtLoad() {\n1255\t fetch('/plugins/varaverk/api/partnership.php')\n1256\t .then(r => r.json())\n1257\t .then(_render)\n1258\t .catch(() => {});\n1259\t}\n1260\t\n1261\t_vvPtReload = vvPtLoad; // expose to top-level toggle fns\n1262\t\n1263\tvvPtLoad();\n1264\tsetInterval(vvPtLoad, 10000);\n1265\t\n1266\t})();\n1267\t</script>\n1268\t"}]},"uuid":"5868ee18-8ea7-461e-8168-21ef8a3740f3","timestamp":"2026-06-06T14:29:44.891Z","sourceToolAssistantUUID":"be96202e-18c5-4341-b5d1-8e5e0e33297e","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"5868ee18-8ea7-461e-8168-21ef8a3740f3","isSidechain":true,"agentId":"accc3589951fac88e","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01Cusyw8FcnVphFqJjcnJin5","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01XBZBev7P9nwbP21kKqE8NC","name":"Read","input":{"file_path":"/boot/config/plugins/varaverk/Plugin/unraid/pages/scheduler.php","limit":800},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":91601,"cache_read_input_tokens":8141,"output_tokens":221,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":91601},"inference_geo":"not_available","iterations":[{"input_tokens":7,"output_tokens":221,"cache_read_input_tokens":8141,"cache_creation_input_tokens":91601,"cache_creation":{"ephemeral_5m_input_tokens":91601,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"diagnostics":null},"requestId":"req_011CbnAZUCximcsRa8AmqYUy","attributionAgent":"Explore","type":"assistant","uuid":"6f489b1e-70ce-4c68-8f4e-40b889e8abf9","timestamp":"2026-06-06T14:29:44.930Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"6f489b1e-70ce-4c68-8f4e-40b889e8abf9","isSidechain":true,"promptId":"3c1ac879-b13a-4ebc-9aeb-ea73615e9af3","agentId":"accc3589951fac88e","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01XBZBev7P9nwbP21kKqE8NC","type":"tool_result","content":"1\t<?php\n2\trequire_once dirname(__DIR__) . '/include/scheduler.php';\n3\t\n4\t// Setup mode — auto-open a conf file and force the editing sequence\n5\t$vv_setup_conf = preg_match('/^[\\w.]+\\.conf$/', $_GET['vv_setup'] ?? '')\n6\t ? $_GET['vv_setup'] : '';\n7\t\n8\t// Deep-link from Monitor → open a specific script on load (e.g. ?tab=scheduler&vv_script=Media/x.sh)\n9\t$vv_open_script = preg_match('#^[\\w./-]+\\.sh$#', $_GET['vv_script'] ?? '')\n10\t ? $_GET['vv_script'] : '';\n11\t$_vv_my_host = vv_detect_host();\n12\t$_vv_local_host_conf = ($_vv_my_host !== 'unknown') ? $_vv_my_host . '.conf' : 'host1.conf';\n13\t\n14\t$tree = vv_job_tree();\n15\t$customs = vv_custom_scripts();\n16\t$tools = vv_tools_scripts();\n17\t$_library = vv_script_library();\n18\t$_folders = vv_folders_load();\n19\t\n20\t// Full README and Manual for collapsed accordion panels\n21\t$readmePath = SCRIPTS_DIR . '/README.md';\n22\t$manualPath = SCRIPTS_DIR . '/Manual.md';\n23\t$readmeText = file_exists($readmePath) ? file_get_contents($readmePath) : '';\n24\t$manualText = file_exists($manualPath) ? file_get_contents($manualPath) : '';\n25\t\n26\t// Repository tree: all .sh + .md files in git folder order, minus Plugin/\n27\t$_repoFiles = [];\n28\t$_repoBase = rtrim(SCRIPTS_DIR, '/') . '/';\n29\t$_repoSkip = ['Plugin', '.git'];\n30\ttry {\n31\t $_ri = new RecursiveIteratorIterator(\n32\t new RecursiveDirectoryIterator(SCRIPTS_DIR, RecursiveDirectoryIterator::SKIP_DOTS)\n33\t );\n34\t foreach ($_ri as $_rf) {\n35\t if (!$_rf->isFile()) continue;\n36\t if (!preg_match('/\\.(sh|md)$/i', $_rf->getFilename())) continue;\n37\t $_rel = ltrim(str_replace($_repoBase, '', $_rf->getPathname()), '/');\n38\t $_parts = explode('/', $_rel);\n39\t if (in_array($_parts[0], $_repoSkip)) continue;\n40\t $_repoFiles[] = $_rel;\n41\t }\n42\t} catch (Exception $_re) {}\n43\tsort($_repoFiles);\n44\t\n45\t// Docs tree: README and Manual with their per-module children\n46\t$_mainReadme = in_array('README.md', $_repoFiles) ? 'README.md' : null;\n47\t$_childReadmes = array_values(array_filter($_repoFiles, function($f) {\n48\t return preg_match('/^README/i', basename($f)) && $f !== 'README.md';\n49\t}));\n50\tsort($_childReadmes);\n51\t$_mainManual = in_array('Manual.md', $_repoFiles) ? 'Manual.md' : null;\n52\t$_childManuals = array_values(array_filter($_repoFiles, function($f) {\n53\t return preg_match('/^Manual/i', basename($f)) && $f !== 'Manual.md';\n54\t}));\n55\tsort($_childManuals);\n56\t\n57\t// Job stats for notification board\n58\t$totalJobs = 0; $scheduledJobs = 0;\n59\tforeach ($tree as $orch) {\n60\t $totalJobs++;\n61\t if (!empty($orch['cron']) && $orch['enabled']) $scheduledJobs++;\n62\t foreach (($orch['children'] ?? []) as $child) {\n63\t $totalJobs++;\n64\t if (!empty($child['cron']) && $child['enabled']) $scheduledJobs++;\n65\t }\n66\t}\n67\t$runningScripts = [];\n68\texec('pgrep -af bash 2>/dev/null', $psLines);\n69\tforeach ($psLines as $line) {\n70\t if (preg_match('#' . preg_quote(SCRIPTS_DIR, '#') . '/([^\\s]+\\.sh)#', $line, $rm)) {\n71\t $runningScripts[] = basename($rm[1], '.sh');\n72\t }\n73\t}\n74\t$runningScripts = array_unique($runningScripts);\n75\t?>\n76\t\n77\t<div id=\"vv-scheduler\">\n78\t <div id=\"vv-sched-layout\">\n79\t\n80\t <!-- ── Left: script cards ── -->\n81\t <div id=\"vv-sched-left\">\n82\t <div id=\"vv-sched-cards\">\n83\t\n84\t <?php foreach ($tree as $orch): $oid = htmlspecialchars($orch['id']); ?>\n85\t <div class=\"vv-card vv-wide vv-sched-card\" data-id=\"<?= $oid ?>\" data-conf-arrays=\"<?= htmlspecialchars(implode(',', $orch['conf_arrays'] ?? [])) ?>\">\n86\t\n87\t <div class=\"vv-job-row vv-orch-row\">\n88\t <label class=\"vv-toggle\" title=\"Enable/disable\">\n89\t <input type=\"checkbox\" class=\"vv-enabled\"\n90\t <?= $orch['enabled'] ? 'checked' : '' ?>\n91\t onchange=\"vvSaveOrch(this)\">\n92\t <span class=\"vv-slider\"></span>\n93\t </label>\n94\t <span class=\"vv-cog-btn\" onclick=\"vvClickCog(this)\" title=\"Settings\">⚙</span><span class=\"vv-job-label\" onclick=\"vvClickLabel(this)\" style=\"cursor:pointer\"><?= htmlspecialchars($orch['label']) ?></span>\n95\t <?php if (str_starts_with($orch['cron'], 'array_')): ?>\n96\t <span class=\"vv-event-badge\"><?= $orch['cron'] === 'array_start' ? '⚡ Array Start' : '⚡ Array Stop' ?></span>\n97\t <?php endif; ?>\n98\t <input type=\"text\" class=\"vv-cron\" value=\"<?= htmlspecialchars($orch['cron']) ?>\"\n99\t placeholder=\"cron or array_start / array_stop\"\n100\t onblur=\"vvSaveCronBlur(this)\">\n101\t <span class=\"vv-save-check\"></span>\n102\t </div>\n103\t <?php if (!empty($orch['desc'])): ?>\n104\t <div class=\"vv-job-desc\" title=\"<?= htmlspecialchars($orch['desc']) ?>\"><?= htmlspecialchars($orch['desc']) ?></div>\n105\t <?php endif; ?>\n106\t <div class=\"vv-job-actions\">\n107\t <button class=\"vv-btn-sm vv-run-btn\" onclick=\"vvRunJob(this)\">▶ Run</button>\n108\t <button class=\"vv-btn-sm vv-dry-btn\" onclick=\"vvDryRun(this)\">▶ Dry Run</button>\n109\t <?php $orchLog = vv_job_log_path($orch['id']); ?>\n110\t <button class=\"vv-btn-sm vv-log-btn<?= (file_exists($orchLog) && filesize($orchLog) > 0) ? ' vv-has-log' : '' ?>\" onclick=\"vvSelectLog(this)\">Log</button>\n111\t <label class=\"vv-log-label\" title=\"Enable verbose --log output\">\n112\t <input type=\"checkbox\" class=\"vv-log-enabled\"\n113\t <?= $orch['log_enabled'] ? 'checked' : '' ?>\n114\t onchange=\"vvSaveJob(this)\">\n115\t <span>Verbose</span>\n116\t </label>\n117\t <span class=\"vv-job-dot\"></span>\n118\t <?php if (!empty($orch['children'])): ?>\n119\t <button class=\"vv-advanced-toggle\" style=\"margin-left:auto;width:110px\" onclick=\"vvToggleAdvanced(this)\">Advanced ▸</button>\n120\t <?php endif; ?>\n121\t </div>\n122\t\n123\t <?php if (!empty($orch['children'])): ?>\n124\t <div class=\"vv-children\" data-conf-arrays=\"<?= htmlspecialchars(implode(',', $orch['conf_arrays'] ?? [])) ?>\" style=\"display:none;\">\n125\t <?php foreach ($orch['children'] as $child): $cid = htmlspecialchars($child['id']); ?>\n126\t <?php if (($child['type'] ?? 'script') === 'conf_flag'):\n127\t $_rsync = vv_rsync_standalone($child['flag_name']);\n128\t $_rsyncFn = htmlspecialchars($child['flag_name']);\n129\t $_rsyncOid = htmlspecialchars($orch['id']);\n130\t ?>\n131\t <div class=\"vv-script\" data-id=\"<?= $cid ?>\"\n132\t data-type=\"conf_flag\"\n133\t data-flag-name=\"<?= htmlspecialchars($child['flag_name']) ?>\"\n134\t data-flag-value=\"<?= !empty($child['flag_value']) ? '1' : '0' ?>\">\n135\t <div class=\"vv-job-row\">\n136\t <label class=\"vv-toggle\" title=\"Toggle <?= htmlspecialchars($child['flag_name']) ?> in master.conf\">\n137\t <input type=\"checkbox\" class=\"vv-enabled\"\n138\t <?= !empty($child['flag_value']) ? 'checked' : '' ?>\n139\t onchange=\"vvSaveChild(this)\">\n140\t <span class=\"vv-slider\"></span>\n141\t </label>\n142\t <span class=\"vv-job-label\"><?= htmlspecialchars($child['label']) ?></span>\n143\t <span class=\"vv-flag-badge\"<?= empty($child['flag_value']) ? ' style=\"display:none\"' : '' ?>><?= htmlspecialchars($child['flag_name']) ?></span>\n144\t <input type=\"text\" class=\"vv-cron vv-rsync-standalone\"\n145\t value=\"<?= htmlspecialchars($_rsync['cron']) ?>\"\n146\t placeholder=\"standalone cron\">\n147\t <span class=\"vv-save-check\"></span>\n148\t </div>\n149\t <?php if (!empty($child['desc'])): ?>\n150\t <div class=\"vv-job-desc\" title=\"<?= htmlspecialchars($child['desc']) ?>\"><?= htmlspecialchars($child['desc']) ?></div>\n151\t <?php endif; ?>\n152\t <div class=\"vv-job-actions\">\n153\t <button class=\"vv-btn-sm vv-run-btn vv-rsync-standalone\" onclick=\"vvRunJob(this)\">▶ Run</button>\n154\t <button class=\"vv-btn-sm vv-dry-btn vv-rsync-standalone\" onclick=\"vvDryRun(this)\">▶ Dry Run</button>\n155\t <?php $childLog = vv_job_log_path($child['id']); ?>\n156\t <button class=\"vv-btn-sm vv-log-btn<?= (file_exists($childLog) && filesize($childLog) > 0) ? ' vv-has-log' : '' ?> vv-rsync-standalone\" onclick=\"vvSelectLog(this)\">Log</button>\n157\t <input type=\"text\" class=\"vv-rsync-location vv-rsync-standalone\"\n158\t value=\"<?= htmlspecialchars($_rsync['location']) ?>\"\n159\t placeholder=\"/mnt/user/path\"\n160\t data-flag=\"<?= $_rsyncFn ?>\"\n161\t data-orch-id=\"<?= $_rsyncOid ?>\">\n162\t <button class=\"vv-btn-sm vv-rsync-save-btn vv-rsync-standalone\"\n163\t onclick=\"vvSaveRsyncStandalone(this)\">Save</button>\n164\t <span class=\"vv-job-dot\"></span>\n165\t </div>\n166\t </div>\n167\t <?php else: ?>\n168\t <div class=\"vv-script\" data-id=\"<?= $cid ?>\"\n169\t data-conf-managed=\"<?= $child['conf_managed'] ? '1' : '0' ?>\"\n170\t data-conf-enabled=\"<?= $child['conf_enabled'] === true ? '1' : ($child['conf_enabled'] === false ? '0' : '') ?>\"\n171\t data-conf-array=\"<?= htmlspecialchars($child['conf_array'] ?? '') ?>\">\n172\t <div class=\"vv-job-row\">\n173\t <label class=\"vv-toggle\" title=\"Enable/disable\">\n174\t <input type=\"checkbox\" class=\"vv-enabled\"\n175\t <?= $child['enabled'] ? 'checked' : '' ?>\n176\t onchange=\"vvSaveChild(this)\">\n177\t <span class=\"vv-slider\"></span>\n178\t </label>\n179\t <span class=\"vv-cog-btn\" onclick=\"vvClickCog(this)\" title=\"Settings\">⚙</span><span class=\"vv-job-label\" onclick=\"vvClickLabel(this)\" style=\"cursor:pointer\"><?= htmlspecialchars($child['label']) ?></span>\n180\t <input type=\"text\" class=\"vv-cron\" value=\"<?= htmlspecialchars($child['cron']) ?>\"\n181\t placeholder=\"cron (orch off only)\"\n182\t onblur=\"vvSaveCronBlur(this)\">\n183\t <span class=\"vv-save-check\"></span>\n184\t </div>\n185\t <?php if (!empty($child['desc'])): ?>\n186\t <div class=\"vv-job-desc\" title=\"<?= htmlspecialchars($child['desc']) ?>\"><?= htmlspecialchars($child['desc']) ?></div>\n187\t <?php endif; ?>\n188\t <div class=\"vv-job-actions\">\n189\t <button class=\"vv-btn-sm vv-run-btn\" onclick=\"vvRunJob(this)\">▶ Run</button>\n190\t <button class=\"vv-btn-sm vv-dry-btn\" onclick=\"vvDryRun(this)\">▶ Dry Run</button>\n191\t <?php $childLog = vv_job_log_path($child['id']); ?>\n192\t <button class=\"vv-btn-sm vv-log-btn<?= (file_exists($childLog) && filesize($childLog) > 0) ? ' vv-has-log' : '' ?>\" onclick=\"vvSelectLog(this)\">Log</button>\n193\t <label class=\"vv-log-label\" title=\"Enable verbose --log output\">\n194\t <input type=\"checkbox\" class=\"vv-log-enabled\"\n195\t <?= $child['log_enabled'] ? 'checked' : '' ?>\n196\t onchange=\"vvSaveJob(this)\">\n197\t <span>Verbose</span>\n198\t </label>\n199\t <input type=\"text\" class=\"vv-script-args\" placeholder=\"extra args\"\n200\t style=\"display:<?= $orch['enabled'] ? 'none' : '' ?>\">\n201\t <span class=\"vv-job-dot\"></span>\n202\t </div>\n203\t </div>\n204\t <?php endif; ?>\n205\t <?php endforeach; ?>\n206\t </div>\n207\t <?php endif; ?>\n208\t\n209\t </div>\n210\t <?php endforeach; ?>\n211\t\n212\t <!-- Tools container -->\n213\t <div class=\"vv-card vv-wide vv-sched-card vv-tools-card\">\n214\t <div class=\"vv-job-row\">\n215\t <span class=\"vv-job-label\" style=\"font-weight:bold;\">Tools</span>\n216\t <span class=\"vv-custom-count\"><?= count($tools) ?> tool<?= count($tools) !== 1 ? 's' : '' ?></span>\n217\t <button class=\"vv-advanced-toggle\" style=\"margin-left:auto;width:100px\" onclick=\"vvToggleTools(this)\">Tools ▸</button>\n218\t </div>\n219\t <div class=\"vv-children\" id=\"vv-tools-children\" style=\"display:none;\">\n220\t <?php if (empty($tools)): ?>\n221\t <p class=\"vv-custom-empty\">No scripts found in Tools/ or Plugin/*/tools/.</p>\n222\t <?php else: ?>\n223\t <?php foreach ($tools as $ts): $tsid = htmlspecialchars($ts['id']); ?>\n224\t <div class=\"vv-script\" data-id=\"<?= $tsid ?>\">\n225\t <div class=\"vv-job-row\">\n226\t <label class=\"vv-toggle\" title=\"Enable/disable cron\">\n227\t <input type=\"checkbox\" class=\"vv-enabled\"\n228\t <?= $ts['enabled'] ? 'checked' : '' ?>\n229\t onchange=\"vvSaveJob(this)\">\n230\t <span class=\"vv-slider\"></span>\n231\t </label>\n232\t <span class=\"vv-job-label\"><?= htmlspecialchars($ts['label']) ?></span>\n233\t <input type=\"text\" class=\"vv-cron\" value=\"<?= htmlspecialchars($ts['cron']) ?>\"\n234\t placeholder=\"cron expression\"\n235\t onblur=\"vvSaveCronBlur(this)\">\n236\t <span class=\"vv-save-check\"></span>\n237\t </div>\n238\t <?php if (!empty($ts['desc'])): ?>\n239\t <div class=\"vv-job-desc\" title=\"<?= htmlspecialchars($ts['desc']) ?>\"><?= htmlspecialchars($ts['desc']) ?></div>\n240\t <?php endif; ?>\n241\t <div class=\"vv-job-actions\">\n242\t <button class=\"vv-btn-sm vv-run-btn\" onclick=\"vvRunJob(this)\">▶ Run</button>\n243\t <button class=\"vv-btn-sm vv-dry-btn\" onclick=\"vvDryRun(this)\">▶ Dry Run</button>\n244\t <?php $tsLog = vv_job_log_path($ts['id']); ?>\n245\t <button class=\"vv-btn-sm vv-log-btn<?= (file_exists($tsLog) && filesize($tsLog) > 0) ? ' vv-has-log' : '' ?>\" onclick=\"vvSelectLog(this)\">Log</button>\n246\t <label class=\"vv-log-label\" title=\"Enable verbose --log output\">\n247\t <input type=\"checkbox\" class=\"vv-log-enabled\"\n248\t <?= $ts['log_enabled'] ? 'checked' : '' ?>\n249\t onchange=\"vvSaveJob(this)\">\n250\t <span>Verbose</span>\n251\t </label>\n252\t <input type=\"text\" class=\"vv-script-args\" placeholder=\"extra args\">\n253\t <span class=\"vv-job-dot\"></span>\n254\t </div>\n255\t </div>\n256\t <?php endforeach; ?>\n257\t <?php endif; ?>\n258\t </div>\n259\t </div>\n260\t\n261\t <!-- Custom Scripts container -->\n262\t <?php\n263\t // Build set of IDs that belong to a folder\n264\t $_inFolder = [];\n265\t foreach ($_folders as $_fn => $_fs) foreach ($_fs as $_fid) $_inFolder[] = $_fid;\n266\t ?>\n267\t <div class=\"vv-card vv-wide vv-sched-card vv-custom-card\">\n268\t <div class=\"vv-job-row\">\n269\t <span class=\"vv-job-label\" style=\"font-weight:bold;\">Custom Scripts</span>\n270\t <span class=\"vv-custom-count\"><?= count($customs) ?> script<?= count($customs) !== 1 ? 's' : '' ?></span>\n271\t <button class=\"vv-advanced-toggle\" style=\"margin-left:auto;width:100px\" onclick=\"vvToggleCustom(this)\">Scripts ▸</button>\n272\t </div>\n273\t <div class=\"vv-children\" id=\"vv-custom-children\" style=\"display:none;\">\n274\t <?php if (empty($customs)): ?>\n275\t <p class=\"vv-custom-empty\">No custom scripts yet — click <strong>+ Add Script</strong> to create one.</p>\n276\t <?php else: ?>\n277\t\n278\t <?php // ── Folder groups ──────────────────────────────────────────────\n279\t foreach ($_folders as $_fname => $_fscripts):\n280\t $_folderScripts = array_filter($customs, fn($c) => in_array($c['id'], $_fscripts));\n281\t ?>\n282\t <div class=\"vv-folder-group\" data-folder=\"<?= htmlspecialchars($_fname) ?>\">\n283\t <div class=\"vv-folder-row\" onclick=\"vvToggleFolder(this)\">\n284\t <span class=\"vv-folder-chevron\">▸</span>\n285\t <span class=\"vv-folder-name\"><?= htmlspecialchars($_fname) ?></span>\n286\t <span class=\"vv-folder-count\"><?= count($_folderScripts) ?></span>\n287\t </div>\n288\t <div class=\"vv-folder-children\" style=\"display:none\">\n289\t <?php foreach ($_folderScripts as $cs): $csid = htmlspecialchars($cs['id']); ?>\n290\t <div class=\"vv-script\" data-id=\"<?= $csid ?>\">\n291\t <div class=\"vv-job-row\">\n292\t <label class=\"vv-toggle\" title=\"Enable/disable\">\n293\t <input type=\"checkbox\" class=\"vv-enabled\" <?= $cs['enabled'] ? 'checked' : '' ?> onchange=\"vvSaveJob(this)\">\n294\t <span class=\"vv-slider\"></span>\n295\t </label>\n296\t <span class=\"vv-cog-btn\" onclick=\"vvClickCog(this)\" title=\"Settings\">⚙</span><span class=\"vv-job-label\" onclick=\"vvClickLabel(this)\" style=\"cursor:pointer\"><?= htmlspecialchars($cs['label']) ?></span>\n297\t <input type=\"text\" class=\"vv-cron\" value=\"<?= htmlspecialchars($cs['cron']) ?>\" placeholder=\"cron expression\" onblur=\"vvSaveCronBlur(this)\">\n298\t <span class=\"vv-save-check\"></span>\n299\t </div>\n300\t <?php if (!empty($cs['desc'])): ?>\n301\t <div class=\"vv-job-desc\" title=\"<?= htmlspecialchars($cs['desc']) ?>\"><?= htmlspecialchars($cs['desc']) ?></div>\n302\t <?php endif; ?>\n303\t <div class=\"vv-job-actions\">\n304\t <button class=\"vv-btn-sm vv-run-btn\" onclick=\"vvRunJob(this)\">▶ Run</button>\n305\t <button class=\"vv-btn-sm vv-dry-btn\" onclick=\"vvDryRun(this)\">▶ Dry Run</button>\n306\t <button class=\"vv-btn-sm vv-edit-btn\" onclick=\"vvEditScript('<?= $csid ?>')\">Edit</button>\n307\t <?php $csLog = vv_job_log_path($cs['id']); ?>\n308\t <button class=\"vv-btn-sm vv-log-btn<?= (file_exists($csLog) && filesize($csLog) > 0) ? ' vv-has-log' : '' ?>\" onclick=\"vvSelectLog(this)\">Log</button>\n309\t <label class=\"vv-log-label\"><input type=\"checkbox\" class=\"vv-log-enabled\" <?= $cs['log_enabled'] ? 'checked' : '' ?> onchange=\"vvSaveJob(this)\"><span>Verbose</span></label>\n310\t <input type=\"text\" class=\"vv-script-args\" placeholder=\"extra args\">\n311\t <span class=\"vv-job-dot\"></span>\n312\t </div>\n313\t </div>\n314\t <?php endforeach; ?>\n315\t </div>\n316\t </div>\n317\t <?php endforeach; ?>\n318\t\n319\t <?php // ── Unfoldered scripts ──────────────────────────────────────\n320\t foreach ($customs as $cs):\n321\t if (in_array($cs['id'], $_inFolder)) continue;\n322\t $csid = htmlspecialchars($cs['id']);\n323\t ?>\n324\t <div class=\"vv-script\" data-id=\"<?= $csid ?>\">\n325\t <div class=\"vv-job-row\">\n326\t <label class=\"vv-toggle\" title=\"Enable/disable\">\n327\t <input type=\"checkbox\" class=\"vv-enabled\"\n328\t <?= $cs['enabled'] ? 'checked' : '' ?>\n329\t onchange=\"vvSaveJob(this)\">\n330\t <span class=\"vv-slider\"></span>\n331\t </label>\n332\t <span class=\"vv-cog-btn\" onclick=\"vvClickCog(this)\" title=\"Settings\">⚙</span><span class=\"vv-job-label\" onclick=\"vvClickLabel(this)\" style=\"cursor:pointer\"><?= htmlspecialchars($cs['label']) ?></span>\n333\t <input type=\"text\" class=\"vv-cron\" value=\"<?= htmlspecialchars($cs['cron']) ?>\"\n334\t placeholder=\"cron expression\"\n335\t onblur=\"vvSaveCronBlur(this)\">\n336\t <span class=\"vv-save-check\"></span>\n337\t </div>\n338\t <?php if (!empty($cs['desc'])): ?>\n339\t <div class=\"vv-job-desc\" title=\"<?= htmlspecialchars($cs['desc']) ?>\"><?= htmlspecialchars($cs['desc']) ?></div>\n340\t <?php endif; ?>\n341\t <div class=\"vv-job-actions\">\n342\t <button class=\"vv-btn-sm vv-run-btn\" onclick=\"vvRunJob(this)\">▶ Run</button>\n343\t <button class=\"vv-btn-sm vv-dry-btn\" onclick=\"vvDryRun(this)\">▶ Dry Run</button>\n344\t <button class=\"vv-btn-sm vv-edit-btn\" onclick=\"vvEditScript('<?= $csid ?>')\">Edit</button>\n345\t <?php $csLog = vv_job_log_path($cs['id']); ?>\n346\t <button class=\"vv-btn-sm vv-log-btn<?= (file_exists($csLog) && filesize($csLog) > 0) ? ' vv-has-log' : '' ?>\" onclick=\"vvSelectLog(this)\">Log</button>\n347\t <label class=\"vv-log-label\" title=\"Enable verbose --log output\">\n348\t <input type=\"checkbox\" class=\"vv-log-enabled\"\n349\t <?= $cs['log_enabled'] ? 'checked' : '' ?>\n350\t onchange=\"vvSaveJob(this)\">\n351\t <span>Verbose</span>\n352\t </label>\n353\t <input type=\"text\" class=\"vv-script-args\" placeholder=\"extra args\">\n354\t <span class=\"vv-job-dot\"></span>\n355\t </div>\n356\t </div>\n357\t <?php endforeach; ?>\n358\t <?php endif; ?>\n359\t </div>\n360\t </div>\n361\t\n362\t </div><!-- /#vv-sched-cards -->\n363\t\n364\t <div class=\"vv-sched-footer\">\n365\t <button class=\"vv-save-btn vv-add-script-btn\" onclick=\"vvAddScript()\">+ Add Script</button>\n366\t <button class=\"vv-save-btn vv-new-folder-btn\" onclick=\"vvNewFolder()\" title=\"Create a folder in Custom Scripts\">+ Folder</button>\n367\t <button id=\"vv-arrange-btn\" class=\"vv-save-btn\" onclick=\"vvToggleArrange()\" title=\"Drag scripts between orchestrators\">Arrange</button>\n368\t <button id=\"vv-arrange-save-btn\" class=\"vv-save-btn vv-arrange-save-btn\" onclick=\"vvSaveArrange()\" style=\"display:none\">Save Arrangement</button>\n369\t <button id=\"vv-arrange-cancel-btn\" class=\"vv-save-btn vv-delete-btn\" onclick=\"vvCancelArrange()\" style=\"display:none\">Cancel</button>\n370\t <button id=\"vv-delete-script-btn\" class=\"vv-save-btn vv-delete-btn\" onclick=\"vvDeleteScript()\" style=\"display:none\">🗑 Delete</button>\n371\t </div>\n372\t\n373\t </div><!-- /#vv-sched-left -->\n374\t\n375\t <!-- ── Right: suggestions + log panel ── -->\n376\t <div id=\"vv-sched-right\" class=\"vv-panel-visible\">\n377\t <div class=\"vv-card vv-log-card\">\n378\t <div class=\"vv-log-toolbar\" style=\"margin-bottom:8px;\">\n379\t <div style=\"display:flex;align-items:center;gap:8px;\">\n380\t <button id=\"vv-back-btn\" class=\"vv-btn-sm\" onclick=\"vvBackToSuggestions()\" style=\"display:none\">☰ Scheduler Info</button>\n381\t <button id=\"vv-restore-btn\" class=\"vv-btn-sm\" onclick=\"vvRestoreLastLog()\" style=\"display:none\">← <span id=\"vv-restore-label\"></span></button>\n382\t <span id=\"vv-log-title\" style=\"font-size:13px;font-weight:bold;color:#ccc;\">Scheduler Information</span>\n383\t <span id=\"vv-log-dot\" style=\"display:none;width:8px;height:8px;border-radius:50%;background:#4caf50;flex-shrink:0;\"></span>\n384\t </div>\n385\t <div style=\"display:flex;align-items:center;gap:8px;flex-wrap:wrap;\">\n386\t <label id=\"vv-auto-scroll-label\" class=\"vv-log-label\" title=\"Auto-scroll to newest line (pauses when you scroll up, resumes at bottom)\" style=\"display:none\">\n387\t <input type=\"checkbox\" id=\"vv-auto-scroll\" checked> <span>Auto Scroll</span>\n388\t </label>\n389\t <label id=\"vv-invert-log-label\" class=\"vv-log-label\" title=\"Show newest lines at top\" style=\"display:none\">\n390\t <input type=\"checkbox\" id=\"vv-invert-log\" onchange=\"vvToggleInvert(this)\"> <span>Invert</span>\n391\t </label>\n392\t <button id=\"vv-cancel-edit-btn\" class=\"vv-btn-sm vv-delete-btn\" onclick=\"vvBackToSuggestions()\" style=\"display:none\">Cancel</button>\n393\t <button id=\"vv-undo-btn\" class=\"vv-btn-sm vv-undo-redo-btn\" onclick=\"vvUndo()\" style=\"display:none\" disabled title=\"Undo (Ctrl+Z)\">↩ Undo<span class=\"vv-undo-count\" id=\"vv-undo-count\"></span></button>\n394\t <button id=\"vv-redo-btn\" class=\"vv-btn-sm vv-undo-redo-btn\" onclick=\"vvRedo()\" style=\"display:none\" disabled title=\"Redo (Ctrl+Y / Ctrl+Shift+Z)\">↪ Redo</button>\n395\t <button id=\"vv-save-script-btn\" class=\"vv-btn-sm vv-save-script-btn-style\" onclick=\"vvSaveScript()\" style=\"display:none\">Save Script</button>\n396\t <button id=\"vv-save-conf-btn\" class=\"vv-btn-sm vv-save-script-btn-style\" onclick=\"vvSaveConf()\" style=\"display:none\">Save Config</button>\n397\t <button id=\"vv-save-rawconf-btn\" class=\"vv-btn-sm vv-save-script-btn-style\" onclick=\"vvSaveRawConf()\" style=\"display:none\">Save Conf</button>\n398\t <span id=\"vv-log-ts\" class=\"vv-log-ts\" style=\"display:inline-flex;flex-direction:column;line-height:1.3;white-space:nowrap\"></span>\n399\t <input type=\"text\" id=\"vv-log-search\" placeholder=\"Search…\"\n400\t oninput=\"vvFilterLog(this.value)\" style=\"display:none;\">\n401\t <button id=\"vv-stop-btn\" class=\"vv-btn-sm\" onclick=\"vvStopJob()\" style=\"display:none\" title=\"Stop running script and clear any stuck locks\">■ Stop</button>\n402\t <button id=\"vv-clear-btn\" class=\"vv-btn-sm\" onclick=\"vvClearRightLog()\" style=\"display:none\">Clear</button>\n403\t <button id=\"vv-advanced-mode-btn\" class=\"vv-btn-sm vv-adv-mode-btn\" onclick=\"vvToggleAdvancedMode()\" title=\"Toggle Advanced mode — shows full script source and raw conf editing\">Advanced</button>\n404\t </div>\n405\t </div>\n406\t\n407\t <!-- Suggestions view (default) -->\n408\t <div id=\"vv-suggestions\">\n409\t\n410\t <!-- ── Notification Board ── -->\n411\t <div class=\"vv-nb-board\">\n412\t <span class=\"vv-nb-stat\"><?= $scheduledJobs ?> / <?= $totalJobs ?> scheduled</span>\n413\t <span class=\"vv-nb-sep\">·</span>\n414\t <span class=\"vv-nb-stat <?= !empty($runningScripts) ? 'vv-nb-running' : '' ?>\">\n415\t <?= !empty($runningScripts) ? 'Running: ' . htmlspecialchars(implode(', ', $runningScripts)) : 'Idle' ?>\n416\t </span>\n417\t <span class=\"vv-nb-sep vv-adv-only\" style=\"display:none\">·</span>\n418\t <?php $confFiles = vv_get_conf_files(); ?>\n419\t <?php if (!empty($confFiles)): ?>\n420\t <span class=\"vv-nb-conf-btns vv-adv-only\" style=\"display:none\">\n421\t <?php foreach ($confFiles as $cf): ?>\n422\t <button class=\"vv-btn-sm vv-nb-conf-btn\" onclick=\"vvEditRawConf('<?= htmlspecialchars($cf) ?>')\"><?= htmlspecialchars($cf) ?></button>\n423\t <?php endforeach; ?>\n424\t </span>\n425\t <?php endif; ?>\n426\t <button class=\"vv-btn-sm vv-adv-only\" id=\"vv-git-pull-btn\"\n427\t onclick=\"vvGitPull(this)\"\n428\t title=\"Pull latest scripts from repository and run conf upgrade\"\n429\t style=\"display:none;margin-left:auto;\">↻ Git Pull</button>\n430\t </div>\n431\t\n432\t <!-- ── How do I use this (pinned) ── -->\n433\t <div class=\"vv-sug-block vv-info-block\" id=\"vv-how-to-use\" data-save-key=\"how-to-use\">\n434\t <div class=\"vv-sug-header\" onclick=\"vvToggleSug(this)\">\n435\t <span class=\"vv-sug-chevron\">▾</span>\n436\t <span class=\"vv-sug-title\">How do I use this</span>\n437\t </div>\n438\t <div class=\"vv-sug-body vv-info-body\">\n439\t <ul class=\"vv-info-cols\">\n440\t <li><strong>Toggle</strong> — saves immediately to schedule.json</li>\n441\t <li><strong>Cron field</strong> — auto-saved on focus-out; hint shows plain English; red border = invalid</li>\n442\t <li><strong>⚙ Cog</strong> — opens settings panel: script header + matching README/Manual sections + config fields</li>\n443\t <li><strong>Script name</strong> — click navigates to the matching entry in the Orch tree (right panel)</li>\n444\t <li><strong>Run</strong> — fires script immediately; resets the cron timer so next fire is one full interval later</li>\n445\t <li><strong>Dry Run</strong> — same as Run but passes <code>--dry-run</code>; no changes written to disk</li>\n446\t <li><strong>Log</strong> — opens script log here; auto-scrolls; use Search box to filter/highlight lines</li>\n447\t <li><strong>Stop</strong> — SIGTERM → 3 s → SIGKILL; clears stuck lock files</li>\n448\t <li><strong>Verbose</strong> — appends <code>--log</code> flag for per-item detail output</li>\n449\t <li><strong>Cancel</strong> — discards changes and returns here; file is never touched until Save</li>\n450\t <li><strong>⚡ Array events</strong> — set any cron field to <code>array_start</code> or <code>array_stop</code>; badge appears; fires on Unraid array start/stop; works on any orch or custom script</li>\n451\t <li><strong>Arrange</strong> — drag scripts between orchs; drop to right panel removes from orch; Save Arrangement commits to master.conf</li>\n452\t <li><strong>+ Folder</strong> — collapsible subfolder in Custom Scripts; drag scripts in</li>\n453\t <li><strong>Suggested cron</strong> — click the cron code badge in the Orch tree to apply and save instantly</li>\n454\t <li><strong>Status dot (tree)</strong> — green/orange/red ● = last run ok/warn/error; dim = never run; hover for detail</li>\n455\t <li><strong>Cron Calculator</strong> — type an expression or plain English; shows description + next 5 fires; Apply pushes to last-focused cron field</li>\n456\t <li><strong>Recent Activity</strong> — last 24 runs; click a row to open that log; auto-expands on errors</li>\n457\t <li><strong>Recent Errors</strong> — last error per script (7 days); click to open log; auto-expands on new errors</li>\n458\t <li><strong>Advanced</strong> — top-right button, turns blue; ⚙ shows enriched view (header + docs + config); raw conf editing unlocked</li>\n459\t <li><strong>Log search</strong> — filter box in toolbar when log is open; highlights matches, dims others</li>\n460\t <li><strong>← Scheduler Info</strong> — returns here from any log, editor, conf, or script view</li>\n461\t <li class=\"vv-info-sep\">Orchestrator & Child Behaviour</li>\n462\t <li><strong>Orch ON</strong> — sole trigger; cron fires the orch; it calls children in sequence</li>\n463\t <li><strong>Orch OFF</strong> — never runs automatically; children suppressed; each can get its own standalone cron</li>\n464\t <li><strong>Child (orch ON)</strong> — toggle comments/uncomments the script in master.conf</li>\n465\t <li><strong>Child (orch ON, no array)</strong> — orch hardcodes the call; toggle is display-only</li>\n466\t <li><strong>Child (orch OFF)</strong> — enter a cron to run it standalone</li>\n467\t <li><strong>Rsync badge</strong> — toggle writes TIER_RSYNC_ENABLED directly to master.conf</li>\n468\t <li><strong>Rsync (orch OFF)</strong> — fill location + standalone cron + Save; both required for independent firing</li>\n469\t </ul>\n470\t </div>\n471\t </div>\n472\t\n473\t <!-- ── Next Runs ── -->\n474\t <div class=\"vv-sug-block\" data-save-key=\"next-runs\">\n475\t <div class=\"vv-sug-header\" onclick=\"vvToggleSug(this)\">\n476\t <span class=\"vv-sug-chevron\">▾</span>\n477\t <span class=\"vv-sug-title\">Next Runs</span>\n478\t </div>\n479\t <div class=\"vv-sug-body\" id=\"vv-nextruns-body\">\n480\t <div class=\"vv-board-placeholder\">calculating…</div>\n481\t </div>\n482\t </div>\n483\t\n484\t <!-- ── Cron Calculator ── -->\n485\t <div class=\"vv-sug-block\" data-save-key=\"calc\">\n486\t <div class=\"vv-sug-header\" onclick=\"vvToggleSug(this)\">\n487\t <span class=\"vv-sug-chevron\">▸</span>\n488\t <span class=\"vv-sug-title\">Cron Calculator</span>\n489\t </div>\n490\t <div class=\"vv-sug-body\" style=\"display:none\">\n491\t <div class=\"vv-calc-wrap\">\n492\t <input type=\"text\" id=\"vv-calc-input\" class=\"vv-calc-in\"\n493\t placeholder=\"*/15 * * * * or every day at 3am\"\n494\t oninput=\"vvCalcUpdate(this.value)\"\n495\t onkeydown=\"if(event.key==='Enter')vvCalcApply()\">\n496\t <div id=\"vv-calc-result\"></div>\n497\t <button class=\"vv-btn-sm vv-calc-apply-btn\" id=\"vv-calc-apply\"\n498\t onclick=\"vvCalcApply()\" style=\"display:none\">\n499\t Apply to <span id=\"vv-calc-target-label\">selected</span>\n500\t </button>\n501\t </div>\n502\t </div>\n503\t </div>\n504\t\n505\t <!-- ── Recent Activity ── -->\n506\t <div class=\"vv-sug-block\" data-save-key=\"activity\">\n507\t <div class=\"vv-sug-header\" onclick=\"vvToggleSug(this)\">\n508\t <span class=\"vv-sug-chevron\">▸</span>\n509\t <span class=\"vv-sug-title\">Recent Activity</span>\n510\t <span id=\"vv-activity-badge\" class=\"vv-hdr-badge vv-hdr-badge-red\" style=\"display:none\"></span>\n511\t </div>\n512\t <div class=\"vv-sug-body\" style=\"display:none\" id=\"vv-activity-body\">\n513\t <div class=\"vv-board-placeholder\">loading…</div>\n514\t </div>\n515\t </div>\n516\t\n517\t <!-- ── Recent Errors ── -->\n518\t <div class=\"vv-sug-block\" data-save-key=\"errors\">\n519\t <div class=\"vv-sug-header\" onclick=\"vvToggleSug(this)\">\n520\t <span class=\"vv-sug-chevron\">▸</span>\n521\t <span class=\"vv-sug-title\">Recent Errors</span>\n522\t <span id=\"vv-errors-badge\" class=\"vv-hdr-badge vv-hdr-badge-red\" style=\"display:none\"></span>\n523\t </div>\n524\t <div class=\"vv-sug-body\" style=\"display:none\" id=\"vv-errors-body\">\n525\t <div class=\"vv-board-placeholder\">loading…</div>\n526\t </div>\n527\t </div>\n528\t\n529\t <!-- ── Active Locks ── -->\n530\t <div class=\"vv-sug-block\" data-save-key=\"locks\">\n531\t <div class=\"vv-sug-header\" onclick=\"vvToggleSug(this)\">\n532\t <span class=\"vv-sug-chevron\">▸</span>\n533\t <span class=\"vv-sug-title\">Active Locks</span>\n534\t <span id=\"vv-locks-badge\" class=\"vv-hdr-badge vv-hdr-badge-orange\" style=\"display:none\"></span>\n535\t </div>\n536\t <div class=\"vv-sug-body\" style=\"display:none\" id=\"vv-locks-body\">\n537\t <div class=\"vv-board-placeholder\">loading…</div>\n538\t </div>\n539\t </div>\n540\t\n541\t <!-- ── Partner ── -->\n542\t <div class=\"vv-sug-block\" data-save-key=\"partner\">\n543\t <div class=\"vv-sug-header\" onclick=\"vvToggleSug(this)\">\n544\t <span class=\"vv-sug-chevron\">▸</span>\n545\t <span class=\"vv-sug-title\">Partner</span>\n546\t <span id=\"vv-partner-hdr\" style=\"font-size:11px;color:#555;flex-shrink:0;\">—</span>\n547\t </div>\n548\t <div class=\"vv-sug-body\" style=\"display:none\" id=\"vv-partner-body\">\n549\t <div class=\"vv-board-placeholder\">loading…</div>\n550\t </div>\n551\t </div>\n552\t\n553\t <!-- ── Disabled on this host ── -->\n554\t <?php\n555\t $_confMap = vv_conf_script_map();\n556\t $_disabled = array_filter($_confMap, fn($v) => $v['enabled'] === false);\n557\t ?>\n558\t <div class=\"vv-sug-block\" data-save-key=\"disabled\">\n559\t <div class=\"vv-sug-header\" onclick=\"vvToggleSug(this)\">\n560\t <span class=\"vv-sug-chevron\">▸</span>\n561\t <span class=\"vv-sug-title\">Disabled on this host</span>\n562\t <?php if (!empty($_disabled)): ?>\n563\t <span class=\"vv-hdr-badge vv-hdr-badge-gray\"><?= count($_disabled) ?></span>\n564\t <?php endif; ?>\n565\t </div>\n566\t <div class=\"vv-sug-body\" style=\"display:none\">\n567\t <?php if (empty($_disabled)): ?>\n568\t <div class=\"vv-board-placeholder\">All scripts enabled.</div>\n569\t <?php else: ?>\n570\t <div class=\"vv-disabled-list\">\n571\t <?php foreach ($_disabled as $_drel => $_dv):\n572\t $_dname = str_replace('_', ' ', strtolower(preg_replace('/_SCRIPTS$/', '', $_dv['array'] ?? '')));\n573\t ?>\n574\t <div class=\"vv-disabled-row\">\n575\t <span class=\"vv-disabled-name\"><?= htmlspecialchars(basename($_drel, '.sh')) ?></span>\n576\t <span class=\"vv-disabled-grp\"><?= htmlspecialchars($_dname) ?></span>\n577\t </div>\n578\t <?php endforeach; ?>\n579\t </div>\n580\t <?php endif; ?>\n581\t </div>\n582\t </div>\n583\t\n584\t <!-- ── Fallback ── -->\n585\t <div class=\"vv-sug-block vv-info-block\" data-save-key=\"fallback\">\n586\t <div class=\"vv-sug-header\" onclick=\"vvToggleSug(this)\">\n587\t <span class=\"vv-sug-chevron\">▾</span>\n588\t <span class=\"vv-sug-title\">Fallback</span>\n589\t </div>\n590\t <div class=\"vv-sug-body\">\n591\t <pre class=\"vv-readme-body\">Not enterprise HA. No SLAs, no quorum nodes, no guaranteed uptime — don't put your billing system on this.\n592\t\n593\tWhat it is: mutual automatic failover between two independent unRAID servers. When one goes down the other starts its containers, cuts over DNS, and keeps users online. When it comes back, everything hands back in the correct sequence — DDNS stops, containers stop, rsync writeback runs, containers start on the primary, primary DDNS starts last — so users hit the returning server only after it's actually ready.\n594\t\n595\tBuilt from scratch. Refined through a year of production testing. The DDNS sequencing and handback order were the hardest parts to get right. Both directions are exercised regularly with fallback_test.sh.</pre>\n596\t </div>\n597\t </div>\n598\t\n599\t <!-- ── How it works ── -->\n600\t <div class=\"vv-sug-block vv-info-block\">\n601\t <div class=\"vv-sug-header\" onclick=\"vvToggleSug(this)\">\n602\t <span class=\"vv-sug-chevron\">▸</span>\n603\t <span class=\"vv-sug-title\">How it works</span>\n604\t </div>\n605\t <div class=\"vv-sug-body\" style=\"display:none\">\n606\t <pre class=\"vv-readme-body\">Varaverk owns the cron layer. schedule.json stores every orchestrator's enabled state, cron expression, and log setting. When you save, the plugin rebuilds /etc/cron.d/varaverk — one entry per enabled orchestrator. That's all cron knows about.\n607\t\n608\tOrchestrators are the only scripts with cron schedules. Each one reads its SCRIPTS list out of master.conf and runs each child in sequence. The children do the actual work. The orchestrator controls the order and the timing. A child failure stops the chain.\n609\t\n610\tmaster.conf is the per-host control plane. Comment out a line to disable that script on this host without touching any code. Both servers run the same repository — different master.conf comments produce different behaviour per machine.\n611\t\n612\tChildren only get their own cron when their orchestrator is OFF. When the orch is on, it owns the timing completely.\n613\t\n614\tcommon.sh provides the shared runtime every script sources: acquire_lock(), log_msg(), info(), success(), warn(), error(). Lock files live in /tmp/varaverk_locks/. acquire_lock() exits cleanly if a prior run is still in progress — nothing piles up.\n615\t\n616\tLogs are written per script to the appdata path. The Log button shows the latest run. Verbose mode passes --log for per-step detail.\n617\t\n618\tArray events bypass cron entirely. array_start fires when Unraid brings the array up; array_stop fires before shutdown. array_started handles all post-start setup in sequence; array_stopping handles the graceful teardown.\n619\t\n620\tThe watchdog tier runs every 15 minutes as a single-pass chain: resource_watchdog frees memory pressure first, docker_watchdog restarts down containers into that headroom, system_watchdog checks storage/WebGUI/network, stability_watchdog reboots as a last resort. The order matters — restarting containers before freeing memory just restarts them into the same pressure.</pre>\n621\t </div>\n622\t </div>\n623\t\n624\t <!-- ── This started as ── -->\n625\t <div class=\"vv-sug-block vv-info-block\">\n626\t <div class=\"vv-sug-header\" onclick=\"vvToggleSug(this)\">\n627\t <span class=\"vv-sug-chevron\">▸</span>\n628\t <span class=\"vv-sug-title\">This started as...</span>\n629\t </div>\n630\t <div class=\"vv-sug-body\" style=\"display:none\">\n631\t <pre class=\"vv-readme-body\">One script. A few lines — checking whether a container was running and restarting it if not. Backgrounded through User Scripts, sleeping a minute between checks, essentially forgotten. It did its job.\n632\t\n633\tThen a conversation with a buddy: wouldn't it be cool if it emailed when something crashed? Sure, added it. Wouldn't it be cool if it handled more containers? Easy — grow the list. Wouldn't it be cool if both servers stayed in sync automatically so neither of us had to remember to do it manually?\n634\t\n635\tThat one took a while.\n636\t\n637\tOrder started to matter. Freeing memory before attempting container restarts meant the containers actually stayed up. Sync had to happen before the media database refresh, not after. Watchdogs couldn't block each other. The big loop that tried to do everything started breaking into pieces — one piece per job, and a sequencer that knew which piece went first.\n638\t\n639\tmaster.conf became the control plane: one file, two different sets of comments for two different hosts. Same codebase, different behaviour per machine. The orchestrator pattern came out of not wanting to duplicate logic.\n640\t\n641\tBy the time the User Scripts plugin couldn't clearly show what was actually scheduled, whether something was running, or why a sync had quietly stopped working two weeks ago — the interface became the next thing to build.\n642\t\n643\tVaraverk started as a status table and a schedule editor. The scheduler became the main thing. The monitor followed.\n644\t\n645\tStill the same two servers, two households, the same media stack running itself. The scripts just grew up around it.</pre>\n646\t </div>\n647\t </div>\n648\t\n649\t <!-- ── Docs tree ── -->\n650\t <div class=\"vv-info-divider\">Docs</div>\n651\t <div id=\"vv-docs-tree\">\n652\t\n653\t <?php if ($_mainReadme || !empty($_childReadmes)): ?>\n654\t <div class=\"vv-sb-entry\">\n655\t <div class=\"vv-sb-row vv-sb-orch-row\"\n656\t data-id=\"README.md\"\n657\t data-hdr=\"\"\n658\t data-desc=\"Project README\"\n659\t onclick=\"vvSelectScript(this)\">\n660\t <?php if (!empty($_childReadmes)): ?>\n661\t <span class=\"vv-sb-expand\" onclick=\"vvToggleSbChildren(this,event)\">▸</span>\n662\t <?php else: ?>\n663\t <span class=\"vv-sb-expand vv-sb-leaf\"> </span>\n664\t <?php endif; ?>\n665\t <span class=\"vv-sb-name\">README</span>\n666\t </div>\n667\t <?php if (!empty($_childReadmes)): ?>\n668\t <div class=\"vv-sb-children\" style=\"display:none\">\n669\t <?php foreach ($_childReadmes as $_cr):\n670\t $_crLabel = preg_replace('/^README-?/i', '', basename($_cr));\n671\t $_crLabel = preg_replace('/\\.md$/i', '', $_crLabel);\n672\t $_crLabel = str_replace('_', ' ', $_crLabel) ?: dirname($_cr);\n673\t ?>\n674\t <div class=\"vv-sb-row vv-sb-child-row\"\n675\t data-id=\"<?= htmlspecialchars($_cr) ?>\"\n676\t data-hdr=\"\"\n677\t data-desc=\"\"\n678\t onclick=\"vvSelectScript(this)\">\n679\t <span class=\"vv-sb-child-indent\">└</span>\n680\t <span class=\"vv-sb-name\"><?= htmlspecialchars($_crLabel) ?></span>\n681\t </div>\n682\t <?php endforeach; ?>\n683\t </div>\n684\t <?php endif; ?>\n685\t </div>\n686\t <?php endif; ?>\n687\t\n688\t <?php if ($_mainManual || !empty($_childManuals)): ?>\n689\t <div class=\"vv-sb-entry\">\n690\t <div class=\"vv-sb-row vv-sb-orch-row\"\n691\t data-id=\"Manual.md\"\n692\t data-hdr=\"\"\n693\t data-desc=\"Project Manual\"\n694\t onclick=\"vvSelectScript(this)\">\n695\t <?php if (!empty($_childManuals)): ?>\n696\t <span class=\"vv-sb-expand\" onclick=\"vvToggleSbChildren(this,event)\">▸</span>\n697\t <?php else: ?>\n698\t <span class=\"vv-sb-expand vv-sb-leaf\"> </span>\n699\t <?php endif; ?>\n700\t <span class=\"vv-sb-name\">Manual</span>\n701\t </div>\n702\t <?php if (!empty($_childManuals)): ?>\n703\t <div class=\"vv-sb-children\" style=\"display:none\">\n704\t <?php foreach ($_childManuals as $_cm):\n705\t $_cmLabel = preg_replace('/^Manual-?/i', '', basename($_cm));\n706\t $_cmLabel = preg_replace('/\\.md$/i', '', $_cmLabel);\n707\t $_cmLabel = str_replace('_', ' ', $_cmLabel) ?: dirname($_cm);\n708\t ?>\n709\t <div class=\"vv-sb-row vv-sb-child-row\"\n710\t data-id=\"<?= htmlspecialchars($_cm) ?>\"\n711\t data-hdr=\"\"\n712\t data-desc=\"\"\n713\t onclick=\"vvSelectScript(this)\">\n714\t <span class=\"vv-sb-child-indent\">└</span>\n715\t <span class=\"vv-sb-name\"><?= htmlspecialchars($_cmLabel) ?></span>\n716\t </div>\n717\t <?php endforeach; ?>\n718\t </div>\n719\t <?php endif; ?>\n720\t </div>\n721\t <?php endif; ?>\n722\t\n723\t </div><!-- /#vv-docs-tree -->\n724\t\n725\t <!-- ── Script browser tree ── -->\n726\t <div class=\"vv-info-divider\">Orch</div>\n727\t <div id=\"vv-sb-tree\">\n728\t <?php foreach ($tree as $orch):\n729\t $orchId = $orch['id'];\n730\t $orchHdr = vv_script_header_clean(SCRIPTS_DIR . '/' . $orchId);\n731\t $hasKids = !empty($orch['children']);\n732\t ?>\n733\t <div class=\"vv-sb-entry\">\n734\t <div class=\"vv-sb-row vv-sb-orch-row\"\n735\t data-id=\"<?= htmlspecialchars($orchId) ?>\"\n736\t data-hdr=\"<?= htmlspecialchars($orchHdr) ?>\"\n737\t data-desc=\"<?= htmlspecialchars($orch['desc']) ?>\"\n738\t onclick=\"vvSelectScript(this)\">\n739\t <?php if ($hasKids): ?>\n740\t <span class=\"vv-sb-expand\" onclick=\"vvToggleSbChildren(this,event)\">▸</span>\n741\t <?php else: ?>\n742\t <span class=\"vv-sb-expand vv-sb-leaf\"> </span>\n743\t <?php endif; ?>\n744\t <span class=\"vv-sb-name\"><?= htmlspecialchars($orch['label']) ?></span>\n745\t <?php if (str_starts_with($orch['cron'], 'array_')): ?>\n746\t <span class=\"vv-event-badge vv-sb-badge\">⚡</span>\n747\t <?php elseif ($orch['suggested_cron']): ?>\n748\t <code class=\"vv-sug-cron vv-sb-cron\" style=\"cursor:pointer\" title=\"Click to apply cron\" onclick=\"vvApplySugCron(this,event)\"><?= htmlspecialchars($orch['suggested_cron']) ?></code>\n749\t <?php endif; ?>\n750\t <?php\n751\t $_os = @json_decode(@file_get_contents(vv_job_stat_path($orchId)), true);\n752\t $_or = $_os['status'] ?? '';\n753\t $_oc = $_or === 'ok' ? 'vv-stat-ok' : ($_or === 'warn' ? 'vv-stat-warn' : ($_or === 'error' ? 'vv-stat-error' : ($_or === 'skipped' ? 'vv-stat-skip' : ($orch['enabled'] ? 'vv-sug-on' : 'vv-stat-none'))));\n754\t ?>\n755\t <span class=\"<?= $_oc ?> vv-sb-status\" title=\"<?= $_or ? htmlspecialchars(\"last: $_or\") : ($orch['enabled'] ? 'enabled' : 'never run') ?>\">\n756\t <?= ($_or || $orch['enabled']) ? '●' : '○' ?>\n757\t </span>\n758\t </div>\n759\t <?php if ($hasKids): ?>\n760\t <div class=\"vv-sb-children\" style=\"display:none\">\n761\t <?php foreach ($orch['children'] as $child):\n762\t $childHdr = vv_script_header_clean(SCRIPTS_DIR . '/' . $child['id']);\n763\t ?>\n764\t <div class=\"vv-sb-row vv-sb-child-row\"\n765\t data-id=\"<?= htmlspecialchars($child['id']) ?>\"\n766\t data-hdr=\"<?= htmlspecialchars($childHdr) ?>\"\n767\t data-desc=\"<?= htmlspecialchars($child['desc']) ?>\"\n768\t onclick=\"vvSelectScript(this)\">\n769\t <span class=\"vv-sb-child-indent\">└</span>\n770\t <span class=\"vv-sb-name\"><?= htmlspecialchars($child['label']) ?></span>\n771\t <?php if (!empty($child['suggested_cron'])): ?>\n772\t <code class=\"vv-sug-cron vv-sb-cron\" style=\"cursor:pointer\" title=\"Click to apply cron\" onclick=\"vvApplySugCron(this,event)\"><?= htmlspecialchars($child['suggested_cron']) ?></code>\n773\t <?php endif; ?>\n774\t <?php\n775\t $_cs = @json_decode(@file_get_contents(vv_job_stat_path($child['id'])), true);\n776\t $_cr = $_cs['status'] ?? '';\n777\t $_cc = $_cr === 'ok' ? 'vv-stat-ok' : ($_cr === 'warn' ? 'vv-stat-warn' : ($_cr === 'error' ? 'vv-stat-error' : ($_cr === 'skipped' ? 'vv-stat-skip' : (!empty($child['conf_enabled']) ? 'vv-sug-on' : 'vv-stat-none'))));\n778\t ?>\n779\t <span class=\"<?= $_cc ?> vv-sb-status\" title=\"<?= $_cr ? htmlspecialchars(\"last: $_cr\") : (!empty($child['conf_enabled']) ? 'enabled' : 'never run') ?>\">\n780\t <?= ($_cr || !empty($child['conf_enabled'])) ? '●' : '○' ?>\n781\t </span>\n782\t </div>\n783\t <?php endforeach; ?>\n784\t </div>\n785\t <?php endif; ?>\n786\t </div>\n787\t <?php endforeach; ?>\n788\t </div><!-- /#vv-sb-tree -->\n789\t\n790\t <!-- ── Repository separator + tree (Advanced mode only) ── -->\n791\t <div class=\"vv-info-divider vv-adv-only\" style=\"display:none;margin-top:8px;\">Repository</div>\n792\t <?php if (!empty($_repoFiles)): ?>\n793\t <div class=\"vv-sug-block vv-info-block vv-adv-only\" style=\"display:none;margin-top:4px;\">\n794\t <div class=\"vv-sug-header\" onclick=\"vvToggleSug(this)\">\n795\t <span class=\"vv-sug-chevron\">▸</span>\n796\t <span class=\"vv-sug-title\">Varaverk</span>\n797\t </div>\n798\t <div class=\"vv-sug-body\" style=\"display:none;padding:0;\">\n799\t <?php\n800\t $_rootFiles = array_values(array_filter($_repoFiles, function($f) { return strpos($f, '/') === false; }));"}]},"uuid":"ab0de013-9865-4ed5-b2b8-56ecbad119df","timestamp":"2026-06-06T14:29:45.090Z","sourceToolAssistantUUID":"6f489b1e-70ce-4c68-8f4e-40b889e8abf9","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"ab0de013-9865-4ed5-b2b8-56ecbad119df","isSidechain":true,"agentId":"accc3589951fac88e","message":{"model":"claude-haiku-4-5-20251001","id":"msg_013WqYakCLdrqbMjH2wFuuev","type":"message","role":"assistant","content":[{"type":"text","text":"Let me continue reading the remaining pages."}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":69307,"cache_read_input_tokens":99742,"cache_creation":{"ephemeral_5m_input_tokens":69307,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null},"requestId":"req_011CbnAZnUZLhHRUr5fk72e8","attributionAgent":"Explore","type":"assistant","uuid":"004457fe-f1dc-44f4-a0ed-5da92938ebf9","timestamp":"2026-06-06T14:29:48.081Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"004457fe-f1dc-44f4-a0ed-5da92938ebf9","isSidechain":true,"agentId":"accc3589951fac88e","message":{"model":"claude-haiku-4-5-20251001","id":"msg_013WqYakCLdrqbMjH2wFuuev","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01FXHWu4czYF9q4hHwf4WrLK","name":"Read","input":{"file_path":"/boot/config/plugins/varaverk/Plugin/unraid/pages/settings.php","limit":200},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":69307,"cache_read_input_tokens":99742,"cache_creation":{"ephemeral_5m_input_tokens":69307,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null},"requestId":"req_011CbnAZnUZLhHRUr5fk72e8","attributionAgent":"Explore","type":"assistant","uuid":"f7dd3cfd-ddb8-4e6b-8a68-4943749abd4d","timestamp":"2026-06-06T14:29:48.743Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"f7dd3cfd-ddb8-4e6b-8a68-4943749abd4d","isSidechain":true,"promptId":"3c1ac879-b13a-4ebc-9aeb-ea73615e9af3","agentId":"accc3589951fac88e","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01FXHWu4czYF9q4hHwf4WrLK","type":"tool_result","content":"1\t<?php\n2\t$_myHost = vv_detect_host();\n3\t$_vars = vv_conf_vars();\n4\t$_myId = strtoupper($_myHost);\n5\t\n6\t// Storage\n7\t$_confKey = $_myId . '_STORAGE_MODE_INTERNAL';\n8\t$_confVal = $_vars[$_confKey] ?? null;\n9\t\n10\t// Notifications\n11\t$_notifyUnraid = ($_vars['NOTIFY_UNRAID'] ?? 'true') !== 'false';\n12\t$_enableLogging = ($_vars['ENABLE_LOGGING'] ?? 'false') === 'true';\n13\t$_discordKey = $_myId . '_DISCORD_WEBHOOK';\n14\t$_discordHook = $_vars[$_discordKey] ?? '';\n15\t\n16\t// API key\n17\t$_apiKeyVar = $_myId . '_UNRAID_API_KEY';\n18\t$_apiKey = $_vars[$_apiKeyVar] ?? '';\n19\t$_apiPreview = $_apiKey ? substr($_apiKey, 0, 8) . '...' . substr($_apiKey, -4) : '';\n20\t?>\n21\t<style>\n22\t.vv-set-card { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:14px 16px;margin-bottom:14px; }\n23\t.vv-set-hdr { font-size:11px;font-weight:700;color:#666;text-transform:uppercase;letter-spacing:.07em;margin-bottom:12px; }\n24\t.vv-set-row { display:flex;justify-content:space-between;align-items:baseline;gap:8px;margin:4px 0; }\n25\t.vv-set-lbl { font-size:11px;color:#444; }\n26\t.vv-set-val { font-size:12px;color:#888;text-align:right;font-family:monospace; }\n27\t.vv-set-badge { display:inline-block;font-size:10px;padding:2px 8px;border-radius:3px;font-weight:700;letter-spacing:.04em; }\n28\t.vv-set-badge.internal { background:#0d1f0d;color:#4caf50;border:1px solid #1a3a1a; }\n29\t.vv-set-badge.flash { background:#1a1200;color:#ffb74d;border:1px solid #3a2800; }\n30\t.vv-set-badge.custom { background:#0a1a2a;color:#4a9eff;border:1px solid #1a3a5a; }\n31\t.vv-set-sep { border:none;border-top:1px solid #1e1e1e;margin:10px 0; }\n32\t.vv-set-btn { background:#1a1a1a;border:1px solid #333;color:#888;font-size:11px;padding:5px 14px;\n33\t border-radius:3px;cursor:pointer;transition:border-color .15s,color .15s; }\n34\t.vv-set-btn:hover { border-color:#555;color:#ccc; }\n35\t.vv-set-btn:disabled { opacity:.4;cursor:default; }\n36\t.vv-set-btn.primary { background:#1a2a1a;border-color:#2d4a2d;color:#4caf50; }\n37\t.vv-set-btn.primary:hover { background:#223a22; }\n38\t.vv-set-btn.warn { background:#1f1500;border-color:#3a2800;color:#ffb74d; }\n39\t.vv-set-btn.warn:hover { background:#2a1e00; }\n40\t.vv-set-out { background:#080808;border:1px solid #1a1a1a;border-radius:3px;padding:10px 12px;\n41\t font-family:monospace;font-size:10px;color:#555;white-space:pre-wrap;word-break:break-all;\n42\t max-height:320px;overflow-y:auto;margin-top:10px;display:none;\n43\t scrollbar-width:none; }\n44\t.vv-set-out::-webkit-scrollbar { display:none; }\n45\t.vv-set-info { font-size:11px;color:#444;line-height:1.6;margin-bottom:10px; }\n46\t.vv-set-warn-box { background:#1a1200;border:1px solid #3a2800;border-radius:4px;\n47\t padding:8px 12px;font-size:11px;color:#ffb74d;margin-top:8px;display:none; }\n48\t.vv-set-tog-wrap { display:flex;align-items:center;gap:10px;cursor:pointer;user-select:none; }\n49\t.vv-set-tog-track{ width:32px;height:18px;border-radius:9px;background:#222;border:1px solid #333;\n50\t position:relative;transition:background .15s,border-color .15s;flex-shrink:0; }\n51\t.vv-set-tog-track.on { background:#1a3a1a;border-color:#2d5a2d; }\n52\t.vv-set-tog-track::after { content:'';position:absolute;top:2px;left:2px;width:12px;height:12px;\n53\t border-radius:50%;background:#555;transition:left .15s,background .15s; }\n54\t.vv-set-tog-track.on::after { left:16px;background:#4caf50; }\n55\t.vv-set-tog-lbl { font-size:12px;color:#888; }\n56\t.vv-set-tog-sub { font-size:10px;color:#3a3a3a;margin-top:1px; }\n57\t.vv-set-inp { background:#0d0d0d;border:1px solid #2a2a2a;border-radius:3px;color:#888;\n58\t font-size:12px;padding:5px 8px;outline:none;width:100%;box-sizing:border-box;\n59\t font-family:monospace; }\n60\t.vv-set-inp:focus{ border-color:#444; }\n61\t</style>\n62\t\n63\t<div style=\"max-width:740px;margin:0 auto;\">\n64\t\n65\t <!-- Storage Location card -->\n66\t <div class=\"vv-set-card\" id=\"vv-stor-card\">\n67\t <div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;\">\n68\t <span class=\"vv-set-hdr\" style=\"margin-bottom:0;\">Storage Location</span>\n69\t <span id=\"vv-stor-badge\" class=\"vv-set-badge\">…</span>\n70\t </div>\n71\t\n72\t <div id=\"vv-stor-info\" style=\"font-size:11px;color:#3a3a3a;\">Loading…</div>\n73\t\n74\t <hr class=\"vv-set-sep\">\n75\t\n76\t <div class=\"vv-set-info\">\n77\t Varaverk stores all scripts, configuration, state files, and the git repository in\n78\t <code style=\"font-size:10px;color:#4a7a4a;background:#080808;padding:1px 4px;border-radius:2px;\">SCRIPTS_DIR</code>.\n79\t <br>\n80\t <strong style=\"color:#555;\">Internal NVMe/SSD</strong> — direct access, zero write-wear concern, git pull/push from boot volume.<br>\n81\t <strong style=\"color:#555;\">USB Flash</strong> — preserves flash lifetime. Requires array to be started. git pull syncs\n82\t <code style=\"font-size:10px;color:#4a7a4a;background:#080808;padding:1px 4px;border-radius:2px;\">Plugin/</code>\n83\t back to <code style=\"font-size:10px;color:#4a7a4a;background:#080808;padding:1px 4px;border-radius:2px;\">/boot/</code>\n84\t after each pull so the webUI always stays current.\n85\t </div>\n86\t\n87\t <div id=\"vv-stor-warn-flash\" class=\"vv-set-warn-box\">\n88\t Array must be started for Varaverk to function in flash mode. The plugin tab is always accessible.\n89\t </div>\n90\t <div id=\"vv-stor-warn-noarray\" class=\"vv-set-warn-box\">\n91\t Array is not started — cannot migrate to flash mode right now. Start the array first.\n92\t </div>\n93\t\n94\t <div style=\"display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:10px;\">\n95\t <button id=\"vv-stor-migrate-btn\" class=\"vv-set-btn\" onclick=\"vvStorMigrate()\" style=\"display:none;\"></button>\n96\t <button class=\"vv-set-btn\" onclick=\"vvStorLoad()\" style=\"font-size:10px;padding:4px 10px;\">↻ Refresh</button>\n97\t <span id=\"vv-stor-fb\" style=\"font-size:11px;color:#444;\"></span>\n98\t </div>\n99\t\n100\t <pre class=\"vv-set-out\" id=\"vv-stor-out\"></pre>\n101\t </div>\n102\t\n103\t <!-- Notifications card -->\n104\t <div class=\"vv-set-card\">\n105\t <div class=\"vv-set-hdr\">Notifications</div>\n106\t\n107\t <div style=\"display:flex;flex-direction:column;gap:10px;margin-bottom:14px;\">\n108\t <label class=\"vv-set-tog-wrap\">\n109\t <div class=\"vv-set-tog-track<?= $_notifyUnraid ? ' on' : '' ?>\" id=\"vv-ntf-unraid\"\n110\t onclick=\"vvNtfToggle(this,'NOTIFY_UNRAID')\"></div>\n111\t <div>\n112\t <div class=\"vv-set-tog-lbl\">Unraid native notifications</div>\n113\t <div class=\"vv-set-tog-sub\">Appears in the Unraid bell icon — job completions, warnings, errors</div>\n114\t </div>\n115\t </label>\n116\t <label class=\"vv-set-tog-wrap\">\n117\t <div class=\"vv-set-tog-track<?= $_enableLogging ? ' on' : '' ?>\" id=\"vv-ntf-logging\"\n118\t onclick=\"vvNtfToggle(this,'ENABLE_LOGGING')\"></div>\n119\t <div>\n120\t <div class=\"vv-set-tog-lbl\">Verbose logging</div>\n121\t <div class=\"vv-set-tog-sub\">Adds detailed [LOG] lines to script output — useful for debugging scheduled runs</div>\n122\t </div>\n123\t </label>\n124\t </div>\n125\t\n126\t <hr class=\"vv-set-sep\">\n127\t\n128\t <div style=\"margin-bottom:6px;\">\n129\t <div style=\"font-size:11px;color:#555;margin-bottom:5px;\">\n130\t Discord webhook\n131\t <span style=\"color:#2a2a2a;font-size:10px;margin-left:4px;\"><?= htmlspecialchars($_discordKey) ?></span>\n132\t </div>\n133\t <div style=\"display:flex;gap:6px;align-items:center;\">\n134\t <input class=\"vv-set-inp\" id=\"vv-ntf-discord\" type=\"text\"\n135\t placeholder=\"https://discord.com/api/webhooks/…\"\n136\t value=\"<?= htmlspecialchars($_discordHook) ?>\">\n137\t <button class=\"vv-set-btn primary\" onclick=\"vvNtfSaveWebhook()\" id=\"vv-ntf-save\"\n138\t style=\"white-space:nowrap;flex-shrink:0;\">Save</button>\n139\t </div>\n140\t <div style=\"font-size:10px;color:#2a2a2a;margin-top:4px;\">Leave blank to disable Discord notifications for this host</div>\n141\t </div>\n142\t <span id=\"vv-ntf-fb\" style=\"font-size:11px;\"></span>\n143\t </div>\n144\t\n145\t <!-- Unraid API Key card -->\n146\t <div class=\"vv-set-card\">\n147\t <div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;\">\n148\t <span class=\"vv-set-hdr\" style=\"margin-bottom:0;\">Unraid API Keys</span>\n149\t <span id=\"vv-api-badge\" class=\"vv-set-badge\">…</span>\n150\t </div>\n151\t\n152\t <div id=\"vv-api-hosts\" style=\"margin-bottom:12px;\"></div>\n153\t\n154\t <div style=\"font-size:11px;color:#444;margin-bottom:12px;line-height:1.6;\">\n155\t Each host registers\n156\t <code style=\"font-size:10px;color:#4a7a4a;background:#080808;padding:1px 4px;border-radius:2px;\">Varaverk_HOST1</code>\n157\t in <em>every</em> machine's local Unraid registry — own machine plus all partners.\n158\t The key values are stored in the private <code style=\"font-size:10px;color:#4a7a4a;background:#080808;padding:1px 4px;border-radius:2px;\">host*.conf</code>\n159\t so each host can query every other host's GraphQL API directly — real-time monitoring, no SSH.\n160\t During offboard, partner keys are removed from each registry automatically.\n161\t </div>\n162\t\n163\t <div style=\"display:flex;gap:8px;align-items:center;flex-wrap:wrap;\">\n164\t <button class=\"vv-set-btn\" onclick=\"vvApiCheck()\" id=\"vv-api-check-btn\">↻ Check status</button>\n165\t <button class=\"vv-set-btn\" onclick=\"vvApiRenew(false)\" id=\"vv-api-local-btn\">Renew local</button>\n166\t <button class=\"vv-set-btn primary\" onclick=\"vvApiRenew(true)\" id=\"vv-api-all-btn\">Setup all host keys</button>\n167\t <span id=\"vv-api-fb\" style=\"font-size:11px;color:#444;\"></span>\n168\t </div>\n169\t <pre class=\"vv-set-out\" id=\"vv-api-out\"></pre>\n170\t </div>\n171\t\n172\t</div>\n173\t\n174\t<script>\n175\tvar _vvStorData = null;\n176\t\n177\tfunction vvStorLoad() {\n178\t const info = document.getElementById('vv-stor-info');\n179\t const badge = document.getElementById('vv-stor-badge');\n180\t if (info) info.textContent = 'Loading…';\n181\t\n182\t fetch('/plugins/varaverk/api/storage.php?action=status&_=' + Date.now())\n183\t .then(r => r.json())\n184\t .then(d => {\n185\t _vvStorData = d;\n186\t if (!d.ok) { if (info) info.textContent = 'Error loading status'; return; }\n187\t _vvStorRender(d);\n188\t })\n189\t .catch(() => { if (info) info.textContent = 'Request failed'; });\n190\t}\n191\t\n192\tfunction _vvStorRender(d) {\n193\t const badge = document.getElementById('vv-stor-badge');\n194\t const info = document.getElementById('vv-stor-info');\n195\t const btn = document.getElementById('vv-stor-migrate-btn');\n196\t const wFlash = document.getElementById('vv-stor-warn-flash');\n197\t const wArr = document.getElementById('vv-stor-warn-noarray');\n198\t\n199\t // Badge\n200\t if (badge) {"}]},"uuid":"1e8bd860-875d-4ec1-9ff4-a57f60e4e387","timestamp":"2026-06-06T14:29:48.767Z","sourceToolAssistantUUID":"f7dd3cfd-ddb8-4e6b-8a68-4943749abd4d","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"1e8bd860-875d-4ec1-9ff4-a57f60e4e387","isSidechain":true,"agentId":"accc3589951fac88e","message":{"model":"claude-haiku-4-5-20251001","id":"msg_013WqYakCLdrqbMjH2wFuuev","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01GRjSS3AGsqjQdtST6bhs5B","name":"Read","input":{"file_path":"/boot/config/plugins/varaverk/Plugin/unraid/pages/setup.php","limit":200},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":69307,"cache_read_input_tokens":99742,"cache_creation":{"ephemeral_5m_input_tokens":69307,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null},"requestId":"req_011CbnAZnUZLhHRUr5fk72e8","attributionAgent":"Explore","type":"assistant","uuid":"18e20743-720b-4d78-890d-f1ad06f73c1e","timestamp":"2026-06-06T14:29:48.960Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"18e20743-720b-4d78-890d-f1ad06f73c1e","isSidechain":true,"promptId":"3c1ac879-b13a-4ebc-9aeb-ea73615e9af3","agentId":"accc3589951fac88e","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01GRjSS3AGsqjQdtST6bhs5B","type":"tool_result","content":"1\t<?php\n2\t// First-run setup wizard — uniform flow for all hosts.\n3\t// Step 1: auto-detect environment + server identity form.\n4\t// Step 2: auto-populate + guide + checklist.\n5\t// master.conf pull (for partner servers) lives in the checklist, not here.\n6\t\n7\t$detectedHostname = vv_get_hostname();\n8\t?>\n9\t<link rel=\"stylesheet\" href=\"/plugins/varaverk/css/varaverk.css\">\n10\t<style>\n11\t#vv-setup {\n12\t max-width: 580px; margin: 40px auto 0;\n13\t background: #141414; border: 1px solid #2a2a2a;\n14\t border-radius: 6px; padding: 36px 40px 40px;\n15\t font-family: monospace; color: #ccc;\n16\t}\n17\t#vv-setup h1 { margin: 0 0 4px; font-size: 17px; color: #e0e0e0; font-weight: normal; letter-spacing: .04em; }\n18\t.vv-sub { font-size: 12px; color: #555; margin-bottom: 28px; }\n19\t.vv-field { margin-bottom: 18px; }\n20\t.vv-field label { display: block; font-size: 11px; color: #888; margin-bottom: 5px; text-transform: uppercase; letter-spacing: .06em; }\n21\t.vv-field input[type=text],\n22\t.vv-field select {\n23\t width: 100%; box-sizing: border-box; background: #0d0d0d;\n24\t border: 1px solid #333; color: #ddd; padding: 7px 10px;\n25\t border-radius: 3px; font-family: monospace; font-size: 13px;\n26\t}\n27\t.vv-field input:focus, .vv-field select:focus { outline: none; border-color: #555; }\n28\t.vv-hint { font-size: 11px; color: #555; margin-top: 4px; }\n29\t.vv-role-row { display: flex; gap: 10px; margin-bottom: 22px; }\n30\t.vv-role-btn { flex: 1; padding: 9px 0; background: #1a1a1a; border: 1px solid #333;\n31\t border-radius: 3px; color: #777; font-family: monospace; font-size: 12px;\n32\t cursor: pointer; text-align: center; transition: border-color .15s, color .15s; }\n33\t.vv-role-btn.active { border-color: #555; color: #ccc; background: #1e1e1e; }\n34\t.vv-cond { display: none; }\n35\t.vv-cond.show { display: block; }\n36\thr.vv-hr { border: none; border-top: 1px solid #1e1e1e; margin: 22px 0; }\n37\t.vv-btn { width: 100%; padding: 10px; background: #1e1e1e; border: 1px solid #444;\n38\t color: #ccc; font-family: monospace; font-size: 13px; border-radius: 3px;\n39\t cursor: pointer; letter-spacing: .03em; }\n40\t.vv-btn:hover { border-color: #666; color: #eee; }\n41\t.vv-btn:disabled { opacity: .4; cursor: default; }\n42\t#vv-status { margin-top: 10px; font-size: 12px; color: #666; text-align: center; min-height: 16px; }\n43\t#vv-status.ok { color: #4a8; }\n44\t#vv-status.err { color: #a44; }\n45\t\n46\t/* Detection banner */\n47\t#vv-detect-banner {\n48\t background: #0d0d0d; border: 1px solid #2a2a2a; border-radius: 3px;\n49\t padding: 11px 14px; margin-bottom: 22px; font-size: 12px; line-height: 1.8; color: #666;\n50\t}\n51\t#vv-detect-banner .vv-det-row { display: flex; gap: 8px; }\n52\t#vv-detect-banner .vv-det-lbl { color: #555; min-width: 100px; }\n53\t#vv-detect-banner .vv-det-val { color: #999; }\n54\t#vv-detect-banner .loading { color: #444; font-style: italic; }\n55\t\n56\t/* Step 2 */\n57\t#vv-step2 { display: none; }\n58\t.vv-guide {\n59\t background: #0d0d0d; border: 1px solid #2a2a2a; border-radius: 3px;\n60\t padding: 13px 16px; margin-bottom: 20px; font-size: 12px; color: #666; line-height: 1.9;\n61\t}\n62\t.vv-guide ol { margin: 8px 0 0 16px; padding: 0; }\n63\t.vv-guide li { margin-bottom: 3px; }\n64\t.vv-cl-title { font-size: 11px; color: #555; text-transform: uppercase; letter-spacing: .06em; margin-bottom: 10px; }\n65\t.vv-cl-item { display: flex; align-items: flex-start; gap: 10px; padding: 7px 0;\n66\t border-bottom: 1px solid #1a1a1a; font-size: 12px; }\n67\t.vv-cl-item:last-child { border-bottom: none; }\n68\t.vv-cl-icon { font-size: 13px; min-width: 16px; margin-top: 1px; }\n69\t.vv-cl-body { flex: 1; }\n70\t.vv-cl-label { color: #bbb; }\n71\t.vv-cl-detail{ color: #555; font-size: 11px; margin-top: 2px; }\n72\t.vv-cl-act { margin-top: 5px; }\n73\t.vv-cl-act button { padding: 4px 10px; background: #1a1a1a; border: 1px solid #333; color: #888;\n74\t font-family: monospace; font-size: 11px; border-radius: 2px; cursor: pointer; }\n75\t.vv-cl-act button:hover { border-color: #555; color: #bbb; }\n76\t.vv-cl-err { font-size: 11px; color: #a44; margin-top: 4px; }\n77\t</style>\n78\t\n79\t<div id=\"vv-setup\">\n80\t\n81\t <h1>⬡ Varaverk — First Run</h1>\n82\t <div class=\"vv-sub\">Set up this server before the plugin can start.</div>\n83\t\n84\t <!-- ── Step 1: Detection + identity ──────────────────────────────────────── -->\n85\t <div id=\"vv-step1\">\n86\t\n87\t <div id=\"vv-detect-banner\"><div class=\"loading\">Detecting environment…</div></div>\n88\t\n89\t <div class=\"vv-field\">\n90\t <label>This server's hostname</label>\n91\t <input type=\"text\" id=\"vv-hostname\" value=\"<?= htmlspecialchars($detectedHostname) ?>\" autocomplete=\"off\" spellcheck=\"false\">\n92\t <div class=\"vv-hint\">Must match Unraid Settings → Identification exactly (case-sensitive)</div>\n93\t </div>\n94\t\n95\t <hr class=\"vv-hr\">\n96\t <label style=\"display:block;font-size:11px;color:#888;text-transform:uppercase;letter-spacing:.06em;margin-bottom:10px;\">Server role</label>\n97\t <div class=\"vv-role-row\">\n98\t <div class=\"vv-role-btn active\" id=\"vv-role-primary\" onclick=\"vvSetRole('primary')\">\n99\t Primary<br><span style=\"color:#555;font-size:10px;\">HOST1 · first server</span>\n100\t </div>\n101\t <div class=\"vv-role-btn\" id=\"vv-role-partner\" onclick=\"vvSetRole('partner')\">\n102\t Partner<br><span style=\"color:#555;font-size:10px;\">HOST2+ · joining primary</span>\n103\t </div>\n104\t </div>\n105\t\n106\t <div class=\"vv-cond\" id=\"vv-cond-primary\">\n107\t <div class=\"vv-field\">\n108\t <label>Partner's hostname <span style=\"color:#444;font-size:10px;\">(optional — can fill in later)</span></label>\n109\t <input type=\"text\" id=\"vv-partner-hostname\" value=\"\" placeholder=\"unRAID-PartnerServer\" autocomplete=\"off\" spellcheck=\"false\">\n110\t </div>\n111\t </div>\n112\t\n113\t <div class=\"vv-cond\" id=\"vv-cond-partner\">\n114\t <div class=\"vv-field\">\n115\t <label>Primary server's hostname <span style=\"color:#a44;font-size:10px;\">required</span></label>\n116\t <input type=\"text\" id=\"vv-primary-hostname\" value=\"\" placeholder=\"unRAID-PrimaryServer\" autocomplete=\"off\" spellcheck=\"false\">\n117\t </div>\n118\t <div class=\"vv-field\">\n119\t <label>Your slot</label>\n120\t <select id=\"vv-partner-slot\">\n121\t <option value=\"host2\">HOST2</option>\n122\t <option value=\"host3\">HOST3</option>\n123\t <option value=\"host4\">HOST4</option>\n124\t </select>\n125\t </div>\n126\t <div style=\"font-size:11px;color:#555;margin-bottom:4px;\">\n127\t SSH key and master.conf pull are handled automatically after save.\n128\t </div>\n129\t </div>\n130\t\n131\t <button class=\"vv-btn\" id=\"vv-main-btn\" onclick=\"vvDoSave()\">Save and continue →</button>\n132\t <div id=\"vv-status\"></div>\n133\t </div>\n134\t\n135\t <!-- ── Step 2: Populate + guide + checklist ───────────────────────────────── -->\n136\t <div id=\"vv-step2\">\n137\t <hr class=\"vv-hr\">\n138\t <div style=\"font-size:10px;color:#555;text-transform:uppercase;letter-spacing:.06em;margin-bottom:14px;\">Step 2 of 2</div>\n139\t\n140\t <div id=\"vv-populate-status\" style=\"font-size:12px;color:#555;margin-bottom:14px;\">⟳ Running auto-populate…</div>\n141\t\n142\t <div class=\"vv-guide\">\n143\t <strong style=\"color:#888;\">Quick start</strong>\n144\t <ol>\n145\t <li>Create your Unraid API key below — needed for live monitor stats</li>\n146\t <li>Open <strong>Scheduler → Edit host.conf</strong> — only three things need manual entry:<br>\n147\t <span style=\"color:#444;\">\n148\t <code>EMBY_API_KEY</code> — Emby Dashboard → API Keys → + New Key<br>\n149\t <code>DISCORD_WEBHOOK</code> — for notifications (optional)<br>\n150\t <code>DAILY_SYNC_SHARES</code> — media paths to rsync nightly<br>\n151\t Everything else was auto-populated or has working defaults\n152\t </span></li>\n153\t <li>If partnering: the checklist below will guide you through pulling HOST1's config and running onboard</li>\n154\t </ol>\n155\t </div>\n156\t\n157\t <div style=\"display:flex;gap:10px;align-items:center;margin-bottom:14px;\">\n158\t <button id=\"vv-key-btn\" onclick=\"vvCreateKey(this)\" class=\"vv-btn\" style=\"flex:1;background:#1a3a1a;border-color:#2e6b2e;color:#6fcf97;\">\n159\t Create API Key\n160\t </button>\n161\t <a href=\"#\" onclick=\"vvGoScheduler(event)\" style=\"font-size:11px;color:#444;text-decoration:none;white-space:nowrap;\">Skip →</a>\n162\t </div>\n163\t <div id=\"vv-key-status\" style=\"font-size:12px;min-height:14px;margin-bottom:18px;\"></div>\n164\t\n165\t <hr class=\"vv-hr\">\n166\t <div class=\"vv-cl-title\">Setup checklist</div>\n167\t <div id=\"vv-checklist\"><div style=\"font-size:12px;color:#444;\">Loading…</div></div>\n168\t\n169\t <div style=\"margin-top:18px;text-align:right;\">\n170\t <a href=\"#\" onclick=\"vvGoScheduler(event)\" style=\"font-size:12px;color:#444;text-decoration:none;\">Go to Scheduler →</a>\n171\t </div>\n172\t </div>\n173\t\n174\t</div>\n175\t\n176\t<script>\n177\tlet _vvRedirect = '?tab=scheduler';\n178\t\n179\t// ── Detection banner ──────────────────────────────────────────────────────────\n180\t(function() {\n181\t fetch('/plugins/varaverk/api/setup.php?action=detect&_=' + Date.now())\n182\t .then(r => r.json()).then(d => {\n183\t const b = document.getElementById('vv-detect-banner');\n184\t if (!d.ok) { b.innerHTML = '<span style=\"color:#555\">Detection unavailable</span>'; return; }\n185\t const modeLabel = d.mode === 'internal'\n186\t ? '<span style=\"color:#4a8\">internal (NVMe/SSD)</span>'\n187\t : '<span style=\"color:#a84\">flash mode (USB boot)</span>';\n188\t b.innerHTML =\n189\t '<div class=\"vv-det-row\"><span class=\"vv-det-lbl\">OS</span><span class=\"vv-det-val\">Unraid ' + (d.unraid_ver||'') + '</span></div>' +\n190\t '<div class=\"vv-det-row\"><span class=\"vv-det-lbl\">Boot device</span><span class=\"vv-det-val\">' + d.boot_device + ' (' + d.transport + ')</span></div>' +\n191\t '<div class=\"vv-det-row\"><span class=\"vv-det-lbl\">Storage mode</span><span class=\"vv-det-val\">' + modeLabel + '</span></div>' +\n192\t '<div class=\"vv-det-row\"><span class=\"vv-det-lbl\">Scripts dir</span><span class=\"vv-det-val\" style=\"color:#666\">' + d.scripts_dir + '</span></div>';\n193\t const hf = document.getElementById('vv-hostname');\n194\t if (hf && !hf.value.trim()) hf.value = d.hostname;\n195\t }).catch(() => {\n196\t document.getElementById('vv-detect-banner').innerHTML = '<span style=\"color:#444\">Detection unavailable</span>';\n197\t });\n198\t})();\n199\t\n200\t// ── Role toggle ───────────────────────────────────────────────────────────────"}]},"uuid":"8ceef811-28b1-4dac-8942-e04b0fa45b3b","timestamp":"2026-06-06T14:29:48.979Z","sourceToolAssistantUUID":"18e20743-720b-4d78-890d-f1ad06f73c1e","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"8ceef811-28b1-4dac-8942-e04b0fa45b3b","isSidechain":true,"agentId":"accc3589951fac88e","message":{"model":"claude-haiku-4-5-20251001","id":"msg_013WqYakCLdrqbMjH2wFuuev","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01JX4DEiCjsVXFhPYGPDPjYY","name":"Read","input":{"file_path":"/boot/config/plugins/varaverk/Plugin/unraid/pages/watchdog.php","limit":200},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":69307,"cache_read_input_tokens":99742,"output_tokens":246,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":69307},"inference_geo":"not_available","iterations":[{"input_tokens":7,"output_tokens":246,"cache_read_input_tokens":99742,"cache_creation_input_tokens":69307,"cache_creation":{"ephemeral_5m_input_tokens":69307,"ephemeral_1h_input_tokens":0},"type":"message"}],"speed":"standard"},"diagnostics":null},"requestId":"req_011CbnAZnUZLhHRUr5fk72e8","attributionAgent":"Explore","type":"assistant","uuid":"c6910a2e-7b19-419a-afc0-79fbc437c72b","timestamp":"2026-06-06T14:29:49.217Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"c6910a2e-7b19-419a-afc0-79fbc437c72b","isSidechain":true,"promptId":"3c1ac879-b13a-4ebc-9aeb-ea73615e9af3","agentId":"accc3589951fac88e","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01JX4DEiCjsVXFhPYGPDPjYY","type":"tool_result","content":"1\t<style>\n2\t.vv-wd-card { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:10px;min-width:0; }\n3\t.vv-wd-sec { font-size:10px;font-weight:bold;color:#444;letter-spacing:.07em;text-transform:uppercase;margin-bottom:6px; }\n4\t.vv-wd-row { display:flex;justify-content:space-between;align-items:baseline;gap:6px;margin:2px 0; }\n5\t.vv-wd-lbl { font-size:11px;color:#444;white-space:nowrap; }\n6\t.vv-wd-val { font-size:12px;color:#bbb;text-align:right; }\n7\t.vv-wd-sep { border:none;border-top:1px solid #1e1e1e;margin:6px 0; }\n8\t.vv-wd-pill { font-size:10px;padding:1px 6px;border-radius:2px;background:#1e1e1e;color:#666;border:1px solid #272727; }\n9\t.vv-wd-pill.ok { background:#0d1f0d;color:#4caf50;border-color:#1a3a1a; }\n10\t.vv-wd-pill.warn { background:#1f1500;color:#ffb74d;border-color:#3a2800; }\n11\t.vv-wd-pill.err { background:#200d0d;color:#ef5350;border-color:#3a1a1a; }\n12\t.vv-wd-pill-row { display:flex;flex-wrap:wrap;gap:4px;margin-top:4px; }\n13\t.vv-wd-bar { height:5px;border-radius:2px;background:#1e1e1e;margin-top:3px;overflow:hidden; }\n14\t.vv-wd-bar-fill{ height:100%;border-radius:2px;transition:width .3s; }\n15\t.vv-wd-badge { font-size:11px;font-weight:bold;padding:2px 8px;border-radius:3px; }\n16\t.vv-wd-badge.ok { background:#0d1f0d;color:#4caf50; }\n17\t.vv-wd-badge.soft { background:#1f1f00;color:#cddc39; }\n18\t.vv-wd-badge.med { background:#1f1000;color:#ffb74d; }\n19\t.vv-wd-badge.hard { background:#200d0d;color:#ef5350; }\n20\t.vv-wd-node-h { display:flex;align-items:center;gap:8px;margin-bottom:10px; }\n21\t.vv-wd-node-id { font-size:12px;font-weight:bold;color:#666;letter-spacing:.06em;text-transform:uppercase; }\n22\t.vv-wd-dot { width:6px;height:6px;border-radius:50%;flex-shrink:0; }\n23\t.vv-wd-strike-row { display:flex;align-items:baseline;gap:6px;margin:2px 0; }\n24\t.vv-wd-strike-name{ font-size:11px;color:#777;flex:1; }\n25\t.vv-wd-strike-cnt { font-size:11px;font-weight:bold;color:#ffb74d; }\n26\t.vv-wd-reboot-ts { font-size:11px;color:#555;margin:1px 0; }\n27\t.vv-wd-ctr-row { display:flex;justify-content:space-between;align-items:baseline;margin:2px 0; }\n28\t.vv-wd-ctr-name{ font-size:11px;color:#888; }\n29\t.vv-wd-ctr-lim { font-size:11px;color:#555; }\n30\t.vv-wd-pressure{ grid-column:1/-1;border-color:#3a2000;background:#1a1000; }\n31\t\n32\t/* One host per row — inner grid sizes all cards equally */\n33\t.vv-wd-host-row {\n34\t grid-column: 1 / -1;\n35\t display: grid;\n36\t grid-template-columns: repeat(5, 1fr);\n37\t gap: 12px;\n38\t}\n39\t@media (max-width: 900px) {\n40\t .vv-wd-host-row { grid-template-columns: 1fr; }\n41\t}\n42\t</style>\n43\t\n44\t<div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding:0 2px;\">\n45\t <span style=\"font-size:13px;font-weight:bold;color:#888;text-transform:uppercase;letter-spacing:.06em;\">Watchdog</span>\n46\t <span style=\"font-size:11px;color:#3a3a3a;\" id=\"vv-wd-ts\"></span>\n47\t</div>\n48\t\n49\t<div id=\"vv-wd-grid\" style=\"display:grid;grid-template-columns:repeat(8,1fr);gap:12px;\">\n50\t <div style=\"grid-column:1/-1;color:#444;font-size:12px;padding:16px 0;text-align:center;\">Loading…</div>\n51\t</div>\n52\t\n53\t<script>\n54\t(function() {\n55\t\n56\tconst GB = 1073741824;\n57\t\n58\tfunction _relTime(ts) {\n59\t if (!ts) return '—';\n60\t const d = Math.floor(Date.now() / 1000) - ts;\n61\t if (d < 60) return 'just now';\n62\t if (d < 3600) return Math.floor(d / 60) + 'm ago';\n63\t if (d < 86400)return Math.floor(d / 3600) + 'h ' + Math.floor((d % 3600) / 60) + 'm ago';\n64\t return Math.floor(d / 86400) + 'd ago';\n65\t}\n66\t\n67\tfunction _fmtBytes(b) {\n68\t if (b >= GB) return (b / GB).toFixed(1) + ' GB';\n69\t if (b >= 1048576) return (b / 1048576).toFixed(0) + ' MB';\n70\t return (b / 1024).toFixed(0) + ' KB';\n71\t}\n72\t\n73\tfunction _dur(s) {\n74\t const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60);\n75\t if (d) return d + 'd ' + h + 'h';\n76\t if (h) return h + 'h ' + m + 'm';\n77\t return m + 'm';\n78\t}\n79\t\n80\tfunction _row(lbl, val) {\n81\t return `<div class=\"vv-wd-row\"><span class=\"vv-wd-lbl\">${lbl}</span><span class=\"vv-wd-val\">${val}</span></div>`;\n82\t}\n83\t\n84\tfunction _bar(pct, col) {\n85\t return `<div class=\"vv-wd-bar\"><div class=\"vv-wd-bar-fill\" style=\"width:${Math.min(pct,100)}%;background:${col}\"></div></div>`;\n86\t}\n87\t\n88\tfunction _pill(label, cls) {\n89\t return `<span class=\"vv-wd-pill ${cls}\">${label}</span>`;\n90\t}\n91\t\n92\tfunction _levelLabel(level) {\n93\t return ['OK', 'SOFT', 'MEDIUM', 'HARD'][level] || '?';\n94\t}\n95\tfunction _levelCls(level) {\n96\t return ['ok', 'soft', 'med', 'hard'][level] || 'ok';\n97\t}\n98\t\n99\t// ── Pressure alert card ───────────────────────────────────────────────────────\n100\tfunction _pressureCard(node) {\n101\t const st = node.states;\n102\t if (!st || st.rw_level === 0) return '';\n103\t const level = st.rw_level;\n104\t const cls = _levelCls(level);\n105\t const label = _levelLabel(level);\n106\t\n107\t const paused = (st.rw_paused || []).filter(Boolean);\n108\t const stopped = (st.rw_stopped || []).filter(Boolean);\n109\t const pausedHtml = paused.length ? paused.map(c => _pill(c, 'warn')).join('') : '';\n110\t const stoppedHtml = stopped.length ? stopped.map(c => _pill(c, 'err')).join('') : '';\n111\t\n112\t return `<div class=\"vv-wd-card vv-wd-pressure\" style=\"grid-column:1/-1\">\n113\t <div style=\"display:flex;align-items:center;gap:10px;margin-bottom:8px;\">\n114\t <span class=\"vv-wd-badge ${cls}\">PRESSURE ${label}</span>\n115\t <span style=\"font-size:11px;color:#7a5020;\">${node.id} (${node.hostname})</span>\n116\t ${st.mem_shutdown ? `<span class=\"vv-wd-badge hard\" style=\"margin-left:auto;\">MEM SHUTDOWN ACTIVE</span>` : ''}\n117\t </div>\n118\t ${paused.length ? `<div style=\"margin-bottom:4px;\"><span class=\"vv-wd-lbl\">Paused:</span> <span class=\"vv-wd-pill-row\" style=\"display:inline-flex;\">${pausedHtml}</span></div>` : ''}\n119\t ${stopped.length ? `<div><span class=\"vv-wd-lbl\">Stopped:</span> <span class=\"vv-wd-pill-row\" style=\"display:inline-flex;\">${stoppedHtml}</span></div>` : ''}\n120\t </div>`;\n121\t}\n122\t\n123\t// ── System health card ────────────────────────────────────────────────────────\n124\tfunction _systemCard(node, cfg) {\n125\t const sys = node.system;\n126\t if (!sys) {\n127\t return `<div class=\"vv-wd-card\">\n128\t <div class=\"vv-wd-node-h\">\n129\t <span class=\"vv-wd-dot\" style=\"background:#444\"></span>\n130\t <span class=\"vv-wd-node-id\">${node.id}</span>\n131\t <span style=\"font-size:11px;color:#3a3a3a;\">${node.hostname}</span>\n132\t <span class=\"vv-wd-badge\" style=\"margin-left:auto;background:#1a1a1a;color:#444\">UNREACHABLE</span>\n133\t </div>\n134\t </div>`;\n135\t }\n136\t\n137\t const memGb = sys.mem_avail / GB;\n138\t const memTotGb = sys.mem_total / GB;\n139\t const usedPct = memTotGb > 0 ? ((memTotGb - memGb) / memTotGb) * 100 : 0;\n140\t const memCol = memGb < cfg.sys_mem_gb ? '#ef5350'\n141\t : memGb < cfg.rw_hard_gb ? '#ef5350'\n142\t : memGb < cfg.rw_medium_gb ? '#ffb74d'\n143\t : memGb < cfg.rw_soft_gb ? '#cddc39' : '#4caf50';\n144\t\n145\t const loadPct = sys.cores > 0 ? (sys.load1 / (sys.cores * cfg.rw_load_med)) * 100 : 0;\n146\t const loadCol = sys.load1 > sys.cores * cfg.rw_load_med ? '#ef5350'\n147\t : sys.load1 > sys.cores * cfg.rw_load_soft ? '#ffb74d'\n148\t : '#4caf50';\n149\t\n150\t const apiOnly = sys.api_only === true;\n151\t const daemonDot = sys.daemon_ok === null ? '#555' : sys.daemon_ok ? '#4caf50' : '#ef5350';\n152\t const daemonTxt = sys.daemon_ok === null ? '—' : sys.daemon_ok ? 'daemon ok' : 'daemon err';\n153\t\n154\t const st = node.states || {};\n155\t const level = st.rw_level || 0;\n156\t const dotCol = level >= 3 ? '#ef5350' : level >= 2 ? '#ffb74d' : level >= 1 ? '#cddc39'\n157\t : apiOnly ? '#4a7a9b' // blue-grey: API-only, no watchdog state\n158\t : '#4caf50';\n159\t\n160\t return `<div class=\"vv-wd-card\">\n161\t <div class=\"vv-wd-node-h\">\n162\t <span class=\"vv-wd-dot\" style=\"background:${dotCol}\"></span>\n163\t <span class=\"vv-wd-node-id\">${node.id}</span>\n164\t <span style=\"font-size:11px;color:#3a3a3a;\">${node.hostname}</span>\n165\t <span class=\"vv-wd-badge ${apiOnly ? '' : _levelCls(level)}\"\n166\t style=\"margin-left:auto;${apiOnly ? 'background:#0d1f2a;color:#4a9eff;' : ''}\"\n167\t >${apiOnly ? 'API ONLY' : _levelLabel(level)}</span>\n168\t </div>\n169\t <div class=\"vv-wd-sec\">System</div>\n170\t ${_row('RAM free', `<span style=\"color:${memCol}\">${_fmtBytes(sys.mem_avail)}</span> / ${_fmtBytes(sys.mem_total)}`)}\n171\t ${_bar(usedPct, memCol)}\n172\t <div style=\"display:flex;justify-content:space-between;margin-top:1px;font-size:10px;color:#333\">\n173\t <span>free</span>\n174\t <span>${cfg.rw_soft_gb}G soft · ${cfg.rw_hard_gb}G hard · ${cfg.sys_mem_gb}G reboot</span>\n175\t </div>\n176\t <div style=\"height:5px\"></div>\n177\t ${!apiOnly ? `${_row('Load avg', `<span style=\"color:${loadCol}\">${sys.load1.toFixed(2)}</span> / ${sys.cores} cores`)}\n178\t ${_bar(loadPct, loadCol)}\n179\t <div style=\"height:5px\"></div>` : ''}\n180\t ${_row('Uptime', sys.uptime ? _dur(sys.uptime) : '—')}\n181\t ${_row('Docker', `<span style=\"color:${daemonDot}\">${daemonTxt}</span>`)}\n182\t ${!apiOnly ? (sys.oom_count > 0 ? _row('OOM kills', `<span style=\"color:#ef5350\">${sys.oom_count}</span>`) : _row('OOM kills', '<span style=\"color:#333\">0</span>')) : ''}\n183\t ${apiOnly ? `<div style=\"font-size:10px;color:#333;margin-top:6px;\">Watchdog state unavailable — SSH not configured</div>` : ''}\n184\t </div>`;\n185\t}\n186\t\n187\t// ── Docker watchdog state card ────────────────────────────────────────────────\n188\tfunction _dockerCard(node, cfg) {\n189\t const st = node.states;\n190\t if (!st) return `<div class=\"vv-wd-card\"><div class=\"vv-wd-sec\">Docker Watchdog</div><div style=\"color:#3a3a3a;font-size:11px;padding:8px 0;\">No data</div></div>`;\n191\t\n192\t const strikes = Object.entries(st.ctr_strikes || {});\n193\t const skiplist = st.skiplist || [];\n194\t const restarts = (st.restarts || []).slice(0, 10);\n195\t const daemonCls = st.daemon_strikes > 0 ? 'err' : 'ok';\n196\t const allOk = strikes.length === 0 && skiplist.length === 0 && st.daemon_strikes === 0;\n197\t\n198\t // Group restarts by container for last-24h summary\n199\t const rCounts = {};\n200\t for (const r of restarts) {"}]},"uuid":"75be0489-ac78-4eac-8984-bfebdd47312a","timestamp":"2026-06-06T14:29:49.237Z","sourceToolAssistantUUID":"c6910a2e-7b19-419a-afc0-79fbc437c72b","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|
||
{"parentUuid":"75be0489-ac78-4eac-8984-bfebdd47312a","isSidechain":true,"agentId":"accc3589951fac88e","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01Y1rk8Nb3TqoWtsmgZdCVYN","type":"message","role":"assistant","content":[{"type":"text","text":"Now I have examined all 11 PHP pages and the CSS file. Let me compile a comprehensive audit report with all findings.\n\n## COMPREHENSIVE AUDIT: Varaverk Unraid Plugin Pages\n\n### FILE INVENTORY\n**11 PHP Pages Reviewed:**\n1. arrs.php\n2. auth.php\n3. docker.php\n4. fallback.php\n5. monitor.php (partial - very large file)\n6. partnership.php\n7. rsync.php\n8. scheduler.php\n9. settings.php\n10. setup.php\n11. watchdog.php\n\n**CSS File:** varaverk.css (947 lines, well-organized shared stylesheet)\n\n---\n\n## 1. VISUAL INCONSISTENCIES\n\n### Tab/Button Styling Divergence\n\n**ISSUE: arrs.php uses non-standard toggle styling**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/arrs.php` (lines 71-76)\n- Uses custom `.vv-arr-tog-track` (28px width, 16px height) with inline `onclick` handlers\n- **REFERENCE CORRECT VERSION:** docker.php and watchdog.php use `.vv-toggle` from varaverk.css (36px width, 20px height)\n- CSS defines standardized toggle at varaverk.css:733-740\n- **FIX:** Replace arrs.php toggle HTML/CSS with standard `.vv-toggle` pattern from other pages\n\n**ISSUE: auth.php uses distinct modal system**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/auth.php` (lines 88-91)\n- Modal overlay at lines 229-231 with `.vv-au-overlay` and `.vv-au-modal`\n- Other pages (partnership, rsync) use `.vv-card` pattern in modal-like containers\n- **NOTE:** This is acceptable if modals are auth-specific, but overlay color (#0009) differs from varaverk.css patterns\n- **REFERENCE:** varaverk.css doesn't define a global modal system—each page rolls its own\n\n**ISSUE: Buttons lack unified color coding**\n- arrs.php: Uses inline `style` attribute for button colors (lines 262-276, 381-382)\n- docker.php: Uses `.vv-dk-btn`, `.vv-dk-btn.active`, `.vv-dk-btn.warn`, `.vv-dk-btn.prim` (CSS lines 2-9)\n- scheduler.php: Uses `.vv-btn-sm`, `.vv-run-btn`, `.vv-dry-btn`, `.vv-log-btn`, `.vv-edit-btn` (varaverk.css:121-312)\n- partnership.php: Uses `.vv-pt-action-btn.run`, `.vv-pt-action-btn.warn`, `.vv-pt-action-btn.info` (lines 20-24)\n- **FIX:** Consolidate button classes in varaverk.css; many pages duplicate button styling inline\n\n### Spacing & Layout\n\n**ISSUE: Card padding inconsistency**\n- arrs.php: `.vv-arr-card { padding: 12px 14px; }` (line 3)\n- monitor.php: `.vv-card { padding: 12px; }` (varaverk.css:29)\n- auth.php: `.vv-au-card { border-radius: 6px; overflow: hidden; }` — no explicit padding (line 41)\n- watchdog.php: `.vv-wd-card { padding: 10px; }` (line 2)\n- **INCONSISTENCY:** 10px, 12px, and 12px 14px (horizontal 14px), no unified standard\n- **REFERENCE CORRECT:** Use 12px across all pages (varaverk.css standard)\n\n**ISSUE: Icon/badge sizing varies widely**\n- arrs.php: State dot `.vv-arr-dot { width: 7px; height: 7px; }` (line 9)\n- watchdog.php: `.vv-wd-dot { width: 6px; height: 6px; }` (line 22)\n- partnership.php: `.vv-pt-dot { width: 8px; height: 8px; }` (line 18)\n- docker.php: No explicit dot class, uses inline styles\n- **FIX:** Define single `.vv-status-dot` or `.vv-dot` class in varaverk.css (likely 7px—most common)\n\n### Header/Title Typography\n\n**ISSUE: h3 styling scattered across inline styles vs CSS**\n- varaverk.css (31-34): `.vv-card h3` defines standard — uppercase, #888 color, 13px font-size, letter-spacing\n- auth.php: `.vv-au-modal h3` at line 92 duplicates these rules inline (uppercase, 12px, #888)\n- docker.php: No h3 class; uses `<span class=\"vv-dk-title\">` (line 3) instead\n- **ISSUE:** Some pages bypass `.vv-card h3` by using non-h3 elements or redefining\n- **FIX:** Enforce `.vv-card h3` usage; audit all pages for h3 and replace with consistent pattern\n\n### Color Token Inconsistencies\n\n**ISSUE: Same semantic colors have different hex values**\n- Accent green OK: `#4caf50` (varaverk.css), `#6fcf97` (arrs.php line 212, docker.php line 35), `#6aaa6a` (arrs.php line 945)\n- Error red: `#f44336` (varaverk.css), `#ef5350` (arrs.php line 206), `#ef9a9a` (auth.php line 354)\n- Warning orange: `#ff9800` (varaverk.css), `#ffb74d` (auth.php line 354), `#aa7020` (partnership.php line 123)\n- **IMPACT:** Severely fragmented color palette. Primary colors should be CSS variables or constants\n- **FIX:** Create CSS color variables (--color-success, --color-error, --color-warn) and use throughout\n\n---\n\n## 2. UX GAPS\n\n### Missing Loading States\n\n**ISSUE: arrs.php has no loading indicator while fetching**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/arrs.php` (lines 411-417)\n- `vvArrsLoad()` makes fetch call but initial grid shows \"Loading…\" forever if API fails silently\n- **REFERENCE CORRECT:** monitor.php shows \"Loading…\" in grid (line 11, line 15) during initial page load\n- **FIX:** Add error state: if fetch fails, show \"Error loading arrs — check API\" in grid with retry button\n\n**ISSUE: auth.php modals have no busy states on buttons**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/auth.php` (lines 456-461)\n- When user clicks \"Save\" button in proxy modal, button text doesn't change to \"Saving…\"\n- User can click multiple times, spawning duplicate requests\n- **REFERENCE CORRECT:** scheduler.php shows loading: buttons get `disabled=true` and text changes (scheduler lines ~1600+)\n- **FIX:** Add `btn.disabled = true; btn.textContent = 'Saving…'` before fetch; restore on completion\n\n**ISSUE: docker.php folder rename has no in-flight feedback**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/docker.php` (lines 289-295)\n- Input blur handler makes fetch but input field stays active with old text until response\n- User can't tell if rename is pending\n- **FIX:** Add grey text color or opacity 0.6 while fetch is in flight\n\n### Missing Empty States\n\n**ISSUE: rsync.php bandwidth history shows \"No data\" but not for all sections**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/rsync.php` (lines 671-675)\n- `_bwSection()` returns empty card if no history, but window cards (critical/daily/weekly) may be empty with no message\n- **FIX:** Add empty state message to each window card: \"No runs in history\" or \"Not configured\"\n\n**ISSUE: scheduler.php info panels have placeholders but no real empty states**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/scheduler.php` (lines 479-551)\n- \"Next Runs\", \"Recent Activity\", \"Recent Errors\" all show \"loading…\" but no fallback for \"No data\"\n- If no errors exist, should show \"All clear\" with icon\n- **FIX:** Add real empty state HTML: `<div class=\"vv-board-placeholder\">All clear ✓</div>`\n\n### Missing Confirmation on Destructive Actions\n\n**ISSUE: auth.php user/group deletion uses confirm() but inconsistently**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/auth.php` (line 496, 656, 745, 746)\n- Uses browser `confirm()` dialog—works but looks jarring in modern UI\n- **REFERENCE:** partnership.php (lines 191, 201, 231) uses same pattern\n- **UX PROBLEM:** No inline confirmation modal that matches the design language\n- **FIX:** Create a reusable modal confirm UI (CSS-only, no browser dialog) matching `.vv-au-overlay` pattern\n\n**ISSUE: docker.php folder delete only uses single confirm()**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/docker.php` (line 304)\n- `confirm('Delete \"${f.name}\"?')` — but consequence (containers ungrouped) not explained\n- User might think containers are deleted, not just ungrouped\n- **FIX:** Change message: `'Delete folder \"${f.name}\"?\\n\\nContainers will be moved to Ungrouped.'`\n\n**ISSUE: scheduler.php delete script button (#vv-delete-script-btn) has no confirmation**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/scheduler.php` (line 370)\n- Button is hidden by default, but when shown, onclick=\"vvDeleteScript()\" doesn't show confirm dialog\n- Destructive action (deletes custom script file) with no \"are you sure?\"\n- **FIX:** Add confirm dialog in `vvDeleteScript()` JavaScript function\n\n### Missing Error Feedback\n\n**ISSUE: fallback.php has no error state for API failure**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/fallback.php` (lines 232-237)\n- `vvFbLoad()` fetch `.catch(() => {})` silently ignores errors\n- If API unreachable, grid shows \"Loading…\" forever—no error message\n- **FIX:** Show error card: `<div>Error loading fallback data — check API</div>` on fetch failure\n\n**ISSUE: partnership.php settings load has no error UI**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/partnership.php` (lines 494-512)\n- `vvPtLoadSettings()` shows error HTML but format is bare: `<div style=\"color:#f88;...\">Error: ...</div>`\n- Uses `#f88` (not standard error color) and no error icon\n- **FIX:** Use `.vv-au-err` class with proper error styling; add icon (⚠ or ✗)\n\n### Missing Status Indicators\n\n**ISSUE: rsync.php manual sync has no \"in progress\" visual**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/rsync.php` (lines 234-245)\n- Run button disables but output pre stays hidden until fetch completes\n- User doesn't know sync is running until output appears\n- **REFERENCE CORRECT:** scheduler.php shows dot spinner during run\n- **FIX:** Add pulsing indicator or \"Running…\" text that appears immediately on button click\n\n**ISSUE: docker.php \"Edit folders\" mode has no mode indicator**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/docker.php` (lines 362-367)\n- Button text toggles between \"Edit folders\" / \"Done editing\" (`.active` class)\n- But page title doesn't change—user might not notice edit mode is active\n- **FIX:** Add banner at top of grid: `<div class=\"vv-banner\">Drag containers between folders. Click Save when done.</div>`\n\n---\n\n## 3. INLINE STYLES VS CSS\n\n### Critical Finding: Massive CSS Duplication Across Pages\n\n**CSS in varaverk.css: 947 lines (primary stylesheet)**\n\n**Inline `<style>` blocks by page:**\n\n1. **arrs.php** (lines 1-85): 85 lines\n - Duplicates: `.vv-card` padding, `.vv-row` layout, card styling\n - NEW styles: `.vv-arr-hdr`, `.vv-arr-stats`, `.vv-arr-disk-*`\n - **SCORE:** 40% shared, 60% new. ACCEPTABLE—arrs has unique card structure\n\n2. **auth.php** (lines 1-120): 120 lines\n - Duplicates: button colors, modal overlay (basic), toggle switches\n - Unique: `.vv-au-badge` (many color variants), `.vv-au-tbl` (table styling)\n - **SCORE:** 30% shared, 70% new. **PROBLEM: Modal should be in varaverk.css as reusable component**\n\n3. **docker.php** (lines 1-74): 74 lines\n - Duplicates: button colors, card styling\n - Unique: `.vv-dk-folder` (folder card layout), `.vv-dk-ctr` (container row), popover menu\n - **SCORE:** 20% shared, 80% new. ACCEPTABLE—docker layout is unique\n\n4. **fallback.php** (lines 1-34): 34 lines\n - Duplicates: card styling, badge colors, state dots\n - Unique: `.vv-fb-active` (active state banner), `.vv-fb-tier-*` (tier display)\n - **SCORE:** 50% shared, 50% new. MIXED—could consolidate badge colors\n\n5. **partnership.php** (lines 1-57): 57 lines\n - Duplicates: card styling, action button colors, toggle switches\n - Unique: `.vv-pt-node` (node card), `.vv-pt-action-btn.run/warn/info` (action buttons)\n - **SCORE:** 30% shared, 70% new. **PROBLEM: Action button variants should be CSS base with modifiers**\n\n6. **rsync.php** (lines 1-117): 117 lines\n - Duplicates: card styling, button colors, toggle styling\n - Unique: `.vv-ry-toggle-*`, `.vv-ry-win-*` (window cards), profile history styles\n - **SCORE:** 20% shared, 80% new. ACCEPTABLE—rsync windows are unique layout\n\n7. **scheduler.php** (lines 0): **NO `<style>` tag—EXCELLENT**\n - Purely relies on varaverk.css\n - Uses `.vv-btn-sm`, `.vv-run-btn`, `.vv-dry-btn`, etc. from CSS\n - **SCORE:** 100% CSS file. **BEST PRACTICE**\n\n8. **settings.php** (lines 1-61): 61 lines\n - Duplicates: card styling, button colors, toggle switches\n - Unique: `.vv-set-tog-*` (toggle styling), badge variants\n - **SCORE:** 40% shared, 60% new. MIXED\n\n9. **setup.php** (lines 1-77): 77 lines\n - NO duplication of varaverk.css—this is first-run wizard, self-contained\n - Unique: entire layout is custom (setup card, role buttons, detection banner)\n - **SCORE:** 0% shared, 100% new. ACCEPTABLE—intentionally isolated UI\n\n10. **watchdog.php** (lines 1-42): 42 lines\n - Duplicates: card styling, badge colors, bar styling\n - Unique: `.vv-wd-*` pressure card, strike display\n - **SCORE:** 40% shared, 60% new. MIXED\n\n### CSS Organization in varaverk.css\n\n**STRENGTHS:**\n- Well-commented section headers (lines with `/* ── comment ── */`)\n- Logical grouping: tabs (6-9), cards (28-30), buttons (121-312), editor (338-464), scheduler (517-687)\n- Responsive design properly cascaded (media queries at end, 189-282)\n- Single-responsibility classes (`.vv-banner-ok`, `.vv-banner-warn`, not `.vv-banner-success`)\n\n**WEAKNESSES:**\n- Color values hardcoded everywhere; no CSS variables (e.g., `#4caf50` appears 50+ times)\n- No comment block for \"BUTTON STYLES\" section—section just starts at line 121\n- Button variants split: `.vv-btn-sm` (121-123) vs `.vv-run-btn` (292) vs page-specific `.vv-au-btn` (auth.php)\n- No documented component examples or usage guide\n\n### RECOMMENDATION\n\n**CONSOLIDATE TO 3 CATEGORIES:**\n\n1. **Base varaverk.css** (keep as-is): 947 lines, shared framework\n2. **Page-specific `<style>` blocks** (acceptable):\n - docker.php, rsync.php, arrs.php (unique layouts)\n - setup.php (isolated first-run flow)\n - Keep under 100 lines each; no duplication of button/card colors\n3. **MOVE TO varaverk.css** (extract):\n - All modal styling from auth.php (`.vv-au-overlay`, `.vv-au-modal`)—make reusable `.vv-modal-*`\n - All button color variants (consolidate `.vv-run-btn`, `.vv-pt-action-btn.run`, `.vv-dk-btn.prim`)\n - Toggle switch styling (currently 4 different toggle implementations)\n\n---\n\n## 4. BROKEN OR INCOMPLETE FLOWS\n\n### Unfinished Implementations\n\n**ISSUE: arrs.php \"Fetch now\" button appears incomplete**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/arrs.php` (lines 262-264, 274-276)\n- Shows \"↻ Fetch now\" button for remote nodes with no cache data\n- Button calls `vvArrsRefreshRemote()` (line 422) and reloads data\n- **PROBLEM:** No feedback while fetching—button doesn't disable or show \"Fetching…\"\n- **FIX:** Add `btn.disabled = true; btn.textContent = '↻ Fetching…'` in `vvArrsRefreshRemote()`\n\n**ISSUE: docker.php popover menu positioning is hardcoded**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/docker.php` (lines 330-332)\n- ```javascript\n pop.style.left = Math.min(x, vw-180)+'px';\n pop.style.top = Math.min(y+8, vh-180)+'px';\n ```\n- Assumes popover is 180px—if content grows, positioning breaks\n- **FIX:** Calculate actual popover dimensions or use CSS `position: fixed; max-width: 200px`\n\n**ISSUE: scheduler.php cron field validation shows red border but no error message**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/scheduler.php` (lines 98-100)\n- Cron input has `onblur=\"vvSaveCronBlur(this)\"` but JavaScript (not included in excerpt) doesn't show error tooltip\n- User sees red border but doesn't know what's wrong\n- **FIX:** Add title attribute or inline error message below field\n\n**ISSUE: partnership.php \"Phase 2\" buttons are disabled but visually unclear**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/partnership.php` (lines 1085-1086)\n- Re-run Phase 2 button has `style=\"opacity:.35;...\"` (dimmed)\n- But button is still clickable—just disabled via `onclick` logic\n- User might think button is broken, not just \"already done\"\n- **FIX:** Add `disabled` attribute or replace opacity with grey color scheme\n\n### Incomplete State Management\n\n**ISSUE: rsync.php profile editor loses form state on cancel**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/rsync.php` (lines 1036-1039)\n- \"Cancel\" button reverts to view mode but doesn't reset unsaved form changes\n- User might lose work if they accidentally switch modes\n- **NOTE:** This is acceptable if user intends to lose changes; confirm on cancel\n\n**ISSUE: auth.php user/group modal doesn't validate input before save**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/auth.php` (lines 556-576)\n- Modal has fields for username, display name, email\n- Email field is required but form doesn't check `.validity` or run HTML5 validation\n- User can submit blank email, gets server error with no clear feedback\n- **FIX:** Add `required` attribute to inputs; validate on save button click with visual feedback\n\n### Placeholder Text\n\n**ISSUE: setup.php first-run shows \"Loading…\" in detection banner**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/setup.php` (line 87)\n- Fetches `/plugins/varaverk/api/setup.php?action=detect` but if API is slow, user sees \"Detecting environment…\" for too long\n- No timeout—if API never responds, banner never updates\n- **FIX:** Add 5s timeout; show \"Detection unavailable\" if timeout\n\n**ISSUE: scheduler.php suggestions show \"calculating…\" placeholder**\n- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/scheduler.php` (line 480)\n- Next Runs section shows `<div class=\"vv-board-placeholder\">calculating…</div>`\n- If JavaScript doesn't run or fails, placeholder stays visible\n- **FIX:** Replace with real empty state after initial load, not placeholder\n\n---\n\n## 5. CROSS-PAGE DUPLICATION\n\n### Duplicated JavaScript Functions\n\n**Toggle switch onclick pattern (repeated 5 times):**\n- arrs.php line 345: `vvArrToggle(this, 'KEY', file)`\n- auth.php line 427-429: similar toggle logic (inline in modal builder)\n- docker.php line 265: `onclick=\"event.stopPropagation();\"`\n- partnership.php line 298-316: `vvPtToggleSync(el, varName, enabled)`\n- settings.php line N/A: uses similar pattern\n\n**CONSOLIDATION OPPORTUNITY:** Create single `vvToggle(element, key, file, onSuccess)` in shared JS file\n\n**Color formatting functions (duplicated):**\n- arrs.php lines 100-117: `_rel()`, `_n()`, `_gb()`, `_tb()`, `_sz()` (size formatters)\n- docker.php lines 116-119: `_shortImage()` (regex strip)\n- fallback.php lines 48-62: `_dur()`, `_fmtDelay()` (duration formatters)\n- partnership.php lines 596-612: `_relTime()`, `_uptime()` (time formatters)\n- rsync.php lines 554-560: `_fmtBytes()`, `_dur()` (duplicate implementations)\n- watchdog.php lines 67-78: `_relTime()`, `_fmtBytes()`, `_dur()` (DUPLICATE again)\n\n**CONSOLIDATION OPPORTUNITY:** Create `varaverk-utils.js` with shared formatters:\n```javascript\n// Shared utilities\nfunction vvFormatRelTime(ts) { ... }\nfunction vvFormatBytes(b) { ... }\nfunction vvFormatDuration(s) { ... }\nfunction vvFormatSize(b) { ... }\n```\n\n**Loading/error state management (duplicated with variations):**\n- arrs.php line 392-396: Shows \"No arrs configured\" message\n- docker.php line 245: Shows \"No containers found\"\n- fallback.php line 219: Shows \"No nodes configured\"\n- monitor.php: Shows \"Loading…\" in grid\n- partnership.php line 226: Shows \"No hosts found in master.conf\"\n\n**Pattern:** Each page reinvents \"empty state\" logic\n**CONSOLIDATION:** Create reusable `vvShowEmptyState(container, message, icon)` function\n\n### Duplicated HTML/CSS Patterns\n\n**Modal overlay pattern (exact duplication):**\n- auth.php lines 229-231:\n```html\n<div class=\"vv-au-overlay\" id=\"vv-au-overlay\">\n <div class=\"vv-au-modal\" id=\"vv-au-modal\"></div>\n</div>\n```\n- Similar pattern in other modals but not fully standardized\n\n**Button styling classes (MANY variants, inconsistent naming):**\n- `.vv-btn-sm` (varaverk.css, scheduler)\n- `.vv-run-btn` (varaverk.css, scheduler)\n- `.vv-au-btn` (auth.php, local style)\n- `.vv-au-btn.prim` (auth.php)\n- `.vv-pt-action-btn` (partnership.php, local style)\n- `.vv-pt-action-btn.run` (partnership.php)\n- `.vv-dk-btn` (docker.php, local style)\n- `.vv-dk-btn.prim` (docker.php)\n- `.vv-ry-toggle` (rsync.php, local style)\n\n**CONSOLIDATION:** Create unified button system in varaverk.css:\n```css\n.vv-btn { padding: 5px 14px; border-radius: 4px; cursor: pointer; }\n.vv-btn.primary { background: #4caf50; color: #fff; }\n.vv-btn.warn { background: #ff9800; color: #000; }\n.vv-btn.danger { background: #f44336; color: #fff; }\n```\n\n**Table styling (duplicated):**\n- auth.php lines 20-26: `.vv-au-tbl`, `.vv-au-tbl th`, `.vv-au-tbl td`\n- varaverk.css lines 81-86: `#vv-docker-table`, `#vv-docker-table th`, `#vv-docker-table td`\n\n**CONSOLIDATION:** Create `.vv-table` base class in varaverk.css; pages can extend with specific colors/widths\n\n### Code Duplication by Page\n\n| Page | Duplicated Functions | Duplicated CSS | Notes |\n|------|---------------------|-----------------|-------|\n| arrs.php | _rel, _n, _sz, _gb (5 formatters) | .vv-arr-* (good—unique) | Formatters should be shared |\n| auth.php | _esc (reused in modals), modals from scratch | Button variants, modal overlay | Modal should be component |\n| docker.php | _shortImage, _esc, _hidePopover | .vv-dk-* (good—unique) | Popover could be component |\n| fallback.php | _dur, _fmtDelay, _activeTier | Limited duplication | Formatters should be shared |\n| monitor.php | vvMeter, vvDrawChart, vvFmtBps, vvDiskRow | Heavy inline styles | Massive inline CSS (custom charts) |\n| partnership.php | _relTime, _uptime, _row (3 helpers) | Button variants | Time formatters should be shared |\n| rsync.php | _rel, _dur, _fmtBytes (DUPLICATE fallback.php) | Heavy inline styles | Copy-paste from fallback |\n| scheduler.php | None detected (relies on CSS) | None | BEST PRACTICE |\n| settings.php | No major duplication | Button styles | Good isolation |\n| setup.php | No major duplication | Self-contained UI | Good isolation |\n| watchdog.php | _relTime, _fmtBytes, _dur (DUPLICATES fallback + rsync) | Limited duplication | Copy-paste chain |\n\n**EXACT DUPLICATES FOUND:**\n1. `_dur()` function: fallback.php (48-54), rsync.php (546-551), watchdog.php (73-78)\n2. `_relTime()` function: partnership.php (596-604), watchdog.php (58-65)\n3. `_fmtBytes()` function: rsync.php (554-560), watchdog.php (67-71)\n\n---\n\n## 6. COMPREHENSIVE INVENTORY BY PAGE\n\n### **arrs.php** (470 lines)\n**Layout:** Grid of cards per node, sync/recovery cards, settings card\n**Styling:**\n- Inline `<style>` block: 85 lines (lines 1-85)\n- Uses `.vv-card` from varaverk.css for outer wrapper\n- Inline styles in generated HTML: e.g., line 170 `style=\"color:#555;\"`, line 237 `dotCol`\n**Issues Found:**\n- Toggle switch uses custom `.vv-arr-tog-track` (not `.vv-toggle` from varaverk.css)\n- Formatter functions `_rel`, `_n`, `_gb`, `_tb`, `_sz` should be shared\n- No error state if API fails\n- \"Fetch now\" button has no loading feedback\n**UX Gaps:**\n- Offline node shows opacity 0.6 but no \"Offline\" badge\n- Recovery age input has no validation (accepts negative numbers)\n- No confirmation before saving recovery age\n\n---\n\n### **auth.php** (1,022 lines)\n**Layout:** 4 tabs (Proxies, Users & Groups, ACL, Certs)\n**Styling:**\n- Inline `<style>` block: 120 lines (lines 1-120)\n- Uses `.vv-au-*` classes extensively (button, modal, badge, toggle)\n- Modal overlay at lines 229-231\n**Issues Found:**\n- Modal system NOT in varaverk.css (should be reusable)\n- Button colors duplicated from varaverk.css: `.vv-au-btn.prim` mirrors `.vv-btn-sm.active`\n- No loading state on Save button in proxy modal (lines 456-461)\n- User/group deletion uses browser `confirm()` not styled modal\n- ACL rule editor has no inline validation\n**UX Gaps:**\n- Modals close immediately on success but don't show confirmation message\n- No \"copy to clipboard\" for API key value\n- Cert grid shows \"Loading…\" but no timeout or error fallback\n- Toggle state in modal (`_togHtml()`, line 275) doesn't show immediate feedback\n\n---\n\n### **docker.php** (390 lines)\n**Layout:** Grid with folder cards, ungrouped section, popover menu for moving containers\n**Styling:**\n- Inline `<style>` block: 74 lines (lines 1-74)\n- Uses `.vv-dk-*` classes (unique layout)\n- Popover at lines 68, lines 321-355\n**Issues Found:**\n- Popover positioning hardcoded to 180px (lines 330-332)—breaks if content wider\n- Folder rename input blur handler makes API call but no \"saving…\" feedback\n- Folder hover shows delete ×, but no visual confirmation before delete\n- \"Edit folders\" mode has no visual banner indicating user is in edit mode\n**UX Gaps:**\n- New folder creation via `prompt()` not styled modal\n- Container icon missing handler shows placeholder (line 131) but doesn't indicate why image failed\n- No empty state for \"Ungrouped\" section if all containers are foldered\n- Drift banner appears full-width but text doesn't wrap well on narrow screens\n\n---\n\n### **fallback.php** (243 lines)\n**Layout:** Active fallback card (if in failover), per-node tier cards\n**Styling:**\n- Inline `<style>` block: 34 lines (lines 1-34)\n- Uses `.vv-fb-*` classes (fallback-specific)\n- Leverages `.vv-card` and `.vv-banner` from varaverk.css\n**Issues Found:**\n- `_dur()` and `_fmtDelay()` functions should be shared\n- Tier pills lack hover feedback or explanation\n- No error state if API fails (fetch `.catch(() => {})` at line 236 is silent)\n**UX Gaps:**\n- Handback strikes display uses colored dots but no legend\n- \"Active tier\" shows numeric (1, 2, 3, 4) without labels\n- No indication what \"covering\" means for non-partnership users\n\n---\n\n### **monitor.php** (1,074+ lines, partial read)\n**Layout:** Grid of monitoring cards: System, UPS, CPU, Memory, Network, Scripts, Fallback, Partner, Containers, Rsync, GPU, Transcode, Streams, Watchdog, Parity, Storage, Array\n**Styling:**\n- NO inline `<style>` block at top (relies on varaverk.css)\n- Massive inline styles in generated HTML:\n - Line 203-210: `vvMeter()` generates inline `style` for meter bars\n - Line 424-428: Memory display has inline color hex in HTML\n - Line 929-960: UPS display inline `style` attributes throughout\n**Issues Found:**\n- `vvMeter()` function generates inline styles instead of using CSS class (lines 203-210)\n- `vvDrawChart()` uses canvas with inline colors passed as parameters\n- CPU core bars use inline hex colors (line 359)\n- No loading state for individual cards (all load simultaneously)\n- API status banner (lines 604-621) shows inline styles with hard-coded colors\n**UX Gaps:**\n- No \"retry\" button if individual card fails to load\n- API unreachable warning uses orange color but no icon\n- Partner metrics missing API key shows \"—\" but no explanation\n- Streams card has no empty state message\n\n---\n\n### **partnership.php** (1,268 lines, large file)\n**Layout:** Config bar, host settings cards, node grid (2-4 columns), sync health card, actions card, settings accordion\n**Styling:**\n- Inline `<style>` block: 57 lines (lines 1-57)\n- Uses `.vv-pt-*` classes (partnership-specific)\n- Modal structure similar to auth.php (no separate `<style>`)\n**Issues Found:**\n- Action button variants `.vv-pt-action-btn.run/.warn/.info` should extend unified `.vv-btn` in varaverk.css\n- Modal for editing uses styled div (lines 711-854) but pattern not standardized\n- Phase 2 button (line 1085) uses `style=\"opacity:.35\"` instead of `disabled` attribute\n- Settings section uses `<style>` block (lines 37-57) for settings editing (.vv-set-*) but should share\n**UX Gaps:**\n- Onboard progress badges (Phase 0/1/2) lack visual numbering or progress bar\n- \"Test\" ping button shows result in span (line 985) but text color varies (green/red)\n- Delete keys panel (lines 1094-1122) is deeply nested; unclear when user is in this mode\n- Settings panel accordion doesn't scroll open selected item into view\n\n---\n\n### **rsync.php** (1,133 lines, partial read)\n**Layout:** Manual sync card (two-column path browser + flags), profile activity chart, recent runs table, settings card, info/reference panels\n**Styling:**\n- Inline `<style>` block: 117 lines (lines 1-117)\n- Uses `.vv-ry-*` classes (rsync-specific) and `.vv-ms-*` (manual sync)\n- Heavy use of inline styles in generated charts\n**Issues Found:**\n- `_rel()`, `_dur()`, `_fmtBytes()` are EXACT duplicates from fallback.php and watchdog.php\n- Profile dropdown (line 1023) has no loading state when fetching profiles\n- Manual sync output pre (line 245) hidden until fetch completes—no \"running\" indicator\n- Browse button doesn't disable while loading (line 150)\n- Flag checkboxes have hover feedback (.vv-rp-flag) but some flags missing visual distinction\n**UX Gaps:**\n- BW limit input accepts 0 but no explanation what \"0\" means (unlimited?)\n- Profile history bars (lines 701-705) show relative time but no absolute units\n- No confirmation before running manual rsync (could take hours)\n- Recent runs table shows only last 20 rows but no \"load more\" or pagination\n\n---\n\n### **scheduler.php** (800+ lines, partial read)\n**Layout:** Left: orchestrator cards with child scripts, custom scripts, tools. Right: suggestions panel (next runs, calculator, activity, errors, etc.) and log viewer\n**Styling:**\n- **NO inline `<style>` block** ✓ (BEST PRACTICE)\n- Relies entirely on varaverk.css\n- Uses `.vv-btn-sm`, `.vv-run-btn`, `.vv-dry-btn`, `.vv-log-btn`, `.vv-edit-btn`\n**Issues Found:**\n- Cron input has red border on invalid but no error message (line 98-100)\n- Script label click (line 94) should navigate right panel but no indication user can click\n- \"Add Script\" button requires folder name (via `prompt()`) not modal dialog\n- Delete script button (line 370) has no confirmation dialog\n**UX Gaps:**\n- Cron calculator (lines 484-503) shows \"Apply to selected\" but not all cron fields support this\n- Recent errors section (lines 517-527) auto-expands but no scroll-into-view\n- Arrangement mode (Arrange button, line 367) has no visual banner showing mode is active\n- Log search (line 399) only works on displayed log not entire history\n\n---\n\n### **settings.php** (173 lines)\n**Layout:** Cards for storage location, notifications, API key management\n**Styling:**\n- Inline `<style>` block: 61 lines (lines 1-61)\n- Uses `.vv-set-*` classes (settings-specific)\n- Toggle switches (lines 49-54) use custom implementation\n**Issues Found:**\n- Storage migrate button disabled state unclear (opacity or true `disabled`?)\n- Discord webhook input has no validation for URL format\n- API key display shows preview only (first 8 + last 4) but no \"copy\" button\n**UX Gaps:**\n- Storage status shows badge but no tooltip explaining what each storage mode means\n- Notification toggles don't persist immediately (need to understand async behavior)\n- API check button shows output pre but max-height: 320px might truncate long output\n\n---\n\n### **setup.php** (200 lines)\n**Layout:** Two-step wizard (detection + identity, then populate + checklist)\n**Styling:**\n- Inline `<style>` block: 77 lines (lines 1-77)\n- Self-contained; no duplication of varaverk.css\n- Monospace font for wizard (intentional)\n**Issues Found:**\n- Detection banner (line 87) shows \"Detecting…\" with no timeout—if API slow, stays visible\n- Step 2 populate status (line 140) shows placeholder; if JS fails, stays visible\n- Role button styling (lines 30-33) uses inline styles not classes\n**UX Gaps:**\n- Partner hostname field (line 115) required on Partner role but not validated before save\n- Checklist links (line 161, 170) use `<a href=\"#\" onclick=\"...\">` not `<button>`\n- No progress indicator showing 1/2 until step 2 starts\n\n---\n\n### **watchdog.php** (200+ lines, partial read)\n**Layout:** Host rows, each with pressure alert, system card, docker card, stability card\n**Styling:**\n- Inline `<style>` block: 42 lines (lines 1-42)\n- Uses `.vv-wd-*` classes (watchdog-specific)\n- Heavy grid layout (lines 33-41) with media queries\n**Issues Found:**\n- `_relTime()`, `_fmtBytes()`, `_dur()` are EXACT duplicates from fallback and rsync\n- Strike row display (line 23-25) uses inline color but no semantic meaning\n- Pressure alert (lines 100-121) shows container names as pills but no status (running/paused/stopped)\n**UX Gaps:**\n- OOM kills count shows \"0\" in grey but no context (is this good?)\n- Reboot timestamp (line 152) shown but no indication when next reboot scheduled\n- Load avg shown as float (e.g., 1.23) but no context relative to core count visible in row\n\n---\n\n## SUMMARY TABLE\n\n| Finding Category | Severity | Count | Details |\n|------------------|----------|-------|---------|\n| **Visual Inconsistencies** | HIGH | 8 | Toggle sizes, button colors, spacing, icons |\n| **UX Gaps** | MEDIUM | 15 | Missing loading states, confirmations, empty states |\n| **Inline Styles** | MEDIUM | 7 pages | Heavy duplication; consolidate to CSS |\n| **Broken Flows** | LOW | 6 | Incomplete feedback, hard-coded positioning |\n| **Cross-Page Duplication** | HIGH | 20+ | Shared functions (`_dur`, `_relTime`, `_fmtBytes`) repeated 3+ times |\n\n### QUICK WINS (2–4 hour fixes)\n1. Create `varaverk-utils.js` with shared formatters\n2. Consolidate button color classes in varaverk.css\n3. Extract modal system from auth.php to reusable component\n4. Add loading states to all async operations\n5. Replace `confirm()` with styled modal\n\n### MEDIUM EFFORT (1 week refactor)\n1. Move auth modal styling to varaverk.css\n2. Unify toggle switch implementations (currently 5 variants)\n3. Create error state patterns for all pages\n4. Add empty-state UI components\n5. Consolidate color values to CSS variables"}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":7,"cache_creation_input_tokens":13340,"cache_read_input_tokens":169049,"cache_creation":{"ephemeral_5m_input_tokens":13340,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null},"requestId":"req_011CbnAa6M6WjWUta6Wk7rZB","attributionAgent":"Explore","type":"assistant","uuid":"98a95d55-d7ce-4dff-b505-20720ba087a0","timestamp":"2026-06-06T14:31:29.558Z","userType":"external","entrypoint":"cli","cwd":"/boot/config/plugins/varaverk","sessionId":"6cd2156e-fb44-400a-86c7-5844179fe511","version":"2.1.167","gitBranch":"HEAD"}
|