anki-progress.sh and its embedded app.py assumed a family/kids use case
(dashboard title, ntfy topic default, Authelia warning text, comments)
that was never actually stated — nothing in this repo should assume who
the accounts belong to. Retitled to plain "Anki Progress" throughout,
default ntfy topic changed from family-anki to anki-progress, and every
"family member" reference reworded to "account".
Also adds tools/anki-deck-math.py, tools/anki-deck-periodic.py, and
tools/anki-deck-visual.py — the Anki deck-generation scripts developed
earlier in this session, now committed as standalone, self-documented
tools (same tools/*.{sh,py} convention as tools/dedupe-finder.py) rather
than living only in chat. Each script's own header docstring carries the
full one-time setup (venv, genanki + piper-tts, downloading a voice) and
usage — anki-deck-periodic.py and anki-deck-visual.py point back to
anki-deck-math.py's copy rather than repeating it three times. Content is
generic (multiplication/division/addition/subtraction/fractions/decimals,
the periodic table, shapes/clocks/coin-counting) — nothing here assumes
who's using it or why.
Re-verified after the rename: the embedded app.py still passes its full
logic test suite once written out by the installer, and all three
tools/anki-deck-*.py scripts still build correct decks under
--dry-run-tts after their docstrings were rewritten.
Adds a README.md section pointing at the three scripts, and updates the
anki-progress Services table entry to drop "family" from its wording.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DyceEVVeQ33EeS6C1PDv5
Read-only dashboard + ntfy notifications for an anki-sync-server
instance, following this repo's standard Docker-service template
(multi-instance, port scanning, DRY_RUN, update/fresh/cancel).
Dashboard shows reviews today/this week, accuracy, and current streak
per account. A background loop detects when a study session starts
(first review after a configurable inactivity gap, default 30 min) and
sends one ntfy notification a configurable delay later (default 10 min,
per request) if the session is still going — not on every review, and
not twice for the same session.
Reads every account's collection.anki2 with SQLite's read-only mode
(file:...?mode=ro) — never opens for write, so it can't corrupt or lock
out the live sync server or a syncing client. Verified this concurrently
against a real writer with no lock conflict, plus the streak/session/
notify-state logic against synthetic review timelines covering gapped
streaks, multi-session boundaries, and the no-duplicate-notification
requirement, before ever writing the installer around it.
Requires an anki-sync-server instance (hard dependency, checked at
install time, chains only that one direction per this repo's
"Chaining into another service" convention) and auto-detects a local
ntfy install to reach it directly over caddy_net instead of requiring
a public URL. Follows security-dashboard.sh's Authelia pattern: local
Authelia used automatically, remote Authelia offered otherwise, since
this exposes every family member's personal study activity.
Updates the Services table in README.md per the three-step rule.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DyceEVVeQ33EeS6C1PDv5
afrima/anki-sync-server is built on gcr.io/distroless/static-debian12:
nonroot — the process always runs as that image's fixed nonroot UID/GID
(65532), never as ACTUAL_USER, and distroless ships no shell so nothing
inside the container can chown its own data dir at startup.
The installer's final chown gave the whole instance directory to
ACTUAL_USER, including ./data, which the container then can't write to
— it fails outright the moment it tries to create anything under /data
(e.g. a new user's collection), not just at sync time. Confirmed live.
Fix: re-chown ./data to 65532:65532 specifically, applied *after* the
existing ACTUAL_USER chown (not before — that call recurses over the
whole instance dir and would just clobber it). docker-compose.yml/.env/
README.md stay owned by ACTUAL_USER as before. The same fix is applied
in the "update" path so an already-broken existing install self-heals
on the next non-destructive update, without touching its port, Caddy
config, or accounts.
Verified live: fresh install now leaves data/ owned by 65532:65532
while the rest of the instance dir stays ACTUAL_USER; simulating a
pre-fix broken install (data/ owned by root) and running "update"
correctly repairs it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DyceEVVeQ33EeS6C1PDv5
Re-running the installer against an existing install now offers "Manage
sync accounts" (add / remove / rotate a password) as a first-class menu
option, instead of requiring a hand-edit of .env kept in lockstep with
docker-compose.yml.
_anki_rewrite_account_block() regenerates the SYNC_USERn lines in both
files from the current account list, always renumbered contiguously from
1, and is shared by the initial install and every management mutation so
they can't drift apart. Only SYNC_USER/ANKI_SYNC_* lines are touched —
port, Caddy wiring, and everything else in either file is left alone.
Verified against a stubbed docker/ss sandbox: add, remove (mid-list, with
renumbering), and password rotation all produce the expected .env/
docker-compose.yml diffs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DyceEVVeQ33EeS6C1PDv5
Docker-based sync backend for the Anki app (afrima/anki-sync-server, the
official Rust sync server). Supports multiple independent accounts per
instance, the repo's multi-instance pattern, port collision avoidance,
Caddy wiring, and update/fresh/cancel reinstall detection.
No Authelia gate — this is a raw sync API the Anki client talks to, not
a browser session, so a forward_auth portal in front of it would just
break every sync request; SYNC_USER1/SYNC_USER2/... is its own auth
boundary.
Companion services/anki-sync-server.md covers client setup (Desktop,
AnkiDroid, AnkiMobile) and the Quizlet import/export walkthrough.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DyceEVVeQ33EeS6C1PDv5
The "already equal" early-exit compared only remember_me against the typed
value, so re-entering an unchanged remember_me (the exact case for anyone
who'd set it before the earlier fix existed) skipped the inactivity write
entirely, leaving inactivity stuck at its old mismatched value. Now only
skips when both keys already match the typed duration.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148pWopbt3tEKZWHYuHTb3c
inactivity (idle timeout) was independent of remember_me and stayed at a
much shorter default (2h), so a long remember_me got silently overridden
by ordinary daily gaps between visits. install_authelia()'s template now
defaults inactivity to match remember_me, and the "Change remember me
duration" menu option now writes both keys together instead of just one.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148pWopbt3tEKZWHYuHTb3c
The final echo summary still advertised a removed `apps` command and left out
everything added since (cores, backup, controllers, steam-add-nonsteam-game,
steam-setup-frontends, cemu-clone-controller, cemu-sync-controllers,
install-completion, etc.). Also add a short pointer to the Steam-as-4-controller-hub
workflow (documented in depth further down in README.md) right in the install
summary, since it's currently only discoverable by reading the generated README.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V99t5754SyXdnMTpVe2ba5
The steam-setup-frontends command and the ES-DE/RetroArch AppImage
download step had no matching section in the ~/docker/wolf/README.md
content this file generates, unlike every other manage.sh command. Adds
one, alongside the existing Cemu-in-Steam section: why you'd want it,
how the mounts/cores are already shared, and the two honest caveats
(returning to Steam from ES-DE only works via this path, and whether
controller mappings sync across separately-paired Wolf clients is
expected but unconfirmed).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Z4nqULUEipWNgBsPoSuAb
Steam Guard's QR-code sign-in can't be scripted (needs a phone approving
a prompt), so this polls for it instead: starts Wolf if needed, waits
for Steam's userdata/ to appear (or proceeds immediately if already
signed in), then re-invokes the existing steam-add-nonsteam-game command
for whichever of ES-DE.AppImage/RetroArch.AppImage was downloaded during
install. Points to ./manage.sh cores all for the shared cores/shaders/
overlays directory rather than duplicating that download logic.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Z4nqULUEipWNgBsPoSuAb
Both projects ship official standalone Linux AppImages separate from
the esde/retroarch Wolf catalog containers this repo already runs.
Adding either one to Steam as a non-Steam game (steam-add-nonsteam-game,
now reaching the same roms/saves/bios/retro-home/retroarch mounts as
the esde/retroarch containers) lets Steam Input assign a 4th controller
its own identity by device path, past Wolf's 3-concrete-pad-type ceiling.
ES-DE is hosted on GitLab rather than GitHub, so this adds a GitLab
Releases API counterpart to the existing GitHub-based download helper.
RetroArch's own buildbot doesn't publish through either API, so this
uses hizzlekizzle/RetroArch-AppImage, the community nightly-build
project the AppImage catalogs themselves point to (flagged as
third-party, same treatment this file already gives the Dolphin
community build). Both downloads are opt-in (default no) and symlink
to a fixed filename so steam-add-nonsteam-game's case-sensitive
substring match finds them regardless of the vendor's own asset name.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Z4nqULUEipWNgBsPoSuAb
Lets a manually-downloaded ES-DE or RetroArch AppImage, added via
./manage.sh steam-add-nonsteam-game, see the same library, cores, and
ES-DE settings/custom systems (TI-99, Wii U) the esde/retroarch
containers already have, instead of starting from an empty config.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Z4nqULUEipWNgBsPoSuAb
docker exec needs an explicit -i flag to forward stdin into the
container process; without it, the heredoc piped into `python3 -`
never reached the containerized script, which ran empty and printed
nothing. cemu-sync-controllers then misread that empty output as
"SDL reported zero joysticks" — a false negative with a working set of
4 controllers already confirmed live in ES-DE, not an actual SDL or
GUID problem. Root-caused against the user's own live output: the
exact same probe script, run directly (not via the manage.sh command),
found real device nodes (js0-js4) present in the same container at the
same time cemu-sync-controllers reported zero.
Audited every other docker exec call in this file for the same
stdin-via-heredoc pattern; this was the only one missing -i.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6
cemu-clone-controller still needed one manual Cemu GUI step per new
device (click +, select it, map one button, Save) just to learn its
real uuid. This eliminates that too: instead of getting the uuid from
Cemu, it asks SDL directly (SDL_JoystickGetDeviceGUID/GetGUIDString via
ctypes against libSDL2 — the exact library Cemu itself links against)
for the live GUID of every controller connected to the active session,
then clones the proven-working mapping onto each one automatically.
Confirmed live end-to-end on the user's real box: querying SDL this
way inside a running ES-DE session reproduced the exact uuids Cemu had
already written by hand for two different controllers (Nintendo Switch
Pro and Xbox One S) — byte-for-byte identical to their real
controllerProfiles/controllerN.xml. This is what makes trusting SDL as
the uuid source safe, after an earlier attempt to reverse-engineer the
GUID's CRC16 portion by hand (tried 7 different CRC16 variants) failed
to match either device.
Also improved cemu-clone-controller's sibling: the synced profile's
display_name now comes from SDL's own live device name (when the probe
reports one) rather than always inheriting the mapping template's name,
which was a real but purely cosmetic issue caught while re-testing.
Verified end-to-end in an isolated harness against the real captured
controller0.xml/controller1.xml: idempotent skip on already-synced
slots (no needless backups, existing files left byte-identical), a new
slot correctly cloned with the live name and same 24-pair mapping set,
and valid XML output.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6
Cemu's own Wolf-facing controller slots (up to 4) still each need a
separate manual bind through Cemu's Input Settings even after
./manage.sh controllers makes them distinct SDL devices — that dialog's
Save button is documented elsewhere in this README as getting cut off
the viewport, making binding all 4 slots by hand painful.
Validated the underlying assumption against two real, separately
hand-mapped profiles pulled live from a working install (Nintendo
Switch Pro on controller0.xml, Xbox One S on controller1.xml): both
contain the exact same 24 <mapping>/<button> pairs, just in a different
order — real proof Cemu's mapping format is device-agnostic and
order-independent, not just the README's prior unverified claim.
New command clones a proven-working <mappings> block onto a new
device's controllerN.xml, needing only that device's own real <uuid>
(which still has to come from Cemu itself — a hand-computed SDL GUID
risks not matching what SDL actually reports for the live device, so
this never guesses one). Getting that uuid only needs a single-button
minimal bind in Cemu's UI, not full mapping, since only the uuid gets
kept from it.
Verified end-to-end against the real captured controller0.xml/
controller1.xml content: cloning controller0's mappings onto
controller1's real uuid reproduces the exact same 24-pair set Cemu
itself wrote, and the output validates as well-formed XML.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6
Copyable follow-up for the existing Cubeb/PulseAudio hang note: checks
whether a full Wolf restart has actually happened, whether PulseAudio
inside the Desktop container has any sinks at all, and pulls Cemu's own
log.txt plus its current <Audio> settings.xml block — narrows "stuck on
Disabled" down to a container-level audio problem vs. Cemu's own
device-switch path before guessing at a fix.
Verified the heredoc escaping by rendering the write_readme block through
a real bash heredoc and confirming $WOLF_DIR interpolates while every
other $ stays literal in the output.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6
Root-caused against the user's own live output: the settings file had
<string name="ROMDirectory" value="" /> — the key was present, just
empty. The self-healing check only tested whether the string
"ROMDirectory" appeared anywhere in the file, so a key that exists with
a blank value (ES-DE can write this itself if its own first-run "select
ROM directory" step goes unanswered in a headless Moonlight session,
saving an empty path back over GOW's template) looked "already
populated" and got skipped — leaving ROM discovery broken even after
the fix had run.
Now ROMDirectory is independently forced to /ROMs whenever its value is
blank, on top of (not instead of) the existing RunInBackground patch —
no longer gated on the key's mere presence. Verified against the exact
reported bug (ROMDirectory present but blank) plus the existing missing-
file, already-correct, and idempotent-rerun scenarios in isolated /tmp
harnesses before touching the real script.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6
Adding an emulator (Cemu, etc.) to Steam as a non-Steam game previously
needed Steam's own Big Picture file-browser flow through Moonlight. This
writes the shortcut directly into Steam's binary shortcuts.vdf instead,
matching this repo's existing no-manual-wizard pattern (ge-proton,
install-ea-app). Motivated by wanting to test whether Steam Input's
per-device controller tracking (distinct device paths, not SDL GUIDs)
can hand Cemu 4 explicitly-assigned controllers where ES-DE/Cemu's own
SDL-based handling can't tell identical controllers apart.
- New generic binary VDF (KeyValues) parser/serializer: round-trips any
existing shortcuts.vdf entries byte-for-byte and only inserts/replaces
the one entry matching the given Exe path, so it's safe against a file
that already has real, hand-configured shortcuts. Verified in isolated
/tmp harnesses: fresh file, idempotent re-add, a second distinct entry,
and preserving a synthetic pre-existing GUI-set entry (icon,
LaunchOptions, tags) untouched.
- Steam's own CATALOG entry had no mount for emulators/ at all (unlike
esde/retroarch) — added emulators:/home/retro/Applications so an
AppImage is actually reachable from inside the Steam container. Like
the ES-DE settings mount, this only takes effect on a freshly created
WolfSteam container (Wolf reuses existing ones) — noted in the README.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6
Previous version only patched an already-populated es_settings.xml,
requiring a manual "connect via Moonlight once" step before the fix
could apply — the user correctly pointed out the script can't know
whether that step has been done, so a manual dependency isn't a real
fix. This embeds GOW's own real es_settings.xml template (fetched from
games-on-whales/gow, RunInBackground pre-flipped false) and writes it
directly whenever the on-disk file is missing or is the old broken
stub (detected by the absence of ROMDirectory, which only a real
GOW/ES-DE-written file has). An already-populated file still only gets
the one RunInBackground line patched, leaving everything else alone.
Verified against all 4 scenarios in isolated /tmp harnesses before
touching the file: missing file, the exact broken stub from the user's
regression, GOW's real full template, and idempotent re-runs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6
Confirmed live on a real install: after the previous commit, ES-DE found
zero games on every system. Root-caused by walking GOW's own
apps/es-de/build/scripts/startup.sh line by line: GOW's image bakes a
FULL es_settings.xml template (100+ settings, including
ROMDirectory=/ROMs, confirmed against the real template file in the
games-on-whales/gow repo) and its own startup script copies that
template into place exactly once, the very first time the container
starts, gated on the settings file not already existing.
The previous fix pre-created a minimal stub file (containing only
RunInBackground) before the container had ever started - GOW's startup
script saw that file "already there", skipped writing its own template
entirely, and ES-DE fell back to vanilla upstream defaults for
everything else in that file, including ROMDirectory, which is not
/ROMs by default. Every system's games vanished as a result.
Also corrects an earlier wrong claim in both the code comment and
README: ES-DE's own *compiled* default for RunInBackground is false, but
GOW's own template deliberately sets it to true - that's the real,
confirmed source of the original background-launch bug, not an
accidental toggle as previously assumed.
Fix: only ever patch an es_settings.xml that already exists with real
content (`[ -s "$_ESDE_SETTINGS" ]`) - never create one. On a genuinely
fresh install, before EmulationStation has ever launched once, the
installer now says so explicitly and defers rather than guessing, with
clear instructions to connect once and re-run. Verified against GOW's
actual real template: the patch flips just RunInBackground (line count
unchanged, ROMDirectory and everything else untouched), is idempotent on
re-run, and the skip-when-absent gate was verified directly against both
the missing-file and populated-file cases.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6
Live user feedback: being asked to pick between 6 numbered entries that
were all the exact same device (client_id repeated 6x from re-pairing)
was genuinely confusing, especially right when the user was already
trying to get to the controller-type poll further down the flow.
The picker now dedupes to distinct client_ids only, first-occurrence
order, tagging a collapsed entry "(paired Nx)" - matches what Wolf's own
get_client_by_id() actually resolves to anyway (first match for a given
id), so nothing is lost by not offering the later duplicates as separate
choices. Verified against the user's real 8-entry (6 duplicate + 2
unique) client list, both with and without an active session, confirming
the controller log-poll still runs correctly right after selection in
both cases.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6
Fixes a real, confirmed-live, cross-system bug: with ES-DE's "Run in
background (while game is launched)" enabled, ES-DE keeps listening to
every controller even after a game/emulator takes focus - a second
controller's input can reach ES-DE's own menu and launch a completely
different game underneath whatever's already running. ES-DE's own
USERGUIDE.md names this exact failure mode.
ES-DE's compiled default for this setting is already off (confirmed
against its real Settings.cpp), but nothing in this repo asserted that
durably, and ~/ES-DE (settings, gamelists, scraped media, logs) had no
mount onto the game drive at all - confirmed against ES-DE's own source
(getAppDataDirectory() is a plain $HOME/ES-DE) and GOW's own es-de
startup script. Wolf normally reuses the same app container across
sessions rather than recreating it each time (confirmed against Wolf's
docker.cpp - it only removes the container on exit if
WOLF_STOP_CONTAINER_ON_EXIT=TRUE, which this repo never sets), so this
mostly didn't bite day to day, but it meant the setting - and everything
else under ~/ES-DE - wasn't safe across an actual reinstall the way
roms/saves/BIOS already are.
Mounts esde-settings/ onto ~/ES-DE/settings specifically (additive,
doesn't touch the existing custom_systems mount) and writes a surgical,
idempotent RunInBackground=false into es_settings.xml on every install -
never a full rewrite, since that file is the user's own complete
settings state. Verified the write logic against fresh-file, existing-
file-with-other-settings, and flip-an-existing-true-to-false cases, plus
idempotent re-runs.
Gamelists/scraped-media durability under ~/ES-DE is a related, noted gap
- not fixed here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6
Two follow-ups from live testing that just confirmed the whole
controllers_override chain works end to end:
1. manage.sh controllers now polls `docker compose logs wolf` for
"Creating <TYPE> joypad for controller <N>" lines (most recent per
slot wins) and shows what it last saw before asking anything, using
it as the suggested default at each prompt. Every prompt is now
labeled "1st controller"/"2nd controller"/etc - always meaning
Wolf's own 0-indexed "controller 0"/"controller 1" - instead of
asking for a raw slot number, closing the exact client-vs-controller
0-vs-1 confusion surfaced live earlier. Documents plainly that this
can show what TYPE a slot last used but can't show or control WHICH
physical controller becomes which slot - that's decided by Moonlight
client-side, upstream of Wolf's API entirely.
2. Documents a full recovery procedure in the Wii U section for when
Cemu's Input Settings Save button is cut off-screen (confirmed live:
happens even maximized, at higher resolutions, and closing the
window doesn't save) - an Alt-drag/Alt+F7 window-manager fix first,
then editing Cemu's own controllerProfiles/controllerN.xml directly
on the host (plain XML, not inside the container) as a full GUI
bypass. Documents the key insight that made this actually work live:
<mapping>/<button> pairs are universal SDL_CONTROLLER_BUTTON_*
semantics (confirmed earlier against real captured profiles from
different controller brands), so a working mapping can be copied
verbatim into a different device's empty <mappings/> block without
redoing the live button-capture in Cemu's GUI at all.
Verified the redesigned manage.sh controllers flow end to end against a
mocked docker compose logs matching the user's real log output (both
the log-seen and no-logs-seen paths), plus the ordinal-suffix logic in
isolation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6
Live user feedback: "Exactly one client is actively streaming right
now" sitting directly above "How many controller slots" read as "only
one controller is connected" - an easy misread since "client" (a paired
Moonlight session/device) and "controller" (a physical gamepad slot
within that session) are conflated by ordinary English but mean
completely different things here. The tool has no way to detect
controller count at all; it only ever picks which Moonlight
session/device to configure.
Reworded every message in this flow to say "Moonlight session/device"
instead of bare "client", and added an explicit line before the
controller-count prompt clarifying it's a separate question the user
has to answer themselves. Verified the full flow still runs correctly
end to end against the same mocked duplicate-heavy client data used
earlier.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6
Confirmed live: the paired-clients list is a wall of meaningless
cert-derived numbers with nothing human-readable attached (verified
against Wolf's own PairedClient API schema - no name, no IP field
exists there at all), so there was no way to tell which entry was the
user's actual device. Made worse by Wolf not deduping repeated pairings
of the same device (the user's real box showed the same client_id 6
times).
Fix: cross-reference against GET /api/v1/sessions, which DOES carry
client_ip for every currently-streaming session, and tag each paired
entry as "ACTIVE - streaming from <ip>" when it matches one. When
exactly one client is actively streaming, it's now auto-selected with
no prompt - the practical case for a single-user setup. Falls back to
the manual picker when nothing's active or multiple clients are (now
showing which IP is which, so the choice is no longer a guess). Verified
against the user's real duplicate-heavy client list plus mocked active-
session data covering: single active session (auto-select), no active
sessions (manual fallback), and two distinct active sessions (manual
fallback with both IPs shown).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6
Confirmed live: a plain-user curl against /var/run/wolf/wolf.sock failed
with "Immediate connect fail ... Permission denied". The socket is
srwxr-xr-x root:root - Docker auto-creates the host-side /var/run/wolf
bind-mount source as root, and Wolf itself runs as root in its
container, so group/other never had write permission on the socket file
(required to connect() to a unix socket, not just read/traverse it -
`ls -la` on the containing dir working fine was a red herring).
Fix: sudo on both curl calls, matching how every other root-owned Wolf
state file ($WOLF_CFG) is already touched elsewhere in this script via
`sudo python3 -`. Documents the sudo prompt in the README too.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6
Spells out what the three controllers_override values actually create
(Xbox One, PS5 DualSense, Switch Pro Controller - confirmed against
inputtino's own source, not just the enum names) instead of leaving the
reader to guess from bare AUTO/XBOX/PS/NINTENDO tokens, plus a note that
forcing a non-matching type only affects on-screen button-prompt
artwork, not functionality.
Adds a 4-controller section: Cemu's own Input Settings goes up to 8
slots and Wolf's wire protocol has no hardcoded 4-controller cap
(confirmed against control/input_handler.cpp), so the ceiling in
practice is the Moonlight client's own limit. With only 3 concrete
forced types available, a 4th controller can't get a guaranteed-unique
GUID - documents the one-at-a-time Cemu binding workaround for that
case, flagged as reasoned but not confirmed live.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6
Root cause (confirmed against inputtino's own uinput device-creation
source, and against Wolf's real virtual-pad architecture): every
virtual gamepad Wolf creates of the same type gets an identical SDL
GUID, since a GUID identifies a controller model, not a physical
instance - the same behavior two real identical controllers would have.
This isn't a Cemu bug specifically; it's why any app whose own
controller picker keys off GUID (Cemu confirmed live) can't reliably
tell two same-type Wolf pads apart, driving every player from
controller 1.
Wolf's own per-client `controllers_override` setting sidesteps this at
the root by forcing each controller slot to a genuinely different pad
type (e.g. slot 1 = Xbox, slot 2 = PlayStation), so their vendor/product
IDs - and SDL GUIDs - actually differ. `./manage.sh controllers` drives
this via Wolf's real REST API (confirmed against its OpenAPI schema,
not its docs page's own stale example command) rather than hand-editing
config.toml: lists paired clients, prompts for a pad type per slot, and
POSTs the update. Needs Wolf's API socket exposed to the host
(WOLF_SOCKET_PATH + a /var/run/wolf bind mount, matching Wolf's own
documented pattern), added to the wolf service's docker-compose.yml.
Full flow verified with a standalone test harness (mocked curl against
real API response shapes, a real AF_UNIX socket file) covering: single-
vs multi-client selection, invalid selection, invalid slot count,
lowercase/unrecognized type input, and the missing-socket path.
Also documents live user feedback that AntiMicroX (added last session)
is confirmed not working for TI-99/4A - flagged in the README rather
than left silently unverified, with this fix noted as the actively
maintained path for the multi-controller problem instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6
The PCEE2 libretro core (a separate, actively-developed PCSX2 port,
WizzardSK/pcee2-libretro) is only in ES-DE's own bundled es_systems.xml
as of 3.5.0, which hasn't been released yet - confirmed against ES-DE's
own CHANGELOG.md and by diffing the v3.4.1 tag's real ps2 system block
against git master's (identical except for this one missing <command>
line). ghcr.io/games-on-whales/es-de:edge always installs the latest
*released* AppImage (confirmed against gow's own Dockerfile, which
queries GitLab's releases API), so the container this repo runs
genuinely has no PCEE2 entry at all yet - not a missing core download,
a missing menu entry.
Backfills it via the same esde-custom-systems/es_systems.xml mechanism
already used for the TI-99/4A and Wii U customizations, replicating the
real v3.4.1 ps2 block verbatim plus the one new command (as the default,
matching upstream's own placement). The core file itself needs no extra
handling - confirmed live against the libretro buildbot that
pcee2_libretro.so.zip is already covered by the normal RetroArch cores
pre-download.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6
AntiMicroX maps gamepad buttons to synthetic keyboard/mouse events -
useful here because ti99sim-sdl's own joystick handling only ever emits
digit keys 1-9 for a raw button (confirmed against its source), with no
path to 0/Enter/Q/Esc, and because it's a plausible angle on Cemu not
reliably telling apart two Wolf virtual pads that share an SDL GUID.
Scoped to just these two ES-DE systems via a second, separately-labeled
"(AntiMicroX)" <command> alongside each one's existing default - ES-DE's
own multi-command "alternative emulators" mechanism - rather than
touching every system. Each launches through a small wrapper script
that starts AntiMicroX hidden (--no-tray --hidden --eventgen uinput,
flags confirmed against AntiMicroX's own commandlineutility.cpp source)
against a profile the user builds themselves via AntiMicroX's own GUI in
the Desktop/XFCE app, execs the real emulator, and kills AntiMicroX on
exit.
AntiMicroX needs /dev/uinput to inject events under ES-DE's Sway/Wayland
session (its XTest backend needs Xwayland, not present here), so the
esde catalog entry now requests it via both GOW_REQUIRED_DEVICES and a
real device grant - and make_app_block/the reinstall-update path both
learn to read/refresh a per-app 'devices' field instead of always
emitting an empty array, so this actually reaches an existing install on
a rerun, not just a fresh one (verified live against a synthetic
pre-uinput-era config.toml).
Also fixes a latent whitespace-eating bug in every custom_systems
strip-and-reappend regex in this file (trailing \s*\n? reached into the
next sibling block's leading indentation once two custom systems
coexist in the same file) - cosmetic only, but it would have made every
rerun churn indentation between the ti994a and wiiu blocks. Caught and
verified via a standalone test harness exercising all four
strip-and-reappend blocks together across repeated passes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6
Joystick buttons beyond the first only reach digit keys 1-9 in
ti99sim-sdl's own source (src/sdl/ti994a-sdl.cpp) - there's no path to
0, Enter, Q, or Esc from a gamepad. Documents the real Esc-to-exit
behavior and full FCTN key row (Alt+1 through Alt+9, Alt+=), both
confirmed against the emulator's own bundled doc/README.html.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLX1yYKJExGSXmgUhxKQG6