App containers were defaulting to a non-local timezone (e.g. BST), showing the
wrong local time in Steam — and a skewed clock can stall EA App's installer.
The kernel clock is shared with the host, so this is a display/TZ issue: fix
it by setting each app's TZ env var, sourced from SITE_TZ at install and from
WOLF_TZ in .env for ./manage.sh apps.
Also generalize the array updater to rewrite both `mounts` and `env` (Wolf
reformats either as multi-line), so re-running apps updates the TZ on
already-installed entries without corrupting the TOML.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
/etc/localtime and /etc/timezone are bind-mounted read-only so the Steam
container shows the correct local time. A wrong clock causes EA App install
scripts to hang indefinitely (SSL/token validation against EA's servers fails
when the client clock is skewed).
Fix the host clock first: sudo timedatectl set-ntp true
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
The GoW firefox image launches `firefox` with no URL and ignores START_URL,
so add-web tiles opened a blank default tab. Instead generate a per-app
Firefox enterprise policy (policies.json) that sets the homepage to the
target URL and bind-mount it over /etc/firefox/policies/policies.json so the
site loads on launch.
Also make add-web replace an existing tile of the same name instead of
erroring, so re-running it repairs a previously broken tile in place.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
Wolf rewrites config.toml on every start and reformats `mounts` as a
multi-line TOML array. update_mounts() only replaced the single line
starting with `mounts =`, leaving the remaining array elements and the
closing `]` orphaned — producing invalid TOML ("Expected '=' after a key")
that crashed Wolf on the next start. This bit any re-run of the installer
(or `./manage.sh apps`) against a config Wolf had already reformatted.
Replace the entire array (from `mounts =` to its closing `]`), and also
clean up orphaned remnants left by a previously corrupted single-line
rewrite so re-running the script repairs a broken config in place.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
Prompts for a display name and URL, auto-fetches the site's apple-touch-icon
or favicon.png, falls back to a manual URL/local-PNG prompt, then injects a
[[profiles.apps]] block using ghcr.io/games-on-whales/firefox:edge with
START_URL set to the target URL. Wolf's virtual gamepad injection means
controller-aware sites (Gamepad API) work naturally through the stream.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
Moonlight shows app tiles in config.toml order with no in-client reordering.
Add ./manage.sh reorder: lists the current [[profiles.apps]] blocks by title,
prompts for a new order (read from /dev/tty since stdin is the heredoc),
rewrites config.toml preserving each block's nested runner subtable and
spacing, then restarts Wolf. Updated help text and README.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
docker compose reads variables from .env automatically. Use ${WOLF_STATE_DIR}
in docker-compose.yml so the state folder path is dynamic — editing .env is
enough to relocate it without regenerating the compose file. IP/MAC/render
node stay baked in (hardware-specific, not in .env).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
The right fix, finally. Instead of fighting Steam's libraryfolders.vdf or
symlinking individual app session homes, point Wolf's entire state folder at
the game drive. Steam's install, downloaded games, and Proton prefixes are
born there because Wolf creates every app's session home under
HOST_APPS_STATE_FOLDER.
- docker-compose.yml: mount $WOLF_STATE_DIR (=<game_dir>/wolf-state) at the
SAME path inside and outside the wolf container, set HOST_APPS_STATE_FOLDER
and WOLF_CFG_FOLDER to it. Same-path mount is required because Wolf hands
host paths to the Docker daemon when spawning sibling app containers.
- config.toml now lives at $WOLF_STATE_DIR/cfg/config.toml; all WOLF_CFG
references updated (install + manage.sh, via WOLF_STATE_DIR in .env).
- Optional migration: move existing /etc/wolf data onto the drive on upgrade.
- Removed all symlink machinery: relocate_steam_home, steam-home dir,
_relocate_steam_home in manage.sh, and the fix-perms relocation step.
fix-perms now just re-owns the state folder + game storage.
- Dropped the redundant Steam app mount; updated README/summary text.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
Seeding libraryfolders.vdf never stuck: Steam rewrites that file on every
launch and forces its primary library back to its own install dir
(/home/retro/.local/share/Steam), which is the Wolf session home on the OS
drive. That's why Steam Storage kept showing /home/retro with the OS drive's
size no matter how many times we seeded the vdf.
New approach: relocate_steam_home symlinks each Wolf Steam session home
(/etc/wolf/<id>/Steam, bind-mounted to /home/retro) at
$GAME_STORAGE_DIR/steam-home. Steam's one and only library is then the game
drive, so installs, game files, and Proton prefixes all land there and the
Storage screen reports the game drive's free space. An existing Steam install
is moved over once (no re-download) before the symlink is created.
Wired into first install, manage.sh start (self-heals on reboot), and
manage.sh fix-perms. Dropped the redundant /mnt/games/steam mount and the
vdf seeding everywhere; updated the README heredoc with the one-time
connect / fix-perms / reconnect sequence.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
After app injection + docker compose restart, Wolf creates per-app session
dirs under /etc/wolf/<id>/Steam. Wait up to 30s for those dirs to appear,
then copy the pre-built libraryfolders.vdf into each one so Steam sees the
game storage location on its very first launch — no manual setup needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
Wolf creates /etc/wolf/<id>/Steam/ session dirs when the container starts,
not at install time. Previously the user had to manually run fix-perms after
first start to propagate libraryfolders.vdf.
manage.sh start now calls _seed_steam_library() after docker compose up,
which copies libraryfolders.vdf from game storage into any Wolf session dir
that doesn't have it yet — so the correct library path (/mnt/games/steam)
is in place before the first Moonlight connection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
Steam reads libraryfolders.vdf from /home/retro/.local/share/Steam/steamapps/
which Wolf serves from /etc/wolf/<session-id>/Steam/. Seeding it only in
/mnt/games/steam/steamapps/ was ignored since Steam doesn't know to look there.
- Seed the VDF into any existing /etc/wolf/<id>/Steam session dirs at install time
- manage.sh fix-perms now propagates the VDF from game storage into every
Wolf session dir so running fix-perms is the one-command remedy on existing
installs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
The drive picker enumerated mounts with `lsblk MOUNTPOINT | df` round-trips,
which could miss a whole-disk mount (e.g. an nvme formatted directly with no
partition table). Switch to `lsblk -Pno NAME,MOUNTPOINT,SIZE,LABEL,UUID` and
parse the key="value" pairs directly — handles whole-disk mounts and empty
LABEL/UUID fields reliably, so every mounted data drive appears as a numbered
choice.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
Creates steamapps/libraryfolders.vdf pointing at /mnt/games/steam during
install so Steam uses the correct library path without needing the user to
navigate Settings → Storage on first launch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
Wolf mounts each app's home dir from /etc/wolf/<session-id>/<App> into the
container as /home/retro. If anything there is root-owned (e.g. left over
from an earlier mount layout), the in-container retro user (uid 1000) hits
"Permission denied" creating ~/.steam and the app exits on launch.
- Add `./manage.sh fix-perms`: chowns every /etc/wolf/<id>/ app home dir
and the on-disk game storage to uid/gid 1000
- Document the /mnt/games mount layout, first-Steam-launch library step,
3DS/Azahar emulator path, and the fix-perms troubleshooting flow in the
generated README
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
Mounting into /home/retro subpaths causes Docker to create intermediate
directories (/.local/share/Steam etc.) as root-owned in the container
overlay before init runs. The GoW init chown step doesn't fully fix these,
so the retro user hits Permission denied on first write.
Move all mounts to neutral /mnt/games/* paths that don't interfere with
the container's home directory setup:
steam → /mnt/games/steam (add as Steam Library Folder via UI)
lutris → /mnt/games/lutris
kodi → /mnt/games/kodi
firefox → /mnt/games/firefox
minecraft → /mnt/games/minecraft
saves → /mnt/games/saves
emulators → /mnt/games/emulators
ROMs stay at /ROMs (GoW ES-DE hardcodes that path).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
Mounting steam:/home/retro conflicted with the GoW Steam image's own
internal /home/retro mount. Mount steam:/home/retro/.local/share/Steam
instead — this is where steamapps/ and Proton prefixes actually live,
persists all game data, and doesn't collide.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
- Steam mount was steam:/home/retro/.steam — games download to
/home/retro/.local/share/Steam/steamapps/ which was NOT mounted and
lived in the ephemeral container layer. Fixed to steam:/home/retro
so the full home dir (library, user data, Proton prefixes) persists.
- Add emulators/ subdir to game storage layout; mounted into ES-DE
container at /home/retro/Applications (ES-DE's default AppImage search path)
- Add optional Azahar (open-source Citra fork) AppImage download at
install time — fetches latest release from GitHub, chmod +x, places
in emulators/ where ES-DE finds it automatically for nintendo3ds ROMs
- Update README to document new storage layout and 3DS emulation setup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
- Drive picker: numbered list with UUID display, mounted + unmounted drives,
explicit system-path exclusions (no regex bare-/ bug), skip whole disks
with mounted partitions
- Pre-create ES-DE ROM system directories (~50 systems) so ES-DE detects
all platforms on first launch
- App selection at install time: numbered menu for Steam, ES-DE, Lutris,
RetroArch, Prism Launcher, Kodi, Firefox, Desktop
- Python catalog-driven TOML injector shared between install and manage.sh;
supports update_mounts() to patch paths on re-run without duplicating entries
- manage.sh `apps` command replaces `add-apps` / `update-storage`: shows
installed apps, interactive picker, updates game storage path from .env
- Game storage written to ~/docker/wolf/.env as GAME_STORAGE_DIR
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
^(/|/boot|...) — the bare / alternate matches every absolute path,
so every non-home mount was silently skipped. Replace with explicit
glob checks so only root, system paths, and [SWAP] are excluded.
Fixes /home/user/drives/1tb-space (and any other secondary mount)
not appearing in the numbered list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
- Display UUID under each mounted and unmounted entry so users can
cross-reference with fstab / blkid output
- Fix detection bug: a whole disk (e.g. sdb) with a mounted partition
(sdb1) was incorrectly listed as unmounted; now checks lsblk children
before including a disk in the unmounted list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
Replace the free-text path prompt with an interactive menu:
- Lists mounted non-system drives as numbered choices (1, 2, ...)
- Each shows mount point, drive label/name, total size, and free space
- Default path is <selected-mountpoint>/games
- Unmounted drives shown as U1, U2, ... (script formats + mounts them)
- Option c for a fully custom path
- Final confirm/edit step lets user tweak the subdir before committing
- Unattended mode defaults to ~/games
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
- Write GAME_STORAGE_DIR to ~/docker/wolf/.env so the chosen path is
visible and editable after setup
- Fix inline Python injection to UPDATE existing Steam/ES-DE mounts in
config.toml instead of silently skipping when entries already exist
(previously re-running setup with a new path had no effect)
- Add update-storage subcommand to manage.sh so the path can be changed
post-install without a full re-run: ./manage.sh update-storage /new/path
- Same fix applied to add-apps in manage.sh
- chown game storage dir to ACTUAL_USER after creating subdirectories
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
New site config key: CADDY_REMOTE_HOST (set via 'sudo ./setup.sh configure').
When set, configure_caddy_for_service operates in "remote" mode instead of
writing to a local Caddyfile:
- Upstream uses CADDY_REMOTE_HOST:PORT (host IP, not container name)
- Snippet saved to ~/docker/caddy-snippets/<subdomain>.caddy
- User is shown scp/rsync commands to copy it to the Caddy machine
Three modes in configure_caddy_for_service (lib/common.sh and inline stubs):
local: ~/docker/caddy/ exists → write Caddyfile + reload (existing behavior)
remote: CADDY_REMOTE_HOST set → save snippet, print copy instructions
none: neither configured → silent return (unchanged)
All 31 service standalone bootstrap stubs updated with the new logic.
CADDY_REMOTE_HOST global added to all 42 standalone bootstrap sections.
setup.sh configure now prompts for CADDY_REMOTE_HOST with a clear explanation.
wolf.sh: add missing stubs (configure_caddy_for_service, write_readme,
prompt_yn, ensure_docker_dir_ownership) and the Authelia/Caddy/start calls
that were missing from the install function.
https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt
New site config key: CADDY_REMOTE_HOST (set via 'sudo ./setup.sh configure').
When set, configure_caddy_for_service operates in "remote" mode instead of
writing to a local Caddyfile:
- Upstream uses CADDY_REMOTE_HOST:PORT (host IP, not container name)
- Snippet saved to ~/docker/caddy-snippets/<subdomain>.caddy
- User is shown scp/rsync commands to copy it to the Caddy machine
Three modes in configure_caddy_for_service (lib/common.sh and inline stubs):
local: ~/docker/caddy/ exists → write Caddyfile + reload (existing behavior)
remote: CADDY_REMOTE_HOST set → save snippet, print copy instructions
none: neither configured → silent return (unchanged)
All 31 service standalone bootstrap stubs updated with the new logic.
CADDY_REMOTE_HOST global added to all 42 standalone bootstrap sections.
setup.sh configure now prompts for CADDY_REMOTE_HOST with a clear explanation.
wolf.sh: add missing stubs (configure_caddy_for_service, write_readme,
prompt_yn, ensure_docker_dir_ownership) and the Authelia/Caddy/start calls
that were missing from the install function.
https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt
- wolf.sh: add Authelia SSO check, configure_caddy_for_service, write_readme,
and prompt to start Wolf (it had none of these)
- homeassistant.sh: add write_readme with access URL and manage commands
- js99er.sh: add write_readme with access URL and manage commands
- minecraft.sh: add write_readme with manage and backup commands
Completes the service audit — all 49 services now have full interactive setup,
standalone bootstrap, and self-documenting README in the deploy directory.
https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt
Each service can now be run directly with sudo bash <service>.sh on any
machine with Docker installed, without needing the full post-install repo.
Uses the shared bootstrap pattern from docs/standalone-template.sh.
All 42 applicable service files now support standalone execution.
https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt
Each service can now be run directly with sudo bash <service>.sh on any
machine with Docker installed, without needing the full post-install repo.
Uses the shared bootstrap pattern from docs/standalone-template.sh.
All 42 applicable service files now support standalone execution.
https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt