One thing to be aware of: /boot is still on USB flash (flash/boot), thats why it installs on appdata first, then during setup wizard it can be selected as normal or internal boot true,


- 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.
- 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".
- 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.
- 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.
- 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.
- 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.
- 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.
- schedule: Create, update, list, or run scheduled cloud agents (routines) that execute on a cron schedule. - When the user wants to schedule a recurring cloud 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").
- claude-api: Reference for the Claude API / Anthropic SDK — model ids, pricing, params, streaming, tool use, MCP, agents, caching, token counting, model migration.
TRIGGER — 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, Fable, 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).
SKIP 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).
- 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).
- init: Initialize a new CLAUDE.md file with codebase documentation
- review: Review a pull request
- security-review: Complete a security review of the pending changes on the current branch

update-config keybindings-help verify code-review simplify fewer-permission-prompts loop schedule claude-api run init review security-review


Configure boot partition USB installation process


Good to know. So the install flow is: appdata first → setup wizard → user chooses normal or `internal_boot=true` to move/pin to `/boot`.

I'll save that to memory since it affects how the plugin installation and persistence story fits together.


1	---
2	name: project_varaverk
3	description: Varaverk — self-healing mutually-redundant two-server Unraid home media ecosystem
4	metadata:
5	  node_type: memory
6	  type: project
7	  originSessionId: ffec43cd-13e3-4911-878f-40459f7d16a9
8	---
9
10	**Varaverk** is a complete self-healing, self-maintaining, mutually-redundant two-server home server ecosystem. One codebase runs on both servers. No primary/standby — both servers run independently and cover each other when one goes down.
11
12	## The Two Servers
13
14	**HOST1 — unRAID-Gmer4Lfe**
15	- Hardware: Threadripper 1950X, 128GB RAM, ZFS cache pools
16	- Location: Primary site
17	- Domain: Gmer4Lfe.com
18	- Runs: Arrs (Movies, TV, Music), Auth stack (source of truth), Emby (primary)
19
20	**HOST2 — unRAID-Jayred365**
21	- Hardware: Intel i5 10th gen, 64GB RAM
22	- Location: Remote — different building, different power utility
23	- Domain: Gmer4Lfe.us
24
25	## Architecture
26	- Platform adapter layer (Plugin/unraid/adapter.sh) isolates OS-specific calls — scripts never branch on OS
27	- Self-healing, not enterprise HA — goal is minimal media stack disruption
28	- Tailscale for mesh networking between hosts
29
30	## Session State — 2026-06-13
31
32	**What was done this session:**
33	- New Claude Code install after a reinstall. Old data was at /boot/config/claude and /boot/config/claude-bin.
34	- Memory files restored from old install into current install.
35	- claude_startup.sh run manually — created claude-data and claude-bin dirs under /boot/config/plugins/varaverk/, migrated all data, symlinks confirmed working.
36	- Verified Varaverk is fully running from /boot — nothing in appdata. varaverk.cfg SCRIPTS_DIR, DATA_DIR, STATE_DIR, all point to /boot/config/plugins/varaverk.
37	- No code changes made — session was setup/verification only.
38
39	**Stale note in .plg:** The ###2026.05.31 CHANGES entry says "Scripts are git-cloned to appdata on first install" — this is wrong, the actual code clones to /boot/config/plugins/varaverk. Worth fixing on next package build.
40
41	**Flash wear note:** /boot is on USB flash (flash/boot). HOST1_STORAGE_MODE_INTERNAL=true was designed for NVMe/SSD boot. Git writes, logs, and claude data all land on flash — acceptable for now but worth migrating boot to NVMe eventually.
42
43	## Active To-Dos (from Notes_To-Do.md)
44	- Fix fallback strike list timing: ~30s first, ~90s for 3-strike trigger — needs testing
45	- Verify silent toggle switches back on good notifications
46	- Rename folder Unraid_Scripts → Varaverk everywhere, update git script, all traces/scripts
47	- Delete old /boot/config/claude and /boot/config/claude-bin dirs (migrated, no longer needed)
48
49	## Future Design Ideas
50	- Shared auth stack for partner hosts to start shared services
51	- When owner offboards with 2+ servers: auto-promote strongest server (by compute + bandwidth)
52	- Overall setup script that pulls vars automatically (docker names, etc.)
53	- App layer as king: no more direct git — app opens/edits settings, partnership deploys to servers, pushes correct host.conf
54	- Web UI: on initial launch with no state file, open master.conf; lock orchs until setup complete
55	- First-launch guide: owner sets up master.conf → host1.conf → Tailscale shares → onboard → host2/3 install and see state file, default to mirror mode
56
57	**Why:** User is building this as a personal project on Unraid. Design philosophy favors simplicity and independence over enterprise tooling.
58	**How to apply:** Understand the two-server mesh model when suggesting architecture. The app layer / web UI direction is the current strategic focus — moving away from raw git/scripts toward a proper application.
59

---
name: project_varaverk
description: Varaverk — self-healing mutually-redundant two-server Unraid home media ecosystem
metadata:
  node_type: memory
  type: project
  originSessionId: ffec43cd-13e3-4911-878f-40459f7d16a9
---

**Varaverk** is a complete self-healing, self-maintaining, mutually-redundant two-server home server ecosystem. One codebase runs on both servers. No primary/standby — both servers run independently and cover each other when one goes down.

## The Two Servers

**HOST1 — unRAID-Gmer4Lfe**
- Hardware: Threadripper 1950X, 128GB RAM, ZFS cache pools
- Location: Primary site
- Domain: Gmer4Lfe.com
- Runs: Arrs (Movies, TV, Music), Auth stack (source of truth), Emby (primary)

**HOST2 — unRAID-Jayred365**
- Hardware: Intel i5 10th gen, 64GB RAM
- Location: Remote — different building, different power utility
- Domain: Gmer4Lfe.us

## Architecture
- Platform adapter layer (Plugin/unraid/adapter.sh) isolates OS-specific calls — scripts never branch on OS
- Self-healing, not enterprise HA — goal is minimal media stack disruption
- Tailscale for mesh networking between hosts

## Session State — 2026-06-13

**What was done this session:**
- New Claude Code install after a reinstall. Old data was at /boot/config/claude and /boot/config/claude-bin.
- Memory files restored from old install into current install.
- claude_startup.sh run manually — created claude-data and claude-bin dirs under /boot/config/plugins/varaverk/, migrated all data, symlinks confirmed working.
- Verified Varaverk is fully running from /boot — nothing in appdata. varaverk.cfg SCRIPTS_DIR, DATA_DIR, STATE_DIR, all point to /boot/config/plugins/varaverk.
- No code changes made — session was setup/verification only.

**Stale note in .plg:** The ###2026.05.31 CHANGES entry says "Scripts are git-cloned to appdata on first install" — this is wrong, the actual code clones to /boot/config/plugins/varaverk. Worth fixing on next package build.

**Flash wear note:** /boot is on USB flash (flash/boot). HOST1_STORAGE_MODE_INTERNAL=true was designed for NVMe/SSD boot. Git writes, logs, and claude data all land on flash — acceptable for now but worth migrating boot to NVMe eventually.

## Active To-Dos (from Notes_To-Do.md)
- Fix fallback strike list timing: ~30s first, ~90s for 3-strike trigger — needs testing
- Verify silent toggle switches back on good notifications
- Rename folder Unraid_Scripts → Varaverk everywhere, update git script, all traces/scripts
- Delete old /boot/config/claude and /boot/config/claude-bin dirs (migrated, no longer needed)

## Future Design Ideas
- Shared auth stack for partner hosts to start shared services
- When owner offboards with 2+ servers: auto-promote strongest server (by compute + bandwidth)
- Overall setup script that pulls vars automatically (docker names, etc.)
- App layer as king: no more direct git — app opens/edits settings, partnership deploys to servers, pushes correct host.conf
- Web UI: on initial launch with no state file, open master.conf; lock orchs until setup complete
- First-launch guide: owner sets up master.conf → host1.conf → Tailscale shares → onboard → host2/3 install and see state file, default to mirror mode

**Why:** User is building this as a personal project on Unraid. Design philosophy favors simplicity and independence over enterprise tooling.
**How to apply:** Understand the two-server mesh model when suggesting architecture. The app layer / web UI direction is the current strategic focus — moving away from raw git/scripts toward a proper application.


Edit

**Flash wear note:** /boot is on USB flash (flash/boot). HOST1_STORAGE_MODE_INTERNAL=true was designed for NVMe/SSD boot. Git writes, logs, and claude data all land on flash — acceptable for now but worth migrating boot to NVMe eventually.

**Flash wear note:** /boot is on USB flash (flash/boot). HOST1_STORAGE_MODE_INTERNAL=true was designed for NVMe/SSD boot. Git writes, logs, and claude data all land on flash — acceptable for now but worth migrating boot to NVMe eventually.

**Plugin install flow:** Plugin installs to appdata first, then during the setup wizard the user can select "normal" or set `internal_boot=true` to pin it to /boot. This is why the .plg note about appdata isn't wrong per se — it's the staging location before the wizard runs.


The file /root/.claude/projects/-root/memory/project_varaverk.md has been updated successfully. (file state is current in your context — no need to Read it back)

**Flash wear note:** /boot is on USB flash (flash/boot). HOST1_STORAGE_MODE_INTERNAL=true was designed for NVMe/SSD boot. Git writes, logs, and claude data all land on flash — acceptable for now but worth migrating boot to NVMe eventually.

**Plugin install flow:** Plugin installs to appdata first, then during the setup wizard the user can select "normal" or set `internal_boot=true` to pin it to /boot. This is why the .plg note about appdata isn't wrong per se — it's the staging location before the wizard runs.

---
name: project_varaverk
description: Varaverk — self-healing mutually-redundant two-server Unraid home media ecosystem
metadata:
  node_type: memory
  type: project
  originSessionId: ffec43cd-13e3-4911-878f-40459f7d16a9
---

**Varaverk** is a complete self-healing, self-maintaining, mutually-redundant two-server home server ecosystem. One codebase runs on both servers. No primary/standby — both servers run independently and cover each other when one goes down.

## The Two Servers

**HOST1 — unRAID-Gmer4Lfe**
- Hardware: Threadripper 1950X, 128GB RAM, ZFS cache pools
- Location: Primary site
- Domain: Gmer4Lfe.com
- Runs: Arrs (Movies, TV, Music), Auth stack (source of truth), Emby (primary)

**HOST2 — unRAID-Jayred365**
- Hardware: Intel i5 10th gen, 64GB RAM
- Location: Remote — different building, different power utility
- Domain: Gmer4Lfe.us

## Architecture
- Platform adapter layer (Plugin/unraid/adapter.sh) isolates OS-specific calls — scripts never branch on OS
- Self-healing, not enterprise HA — goal is minimal media stack disruption
- Tailscale for mesh networking between hosts

## Session State — 2026-06-13

**What was done this session:**
- New Claude Code install after a reinstall. Old data was at /boot/config/claude and /boot/config/claude-bin.
- Memory files restored from old install into current install.
- claude_startup.sh run manually — created claude-data and claude-bin dirs under /boot/config/plugins/varaverk/, migrated all data, symlinks confirmed working.
- Verified Varaverk is fully running from /boot — nothing in appdata. varaverk.cfg SCRIPTS_DIR, DATA_DIR, STATE_DIR, all point to /boot/config/plugins/varaverk.
- No code changes made — session was setup/verification only.

**Stale note in .plg:** The ###2026.05.31 CHANGES entry says "Scripts are git-cloned to appdata on first install" — this is wrong, the actual code clones to /boot/config/plugins/varaverk. Worth fixing on next package build.

**Flash wear note:** /boot is on USB flash (flash/boot). HOST1_STORAGE_MODE_INTERNAL=true was designed for NVMe/SSD boot. Git writes, logs, and claude data all land on flash — acceptable for now but worth migrating boot to NVMe eventually.

## Active To-Dos (from Notes_To-Do.md)
- Fix fallback strike list timing: ~30s first, ~90s for 3-strike trigger — needs testing
- Verify silent toggle switches back on good notifications
- Rename folder Unraid_Scripts → Varaverk everywhere, update git script, all traces/scripts
- Delete old /boot/config/claude and /boot/config/claude-bin dirs (migrated, no longer needed)

## Future Design Ideas
- Shared auth stack for partner hosts to start shared services
- When owner offboards with 2+ servers: auto-promote strongest server (by compute + bandwidth)
- Overall setup script that pulls vars automatically (docker names, etc.)
- App layer as king: no more direct git — app opens/edits settings, partnership deploys to servers, pushes correct host.conf
- Web UI: on initial launch with no state file, open master.conf; lock orchs until setup complete
- First-launch guide: owner sets up master.conf → host1.conf → Tailscale shares → onboard → host2/3 install and see state file, default to mirror mode

**Why:** User is building this as a personal project on Unraid. Design philosophy favors simplicity and independence over enterprise tooling.
**How to apply:** Understand the two-server mesh model when suggesting architecture. The app layer / web UI direction is the current strategic focus — moving away from raw git/scripts toward a proper application.

+**Plugin install flow:** Plugin installs to appdata first, then during the setup wizard the user can select "normal" or set `internal_boot=true` to pin it to /boot. This is why the .plg note about appdata isn't wrong per se — it's the staging location before the wizard runs.


Noted and saved. So the full install story is: appdata (staging) → wizard → `/boot` if `internal_boot=true`. That also explains why the `.plg` CHANGES note about appdata isn't strictly wrong — it's just describing the intermediate step before the user configures it.

Is there anything in the plugin code or CLAUDE.md you want updated to reflect this flow more accurately?


lets make sure intitial run and migration still setup this way...


Let me pull up the relevant files to check the current state.


Bash

List Tools directory


ls /boot/config/plugins/varaverk/*.plg 2>/dev/null || ls /boot/config/plugins/*.plg 2>/dev/null | head -5

Find .plg file


/boot/config/plugins/activestreams.plg
/boot/config/plugins/ca.cleanup.appdata.plg
/boot/config/plugins/ca.update.applications.plg
/boot/config/plugins/community.applications.plg
/boot/config/plugins/customtab.plg


Manual-Tools.md
README-Tools.md
bulk_permissions_repair.sh*
claude_startup.sh*
container_data_export.sh*
docker_prune_images.sh*
emby_database_repair.sh*
emby_to_lidarr_sync.sh*
emby_to_radarr_sync.sh*
emby_to_sonarr_sync.sh*
fallback_state_reset.sh*
ramdisk_stop.sh*
smart_long_test.sh*
watchdog_skip_list_manager.sh*
zfs_pool_scrub.sh*


find /boot/config/plugins/varaverk -name "*.plg" 2>/dev/null; find /boot/config/plugins -name "varaverk*" 2>/dev/null

Find varaverk .plg file


1	#!/bin/bash
2	# ==============================================================================================
3	# ================================= Claude Code Startup ========================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Restores Claude Code's persistent data after an Unraid reboot.
9	# /root is RAM — wiped on every boot. This script re-creates symlinks so
10	# Claude's memory, sessions, settings, and binary survive across reboots.
11	#
12	# Storage mode is read from the host conf file:
13	#
14	#   HOST*_STORAGE_MODE_INTERNAL=true   → Internal (boot) mode
15	#       .claude data  → /boot/config/claude
16	#       binary        → /boot/config/claude-bin
17	#       No array dependency — runs even before array mounts.
18	#
19	#   HOST*_STORAGE_MODE_INTERNAL=false  → Appdata mode
20	#       .claude data  → /mnt/user/appdata/claude-code/.claude
21	#       binary        → /mnt/user/appdata/claude-code/local/share/claude
22	#       Requires array to be mounted.
23	#
24	# On first run in either mode, migrates any existing live data to persistent
25	# storage. Subsequent runs only re-create the symlinks.
26	#
27	# Standalone script — no common.sh dependency. Safe to run directly from
28	# terminal or from array_started.sh.
29	#
30	# ==============================================================================================
31	# RUNTIME MODES
32	# ==============================================================================================
33	#
34	# claude_startup.sh
35	#     Set up persistent symlinks only — default, used by array_started.sh on boot.
36	#
37	# claude_startup.sh --launch
38	#     Set up persistent symlinks and launch Claude interactively.
39	#
40	# ==============================================================================================
41
42	LAUNCH=false
43	[[ "$1" == "--launch" ]] && LAUNCH=true
44
45	_log()  { echo "  ✅  $*"; }
46	_warn() { echo "  ⚠   $*"; }
47	_err()  { echo "  ❌  $*" >&2; }
48
49	echo ""
50	echo "━━━ Claude Code Startup ━━━"
51	echo ""
52
53	# ── Detect storage mode ───────────────────────────────────────────────────────────────────────
54	CONF_DIR="/boot/config/plugins/varaverk/Configurations"
55	STORAGE_INTERNAL=false
56	for _conf in "$CONF_DIR"/host*.conf; do
57	    [[ -f "$_conf" ]] || continue
58	    if grep -q "_STORAGE_MODE_INTERNAL=true" "$_conf" 2>/dev/null; then
59	        STORAGE_INTERNAL=true
60	        break
61	    fi
62	done
63
64	if [[ "$STORAGE_INTERNAL" == true ]]; then
65	    CLAUDE_DATA="/boot/config/plugins/varaverk/claude-data"
66	    CLAUDE_BIN="/boot/config/plugins/varaverk/claude-bin"
67	    _log "Storage mode: internal boot"
68	else
69	    PERSIST_DIR="/mnt/user/appdata/claude-code"
70	    CLAUDE_DATA="$PERSIST_DIR/.claude"
71	    CLAUDE_BIN="$PERSIST_DIR/local/share/claude"
72	    _log "Storage mode: appdata"
73	fi
74
75	# ── Array check (appdata mode only) ──────────────────────────────────────────────────────────
76	if [[ "$STORAGE_INTERNAL" == false ]]; then
77	    if ! mountpoint -q /mnt/user 2>/dev/null; then
78	        _err "Array not mounted — /mnt/user not available (required for appdata mode)"
79	        exit 1
80	    fi
81	fi
82
83	# ── Create persistent dirs ────────────────────────────────────────────────────────────────────
84	mkdir -p "$CLAUDE_DATA" "$CLAUDE_BIN"
85
86	# ── Migrate from old /boot/config/claude location (internal mode only) ───────────────────────
87	if [[ "$STORAGE_INTERNAL" == true && -d /boot/config/claude && "$CLAUDE_DATA" != "/boot/config/claude" ]]; then
88	    if [[ -z "$(ls -A "$CLAUDE_DATA" 2>/dev/null)" ]]; then
89	        _warn "Migrating old /boot/config/claude → $CLAUDE_DATA"
90	        cp -a /boot/config/claude/. "$CLAUDE_DATA/"
91	        _log "Migrated .claude data from old location"
92	    fi
93	fi
94
95	# ── Migrate .claude on first run ──────────────────────────────────────────────────────────────
96	if [[ ! -L /root/.claude && -d /root/.claude ]]; then
97	    _warn "First run — migrating /root/.claude → $CLAUDE_DATA"
98	    cp -a /root/.claude/. "$CLAUDE_DATA/"
99	    rm -rf /root/.claude
100	    _log "Migrated .claude (memory, sessions, settings)"
101	elif [[ -z "$(ls -A "$CLAUDE_DATA" 2>/dev/null)" && -d /root/.claude ]]; then
102	    _warn "Persistent storage empty — copying current .claude data"
103	    cp -a /root/.claude/. "$CLAUDE_DATA/"
104	    _log "Copied .claude data to persistent storage"
105	fi
106
107	# ── Migrate Claude binaries on first run ──────────────────────────────────────────────────────
108	if [[ ! -L /root/.local/share/claude && -d /root/.local/share/claude ]]; then
109	    _warn "First run — migrating Claude binaries → $CLAUDE_BIN"
110	    cp -a /root/.local/share/claude/. "$CLAUDE_BIN/"
111	    _log "Migrated Claude binaries"
112	fi
113
114	# ── Create symlinks ───────────────────────────────────────────────────────────────────────────
115	mkdir -p /root/.local/share /root/.local/bin
116
117	[[ -d /root/.claude && ! -L /root/.claude ]] && rm -rf /root/.claude
118	ln -sfn "$CLAUDE_DATA" /root/.claude
119	_log ".claude        → $CLAUDE_DATA"
120
121	[[ -d /root/.local/share/claude && ! -L /root/.local/share/claude ]] && rm -rf /root/.local/share/claude
122	ln -sfn "$CLAUDE_BIN" /root/.local/share/claude
123	_log "claude binary  → $CLAUDE_BIN"
124
125	# ── Symlink CLAUDE.md ─────────────────────────────────────────────────────────────────────────
126	CLAUDE_MD="/boot/config/plugins/varaverk/CLAUDE.md"
127	if [[ -f "$CLAUDE_MD" ]]; then
128	    ln -sfn "$CLAUDE_MD" /root/CLAUDE.md
129	    _log "CLAUDE.md      → $CLAUDE_MD"
130	else
131	    _warn "CLAUDE.md not found at $CLAUDE_MD — skipping symlink"
132	fi
133
134	# ── Point the claude binary at the latest installed version ───────────────────────────────────
135	LATEST=$(ls "$CLAUDE_BIN/versions/" 2>/dev/null | sort -V | tail -1)
136	if [[ -z "$LATEST" ]]; then
137	    _err "No Claude versions found in $CLAUDE_BIN/versions/"
138	    _err "Install Claude Code first:  npm install -g @anthropic-ai/claude-code"
139	    exit 1
140	fi
141
142	ln -sfn "$CLAUDE_BIN/versions/$LATEST" /root/.local/bin/claude
143	_log "claude v$LATEST ready"
144
145	echo ""
146
147	if [[ "$LAUNCH" == false ]]; then
148	    _log "Setup complete — run 'claude' to start"
149	    echo ""
150	    exit 0
151	fi
152
153	# ── Launch ────────────────────────────────────────────────────────────────────────────────────
154	export PATH="$HOME/.local/bin:$PATH"
155	exec claude
156

#!/bin/bash
# ==============================================================================================
# ================================= Claude Code Startup ========================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Restores Claude Code's persistent data after an Unraid reboot.
# /root is RAM — wiped on every boot. This script re-creates symlinks so
# Claude's memory, sessions, settings, and binary survive across reboots.
#
# Storage mode is read from the host conf file:
#
#   HOST*_STORAGE_MODE_INTERNAL=true   → Internal (boot) mode
#       .claude data  → /boot/config/claude
#       binary        → /boot/config/claude-bin
#       No array dependency — runs even before array mounts.
#
#   HOST*_STORAGE_MODE_INTERNAL=false  → Appdata mode
#       .claude data  → /mnt/user/appdata/claude-code/.claude
#       binary        → /mnt/user/appdata/claude-code/local/share/claude
#       Requires array to be mounted.
#
# On first run in either mode, migrates any existing live data to persistent
# storage. Subsequent runs only re-create the symlinks.
#
# Standalone script — no common.sh dependency. Safe to run directly from
# terminal or from array_started.sh.
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# claude_startup.sh
#     Set up persistent symlinks only — default, used by array_started.sh on boot.
#
# claude_startup.sh --launch
#     Set up persistent symlinks and launch Claude interactively.
#
# ==============================================================================================

LAUNCH=false
[[ "$1" == "--launch" ]] && LAUNCH=true

_log()  { echo "  ✅  $*"; }
_warn() { echo "  ⚠   $*"; }
_err()  { echo "  ❌  $*" >&2; }

echo ""
echo "━━━ Claude Code Startup ━━━"
echo ""

# ── Detect storage mode ───────────────────────────────────────────────────────────────────────
CONF_DIR="/boot/config/plugins/varaverk/Configurations"
STORAGE_INTERNAL=false
for _conf in "$CONF_DIR"/host*.conf; do
    [[ -f "$_conf" ]] || continue
    if grep -q "_STORAGE_MODE_INTERNAL=true" "$_conf" 2>/dev/null; then
        STORAGE_INTERNAL=true
        break
    fi
done

if [[ "$STORAGE_INTERNAL" == true ]]; then
    CLAUDE_DATA="/boot/config/plugins/varaverk/claude-data"
    CLAUDE_BIN="/boot/config/plugins/varaverk/claude-bin"
    _log "Storage mode: internal boot"
else
    PERSIST_DIR="/mnt/user/appdata/claude-code"
    CLAUDE_DATA="$PERSIST_DIR/.claude"
    CLAUDE_BIN="$PERSIST_DIR/local/share/claude"
    _log "Storage mode: appdata"
fi

# ── Array check (appdata mode only) ──────────────────────────────────────────────────────────
if [[ "$STORAGE_INTERNAL" == false ]]; then
    if ! mountpoint -q /mnt/user 2>/dev/null; then
        _err "Array not mounted — /mnt/user not available (required for appdata mode)"
        exit 1
    fi
fi

# ── Create persistent dirs ────────────────────────────────────────────────────────────────────
mkdir -p "$CLAUDE_DATA" "$CLAUDE_BIN"

# ── Migrate from old /boot/config/claude location (internal mode only) ───────────────────────
if [[ "$STORAGE_INTERNAL" == true && -d /boot/config/claude && "$CLAUDE_DATA" != "/boot/config/claude" ]]; then
    if [[ -z "$(ls -A "$CLAUDE_DATA" 2>/dev/null)" ]]; then
        _warn "Migrating old /boot/config/claude → $CLAUDE_DATA"
        cp -a /boot/config/claude/. "$CLAUDE_DATA/"
        _log "Migrated .claude data from old location"
    fi
fi

# ── Migrate .claude on first run ──────────────────────────────────────────────────────────────
if [[ ! -L /root/.claude && -d /root/.claude ]]; then
    _warn "First run — migrating /root/.claude → $CLAUDE_DATA"
    cp -a /root/.claude/. "$CLAUDE_DATA/"
    rm -rf /root/.claude
    _log "Migrated .claude (memory, sessions, settings)"
elif [[ -z "$(ls -A "$CLAUDE_DATA" 2>/dev/null)" && -d /root/.claude ]]; then
    _warn "Persistent storage empty — copying current .claude data"
    cp -a /root/.claude/. "$CLAUDE_DATA/"
    _log "Copied .claude data to persistent storage"
fi

# ── Migrate Claude binaries on first run ──────────────────────────────────────────────────────
if [[ ! -L /root/.local/share/claude && -d /root/.local/share/claude ]]; then
    _warn "First run — migrating Claude binaries → $CLAUDE_BIN"
    cp -a /root/.local/share/claude/. "$CLAUDE_BIN/"
    _log "Migrated Claude binaries"
fi

# ── Create symlinks ───────────────────────────────────────────────────────────────────────────
mkdir -p /root/.local/share /root/.local/bin

[[ -d /root/.claude && ! -L /root/.claude ]] && rm -rf /root/.claude
ln -sfn "$CLAUDE_DATA" /root/.claude
_log ".claude        → $CLAUDE_DATA"

[[ -d /root/.local/share/claude && ! -L /root/.local/share/claude ]] && rm -rf /root/.local/share/claude
ln -sfn "$CLAUDE_BIN" /root/.local/share/claude
_log "claude binary  → $CLAUDE_BIN"

# ── Symlink CLAUDE.md ─────────────────────────────────────────────────────────────────────────
CLAUDE_MD="/boot/config/plugins/varaverk/CLAUDE.md"
if [[ -f "$CLAUDE_MD" ]]; then
    ln -sfn "$CLAUDE_MD" /root/CLAUDE.md
    _log "CLAUDE.md      → $CLAUDE_MD"
else
    _warn "CLAUDE.md not found at $CLAUDE_MD — skipping symlink"
fi

# ── Point the claude binary at the latest installed version ───────────────────────────────────
LATEST=$(ls "$CLAUDE_BIN/versions/" 2>/dev/null | sort -V | tail -1)
if [[ -z "$LATEST" ]]; then
    _err "No Claude versions found in $CLAUDE_BIN/versions/"
    _err "Install Claude Code first:  npm install -g @anthropic-ai/claude-code"
    exit 1
fi

ln -sfn "$CLAUDE_BIN/versions/$LATEST" /root/.local/bin/claude
_log "claude v$LATEST ready"

echo ""

if [[ "$LAUNCH" == false ]]; then
    _log "Setup complete — run 'claude' to start"
    echo ""
    exit 0
fi

# ── Launch ────────────────────────────────────────────────────────────────────────────────────
export PATH="$HOME/.local/bin:$PATH"
exec claude


/boot/config/plugins/varaverk/Plugin/varaverk.plg
/boot/config/plugins/varaverk
/boot/config/plugins/varaverk.plg
/boot/config/plugins/varaverk/varaverk.cron
/boot/config/plugins/varaverk/varaverk-2026.05.31-noarch-1.txz
/boot/config/plugins/varaverk/varaverk.cfg
/boot/config/plugins/varaverk/Plugin/varaverk.plg
/boot/config/plugins/varaverk/Plugin/dist/varaverk-2026.05.31-noarch-1.txz.sha256
/boot/config/plugins/varaverk/State_Files/varaverk_setup.db
/boot/config/plugins/varaverk/Plugin/unraid/js/varaverk.js
/boot/config/plugins/varaverk/Plugin/unraid/icons/varaverk.png
/boot/config/plugins/varaverk/Plugin/unraid/css/varaverk.css


1	<?xml version='1.0' standalone='yes'?>
2	<!DOCTYPE PLUGIN [
3	<!ENTITY name    "varaverk">
4	<!ENTITY author  "gmer4lfe">
5	<!ENTITY version "2026.05.31">
6	<!ENTITY sha256  "d588470d6cc7f284601cb56039d5dfea6fb5bb1ee2c800657438a8edacafbd01">
7	<!ENTITY launch  "varaverk/monitor">
8	<!ENTITY github  "https://github.com/FailedProxy/Varaverk">
9	<!ENTITY branch  "main">
10	<!ENTITY cfgdir  "/boot/config/plugins/varaverk">
11	<!ENTITY plugdir "/usr/local/emhttp/plugins/varaverk">
12	<!ENTITY pkg     "varaverk-&version;-noarch-1.txz">
13	]>
14	<PLUGIN name="&name;" author="&author;" version="&version;" launch="&launch;"
15	        support="https://github.com/FailedProxy/Varaverk/issues"
16	        icon="/plugins/varaverk/icons/varaverk.png">
17
18	<CHANGES>
19	###2026.05.31
20	- Packaged release: web files now ship as a .txz that Unraid reinstalls to RAM on every boot
21	- Survives reboots with zero manual steps (no symlink, no go script) — fixes plugin vanishing after OS upgrades
22	- Scripts are git-cloned to appdata on first install; web files stay on flash (~200KB)
23	- Updates handled in-UI (git pull); the plugin no longer pulls on every boot
24
25	###2026.05.30
26	- First-run setup wizard: auto-detects hostname, creates master.conf + host conf from templates
27	- Scheduler setup mode: after wizard, master.conf and host conf open sequentially with forced save flow
28	- Partnership tab: Onboard button highlighted on arrival from wizard; disabled until partner is configured
29	- HOST2 install paths: state-file pull, master.conf push detection, conf-only flow
30	- Onboard Step 9: master.conf automatically pushed to all listed hosts on onboard completion
31	- Graceful pre-onboard state: neutral banners instead of error warnings before SSH is configured
32	- GitHub link in tab bar and Settings page; Community Apps support URL
33
34	###2026.05.28
35	- Initial release: Monitor, Scheduler, Docker, Watchdog, Partnership, Fallback, Arrs tabs
36	- Mutual container fallback with tiered escalation and strike-confirmed handback
37	- Partnership lifecycle: onboard, offboard, transfer
38	- Rsync profile system with per-share container stops and writeback
39	- Watchdog: Tier 1 (explicit) + Tier 2 (global scan) container monitoring
40	- master.conf push-on-save to all configured partners via SSH
41	</CHANGES>
42
43	<!--
44	  ── 1. Web files symlink (runs on every boot) ──────────────────────────────────
45	  Instead of extracting a .txz, we symlink the installed plugin web dir directly
46	  to the workspace on flash. Changes to Plugin/unraid/ are live instantly — no
47	  package build, no sync step. /boot is always mounted before this runs.
48	  ────────────────────────────────────────────────────────────────────────────────
49	-->
50	<FILE Run="/bin/bash">
51	<INLINE>
52	<![CDATA[
53	#!/bin/bash
54	WEB_DIR="/usr/local/emhttp/plugins/varaverk"
55	SRC="/boot/config/plugins/varaverk/Plugin/unraid"
56	[[ -L "$WEB_DIR" ]] && rm -f "$WEB_DIR"
57	[[ -d "$WEB_DIR" ]] && rm -rf "$WEB_DIR"
58	ln -sf "$SRC" "$WEB_DIR"
59	echo "[Varaverk] web dir symlinked → $SRC"
60	]]>
61	</INLINE>
62	</FILE>
63
64	<!--
65	  ── 2. Scripts bootstrap (first install only) ──────────────────────────────────
66	  Clones the repo directly into the plugin config dir on flash (/boot/config/plugins/varaverk).
67	  No array dependency — scripts live on flash (64GB NVMe) and are available at boot.
68	  Uses git init+fetch+reset so the clone works into the non-empty cfgdir (varaverk.cfg,
69	  varaverk-*.txz etc. are already there). Never auto-pulls — updates via the UI git pull.
70
71	  Clone source priority:
72	    1. Gitea (internal) — reads settings from varaverk.cfg; detects container IP at runtime
73	    2. GitHub (public)  — HTTPS fallback if Gitea is unreachable
74	  ────────────────────────────────────────────────────────────────────────────────
75	-->
76	<FILE Run="/bin/bash" Method="install">
77	<INLINE>
78	<![CDATA[
79	#!/bin/bash
80	PLUGIN="varaverk"
81	CFG_DIR="/boot/config/plugins/$PLUGIN"
82	CFG_FILE="$CFG_DIR/varaverk.cfg"
83	GITHUB="https://github.com/FailedProxy/Varaverk"
84	BRANCH="main"
85	LOG="$CFG_DIR/install.log"
86
87	mkdir -p "$CFG_DIR"
88	log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG"; }
89
90	# Scripts live in the plugin dir on flash — no array needed.
91	SCRIPTS_DIR="$CFG_DIR"
92	CONF_DIR="$SCRIPTS_DIR/Configurations"
93
94	# ── Boot device check ─────────────────────────────────────────────────────────
95	# Warn if /boot is on a USB/removable device. Varaverk is designed for internal
96	# NVMe/SSD boot — git repo + state files + data writes on USB will wear it out
97	# fast and may run out of space. Install proceeds but user is warned.
98	_boot_dev=$(df /boot --output=source 2>/dev/null | tail -1)
99	_boot_base=$(lsblk -no pkname "$_boot_dev" 2>/dev/null || basename "${_boot_dev%[0-9p]*}")
100	_removable=$(cat "/sys/block/${_boot_base}/removable" 2>/dev/null || echo "0")
101	if [[ "$_removable" == "1" ]]; then
102	    log "WARNING: /boot is on a removable/USB device ($_boot_dev)"
103	    log "WARNING: Varaverk is designed for internal NVMe/SSD boot."
104	    log "WARNING: Running from USB risks drive wear and space exhaustion."
105	    log "WARNING: Strongly recommend migrating boot to an internal NVMe/SSD drive."
106	fi
107	unset _boot_dev _boot_base _removable
108
109	# Seed varaverk.cfg with defaults (SCRIPTS_DIR + Gitea settings) if not present.
110	# Requires internal NVMe/SSD boot — scripts live on flash, available before array mounts.
111	if [[ ! -f "$CFG_FILE" ]]; then
112	    cat > "$CFG_FILE" <<'CFGEOF'
113	SCRIPTS_DIR="/boot/config/plugins/varaverk"
114	GITEA_CONTAINER="Gitea"
115	GITEA_REPO_PATH="FailedProxy/Varaverk.git"
116	GITEA_SSH_KEY="/root/.ssh/unraid_gitea"
117	SSH_PORT="221"
118	CFGEOF
119	    log "seeded varaverk.cfg"
120	fi
121
122	# Read Gitea settings from varaverk.cfg (allows override without editing .plg).
123	_read_cfg() { grep -oP "(?<=^${1}=\")[^\"]*" "$CFG_FILE" 2>/dev/null || echo "${2}"; }
124	GITEA_CONTAINER=$(_read_cfg GITEA_CONTAINER "Gitea")
125	GITEA_REPO_PATH=$(_read_cfg GITEA_REPO_PATH "FailedProxy/Varaverk.git")
126	GITEA_SSH_KEY=$(_read_cfg   GITEA_SSH_KEY   "/root/.ssh/unraid_gitea")
127	SSH_PORT=$(_read_cfg        SSH_PORT        "221")
128
129	# Clone on first install only; never auto-pull (updates via the UI git pull).
130	if [[ ! -d "$SCRIPTS_DIR/.git" ]]; then
131	    log "initialising repo in $SCRIPTS_DIR ($BRANCH)..."
132
133	    # Locate Gitea: local container → local IP; else Tailscale; else fall back to GitHub.
134	    GITEA_IP=""
135	    if command -v docker >/dev/null 2>&1 && \
136	       docker ps --format "{{.Names}}" 2>/dev/null | grep -q "^${GITEA_CONTAINER}$"; then
137	        GITEA_IP=$(hostname -I | awk '{print $1}')
138	        log "Gitea running locally — using $GITEA_IP"
139	    elif command -v tailscale >/dev/null 2>&1; then
140	        # Try each known peer until we find one hosting Gitea
141	        while IFS= read -r peer_ip; do
142	            if ssh -i "$GITEA_SSH_KEY" -p "$SSH_PORT" \
143	                   -o ConnectTimeout=3 -o StrictHostKeyChecking=no \
144	                   -o BatchMode=yes "git@${peer_ip}" info 2>/dev/null | grep -q "varaverk\|Gitea\|gitea"; then
145	                GITEA_IP="$peer_ip"
146	                log "Gitea found on Tailscale peer $GITEA_IP"
147	                break
148	            fi
149	        done < <(tailscale status --json 2>/dev/null | \
150	                 python3 -c "import json,sys; d=json.load(sys.stdin); \
151	                 [print(v['TailscaleIPs'][0]) for v in d.get('Peer',{}).values() \
152	                  if v.get('TailscaleIPs')]" 2>/dev/null)
153	    fi
154
155	    # init-in-place — git clone would fail because the dir already has files.
156	    git -C "$SCRIPTS_DIR" init >> "$LOG" 2>&1
157
158	    CLONED=false
159	    if [[ -n "$GITEA_IP" && -f "$GITEA_SSH_KEY" ]]; then
160	        GITEA_URL="ssh://git@${GITEA_IP}:${SSH_PORT}/${GITEA_REPO_PATH}"
161	        log "trying Gitea: $GITEA_URL"
162	        git -C "$SCRIPTS_DIR" remote add origin "$GITEA_URL" >> "$LOG" 2>&1
163	        if GIT_SSH_COMMAND="ssh -i $GITEA_SSH_KEY -p $SSH_PORT -o StrictHostKeyChecking=no" \
164	           GIT_TERMINAL_PROMPT=0 \
165	           git -C "$SCRIPTS_DIR" fetch --depth=1 origin "$BRANCH" >> "$LOG" 2>&1; then
166	            git -C "$SCRIPTS_DIR" reset --hard FETCH_HEAD >> "$LOG" 2>&1
167	            git -C "$SCRIPTS_DIR" branch -M "$BRANCH" >> "$LOG" 2>&1
168	            git -C "$SCRIPTS_DIR" branch --set-upstream-to=origin/"$BRANCH" "$BRANCH" >> "$LOG" 2>&1
169	            log "scripts installed from Gitea ($GITEA_IP)"
170	            CLONED=true
171	        else
172	            log "Gitea fetch failed — falling back to GitHub"
173	            git -C "$SCRIPTS_DIR" remote remove origin >> "$LOG" 2>&1 || true
174	        fi
175	    fi
176
177	    if [[ "$CLONED" == false ]]; then
178	        log "trying GitHub: $GITHUB"
179	        git -C "$SCRIPTS_DIR" remote add origin "$GITHUB" >> "$LOG" 2>&1
180	        if GIT_TERMINAL_PROMPT=0 \
181	           git -C "$SCRIPTS_DIR" fetch --depth=1 origin "$BRANCH" >> "$LOG" 2>&1; then
182	            git -C "$SCRIPTS_DIR" reset --hard FETCH_HEAD >> "$LOG" 2>&1
183	            git -C "$SCRIPTS_DIR" branch -M "$BRANCH" >> "$LOG" 2>&1
184	            git -C "$SCRIPTS_DIR" branch --set-upstream-to=origin/"$BRANCH" "$BRANCH" >> "$LOG" 2>&1
185	            log "scripts installed from GitHub"
186	            CLONED=true
187	        else
188	            log "WARNING: both Gitea and GitHub failed — scripts not installed, retry when network is up"
189	            exit 0
190	        fi
191	    fi
192	else
193	    log "repo present — leaving scripts untouched (update from the UI)"
194	fi
195
196	# Seed master.conf from template if absent.
197	mkdir -p "$CONF_DIR"
198	if [[ ! -f "$CONF_DIR/master.conf" && -f "$CONF_DIR/master.conf.template" ]]; then
199	    cp "$CONF_DIR/master.conf.template" "$CONF_DIR/master.conf"
200	    log "seeded master.conf from template"
201	fi
202	log "install step complete"
203	]]>
204	</INLINE>
205	</FILE>
206
207	<!--
208	  ── 3. Remove ───────────────────────────────────────────────────────────────────
209	  Stops background scripts, removes cron, the installed package, and flash config.
210	  Scripts/conf in appdata are left intact (delete manually for a full wipe).
211	  ────────────────────────────────────────────────────────────────────────────────
212	-->
213	<FILE Run="/bin/bash" Method="remove">
214	<INLINE>
215	<![CDATA[
216	#!/bin/bash
217	PLUGIN="varaverk"
218	CFG_DIR="/boot/config/plugins/$PLUGIN"
219	CFG_FILE="$CFG_DIR/varaverk.cfg"
220	CRON_FILE="$CFG_DIR/varaverk.cron"
221	WEB_DIR="/usr/local/emhttp/plugins/$PLUGIN"
222	log() { echo "[Varaverk remove] $*"; }
223
224	[[ -f "$CFG_FILE" ]] && _sd=$(grep -oP '(?<=SCRIPTS_DIR=")[^"]+' "$CFG_FILE" 2>/dev/null)
225	SCRIPTS_DIR="${_sd:-$CFG_DIR}"
226
227	# Stop continuous background scripts.
228	if [[ -f "$SCRIPTS_DIR/Fallback/fallback.sh" ]]; then
229	    bash "$SCRIPTS_DIR/Fallback/fallback.sh" --stop 2>/dev/null && log "fallback.sh stopped" || true
230	fi
231	pkill -f "run_job.sh" 2>/dev/null || true
232	pkill -f "watchdog_orchestrator.sh" 2>/dev/null || true
233
234	# Remove cron entries.
235	if [[ -f "$CRON_FILE" ]]; then
236	    rm -f "$CRON_FILE"
237	    /usr/local/sbin/update_cron 2>/dev/null || true
238	    log "cron removed"
239	fi
240	rm -f /etc/cron.d/varaverk
241
242	# Remove the installed package (and its RAM files).
243	removepkg "$PLUGIN" 2>/dev/null || true
244	[[ -L "$WEB_DIR" ]] && rm -f "$WEB_DIR"
245	[[ -d "$WEB_DIR" ]] && rm -rf "$WEB_DIR"
246	log "web files removed"
247
248	# Remove flash config (incl. cached .txz).
249	rm -rf "$CFG_DIR"
250	log "flash config removed"
251	log "done — scripts/conf in $SCRIPTS_DIR preserved (delete manually for full wipe)"
252	]]>
253	</INLINE>
254	</FILE>
255
256	</PLUGIN>
257

<?xml version='1.0' standalone='yes'?>
<!DOCTYPE PLUGIN [
<!ENTITY name    "varaverk">
<!ENTITY author  "gmer4lfe">
<!ENTITY version "2026.05.31">
<!ENTITY sha256  "d588470d6cc7f284601cb56039d5dfea6fb5bb1ee2c800657438a8edacafbd01">
<!ENTITY launch  "varaverk/monitor">
<!ENTITY github  "https://github.com/FailedProxy/Varaverk">
<!ENTITY branch  "main">
<!ENTITY cfgdir  "/boot/config/plugins/varaverk">
<!ENTITY plugdir "/usr/local/emhttp/plugins/varaverk">
<!ENTITY pkg     "varaverk-&version;-noarch-1.txz">
]>
<PLUGIN name="&name;" author="&author;" version="&version;" launch="&launch;"
        support="https://github.com/FailedProxy/Varaverk/issues"
        icon="/plugins/varaverk/icons/varaverk.png">

<CHANGES>
###2026.05.31
- Packaged release: web files now ship as a .txz that Unraid reinstalls to RAM on every boot
- Survives reboots with zero manual steps (no symlink, no go script) — fixes plugin vanishing after OS upgrades
- Scripts are git-cloned to appdata on first install; web files stay on flash (~200KB)
- Updates handled in-UI (git pull); the plugin no longer pulls on every boot

###2026.05.30
- First-run setup wizard: auto-detects hostname, creates master.conf + host conf from templates
- Scheduler setup mode: after wizard, master.conf and host conf open sequentially with forced save flow
- Partnership tab: Onboard button highlighted on arrival from wizard; disabled until partner is configured
- HOST2 install paths: state-file pull, master.conf push detection, conf-only flow
- Onboard Step 9: master.conf automatically pushed to all listed hosts on onboard completion
- Graceful pre-onboard state: neutral banners instead of error warnings before SSH is configured
- GitHub link in tab bar and Settings page; Community Apps support URL

###2026.05.28
- Initial release: Monitor, Scheduler, Docker, Watchdog, Partnership, Fallback, Arrs tabs
- Mutual container fallback with tiered escalation and strike-confirmed handback
- Partnership lifecycle: onboard, offboard, transfer
- Rsync profile system with per-share container stops and writeback
- Watchdog: Tier 1 (explicit) + Tier 2 (global scan) container monitoring
- master.conf push-on-save to all configured partners via SSH
</CHANGES>

<!--
  ── 1. Web files symlink (runs on every boot) ──────────────────────────────────
  Instead of extracting a .txz, we symlink the installed plugin web dir directly
  to the workspace on flash. Changes to Plugin/unraid/ are live instantly — no
  package build, no sync step. /boot is always mounted before this runs.
  ────────────────────────────────────────────────────────────────────────────────
-->
<FILE Run="/bin/bash">
<INLINE>
<![CDATA[
#!/bin/bash
WEB_DIR="/usr/local/emhttp/plugins/varaverk"
SRC="/boot/config/plugins/varaverk/Plugin/unraid"
[[ -L "$WEB_DIR" ]] && rm -f "$WEB_DIR"
[[ -d "$WEB_DIR" ]] && rm -rf "$WEB_DIR"
ln -sf "$SRC" "$WEB_DIR"
echo "[Varaverk] web dir symlinked → $SRC"
]]>
</INLINE>
</FILE>

<!--
  ── 2. Scripts bootstrap (first install only) ──────────────────────────────────
  Clones the repo directly into the plugin config dir on flash (/boot/config/plugins/varaverk).
  No array dependency — scripts live on flash (64GB NVMe) and are available at boot.
  Uses git init+fetch+reset so the clone works into the non-empty cfgdir (varaverk.cfg,
  varaverk-*.txz etc. are already there). Never auto-pulls — updates via the UI git pull.

  Clone source priority:
    1. Gitea (internal) — reads settings from varaverk.cfg; detects container IP at runtime
    2. GitHub (public)  — HTTPS fallback if Gitea is unreachable
  ────────────────────────────────────────────────────────────────────────────────
-->
<FILE Run="/bin/bash" Method="install">
<INLINE>
<![CDATA[
#!/bin/bash
PLUGIN="varaverk"
CFG_DIR="/boot/config/plugins/$PLUGIN"
CFG_FILE="$CFG_DIR/varaverk.cfg"
GITHUB="https://github.com/FailedProxy/Varaverk"
BRANCH="main"
LOG="$CFG_DIR/install.log"

mkdir -p "$CFG_DIR"
log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG"; }

# Scripts live in the plugin dir on flash — no array needed.
SCRIPTS_DIR="$CFG_DIR"
CONF_DIR="$SCRIPTS_DIR/Configurations"

# ── Boot device check ─────────────────────────────────────────────────────────
# Warn if /boot is on a USB/removable device. Varaverk is designed for internal
# NVMe/SSD boot — git repo + state files + data writes on USB will wear it out
# fast and may run out of space. Install proceeds but user is warned.
_boot_dev=$(df /boot --output=source 2>/dev/null | tail -1)
_boot_base=$(lsblk -no pkname "$_boot_dev" 2>/dev/null || basename "${_boot_dev%[0-9p]*}")
_removable=$(cat "/sys/block/${_boot_base}/removable" 2>/dev/null || echo "0")
if [[ "$_removable" == "1" ]]; then
    log "WARNING: /boot is on a removable/USB device ($_boot_dev)"
    log "WARNING: Varaverk is designed for internal NVMe/SSD boot."
    log "WARNING: Running from USB risks drive wear and space exhaustion."
    log "WARNING: Strongly recommend migrating boot to an internal NVMe/SSD drive."
fi
unset _boot_dev _boot_base _removable

# Seed varaverk.cfg with defaults (SCRIPTS_DIR + Gitea settings) if not present.
# Requires internal NVMe/SSD boot — scripts live on flash, available before array mounts.
if [[ ! -f "$CFG_FILE" ]]; then
    cat > "$CFG_FILE" <<'CFGEOF'
SCRIPTS_DIR="/boot/config/plugins/varaverk"
GITEA_CONTAINER="Gitea"
GITEA_REPO_PATH="FailedProxy/Varaverk.git"
GITEA_SSH_KEY="/root/.ssh/unraid_gitea"
SSH_PORT="221"
CFGEOF
    log "seeded varaverk.cfg"
fi

# Read Gitea settings from varaverk.cfg (allows override without editing .plg).
_read_cfg() { grep -oP "(?<=^${1}=\")[^\"]*" "$CFG_FILE" 2>/dev/null || echo "${2}"; }
GITEA_CONTAINER=$(_read_cfg GITEA_CONTAINER "Gitea")
GITEA_REPO_PATH=$(_read_cfg GITEA_REPO_PATH "FailedProxy/Varaverk.git")
GITEA_SSH_KEY=$(_read_cfg   GITEA_SSH_KEY   "/root/.ssh/unraid_gitea")
SSH_PORT=$(_read_cfg        SSH_PORT        "221")

# Clone on first install only; never auto-pull (updates via the UI git pull).
if [[ ! -d "$SCRIPTS_DIR/.git" ]]; then
    log "initialising repo in $SCRIPTS_DIR ($BRANCH)..."

    # Locate Gitea: local container → local IP; else Tailscale; else fall back to GitHub.
    GITEA_IP=""
    if command -v docker >/dev/null 2>&1 && \
       docker ps --format "{{.Names}}" 2>/dev/null | grep -q "^${GITEA_CONTAINER}$"; then
        GITEA_IP=$(hostname -I | awk '{print $1}')
        log "Gitea running locally — using $GITEA_IP"
    elif command -v tailscale >/dev/null 2>&1; then
        # Try each known peer until we find one hosting Gitea
        while IFS= read -r peer_ip; do
            if ssh -i "$GITEA_SSH_KEY" -p "$SSH_PORT" \
                   -o ConnectTimeout=3 -o StrictHostKeyChecking=no \
                   -o BatchMode=yes "git@${peer_ip}" info 2>/dev/null | grep -q "varaverk\|Gitea\|gitea"; then
                GITEA_IP="$peer_ip"
                log "Gitea found on Tailscale peer $GITEA_IP"
                break
            fi
        done < <(tailscale status --json 2>/dev/null | \
                 python3 -c "import json,sys; d=json.load(sys.stdin); \
                 [print(v['TailscaleIPs'][0]) for v in d.get('Peer',{}).values() \
                  if v.get('TailscaleIPs')]" 2>/dev/null)
    fi

    # init-in-place — git clone would fail because the dir already has files.
    git -C "$SCRIPTS_DIR" init >> "$LOG" 2>&1

    CLONED=false
    if [[ -n "$GITEA_IP" && -f "$GITEA_SSH_KEY" ]]; then
        GITEA_URL="ssh://git@${GITEA_IP}:${SSH_PORT}/${GITEA_REPO_PATH}"
        log "trying Gitea: $GITEA_URL"
        git -C "$SCRIPTS_DIR" remote add origin "$GITEA_URL" >> "$LOG" 2>&1
        if GIT_SSH_COMMAND="ssh -i $GITEA_SSH_KEY -p $SSH_PORT -o StrictHostKeyChecking=no" \
           GIT_TERMINAL_PROMPT=0 \
           git -C "$SCRIPTS_DIR" fetch --depth=1 origin "$BRANCH" >> "$LOG" 2>&1; then
            git -C "$SCRIPTS_DIR" reset --hard FETCH_HEAD >> "$LOG" 2>&1
            git -C "$SCRIPTS_DIR" branch -M "$BRANCH" >> "$LOG" 2>&1
            git -C "$SCRIPTS_DIR" branch --set-upstream-to=origin/"$BRANCH" "$BRANCH" >> "$LOG" 2>&1
            log "scripts installed from Gitea ($GITEA_IP)"
            CLONED=true
        else
            log "Gitea fetch failed — falling back to GitHub"
            git -C "$SCRIPTS_DIR" remote remove origin >> "$LOG" 2>&1 || true
        fi
    fi

    if [[ "$CLONED" == false ]]; then
        log "trying GitHub: $GITHUB"
        git -C "$SCRIPTS_DIR" remote add origin "$GITHUB" >> "$LOG" 2>&1
        if GIT_TERMINAL_PROMPT=0 \
           git -C "$SCRIPTS_DIR" fetch --depth=1 origin "$BRANCH" >> "$LOG" 2>&1; then
            git -C "$SCRIPTS_DIR" reset --hard FETCH_HEAD >> "$LOG" 2>&1
            git -C "$SCRIPTS_DIR" branch -M "$BRANCH" >> "$LOG" 2>&1
            git -C "$SCRIPTS_DIR" branch --set-upstream-to=origin/"$BRANCH" "$BRANCH" >> "$LOG" 2>&1
            log "scripts installed from GitHub"
            CLONED=true
        else
            log "WARNING: both Gitea and GitHub failed — scripts not installed, retry when network is up"
            exit 0
        fi
    fi
else
    log "repo present — leaving scripts untouched (update from the UI)"
fi

# Seed master.conf from template if absent.
mkdir -p "$CONF_DIR"
if [[ ! -f "$CONF_DIR/master.conf" && -f "$CONF_DIR/master.conf.template" ]]; then
    cp "$CONF_DIR/master.conf.template" "$CONF_DIR/master.conf"
    log "seeded master.conf from template"
fi
log "install step complete"
]]>
</INLINE>
</FILE>

<!--
  ── 3. Remove ───────────────────────────────────────────────────────────────────
  Stops background scripts, removes cron, the installed package, and flash config.
  Scripts/conf in appdata are left intact (delete manually for a full wipe).
  ────────────────────────────────────────────────────────────────────────────────
-->
<FILE Run="/bin/bash" Method="remove">
<INLINE>
<![CDATA[
#!/bin/bash
PLUGIN="varaverk"
CFG_DIR="/boot/config/plugins/$PLUGIN"
CFG_FILE="$CFG_DIR/varaverk.cfg"
CRON_FILE="$CFG_DIR/varaverk.cron"
WEB_DIR="/usr/local/emhttp/plugins/$PLUGIN"
log() { echo "[Varaverk remove] $*"; }

[[ -f "$CFG_FILE" ]] && _sd=$(grep -oP '(?<=SCRIPTS_DIR=")[^"]+' "$CFG_FILE" 2>/dev/null)
SCRIPTS_DIR="${_sd:-$CFG_DIR}"

# Stop continuous background scripts.
if [[ -f "$SCRIPTS_DIR/Fallback/fallback.sh" ]]; then
    bash "$SCRIPTS_DIR/Fallback/fallback.sh" --stop 2>/dev/null && log "fallback.sh stopped" || true
fi
pkill -f "run_job.sh" 2>/dev/null || true
pkill -f "watchdog_orchestrator.sh" 2>/dev/null || true

# Remove cron entries.
if [[ -f "$CRON_FILE" ]]; then
    rm -f "$CRON_FILE"
    /usr/local/sbin/update_cron 2>/dev/null || true
    log "cron removed"
fi
rm -f /etc/cron.d/varaverk

# Remove the installed package (and its RAM files).
removepkg "$PLUGIN" 2>/dev/null || true
[[ -L "$WEB_DIR" ]] && rm -f "$WEB_DIR"
[[ -d "$WEB_DIR" ]] && rm -rf "$WEB_DIR"
log "web files removed"

# Remove flash config (incl. cached .txz).
rm -rf "$CFG_DIR"
log "flash config removed"
log "done — scripts/conf in $SCRIPTS_DIR preserved (delete manually for full wipe)"
]]>
</INLINE>
</FILE>

</PLUGIN>


1	SCRIPTS_DIR="/boot/config/plugins/varaverk"
2	GITEA_CONTAINER="Gitea"
3	GITEA_REPO_PATH="FailedProxy/Varaverk.git"
4	GITEA_SSH_KEY="/root/.ssh/unraid_gitea"
5	SSH_PORT="221"
6

SCRIPTS_DIR="/boot/config/plugins/varaverk"
GITEA_CONTAINER="Gitea"
GITEA_REPO_PATH="FailedProxy/Varaverk.git"
GITEA_SSH_KEY="/root/.ssh/unraid_gitea"
SSH_PORT="221"


Let me check the setup wizard and any storage-related PHP to see if the wizard already handles this selection.


find /boot/config/plugins/varaverk/Plugin/unraid -name "*.php" | sort

List all PHP files in plugin


grep -rl "STORAGE_MODE_INTERNAL\|internal_boot\|appdata\|storage.mode\|storage_mode" /boot/config/plugins/varaverk/ --include="*.php" --include="*.sh" --include="*.conf" 2>/dev/null

Find all references to storage mode


There's a `storage_migrate.sh` and `api/storage.php` — let me read those and the setup wizard.


1	#!/bin/bash
2	# ==============================================================================================
3	# ================================= Storage Migration ==========================================
4	# ==============================================================================================
5	#
6	# PURPOSE
7	# ─────────────────────────────────────────────────────────────────────────────
8	# Migrates Varaverk between internal NVMe and USB flash storage modes.
9	#
10	# Internal mode: SCRIPTS_DIR = /boot/config/plugins/varaverk
11	#   All scripts, conf, state, and git repo live on fast internal storage.
12	#   Direct git pull/push. Zero write-wear concern.
13	#
14	# Flash mode:    SCRIPTS_DIR = /mnt/user/appdata/Varaverk
15	#   All scripts, conf, state, and git repo live in appdata.
16	#   Preserves USB flash lifetime. Array must be started for Varaverk to function.
17	#   git_pull_execute.sh syncs Plugin/ back to /boot/ after each pull so the
18	#   Unraid webUI always serves current PHP files.
19	#
20	# What this script updates:
21	#   varaverk.cfg      SCRIPTS_DIR
22	#   master.conf       TARGET_DIR
23	#   host*.conf        HOST*_STORAGE_MODE_INTERNAL
24	#   varaverk.cron     rebuilt via PHP (job paths regenerated for new SCRIPTS_DIR)
25	#
26	# ==============================================================================================
27	# RUNTIME MODES
28	# ==============================================================================================
29	#
30	# storage_migrate.sh --to=internal
31	#     Migrate to /boot/config/plugins/varaverk
32	#
33	# storage_migrate.sh --to=flash
34	#     Migrate to /mnt/user/appdata/Varaverk
35	#
36	# storage_migrate.sh --dry-run --to=<mode>
37	#     Show what would happen — no changes made
38	#
39	# storage_migrate.sh --status
40	#     Show current mode, paths, and boot device info
41	#
42	# ==============================================================================================
43
44	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
45	source "$SCRIPT_DIR/../../../load_config.sh"
46
47	parse_args "$@"
48	acquire_lock
49
50	VV_CFG="/boot/config/plugins/varaverk/varaverk.cfg"
51	INTERNAL_DIR="/boot/config/plugins/varaverk"
52	FLASH_DIR="/mnt/user/appdata/Varaverk"
53
54	# ──────────────────────────────────────────────────────────────────────────────
55	# Parse --to= from raw args (parse_args doesn't handle this flag)
56	TO_MODE=""
57	for arg in "$@"; do
58	    case "$arg" in
59	        --to=internal) TO_MODE="internal" ;;
60	        --to=flash)    TO_MODE="flash"    ;;
61	    esac
62	done
63
64	# ──────────────────────────────────────────────────────────────────────────────
65	# Boot device detection
66	detect_boot_storage() {
67	    local boot_part boot_disk transport
68	    boot_part=$(findmnt -n -o SOURCE /boot 2>/dev/null)
69	    boot_disk=$(lsblk -no pkname "$boot_part" 2>/dev/null)
70	    transport=$(lsblk -dno TRAN "/dev/$boot_disk" 2>/dev/null | tr '[:upper:]' '[:lower:]')
71	    echo "${transport:-unknown}"
72	}
73
74	# ──────────────────────────────────────────────────────────────────────────────
75	# Status
76	if [[ "$SHOW_STATUS" == true ]]; then
77	    TRANSPORT=$(detect_boot_storage)
78	    DETECTED=$([[ "$TRANSPORT" == "usb" ]] && echo "flash" || echo "internal")
79	    echo ""
80	    echo "━━━━━ $ICON_SUMMARY STORAGE STATUS ━━━━━"
81	    echo "$ICON_GEAR  SCRIPTS_DIR:  $SCRIPTS_DIR"
82	    echo "$ICON_GEAR  varaverk.cfg: $VV_CFG"
83	    echo "$ICON_HOST  Boot device:  transport=$TRANSPORT → detected=$DETECTED"
84	    echo "$ICON_GEAR  Target dirs:"
85	    echo "    internal: $INTERNAL_DIR"
86	    echo "    flash:    $FLASH_DIR"
87	    CONF_MODE=$(grep -m1 "${MY_ID}_STORAGE_MODE_INTERNAL" "$CONF_FILE" 2>/dev/null | cut -d= -f2 | tr -d '"' | tr -d '[:space:]')
88	    echo "$ICON_GEAR  conf setting: ${MY_ID}_STORAGE_MODE_INTERNAL=${CONF_MODE:-not set}"
89	    if [[ "$SCRIPTS_DIR" == "$INTERNAL_DIR" ]]; then
90	        echo "$ICON_DONE  Current mode: INTERNAL ✅"
91	    elif [[ "$SCRIPTS_DIR" == "$FLASH_DIR" ]]; then
92	        echo "$ICON_DONE  Current mode: FLASH ✅"
93	    else
94	        echo "$ICON_WARN  Current mode: CUSTOM ($SCRIPTS_DIR)"
95	    fi
96	    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
97	    exit 0
98	fi
99
100	# ──────────────────────────────────────────────────────────────────────────────
101	if [[ -z "$TO_MODE" ]]; then
102	    error "Usage: storage_migrate.sh --to=internal|flash [--dry-run] [--log]"
103	    exit 1
104	fi
105
106	SRC="$SCRIPTS_DIR"
107	DST=$([[ "$TO_MODE" == "internal" ]] && echo "$INTERNAL_DIR" || echo "$FLASH_DIR")
108	NEW_INTERNAL=$([[ "$TO_MODE" == "internal" ]] && echo "true" || echo "false")
109
110	log "$ICON_GEAR Config: to=${TO_MODE} src=${SRC} dst=${DST} dry-run=${DRY_RUN}"
111
112	echo ""
113	echo "━━━━━ $ICON_SYNC Storage Migration ━━━━━"
114	echo "$ICON_GEAR  From: $SRC"
115	echo "$ICON_GEAR  To:   $DST"
116	echo "$ICON_GEAR  Mode: $TO_MODE"
117	[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
118	echo ""
119
120	# ──────────────────────────────────────────────────────────────────────────────
121	# Pre-flight checks
122	if [[ "$SRC" == "$DST" ]]; then
123	    info "Already in $TO_MODE mode — nothing to do"
124	    exit 0
125	fi
126
127	if [[ "$TO_MODE" == "flash" ]]; then
128	    if ! mountpoint -q /mnt/user 2>/dev/null; then
129	        error "Array not started — /mnt/user is not mounted. Start the array before migrating to flash."
130	        exit 1
131	    fi
132	fi
133
134	if [[ ! -f "$SRC/load_config.sh" ]]; then
135	    error "Source directory looks invalid: $SRC (load_config.sh not found)"
136	    exit 1
137	fi
138
139	# ──────────────────────────────────────────────────────────────────────────────
140	# Step 1: Git push — ensure remote has everything before we touch the local repo
141	echo "━━━ $ICON_SYNC Step 1: Git push ━━━"
142	if [[ "$DRY_RUN" == false ]]; then
143	    if [[ -d "$SRC/.git" ]]; then
144	        log "Pushing to Gitea before migration..."
145	        if GIT_SSH_COMMAND="ssh -i $GITEA_SSH_KEY -p $SSH_PORT" \
146	           git -C "$SRC" push origin main 2>&1 | while IFS= read -r line; do echo "  $line"; done; then
147	            echo "  Git push complete ✅"
148	        else
149	            warn "Git push failed — continuing (data safe locally, push manually after migration)"
150	        fi
151	    else
152	        warn "No .git directory in $SRC — skipping push"
153	    fi
154	else
155	    warn "DRY RUN — would push $SRC to Gitea"
156	fi
157
158	# ──────────────────────────────────────────────────────────────────────────────
159	# Step 2: Rsync content to destination
160	echo ""
161	echo "━━━ $ICON_SYNC Step 2: Copy files ━━━"
162	if [[ "$DRY_RUN" == false ]]; then
163	    mkdir -p "$DST"
164	    echo "  rsync: $SRC/ → $DST/"
165	    if rsync -av --delete \
166	        --exclude='.git' \
167	        "$SRC/" "$DST/" 2>&1 | \
168	        grep -v "/$" | \
169	        while IFS= read -r line; do log "$line"; done; then
170	        echo "  Files copied ✅"
171	    else
172	        error "rsync failed — aborting migration"
173	        exit 1
174	    fi
175
176	    # Copy .git separately (rsync --exclude='.git' above skipped it)
177	    echo "  Copying .git..."
178	    if cp -a "$SRC/.git" "$DST/.git" 2>/dev/null || \
179	       rsync -a "$SRC/.git/" "$DST/.git/" 2>/dev/null; then
180	        echo "  .git copied ✅"
181	    else
182	        error ".git copy failed — aborting"
183	        exit 1
184	    fi
185
186	    # Mark git safe directory
187	    git config --global --add safe.directory "$DST" 2>/dev/null
188	else
189	    warn "DRY RUN — would rsync $SRC/ → $DST/ (including .git)"
190	fi
191
192	# ──────────────────────────────────────────────────────────────────────────────
193	# Step 3: Update varaverk.cfg
194	echo ""
195	echo "━━━ $ICON_GEAR Step 3: Update varaverk.cfg ━━━"
196	if [[ "$DRY_RUN" == false ]]; then
197	    if grep -q '^SCRIPTS_DIR' "$VV_CFG"; then
198	        sed -i "s|^SCRIPTS_DIR=.*|SCRIPTS_DIR=\"$DST\"|" "$VV_CFG"
199	    else
200	        echo "SCRIPTS_DIR=\"$DST\"" >> "$VV_CFG"
201	    fi
202	    echo "  SCRIPTS_DIR → $DST ✅"
203	else
204	    warn "DRY RUN — would set SCRIPTS_DIR=\"$DST\" in $VV_CFG"
205	fi
206
207	# ──────────────────────────────────────────────────────────────────────────────
208	# Step 4: Update TARGET_DIR, DATA_DIR, STATE_DIR in master.conf (new location)
209	echo ""
210	echo "━━━ $ICON_GEAR Step 4: Update master.conf paths ━━━"
211	NEW_MASTER="$DST/Configurations/master.conf"
212	if [[ "$DRY_RUN" == false ]]; then
213	    if [[ -f "$NEW_MASTER" ]]; then
214	        sed -i "s|^\(\s*TARGET_DIR\s*=\s*\)\"[^\"]*\"|\1\"$DST\"|"                              "$NEW_MASTER"
215	        sed -i "s|^\(\s*DATA_DIR\s*=\s*\)\"[^\"]*\"|\1\"$DST/data\"|"                           "$NEW_MASTER"
216	        sed -i "s|^\(\s*STATE_DIR\s*=\s*\)\"[^\"]*\"|\1\"$DST/State_Files\"|"                   "$NEW_MASTER"
217	        echo "  TARGET_DIR → $DST ✅"
218	        echo "  DATA_DIR   → $DST/data ✅"
219	        echo "  STATE_DIR  → $DST/State_Files ✅"
220	    else
221	        error "master.conf not found at $NEW_MASTER"
222	        exit 1
223	    fi
224	else
225	    warn "DRY RUN — would update TARGET_DIR, DATA_DIR, STATE_DIR in master.conf"
226	fi
227
228	# ──────────────────────────────────────────────────────────────────────────────
229	# Step 5: Update STORAGE_MODE_INTERNAL in host*.conf (new location)
230	echo ""
231	echo "━━━ $ICON_GEAR Step 5: Update STORAGE_MODE_INTERNAL ━━━"
232	NEW_CONF="$DST/Configurations/${MY_ID,,}.conf"
233	if [[ "$DRY_RUN" == false ]]; then
234	    if [[ -f "$NEW_CONF" ]]; then
235	        if grep -q "${MY_ID}_STORAGE_MODE_INTERNAL" "$NEW_CONF"; then
236	            sed -i "s|^\(\s*${MY_ID}_STORAGE_MODE_INTERNAL\s*=\s*\).*|\1${NEW_INTERNAL}|" "$NEW_CONF"
237	        else
238	            sed -i "/# ━━━ Storage mode/a\\    ${MY_ID}_STORAGE_MODE_INTERNAL=${NEW_INTERNAL}" "$NEW_CONF"
239	        fi
240	        echo "  ${MY_ID}_STORAGE_MODE_INTERNAL → $NEW_INTERNAL ✅"
241	    else
242	        warn "${MY_ID,,}.conf not found at $NEW_CONF — skipping conf update"
243	    fi
244	else
245	    warn "DRY RUN — would set ${MY_ID}_STORAGE_MODE_INTERNAL=$NEW_INTERNAL"
246	fi
247
248	# ──────────────────────────────────────────────────────────────────────────────
249	# Step 6: Rebuild cron (paths must reference new SCRIPTS_DIR)
250	echo ""
251	echo "━━━ $ICON_GEAR Step 6: Rebuild cron ━━━"
252	if [[ "$DRY_RUN" == false ]]; then
253	    RESULT=$(php -r "
254	        define('PLUGIN_CFG',  '/boot/config/plugins/varaverk/varaverk.cfg');
255	        \$_c = @parse_ini_file(PLUGIN_CFG) ?: [];
256	        define('SCRIPTS_DIR', \$_c['SCRIPTS_DIR'] ?? '/boot/config/plugins/varaverk');
257	        define('CONF_DIR',    SCRIPTS_DIR . '/Configurations');
258	        define('DATA_DIR',    SCRIPTS_DIR . '/data');
259	        define('STATE_DIR',   SCRIPTS_DIR . '/State_Files');
260	        define('LOG_DIR',     '/var/log/varaverk');
261	        define('VV_SETUP_STATE_FILE', STATE_DIR . '/varaverk_setup.db');
262	        require_once '/usr/local/emhttp/plugins/varaverk/include/confform.php';
263	        require_once '/usr/local/emhttp/plugins/varaverk/include/scheduler.php';
264	        \$ok = vv_cron_rebuild(vv_schedule_load());
265	        echo \$ok ? 'ok' : 'fail';
266	    " 2>/dev/null)
267	    if [[ "$RESULT" == "ok" ]]; then
268	        echo "  Cron rebuilt ✅"
269	    else
270	        warn "Cron rebuild failed — run Settings → Scheduler → Save to regenerate"
271	    fi
272	else
273	    warn "DRY RUN — would rebuild cron with new SCRIPTS_DIR paths"
274	fi
275
276	# ──────────────────────────────────────────────────────────────────────────────
277	# Step 7: Flash mode — sync Plugin/ to /boot/ so webUI is current
278	if [[ "$TO_MODE" == "flash" && "$DRY_RUN" == false ]]; then
279	    echo ""
280	    echo "━━━ $ICON_SYNC Step 7: Sync Plugin/ → /boot/ ━━━"
281	    if rsync -a --delete "$DST/Plugin/" "$INTERNAL_DIR/Plugin/" 2>/dev/null; then
282	        echo "  Plugin/ synced to /boot/ ✅"
283	    else
284	        warn "Plugin/ sync to /boot/ failed — webUI may be stale"
285	    fi
286	fi
287
288	# ──────────────────────────────────────────────────────────────────────────────
289	# Step 8: Delete old location
290	echo ""
291	echo "━━━ $ICON_GEAR Step $([[ "$TO_MODE" == "flash" ]] && echo 8 || echo 7): Clean up old location ━━━"
292	if [[ "$DRY_RUN" == false ]]; then
293	    if [[ "$SRC" == "$INTERNAL_DIR" ]]; then
294	        # Migrating internal→flash: keep varaverk.cfg and Plugin/ in /boot/, remove everything else
295	        echo "  Removing scripts/conf/state from /boot/ (keeping Plugin/ and varaverk.cfg)..."
296	        find "$SRC" -mindepth 1 -maxdepth 1 \
297	            ! -name 'Plugin' \
298	            ! -name 'varaverk.cfg' \
299	            ! -name '*.plg' \
300	            ! -name '*.txz' \
301	            -exec rm -rf {} + 2>/dev/null
302	        echo "  /boot/ cleaned ✅ (Plugin/ and varaverk.cfg preserved)"
303	    else
304	        # Migrating flash→internal: remove appdata copy entirely
305	        echo "  Removing $SRC..."
306	        rm -rf "$SRC"
307	        echo "  $SRC removed ✅"
308	    fi
309	else
310	    if [[ "$SRC" == "$INTERNAL_DIR" ]]; then
311	        warn "DRY RUN — would remove scripts/conf/state from /boot/ (keeping Plugin/ and varaverk.cfg)"
312	    else
313	        warn "DRY RUN — would remove $SRC"
314	    fi
315	fi
316
317	# ──────────────────────────────────────────────────────────────────────────────
318	echo ""
319	echo "━━━━━ $ICON_DONE Migration complete ━━━━━"
320	echo "$ICON_GEAR  Mode:       $TO_MODE"
321	echo "$ICON_GEAR  SCRIPTS_DIR: $DST"
322	if [[ "$TO_MODE" == "flash" ]]; then
323	    echo ""
324	    warn "IMPORTANT: Varaverk requires the array to be started to function in flash mode."
325	    warn "The webUI plugin tab will load normally at all times (Plugin/ stays in /boot/)."
326	fi
327	echo ""
328	echo "  Reload the Varaverk plugin tab to apply changes."
329	echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
330

#!/bin/bash
# ==============================================================================================
# ================================= Storage Migration ==========================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Migrates Varaverk between internal NVMe and USB flash storage modes.
#
# Internal mode: SCRIPTS_DIR = /boot/config/plugins/varaverk
#   All scripts, conf, state, and git repo live on fast internal storage.
#   Direct git pull/push. Zero write-wear concern.
#
# Flash mode:    SCRIPTS_DIR = /mnt/user/appdata/Varaverk
#   All scripts, conf, state, and git repo live in appdata.
#   Preserves USB flash lifetime. Array must be started for Varaverk to function.
#   git_pull_execute.sh syncs Plugin/ back to /boot/ after each pull so the
#   Unraid webUI always serves current PHP files.
#
# What this script updates:
#   varaverk.cfg      SCRIPTS_DIR
#   master.conf       TARGET_DIR
#   host*.conf        HOST*_STORAGE_MODE_INTERNAL
#   varaverk.cron     rebuilt via PHP (job paths regenerated for new SCRIPTS_DIR)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# storage_migrate.sh --to=internal
#     Migrate to /boot/config/plugins/varaverk
#
# storage_migrate.sh --to=flash
#     Migrate to /mnt/user/appdata/Varaverk
#
# storage_migrate.sh --dry-run --to=<mode>
#     Show what would happen — no changes made
#
# storage_migrate.sh --status
#     Show current mode, paths, and boot device info
#
# ==============================================================================================

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../../../load_config.sh"

parse_args "$@"
acquire_lock

VV_CFG="/boot/config/plugins/varaverk/varaverk.cfg"
INTERNAL_DIR="/boot/config/plugins/varaverk"
FLASH_DIR="/mnt/user/appdata/Varaverk"

# ──────────────────────────────────────────────────────────────────────────────
# Parse --to= from raw args (parse_args doesn't handle this flag)
TO_MODE=""
for arg in "$@"; do
    case "$arg" in
        --to=internal) TO_MODE="internal" ;;
        --to=flash)    TO_MODE="flash"    ;;
    esac
done

# ──────────────────────────────────────────────────────────────────────────────
# Boot device detection
detect_boot_storage() {
    local boot_part boot_disk transport
    boot_part=$(findmnt -n -o SOURCE /boot 2>/dev/null)
    boot_disk=$(lsblk -no pkname "$boot_part" 2>/dev/null)
    transport=$(lsblk -dno TRAN "/dev/$boot_disk" 2>/dev/null | tr '[:upper:]' '[:lower:]')
    echo "${transport:-unknown}"
}

# ──────────────────────────────────────────────────────────────────────────────
# Status
if [[ "$SHOW_STATUS" == true ]]; then
    TRANSPORT=$(detect_boot_storage)
    DETECTED=$([[ "$TRANSPORT" == "usb" ]] && echo "flash" || echo "internal")
    echo ""
    echo "━━━━━ $ICON_SUMMARY STORAGE STATUS ━━━━━"
    echo "$ICON_GEAR  SCRIPTS_DIR:  $SCRIPTS_DIR"
    echo "$ICON_GEAR  varaverk.cfg: $VV_CFG"
    echo "$ICON_HOST  Boot device:  transport=$TRANSPORT → detected=$DETECTED"
    echo "$ICON_GEAR  Target dirs:"
    echo "    internal: $INTERNAL_DIR"
    echo "    flash:    $FLASH_DIR"
    CONF_MODE=$(grep -m1 "${MY_ID}_STORAGE_MODE_INTERNAL" "$CONF_FILE" 2>/dev/null | cut -d= -f2 | tr -d '"' | tr -d '[:space:]')
    echo "$ICON_GEAR  conf setting: ${MY_ID}_STORAGE_MODE_INTERNAL=${CONF_MODE:-not set}"
    if [[ "$SCRIPTS_DIR" == "$INTERNAL_DIR" ]]; then
        echo "$ICON_DONE  Current mode: INTERNAL ✅"
    elif [[ "$SCRIPTS_DIR" == "$FLASH_DIR" ]]; then
        echo "$ICON_DONE  Current mode: FLASH ✅"
    else
        echo "$ICON_WARN  Current mode: CUSTOM ($SCRIPTS_DIR)"
    fi
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    exit 0
fi

# ──────────────────────────────────────────────────────────────────────────────
if [[ -z "$TO_MODE" ]]; then
    error "Usage: storage_migrate.sh --to=internal|flash [--dry-run] [--log]"
    exit 1
fi

SRC="$SCRIPTS_DIR"
DST=$([[ "$TO_MODE" == "internal" ]] && echo "$INTERNAL_DIR" || echo "$FLASH_DIR")
NEW_INTERNAL=$([[ "$TO_MODE" == "internal" ]] && echo "true" || echo "false")

log "$ICON_GEAR Config: to=${TO_MODE} src=${SRC} dst=${DST} dry-run=${DRY_RUN}"

echo ""
echo "━━━━━ $ICON_SYNC Storage Migration ━━━━━"
echo "$ICON_GEAR  From: $SRC"
echo "$ICON_GEAR  To:   $DST"
echo "$ICON_GEAR  Mode: $TO_MODE"
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
echo ""

# ──────────────────────────────────────────────────────────────────────────────
# Pre-flight checks
if [[ "$SRC" == "$DST" ]]; then
    info "Already in $TO_MODE mode — nothing to do"
    exit 0
fi

if [[ "$TO_MODE" == "flash" ]]; then
    if ! mountpoint -q /mnt/user 2>/dev/null; then
        error "Array not started — /mnt/user is not mounted. Start the array before migrating to flash."
        exit 1
    fi
fi

if [[ ! -f "$SRC/load_config.sh" ]]; then
    error "Source directory looks invalid: $SRC (load_config.sh not found)"
    exit 1
fi

# ──────────────────────────────────────────────────────────────────────────────
# Step 1: Git push — ensure remote has everything before we touch the local repo
echo "━━━ $ICON_SYNC Step 1: Git push ━━━"
if [[ "$DRY_RUN" == false ]]; then
    if [[ -d "$SRC/.git" ]]; then
        log "Pushing to Gitea before migration..."
        if GIT_SSH_COMMAND="ssh -i $GITEA_SSH_KEY -p $SSH_PORT" \
           git -C "$SRC" push origin main 2>&1 | while IFS= read -r line; do echo "  $line"; done; then
            echo "  Git push complete ✅"
        else
            warn "Git push failed — continuing (data safe locally, push manually after migration)"
        fi
    else
        warn "No .git directory in $SRC — skipping push"
    fi
else
    warn "DRY RUN — would push $SRC to Gitea"
fi

# ──────────────────────────────────────────────────────────────────────────────
# Step 2: Rsync content to destination
echo ""
echo "━━━ $ICON_SYNC Step 2: Copy files ━━━"
if [[ "$DRY_RUN" == false ]]; then
    mkdir -p "$DST"
    echo "  rsync: $SRC/ → $DST/"
    if rsync -av --delete \
        --exclude='.git' \
        "$SRC/" "$DST/" 2>&1 | \
        grep -v "/$" | \
        while IFS= read -r line; do log "$line"; done; then
        echo "  Files copied ✅"
    else
        error "rsync failed — aborting migration"
        exit 1
    fi

    # Copy .git separately (rsync --exclude='.git' above skipped it)
    echo "  Copying .git..."
    if cp -a "$SRC/.git" "$DST/.git" 2>/dev/null || \
       rsync -a "$SRC/.git/" "$DST/.git/" 2>/dev/null; then
        echo "  .git copied ✅"
    else
        error ".git copy failed — aborting"
        exit 1
    fi

    # Mark git safe directory
    git config --global --add safe.directory "$DST" 2>/dev/null
else
    warn "DRY RUN — would rsync $SRC/ → $DST/ (including .git)"
fi

# ──────────────────────────────────────────────────────────────────────────────
# Step 3: Update varaverk.cfg
echo ""
echo "━━━ $ICON_GEAR Step 3: Update varaverk.cfg ━━━"
if [[ "$DRY_RUN" == false ]]; then
    if grep -q '^SCRIPTS_DIR' "$VV_CFG"; then
        sed -i "s|^SCRIPTS_DIR=.*|SCRIPTS_DIR=\"$DST\"|" "$VV_CFG"
    else
        echo "SCRIPTS_DIR=\"$DST\"" >> "$VV_CFG"
    fi
    echo "  SCRIPTS_DIR → $DST ✅"
else
    warn "DRY RUN — would set SCRIPTS_DIR=\"$DST\" in $VV_CFG"
fi

# ──────────────────────────────────────────────────────────────────────────────
# Step 4: Update TARGET_DIR, DATA_DIR, STATE_DIR in master.conf (new location)
echo ""
echo "━━━ $ICON_GEAR Step 4: Update master.conf paths ━━━"
NEW_MASTER="$DST/Configurations/master.conf"
if [[ "$DRY_RUN" == false ]]; then
    if [[ -f "$NEW_MASTER" ]]; then
        sed -i "s|^\(\s*TARGET_DIR\s*=\s*\)\"[^\"]*\"|\1\"$DST\"|"                              "$NEW_MASTER"
        sed -i "s|^\(\s*DATA_DIR\s*=\s*\)\"[^\"]*\"|\1\"$DST/data\"|"                           "$NEW_MASTER"
        sed -i "s|^\(\s*STATE_DIR\s*=\s*\)\"[^\"]*\"|\1\"$DST/State_Files\"|"                   "$NEW_MASTER"
        echo "  TARGET_DIR → $DST ✅"
        echo "  DATA_DIR   → $DST/data ✅"
        echo "  STATE_DIR  → $DST/State_Files ✅"
    else
        error "master.conf not found at $NEW_MASTER"
        exit 1
    fi
else
    warn "DRY RUN — would update TARGET_DIR, DATA_DIR, STATE_DIR in master.conf"
fi

# ──────────────────────────────────────────────────────────────────────────────
# Step 5: Update STORAGE_MODE_INTERNAL in host*.conf (new location)
echo ""
echo "━━━ $ICON_GEAR Step 5: Update STORAGE_MODE_INTERNAL ━━━"
NEW_CONF="$DST/Configurations/${MY_ID,,}.conf"
if [[ "$DRY_RUN" == false ]]; then
    if [[ -f "$NEW_CONF" ]]; then
        if grep -q "${MY_ID}_STORAGE_MODE_INTERNAL" "$NEW_CONF"; then
            sed -i "s|^\(\s*${MY_ID}_STORAGE_MODE_INTERNAL\s*=\s*\).*|\1${NEW_INTERNAL}|" "$NEW_CONF"
        else
            sed -i "/# ━━━ Storage mode/a\\    ${MY_ID}_STORAGE_MODE_INTERNAL=${NEW_INTERNAL}" "$NEW_CONF"
        fi
        echo "  ${MY_ID}_STORAGE_MODE_INTERNAL → $NEW_INTERNAL ✅"
    else
        warn "${MY_ID,,}.conf not found at $NEW_CONF — skipping conf update"
    fi
else
    warn "DRY RUN — would set ${MY_ID}_STORAGE_MODE_INTERNAL=$NEW_INTERNAL"
fi

# ──────────────────────────────────────────────────────────────────────────────
# Step 6: Rebuild cron (paths must reference new SCRIPTS_DIR)
echo ""
echo "━━━ $ICON_GEAR Step 6: Rebuild cron ━━━"
if [[ "$DRY_RUN" == false ]]; then
    RESULT=$(php -r "
        define('PLUGIN_CFG',  '/boot/config/plugins/varaverk/varaverk.cfg');
        \$_c = @parse_ini_file(PLUGIN_CFG) ?: [];
        define('SCRIPTS_DIR', \$_c['SCRIPTS_DIR'] ?? '/boot/config/plugins/varaverk');
        define('CONF_DIR',    SCRIPTS_DIR . '/Configurations');
        define('DATA_DIR',    SCRIPTS_DIR . '/data');
        define('STATE_DIR',   SCRIPTS_DIR . '/State_Files');
        define('LOG_DIR',     '/var/log/varaverk');
        define('VV_SETUP_STATE_FILE', STATE_DIR . '/varaverk_setup.db');
        require_once '/usr/local/emhttp/plugins/varaverk/include/confform.php';
        require_once '/usr/local/emhttp/plugins/varaverk/include/scheduler.php';
        \$ok = vv_cron_rebuild(vv_schedule_load());
        echo \$ok ? 'ok' : 'fail';
    " 2>/dev/null)
    if [[ "$RESULT" == "ok" ]]; then
        echo "  Cron rebuilt ✅"
    else
        warn "Cron rebuild failed — run Settings → Scheduler → Save to regenerate"
    fi
else
    warn "DRY RUN — would rebuild cron with new SCRIPTS_DIR paths"
fi

# ──────────────────────────────────────────────────────────────────────────────
# Step 7: Flash mode — sync Plugin/ to /boot/ so webUI is current
if [[ "$TO_MODE" == "flash" && "$DRY_RUN" == false ]]; then
    echo ""
    echo "━━━ $ICON_SYNC Step 7: Sync Plugin/ → /boot/ ━━━"
    if rsync -a --delete "$DST/Plugin/" "$INTERNAL_DIR/Plugin/" 2>/dev/null; then
        echo "  Plugin/ synced to /boot/ ✅"
    else
        warn "Plugin/ sync to /boot/ failed — webUI may be stale"
    fi
fi

# ──────────────────────────────────────────────────────────────────────────────
# Step 8: Delete old location
echo ""
echo "━━━ $ICON_GEAR Step $([[ "$TO_MODE" == "flash" ]] && echo 8 || echo 7): Clean up old location ━━━"
if [[ "$DRY_RUN" == false ]]; then
    if [[ "$SRC" == "$INTERNAL_DIR" ]]; then
        # Migrating internal→flash: keep varaverk.cfg and Plugin/ in /boot/, remove everything else
        echo "  Removing scripts/conf/state from /boot/ (keeping Plugin/ and varaverk.cfg)..."
        find "$SRC" -mindepth 1 -maxdepth 1 \
            ! -name 'Plugin' \
            ! -name 'varaverk.cfg' \
            ! -name '*.plg' \
            ! -name '*.txz' \
            -exec rm -rf {} + 2>/dev/null
        echo "  /boot/ cleaned ✅ (Plugin/ and varaverk.cfg preserved)"
    else
        # Migrating flash→internal: remove appdata copy entirely
        echo "  Removing $SRC..."
        rm -rf "$SRC"
        echo "  $SRC removed ✅"
    fi
else
    if [[ "$SRC" == "$INTERNAL_DIR" ]]; then
        warn "DRY RUN — would remove scripts/conf/state from /boot/ (keeping Plugin/ and varaverk.cfg)"
    else
        warn "DRY RUN — would remove $SRC"
    fi
fi

# ──────────────────────────────────────────────────────────────────────────────
echo ""
echo "━━━━━ $ICON_DONE Migration complete ━━━━━"
echo "$ICON_GEAR  Mode:       $TO_MODE"
echo "$ICON_GEAR  SCRIPTS_DIR: $DST"
if [[ "$TO_MODE" == "flash" ]]; then
    echo ""
    warn "IMPORTANT: Varaverk requires the array to be started to function in flash mode."
    warn "The webUI plugin tab will load normally at all times (Plugin/ stays in /boot/)."
fi
echo ""
echo "  Reload the Varaverk plugin tab to apply changes."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"


1	<?php
2	header('Content-Type: application/json');
3	require_once dirname(__DIR__) . '/include/config.php';
4
5	$action = $_GET['action'] ?? $_POST['action'] ?? '';
6
7	// ── Boot device detection ─────────────────────────────────────────────────────
8	function vv_storage_detect_transport(): string {
9	    $part = trim(shell_exec("findmnt -n -o SOURCE /boot 2>/dev/null") ?: '');
10	    if (!$part) return 'unknown';
11	    $disk = trim(shell_exec("lsblk -no pkname " . escapeshellarg($part) . " 2>/dev/null") ?: '');
12	    if (!$disk) return 'unknown';
13	    return strtolower(trim(shell_exec("lsblk -dno TRAN /dev/" . escapeshellarg($disk) . " 2>/dev/null") ?: 'unknown'));
14	}
15
16	// ── Current mode status ───────────────────────────────────────────────────────
17	if ($action === 'status') {
18	    $transport   = vv_storage_detect_transport();
19	    $detected    = ($transport === 'usb') ? 'flash' : 'internal';
20	    $currentDir  = SCRIPTS_DIR;
21	    $internalDir = '/boot/config/plugins/varaverk';
22	    $flashDir    = '/mnt/user/appdata/Varaverk';
23	    $currentMode = ($currentDir === $internalDir) ? 'internal'
24	                 : ($currentDir === $flashDir     ? 'flash' : 'custom');
25
26	    $myHost  = vv_detect_host();
27	    $vars    = vv_conf_vars();
28	    $confKey = strtoupper($myHost) . '_STORAGE_MODE_INTERNAL';
29	    $confVal = $vars[$confKey] ?? null;
30
31	    // Boot device name for display
32	    $bootPart = trim(shell_exec("findmnt -n -o SOURCE /boot 2>/dev/null") ?: '');
33	    $bootDisk = $bootPart ? trim(shell_exec("lsblk -no pkname " . escapeshellarg($bootPart) . " 2>/dev/null") ?: '') : '';
34
35	    echo json_encode([
36	        'ok'           => true,
37	        'current_mode' => $currentMode,
38	        'current_dir'  => $currentDir,
39	        'internal_dir' => $internalDir,
40	        'flash_dir'    => $flashDir,
41	        'transport'    => $transport,
42	        'detected'     => $detected,
43	        'boot_disk'    => $bootDisk ? '/dev/' . $bootDisk : 'unknown',
44	        'conf_key'     => $confKey,
45	        'conf_val'     => $confVal,
46	        'array_started'=> is_dir('/mnt/user') && count(scandir('/mnt/user')) > 2,
47	    ]);
48	    exit;
49	}
50
51	// ── Run migration ─────────────────────────────────────────────────────────────
52	if ($action === 'migrate' && $_SERVER['REQUEST_METHOD'] === 'POST') {
53	    $to = trim($_POST['to'] ?? '');
54	    if (!in_array($to, ['internal', 'flash'], true)) {
55	        echo json_encode(['ok' => false, 'error' => 'Invalid target: must be internal or flash']);
56	        exit;
57	    }
58
59	    $script = dirname(__DIR__) . '/Tools/storage_migrate.sh';
60	    if (!file_exists($script)) {
61	        echo json_encode(['ok' => false, 'error' => 'storage_migrate.sh not found']);
62	        exit;
63	    }
64
65	    set_time_limit(300);
66	    $output = [];
67	    $exit   = 0;
68	    exec('bash ' . escapeshellarg($script) . ' --to=' . escapeshellarg($to) . ' 2>&1', $output, $exit);
69
70	    echo json_encode([
71	        'ok'     => $exit === 0,
72	        'exit'   => $exit,
73	        'output' => implode("\n", $output),
74	    ]);
75	    exit;
76	}
77
78	// ── Auto-detect and write to conf ─────────────────────────────────────────────
79	if ($action === 'detect' && $_SERVER['REQUEST_METHOD'] === 'POST') {
80	    $transport = vv_storage_detect_transport();
81	    $detected  = ($transport === 'usb') ? 'false' : 'true';
82	    $myHost    = vv_detect_host();
83	    $confKey   = strtoupper($myHost) . '_STORAGE_MODE_INTERNAL';
84	    $confFile  = $myHost . '.conf';
85
86	    $results = vv_conf_write_changes([[
87	        'file'  => $confFile,
88	        'key'   => $confKey,
89	        'value' => $detected,
90	        'type'  => 'scalar',
91	    ]]);
92
93	    $ok = !in_array(false, $results, true);
94	    echo json_encode(['ok' => $ok, 'detected' => $detected, 'transport' => $transport]);
95	    exit;
96	}
97
98	// ── Unraid API key status ─────────────────────────────────────────────────────
99	if ($action === 'api_status') {
100	    require_once dirname(__DIR__) . '/include/unraid_api.php';
101	    $localStatus = vv_api_get_status();
102	    $vars        = vv_conf_vars();
103	    $myHost      = vv_detect_host();
104	    $myId        = strtoupper($myHost);
105	    $hn          = trim((string)shell_exec("hostname -s 2>/dev/null | sed 's/^[Uu][Nn][Rr][Aa][Ii][Dd]-//'")) ?: 'Varaverk';
106	    $localKeyName = 'Varaverk ' . $hn;
107
108	    $hosts = [];
109	    foreach ($vars as $k => $v) {
110	        if (!preg_match('/^HOST(\d+)$/', $k, $m) || !$v) continue;
111	        $id      = 'HOST' . $m[1];
112	        $keyVar  = $id . '_UNRAID_API_KEY';
113	        $key     = $vars[$keyVar] ?? '';
114	        $isLocal = ($id === $myId);
115	        $hosts[] = [
116	            'host_id'     => $id,
117	            'hostname'    => $v,
118	            'is_local'    => $isLocal,
119	            'key_var'     => $keyVar,
120	            'key_present' => !empty($key),
121	            'key_preview' => $key ? substr($key, 0, 8) . '...' . substr($key, -4) : null,
122	            'api_ok'      => $isLocal
123	                             ? (!$localStatus['key_missing'] && $localStatus['available'])
124	                             : !empty($key),
125	        ];
126	    }
127	    usort($hosts, fn($a, $b) => strcmp($a['host_id'], $b['host_id']));
128
129	    echo json_encode([
130	        'ok'        => true,
131	        'my_id'     => $myId,
132	        'key_name'  => $localKeyName,
133	        'hosts'     => $hosts,
134	        'fallbacks' => $localStatus['fallbacks'],
135	    ]);
136	    exit;
137	}
138
139	// ── Setup/renew API keys (local + all partners via SSH) ───────────────────────
140	if ($action === 'setup_apikeys' && $_SERVER['REQUEST_METHOD'] === 'POST') {
141	    $script = SCRIPTS_DIR . '/Plugin/unraid/System_Essentials/unraid_api_key_renew.sh';
142	    if (!file_exists($script)) {
143	        echo json_encode(['ok' => false, 'error' => 'unraid_api_key_renew.sh not found']); exit;
144	    }
145	    set_time_limit(60);
146	    $output = []; $exit = 0;
147	    exec('bash ' . escapeshellarg($script) . ' 2>&1', $output, $exit);
148	    echo json_encode(['ok' => $exit === 0, 'exit' => $exit, 'output' => implode("\n", $output)]);
149	    exit;
150	}
151
152	echo json_encode(['ok' => false, 'error' => 'Unknown action']);
153

<?php
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';

$action = $_GET['action'] ?? $_POST['action'] ?? '';

// ── Boot device detection ─────────────────────────────────────────────────────
function vv_storage_detect_transport(): string {
    $part = trim(shell_exec("findmnt -n -o SOURCE /boot 2>/dev/null") ?: '');
    if (!$part) return 'unknown';
    $disk = trim(shell_exec("lsblk -no pkname " . escapeshellarg($part) . " 2>/dev/null") ?: '');
    if (!$disk) return 'unknown';
    return strtolower(trim(shell_exec("lsblk -dno TRAN /dev/" . escapeshellarg($disk) . " 2>/dev/null") ?: 'unknown'));
}

// ── Current mode status ───────────────────────────────────────────────────────
if ($action === 'status') {
    $transport   = vv_storage_detect_transport();
    $detected    = ($transport === 'usb') ? 'flash' : 'internal';
    $currentDir  = SCRIPTS_DIR;
    $internalDir = '/boot/config/plugins/varaverk';
    $flashDir    = '/mnt/user/appdata/Varaverk';
    $currentMode = ($currentDir === $internalDir) ? 'internal'
                 : ($currentDir === $flashDir     ? 'flash' : 'custom');

    $myHost  = vv_detect_host();
    $vars    = vv_conf_vars();
    $confKey = strtoupper($myHost) . '_STORAGE_MODE_INTERNAL';
    $confVal = $vars[$confKey] ?? null;

    // Boot device name for display
    $bootPart = trim(shell_exec("findmnt -n -o SOURCE /boot 2>/dev/null") ?: '');
    $bootDisk = $bootPart ? trim(shell_exec("lsblk -no pkname " . escapeshellarg($bootPart) . " 2>/dev/null") ?: '') : '';

    echo json_encode([
        'ok'           => true,
        'current_mode' => $currentMode,
        'current_dir'  => $currentDir,
        'internal_dir' => $internalDir,
        'flash_dir'    => $flashDir,
        'transport'    => $transport,
        'detected'     => $detected,
        'boot_disk'    => $bootDisk ? '/dev/' . $bootDisk : 'unknown',
        'conf_key'     => $confKey,
        'conf_val'     => $confVal,
        'array_started'=> is_dir('/mnt/user') && count(scandir('/mnt/user')) > 2,
    ]);
    exit;
}

// ── Run migration ─────────────────────────────────────────────────────────────
if ($action === 'migrate' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    $to = trim($_POST['to'] ?? '');
    if (!in_array($to, ['internal', 'flash'], true)) {
        echo json_encode(['ok' => false, 'error' => 'Invalid target: must be internal or flash']);
        exit;
    }

    $script = dirname(__DIR__) . '/Tools/storage_migrate.sh';
    if (!file_exists($script)) {
        echo json_encode(['ok' => false, 'error' => 'storage_migrate.sh not found']);
        exit;
    }

    set_time_limit(300);
    $output = [];
    $exit   = 0;
    exec('bash ' . escapeshellarg($script) . ' --to=' . escapeshellarg($to) . ' 2>&1', $output, $exit);

    echo json_encode([
        'ok'     => $exit === 0,
        'exit'   => $exit,
        'output' => implode("\n", $output),
    ]);
    exit;
}

// ── Auto-detect and write to conf ─────────────────────────────────────────────
if ($action === 'detect' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    $transport = vv_storage_detect_transport();
    $detected  = ($transport === 'usb') ? 'false' : 'true';
    $myHost    = vv_detect_host();
    $confKey   = strtoupper($myHost) . '_STORAGE_MODE_INTERNAL';
    $confFile  = $myHost . '.conf';

    $results = vv_conf_write_changes([[
        'file'  => $confFile,
        'key'   => $confKey,
        'value' => $detected,
        'type'  => 'scalar',
    ]]);

    $ok = !in_array(false, $results, true);
    echo json_encode(['ok' => $ok, 'detected' => $detected, 'transport' => $transport]);
    exit;
}

// ── Unraid API key status ─────────────────────────────────────────────────────
if ($action === 'api_status') {
    require_once dirname(__DIR__) . '/include/unraid_api.php';
    $localStatus = vv_api_get_status();
    $vars        = vv_conf_vars();
    $myHost      = vv_detect_host();
    $myId        = strtoupper($myHost);
    $hn          = trim((string)shell_exec("hostname -s 2>/dev/null | sed 's/^[Uu][Nn][Rr][Aa][Ii][Dd]-//'")) ?: 'Varaverk';
    $localKeyName = 'Varaverk ' . $hn;

    $hosts = [];
    foreach ($vars as $k => $v) {
        if (!preg_match('/^HOST(\d+)$/', $k, $m) || !$v) continue;
        $id      = 'HOST' . $m[1];
        $keyVar  = $id . '_UNRAID_API_KEY';
        $key     = $vars[$keyVar] ?? '';
        $isLocal = ($id === $myId);
        $hosts[] = [
            'host_id'     => $id,
            'hostname'    => $v,
            'is_local'    => $isLocal,
            'key_var'     => $keyVar,
            'key_present' => !empty($key),
            'key_preview' => $key ? substr($key, 0, 8) . '...' . substr($key, -4) : null,
            'api_ok'      => $isLocal
                             ? (!$localStatus['key_missing'] && $localStatus['available'])
                             : !empty($key),
        ];
    }
    usort($hosts, fn($a, $b) => strcmp($a['host_id'], $b['host_id']));

    echo json_encode([
        'ok'        => true,
        'my_id'     => $myId,
        'key_name'  => $localKeyName,
        'hosts'     => $hosts,
        'fallbacks' => $localStatus['fallbacks'],
    ]);
    exit;
}

// ── Setup/renew API keys (local + all partners via SSH) ───────────────────────
if ($action === 'setup_apikeys' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    $script = SCRIPTS_DIR . '/Plugin/unraid/System_Essentials/unraid_api_key_renew.sh';
    if (!file_exists($script)) {
        echo json_encode(['ok' => false, 'error' => 'unraid_api_key_renew.sh not found']); exit;
    }
    set_time_limit(60);
    $output = []; $exit = 0;
    exec('bash ' . escapeshellarg($script) . ' 2>&1', $output, $exit);
    echo json_encode(['ok' => $exit === 0, 'exit' => $exit, 'output' => implode("\n", $output)]);
    exit;
}

echo json_encode(['ok' => false, 'error' => 'Unknown action']);


Now let me check the setup wizard to see where the storage mode question appears.