100 Commits
Author SHA1 Message Date
Harley 03ded14b9f gitea: ROOT_URL -> https://gitea.wingard.pro
Second step of the HTTPS front, deliberately its own generation: pointing
ROOT_URL at an HTTPS name that served nothing would have broken the UI's
absolute links. The vhost (40ae3e00) is proven — ACME issued over HTTP-01 and
https://gitea.wingard.pro/ serves 200 with a chain that validates against the
system trust store, so it is a real Let's Encrypt cert rather than self-signed.

Measured before flipping: Gitea already answers the registry Bearer realm on
https, because it derives the scheme from X-Forwarded-Proto. So Docker's auth
flow was already fully TLS and this flip is for the web UI's own links, so
browsing over https stops emitting http URLs.

DOMAIN stays 10.0.1.49: it only affects newly copied SSH clone URLs, and
existing remotes should not churn.
2026-09-27 16:04:14 -04:00
Harley 40ae3e0026 nginx: serve Gitea over HTTPS on gitea.wingard.pro
Adds a Let's Encrypt-backed vhost in front of Gitea's port 3010, which carries
both the web UI and the Docker registry (/v2/). This is what lets containers
push and pull the local image WITHOUT insecure-registries: that daemon option
needs a config change plus a daemon restart on every client that pulls,
including the arch-vm guest, and what it really buys is skipping cert
verification. A real cert on a name clients already resolve needs no client
configuration at all. Body and timeout limits are raised because image layers
are GB-scale.

Also adds gitea.wingard.pro to networking.hosts and to the Pi-hole records: the
host's own resolver goes to Comcast (*.wingard.pro -> WAN) and the router does
not hairpin, so the host cannot reach its own services by name.

ROOT_URL is deliberately still the plain-HTTP address; it flips to the HTTPS
name in a follow-up once the certificate is proven to issue and serve.
2026-09-27 16:01:57 -04:00
Harley 52aca33469 firewall: scope usage-gauge (8771) to WireGuard 2026-09-27 12:35:07 -04:00
Harley 726e92d358 config: scope gpu-cockpit (8770) to WireGuard + commit the pending SillyTavern vhost
Two unrelated things land together because they were already in the same working tree.

Mine (this session): 8770 added to networking.firewall.interfaces.wg0.allowedTCPPorts
so the WireGuard-only front door of the gpu-cockpit console is reachable from the
phone. Deliberately NOT in the global allowedTCPPorts list - it follows the same
pattern as 8790 (Roman's WebUI), which is scoped to wg0 for the same reason.

Pre-existing, uncommitted when I got here (file mtime 00:02 today), committed rather
than left dangling so the tree is not silently dirty during a rebuild:
- the SillyTavern reverse proxy vhost on 8744, including the libvirt/:8000 hop and the
  duplicate-forwarded-header / IP-whitelist traps documented inline
- 8744 added to both the wg0 and eno1 firewall lists

I have not reviewed the SillyTavern half; it is recorded here so the history matches
the file that gets built.
2026-09-27 12:19:17 -04:00
Harley 4e5e7d2369 mnemosyne: inject only venv-absent packages; fix desktop+webui runtimes that bypassed the wrapper
- mnemosyneSitePkgs listed only top-level paths, so fastembed's runtime deps
  were never on the interpreter path: vector search was dead (vec_* empty),
  recall silently FTS5-only.
- extraPythonPackages cannot be used: fastembed's closure reaches
  huggingface-hub, which the sealed venv ships, and the wrapper's collision
  guard rejects that build.
- hermes-desktop.service and hermes-webui.service spawn the venv python with
  PYTHONPATH=/opt/hermes-agent-standalone only, so Mnemosyne never
  instantiated there (warning every turn since 2026-09-10). The desktop now
  reads environment.etc."hermes-desktop-env" via a drop-in EnvironmentFile;
  the webui gets the same string inline.
2026-09-23 23:17:22 -04:00
Harley f1e2c16694 joplin: self-hosted Joplin Server (notes sync) on :8444, LAN + WireGuard only
- nginx vhost joplin.wingard.pro:8444 + ACME HTTP-01 vhost, real Let's Encrypt cert
- systemd.services.joplin: docker compose stack (joplin/server 3.7.2 + Postgres 16), /opt/joplin
- firewall: 8444 on wg0 + eno1 only (8444 is NOT port-forwarded)
- Pi-hole record: 10.0.1.49 joplin.wingard.pro
- networking.hosts: this host now resolves its own *.wingard.pro service names. It queries
  Comcast DNS directly, never its own Pi-hole, so the names resolved to the WAN IP and the
  LAN-only ports refused the connection (joplin CLI: ECONNREFUSED 71.61.169.167:8444).
- environment.systemPackages: joplin-cli -- the headless agent interface. Joplin's REST Data
  API on :41184 is hosted by the DESKTOP app and does not exist on a headless server.

Verified: gen ri08579xg21bxwkgssw5sjn5yzzhljf8 live; /api/ping 200 with a verified TLS chain
using the plain service name; 2 notes + 1 notebook synced from the CLI and confirmed as rows
in the server's items table.
2026-09-23 01:20:37 -04:00
Harley 0aec321312 nixpkgs: advance to 2026-09-22 (6774f7b); libvirt boot ordering after unlock-8tb 2026-09-22 13:21:49 -04:00
Harley 88d19ea9e2 hermes: upgrade v0.21.0 -> v0.21.4; repin webui units to the current-source symlink 2026-09-22 10:04:43 -04:00
Harley 7397235d30 feat(immich): boot-persistent compose stack unit
Adds systemd.services.immich: oneshot running 'docker compose up -d' from
/opt/immich, wired the same way as karakeep/pia-qbit.

requires = [ "unlock-8tb.service" ] deliberately, NOT storage-8TB.mount: that
unit is fragmentless on this host, so a Requires= on it can never be satisfied
at boot and the unit is silently skipped instead of failing loudly (the bug that
killed three units' autostart until 2026-09-16).

Immich's database AND media both live on the 8TB pool, so RequiresMountsFor
covers the mount-shadow trap -- without it a boot with the pool absent would
resolve the bind sources onto the root filesystem and start Immich against empty
storage with a fresh database.
2026-09-21 22:59:56 -04:00
Harley 7b1fce022b security: add fail2ban SSH ban enforcement
A 24h audit found ~2,350 failed root logins/day from a rotating botnet,
accounting for ~75% of all journal error lines on the host. sshd already
refuses every one (PasswordAuthentication = false; the generated PAM auth
stack is a bare pam_deny), so this was never an intrusion risk -- it was
unbounded retries plus log spam, with no ban layer at all since crowdsec
was removed on 2026-09-12 for being silently inert.

  bantime = 1h, maxretry = 3, findtime = 3600
  bantime-increment on (rndtime 8m, maxtime 48h)
  ignoreIP = every trusted route (LAN, WireGuard, Palantir, libvirt, docker)

findtime is deliberately 1h rather than the 600s default: measured traffic
shows most attacking IPs do only 1-4 attempts per 10 minutes but 8-26 per
hour, so the default window let the drip through. Simulated on the real 24h
sample, coverage goes from 90% (4/600) to 98% (3/3600).

banaction stays at the module default -> iptables-multiport, since
networking.nftables.enable is false on this host.
2026-09-20 21:35:11 -04:00
Harley c380c0e7ac fix(sabnzbd): make the category-perms timer actually fire
RemainAfterExit=true left the unit active(exited), so the timer's plain Start was
a no-op and the 15-min re-assert never ran. Dropped it (unit now ends inactive)
and switched OnUnitActiveSec -> OnCalendar='*:0/15', which is wall-clock and the
right pairing for a oneshot that exits.

Caught by re-testing with the primitive the timer actually uses (systemctl start,
not restart) and by simulating SAB faithfully (os.chmod syscall on the release dir
too, not a shell mkdir under umask 022).
2026-09-20 14:25:27 -04:00
Harley dc6f756240 fix(sabnzbd): pin category dirs to 2775 sabnzbd:media
SAB applies permissions=775 via os.chmod(), a bit-exact syscall that clears
setgid. A category dir left in SAB's primary group makes every release folder
inside inherit 'sabnzbd' instead of 'media', so sonarr/radarr can read+copy but
cannot unlink the source -- unlink needs write+execute on the parent dir -- and
imports die with UnauthorizedAccessException, churning every ~90s.

Oneshot + 15-min timer re-asserts chgrp media + chmod 2775 on complete/ and each
category dir beneath it. Requires unlock-20tb.service (pool is fragmentless, no
.mount unit). Deliberately not a tmpfiles 'd' rule (root-fs shadowing risk).
2026-09-20 14:23:36 -04:00
Harley 6ea3062f49 journald: raise SystemMaxUse 1G -> 4G
Measured journal volume on this host is ~19.5 MB/h, so 1G held under two days of
history and left the journal pinned at its ceiling (986M/37.9M free on 09-17).
On 2026-09-17 01:33-08:40 a runaway container (hexstrike, removed that day) logged
~150k msgs/h through docker's journald driver, 99.8% of that boot's docker volume,
and in that window systemd SIGABRT'd journald twice for missing its 3-minute
watchdog. Flood source gone; this removes the cap pressure. 4G ~= 8 days of
history. Mechanism not fully pinned (no assertion in the cores) - the comment
block records the evidence and the open question.
2026-09-18 08:27:26 -04:00
Harley 4a288c8465 voice: live Kokoro TTS bf_emma -> af_bella (match episodes/replies)
The live speech-to-speech stack spoke with bf_emma (UK) while every generated
episode and voice reply uses af_bella (US). Harley asked for one consistent
voice everywhere. Both flags move together: af_* voices need lang code 'a'.
2026-09-17 23:48:49 -04:00
Harley 8d9051c9ac feat(voice): fully local realtime voice stack (llama.cpp + HF speech-to-speech)
Adds the two services that give hermes-live-voice a local `local` provider,
so the realtime path no longer needs an OpenAI/Gemini key.

  llama-server-local  : Qwen3-4B-Instruct-2507 Q4_K_M on :8080, OpenAI-compatible
  speech-to-speech    : Silero VAD -> Parakeet TDT -> llama-server -> Kokoro TTS
                        on ws://127.0.0.1:8765/v1/realtime

Hermes remains the agent brain; the realtime model only does turn-taking and
calls the gateway tools. Notable constraints, all verified on the box:

- llama-cpp-cuda (new systemPackage) with --jinja, required for tool calls.
- LD_LIBRARY_PATH needs BOTH /run/opengl-driver/lib (libcuda; without it torch
  reports cuda_available=False and silently uses CPU) and gcc's libstdc++
  (pip wheels have no RPATH, Nix binaries do).
- Context 4096 + q8_0 KV and Kokoro pinned to CPU to fit an 8 GB RTX 4060
  alongside the desktop: ~276 + ~1990 + ~2870 MiB, ~1170 MiB free.
- Both units require unlock-8tb.service (never storage-8TB.mount) as the
  weights live on /storage/8TB.

Measured: STT -> LLM -> TTS round trip ~2s.
2026-09-17 03:06:18 -04:00
Harley a44760d8e8 fix(firewall): scope Roman's WebUI (8790) to WireGuard only
It was on wg0 + eno1 like Vaultwarden; Harley's call is that he reaches it
over the tunnel, so no other LAN device should see the login page at all.
2026-09-16 17:34:33 -04:00
Harley dc9487f22e feat(services): isolated Hermes WebUI instance for Roman on 8790
Second WebUI instance pinned to Roman's own profile via
HERMES_WEBUI_ISOLATED_PROFILE, running as the roman system user with
HERMES_HOME outside Harley's shared .hermes home. Firewall scoped to
wg0 + eno1 like Vaultwarden. No new packages: reuses the existing
hermes-agent venv.
2026-09-16 17:23:02 -04:00
Harley 8530117c85 mount deps: require unlock-8tb.service, not the fragmentless storage-8TB.mount
storage-8TB.mount has no fragment (/storage/8TB is deliberately not a
fileSystems entry) and only exists once unlock-8tb.service has mounted the
pool -- after the boot transaction is built. A hard Requires= on it created
no start job, so pia-qbit, karakeep and docker-camofox-browser were silently
discarded at every CLEAN boot: enabled, no journal entry, no failed unit.

Require unlock-8tb.service instead: it has a fragment, is satisfiable at
boot, and still fails LOUDLY if the pool never unlocks.
2026-09-16 12:57:20 -04:00
Harley a193b5413d packages: add glow (terminal markdown reader) system-wide 2026-09-15 13:58:25 -04:00
Harley c40a52e2e1 wg0: make the Exos peer (and pihole-ftl-setup) survive a boot-time network race
The Exos peer is the only wg0 peer whose endpoint is a hostname
(briggs.wiki:51820), so its generated start script needs DNS resolved at unit
start. network-online.target is hollow on this host: NetworkManager-wait-online
is force-disabled for boot speed, so the target is "reached" instantly and the
peer unit raced the network. On 2026-09-14 23:23 it lost that race, exited 1,
and Type=oneshot + Restart=no made the loss permanent: /storage/Exos stayed
dead for 11h, the CIFS automount failed on every trigger, and the no-SSH
transport to Roman went blind with it (only noticed when Harley tried to SSH
in and could not).

pihole-ftl-setup.service exited 1 in the same second for the same reason (it
curls ftl.pi-hole.net and talks to the FTL API before the network is usable)
and had sat in `systemctl --failed` ever since.

- wg0 Exos peer unit: Restart=on-failure + RestartSec=5s. Legal on Type=oneshot
  (only Restart=always/on-success are forbidden), so it retries ~every 6s until
  DNS answers instead of dying once and staying dead. No boot delay, and
  NetworkManager-wait-online stays disabled as Harley intended.
- pihole-ftl-setup: Restart=on-failure + RestartSec=20s, deliberately bounded
  by StartLimitIntervalSec=600 + StartLimitBurst=5, so a permanently broken run
  cannot hammer `pihole -g` blocklist downloads.

Verified after switch (generation
/nix/store/dhgrxdiz4zg14z8qr71xvlb64yr7lvz4-nixos-system-utumno-26.11.20260910.8ce4ef6):
switch rc=0, `readlink -f /run/current-system` moved, generated units read back
with Restart=on-failure / RestartSec=5s (peer) and StartLimitBurst=5 +
RestartSec=20s (pihole), live `systemctl show` matches, `systemctl --failed`
lists 0 units, wg0 back to 5/5 peers with a live Exos handshake, /storage/Exos
readable, and the no-SSH transport answered a fresh read-only request in
1574ms. pihole-ftl-setup was re-run by hand to clear its stale failure: exit 0,
gravity rebuilt.

Diagnosis in one command, should this ever recur:
  /run/wrappers/bin/sudo -n wg show wg0 peers   # must list 5 peers
2026-09-15 10:38:03 -04:00
Harley 2895ef2fae hermes-desktop: add kdotool for real window-presence health checks (the probe trusted a backend socket, so a window-less primary was never healed) 2026-09-12 13:27:33 -04:00
Harley 09fc6066c6 firewall: scope Gitea's 3010 to source networks (it was open on every interface)
Port 3010 sat in networking.firewall.allowedTCPPorts, which accepts it on EVERY
interface, in BOTH the IPv4 and IPv6 chains. eno1 carries this host's global IPv6
addresses (2601:540:cc03:4760::/64), so the host itself was not what limited
reachability -- and interface scoping would NOT have fixed it either, because
internet traffic arrives on eno1 exactly the way LAN traffic does. Allowlisting the
SOURCE networks is what actually closes it.

Replaced with source-scoped accepts via networking.firewall.extraCommands. Confirmed
against the nixpkgs module source that extraCommands renders BEFORE the final
nixos-fw-log-refuse rule, so the accepts are reachable and not dead code:

  127.0.0.0/8         host-local
  10.0.1.0/24         LAN
  10.10.10.0/24       WireGuard
  192.168.2.0/24      Palantir's segment
  192.168.122.0/24    libvirt VM network
  172.16.0.0/12       docker bridges (host-local only)

Deliberately NO IPv6 accept: Gitea is reached as http://10.0.1.49:3010, and the LAN
IPv6 prefix is globally routable, so any v6 accept would re-open the same hole.

Verified live after switch: iptables `nixos-fw` carries the six scoped accepts at
rules 61-66 directly ahead of the refuse rule at 67; ip6tables `nixos-fw` has NO 3010
rule at all; Gitea still returns HTTP 200 on 127.0.0.1 and 10.0.1.49; `/api/v1/version`
=> 1.27.3; `git ls-remote` over 3010 still works. Switch exit 0.

Diagnostic if this bites: a blocked client appears in the journal as
'refused connection:' -- add its subnet here rather than reverting the scoping.
2026-09-12 10:20:12 -04:00
Harley ea249f91b8 crowdsec: remove entirely (it was inert); cap the journal; bound nix parallelism
CrowdSec is gone, at Harley's call. It had been non-functional for days -- the
firewall bouncer never applied bans at nftables, and the agent itself failed on
restart because /var/lib/crowdsec is a symlink to private/crowdsec and a tmpfiles
`d` rule creates a directory but will NOT replace an existing symlink. Rather than
carry a broken IPS, it is removed: activation drops the user/group and the
/etc/tmpfiles.d symlink, and its left-over state (/etc/crowdsec, /var/lib/crowdsec,
/var/lib/private/crowdsec) was deleted by hand.

  !! This host now has NO intrusion-prevention layer. That is deliberate.
  !! To restore: services.crowdsec + services.crowdsec-firewall-bouncer, and give
  !! StateDirectory a REAL directory (tmpfiles type `R`, then `d`) not a symlink.

Two capacity fixes that were owed after the 2026-09-11 collapse:

* nix.settings.max-jobs = 6, cores = 6. The defaults (auto / 0) resolve on this
  32-thread host to 32 builders EACH running a fully parallel make -- ~1024
  concurrent writers on the single dm-crypt+btrfs root device, which is why no
  build could finish (load 100, PSI 92%, 61 D-state, journald killed 34x/8h).
  Bound by hand the same advance built clean on 2026-09-12: 1862 derivations,
  0 errors, peak PSI ~12%. Now declarative, so nobody has to remember the flag.

* services.journald.settings.Journal.SystemMaxUse = "1G". Uncapped, journald
  defaults to 10% of the FILESYSTEM (up to ~46 GiB on this root) on the same
  device as /nix/store, and was being watchdog-killed under build I/O.
  NOTE: this nixpkgs renamed the option -- `services.journald.extraConfig` now
  trips a dead-option assertion, hence the settings form.

Result: the system is now `running` with 0 failed units (was `degraded` with 3-4).
2026-09-12 10:15:48 -04:00
Harley 34e168e4ca nixpkgs: advance 2026-07-19 -> 2026-09-10 (gitea 1.27.3); hold sunshine + rpcs3
Fixes CVE-2026-60004 (Gitea < 1.27.1 RCE). The new snapshot carries Gitea
1.27.3 -- verified live after activation, /api/v1/version -> 1.27.3, web UI
HTTP 200. Registration is now closed (DISABLE_REGISTRATION = true), which
shuts the account-acquisition path the CVE needs.

HOLDS: new `nixpkgs-held` flake input pinned to rev 241313f -- the revision
the running system was built with -- so held packages reproduce their
already-built derivations byte-for-byte and Nix reuses the existing outputs:
  * sunshine: upstream deleted the pinned tag v2026.826.1804 (unfetchable)
  * rpcs3:    new snapshot's 0.0.42-unstable-2026-08-15 fails with a GCC ICE
Both verified before switching: `nix-store -q --deriver <installed path>`
matched `nix eval .drvPath` exactly for each, so neither package recompiled.
(Sunshine's security-wrapper and unit file ARE regenerated by the new
nixpkgs, so the user unit restarts on activation; the binary and its
behaviour are unchanged.)

BUILD METHOD: bounded with --max-jobs 6 --cores 6. The 2026-09-11 attempt
used max-jobs=auto (32 builders) x cores=0 (each builder fully parallel on
32 threads) -> ~1024 concurrent writers on the single dm-crypt+btrfs root
device, which collapsed it: load 100, PSI 92% some / 88% full, 61 tasks in
D-state, systemd-journald killed 34x in 8h, and the build could not finish.
This run: peak PSI ~12%, D-state 0, 1862 derivations, 0 errors.

NOTE: `nixpkgs-held` also removes the reliance on a previous attempt's
unrooted store paths. Those are GC-able (a plain nix-collect-garbage reaped
them on 2026-09-12, resetting an apparent "0 to fetch" back to a full 8.3
GiB download). Keeping the old revision as a first-class flake input makes
the reuse explicit and reproducible instead of incidental.
2026-09-12 02:38:36 -04:00
Harley b253d49afe feat(containers): declare camofox-browser as an oci-container
The stealth-browser fallback container was created by hand with docker run,
so no rebuild could reproduce it and browser-profile state was discarded on
every recreate. Declare it under virtualisation.oci-containers (docker
backend) instead:

- image camofox-browser:152.0.4-x86_64, pull = never (locally built, no registry)
- ports 9377:9377 on 0.0.0.0 (ESP32 status dashboard probes it over the LAN)
- volume state -> /root/.camofox so sessions survive a recreate
- extraOptions --restart=unless-stopped, matching every other Docker workload
- autoRemoveOnStop = false: --rm conflicts with --restart (docker refuses both)

Verified: docker-camofox-browser.service enabled+active, systemd restart
recreates the container end-to-end, /health + LAN 200, 15/15 REST checks.
2026-09-11 08:52:19 -04:00
Harley c890dfed27 browser: add chromium + headless CDP user service on 127.0.0.1:9222
Gives the agent a real CDP browser backend. browser_exec (Browser Use CLI)
resolves browser.cdp_url before falling back to a local Chrome, so this
service is what makes it work on this host. Loopback-only; Camofox on 9377
stays the stealth fallback for anti-bot pages.
2026-09-10 21:35:21 -04:00
Harley 7fda0ffacf vaultwarden: close signups after first account registered
SIGNUPS_ALLOWED true -> false. The setting was explicitly documented as
temporary ("open only until the first account is registered, then set false
and rebuild"); the account now exists. INVITATIONS_ALLOWED was already false,
so no new account can be created without an invite.

Verified live: built env file SIGNUPS_ALLOWED=false; /api/config ->
settings.disableUserRegistration=true.
2026-09-10 20:44:35 -04:00
Harley c8ba0f09da vault: self-host Vaultwarden (Bitwarden-compatible) behind nginx on 8443
- services.vaultwarden on 127.0.0.1:8222, sqlite, DOMAIN includes :8443
- nginx vhost vault.wingard.pro listens on 8443 only, which is NOT
  port-forwarded; firewall opens 8443 on eno1 + wg0 only, so the vault is
  reachable over LAN and WireGuard but never via the *.wingard.pro wildcard
- separate port-80 "vault-acme" vhost serves ACME HTTP-01 tokens only
- security.acme now issues/renews vault, jellyfin and speedtest certs
  automatically, replacing the hand-copied /etc/letsencrypt/live certs that
  had no renewal automation (jellyfin's expired 2026-09-23)
- Pi-hole resolves vault.wingard.pro -> 10.0.1.49 for LAN clients
- daily sqlite-consistent backup to /var/backup/vaultwarden

SIGNUPS_ALLOWED is temporarily true so the first account can be registered;
flip to false afterwards.
2026-09-10 11:06:13 -04:00
Harley b7376423c7 security: add CrowdSec IPS with nftables firewall bouncer
ssh + linux collections, LAPI on loopback, whitelist for trusted LAN/WG
ranges. tmpfiles rule keeps /var/lib/crowdsec a real directory (the module's
StateDirectory symlink is unwritable through ProtectSystem=strict).

This was already live in the working tree; committing it on its own so the
vaultwarden change stays a separate, reviewable commit.
2026-09-10 11:04:55 -04:00
Harley 56ebe57bbb storage: add hermes-q Samba share for Exos command transport (wg0-scoped, hosts allow 10.10.10.2) 2026-09-09 13:39:44 -04:00
Harley 0a4bd15aa6 storage: migrate Exos mount from SSHFS to SMB 2026-09-08 21:15:31 -04:00
Harley 71262e5c7b flake.lock: re-lock hermes-agent narHash after 09-08 fetch to /opt checkout 2026-09-08 02:06:06 -04:00
Harley 33cf28116c checkpoint live config: 20TB LUKS via unlock-20tb, media group + sabnzbd on 20TB, hermes v0.21.0, segno 2026-09-07 16:37:07 -04:00
Harley 6e20a92bdf hermes-dashboard: run under nyra profile on 0.0.0.0 so LAN clients can reach port 9119
Tux profile is gone; the dashboard was bound to 10.10.10.1 (WG-only) and referenced
the removed 'tux' profile, so it couldn't start and Hermes-Relay clients got
connection-refused on the LAN IP. Bind 0.0.0.0, run -p nyra, workdir=nyra.
2026-09-07 01:20:23 -04:00
Harley b1a3f28866 wireguard: migrate wg0 to declarative NixOS config, keep old wg0.conf as legacy
The wg0 interface is now configured via networking.wireguard.interfaces in
configuration.nix (privateKeyFile wg0-key.key). The hand-managed wg0.conf is no
longer read by the system; the old file content is preserved as
wg0.conf.UNUSED-legacy-not-read-by-nixos for reference. The unused legacy
config is intentionally not read by NixOS.
2026-09-02 19:02:29 -04:00
Harley 146176de2e home-assistant: enable ZHA for the Sonoff ZBDongle-E Zigbee dongle
- add zha + zhaquirks (overlay, from prebuilt wheel) + universal-silabs-flasher
  + ha-silabs-firmware-client to services.home-assistant.extraPackages
- put the hass service user in the dialout group
- grant the HA unit access to /dev/ttyUSB* via systemd DeviceAllow (the module's
  DevicePolicy=closed otherwise blocks it -> EPERM and ZHA cannot connect)

Also bundles the pre-existing arch-vm wireguard peer rework that was already in
the working tree.
2026-09-02 18:55:01 -04:00
Harley 97c3003df0 hermes-webui: enable systemd service on port 8787 (localhost)
Serve the WebUI from the sealed uv2nix venv (passthru.hermesVenv) with
HERMES_WEBUI_PYTHON pinned so subprocess agent/gateway runs use the venv
python, not system python3. HERMES_WEBUI_AGENT_DIR points at the source
checkout for run_agent.py + hermes_cli. Localhost-only bind (sessions+memory).
2026-08-29 16:42:39 -04:00
Harley 5a7ade7fed hermes: upgrade to v0.20.5 (2026.8.19); unify nixpkgs node; override inline-snapshot test gate 2026-08-26 22:26:50 -04:00
Harley 5dce5b5d6a home-assistant: add rest-platform python deps (jsonpath-python, xmltodict); add e2fsprogs 2026-08-26 01:30:50 -04:00
Harley 23f223cafa sunshine: restore NVENC library resolution 2026-08-19 21:03:28 -04:00
Harley 046d496a91 boot: restore normal non-serial default 2026-08-18 23:08:47 -04:00
Harley c9263f59d8 boot: test COM1 console without earlycon 2026-08-18 22:31:54 -04:00
Harley ead09e9b28 boot: remove failing serial kernel console parameters 2026-08-18 21:57:16 -04:00
Harley dcfe9a7a2a boot: keep local console primary for serial kernel output 2026-08-18 21:29:19 -04:00
Harley a85ea54c9e boot: serial console on COM1 (ttyS0, 115200) — kernel/initrd/LUKS output over the UtumnoSerial bridge 2026-08-18 20:26:53 -04:00
Harley 6439710a03 firewall: open 8097 (leftover from fsbrowse LAN API session) 2026-08-18 20:26:20 -04:00
Harley 01866f8e35 firewall: open 3233 (espota OTA) and 8099 (fsbrowse LAN API) 2026-08-17 00:56:15 -04:00
Harley 7fe40ea55d esp: add VS Code and fix PlatformIO extension installs 2026-08-14 23:04:08 -04:00
Harley bd9d49ae17 arduino-ide: launch with --use-angle=swiftshader (renderer crash + lag fix) 2026-08-14 22:31:11 -04:00
Harley b18d9ee145 home-assistant: add HA 2026.7.2 (port 8123, config on 8TB, boot-ordered after unlock-8tb) 2026-08-14 14:11:31 -04:00
Harley 31bf878dc0 arduino-ide: overlay adds libxkbfile to FHS sandbox (stuck splash fix) 2026-08-14 11:59:51 -04:00
Harley d2a0ae78fc boot-race: order gitea/arrs/jellyfin after unlock-8tb (8TB LUKS mount) 2026-08-14 11:46:59 -04:00
Harley 87790cad62 arduino-ide: add GUI IDE for ESP development 2026-08-14 11:39:08 -04:00
Harley 9b13aef3bf nix-ld: stub loader + libusb/ftdi/hidapi libs for ESP-IDF toolchains 2026-08-14 03:34:33 -04:00
Harley 1a5bf82a64 esp-devtools: platformio/esptool/arduino-cli/picocom + dialout group for harley 2026-08-14 03:28:48 -04:00
Harley 42c08eb652 rss-filter-proxy: replace ollama-only proxy with general recency filter (port 8087, 30d window) 2026-08-04 23:44:44 -04:00
Harley 1344473fab ollama-feed-proxy: cap Ollama blog feed at latest 20 items (port 8234) 2026-08-04 21:58:36 -04:00
Harley 593ae1ad82 hermes-dashboard: raise WS-orphan reap grace 20s->120s (voice messages reaped mid-staging) 2026-08-04 14:19:48 -04:00
Harley f3e3cfc91c open firewall TCP 3000 for Karakeep 2026-08-04 09:13:39 -04:00
Harley 6ade8ab336 gitignore configuration.nix backups 2026-08-04 09:12:30 -04:00
Harley 5c9c5b8d7c add karakeep bookmark/read-later stack (docker compose, port 3000) via systemd oneshot unit 2026-08-04 09:12:17 -04:00
Harley e29091c978 fix: open firewall ports 8642 (Hermes API server) + 8767 (hermes-relay) for Android app pairing 2026-08-04 01:28:12 -04:00
Harley bf04cefe55 fix: add gcc libstdc++ to hermes wrapper LD_LIBRARY_PATH (sherpa-onnx wake word) 2026-08-03 22:04:32 -04:00
Harley d523b1a6c1 fix: Hermes voice — portaudio via ctypes find_library shim + LD_LIBRARY_PATH 2026-08-03 19:22:03 -04:00
Harley 6289f433ab chore: upgrade Hermes Agent to v0.20.0 2026-08-03 15:09:52 -04:00
Harley 85d2fb5250 fix: open firewall port 8743 for dashboard TLS vhost 2026-08-02 03:08:48 -04:00
Harley 3230f873b4 fix: addSSL=true on dashboard vhost so module emits ssl_certificate (custom listen alone doesn't set hasSSL) 2026-08-02 03:04:55 -04:00
Harley b95f7f9f1b fix: use $host in dashboard vhost (gixy host_spoofing blocks $http_host on NixOS 26.11 validateConfigFile) 2026-08-02 03:03:49 -04:00
Harley 22e6a9f599 feat: add LAN HTTPS vhost for Hermes dashboard (self-signed, :8743) 2026-08-02 03:00:57 -04:00
Harley 28bdf683c0 chore: update Hermes Agent to 0.19.1 2026-08-01 04:26:18 -04:00
Harley 6ce60431d9 fix: include mnemosyne embedding deps in hermes wrapper 2026-07-30 02:19:42 -04:00
Harley 2f5c8cb704 fix: inject mnemosyne PYTHONPATH into hermes dashboard 2026-07-29 23:03:46 -04:00
Harley e9429b3579 Add Caps+1 TV power-off shortcut 2026-07-28 20:11:09 -04:00
Harley b154722504 Pin Hermes Python stack for NixOS updates 2026-07-23 02:13:31 -04:00
Harley 1d46bfded7 fix: use overloadt instead of overload for Caps+grave
The overload() function has a default tap timeout of 0ms, which means
it immediately sends the tap action (esc) on Caps press instead of
activating the nav layer. Using overloadt(nav, esc, 200) requires
holding Caps for 200ms before activating the nav layer, which allows
time to press grave while the layer is active.
2026-07-23 00:10:35 -04:00
Harley 1bf32a21ec fix: add PATH to tv-power-listener service environment
The toggle script needs bash in PATH to execute. The service's
restricted PATH didn't include bash, causing 'env: bash: No such file or directory'.
2026-07-23 00:04:08 -04:00
Harley 0ec64cc6a3 fix: make listener robust with device discovery and retry, add keyd dependency
- Use dynamic device discovery instead of hardcoded event path
- Add retry logic for device errors
- Add after=keyd.service to systemd user service
- Remove requires=keyd.service (system service can't be required by user service)
2026-07-22 23:59:03 -04:00
Harley 47107fcd33 nixify: use python312 directly instead of nix-shell for tv-power-listener
- Remove nix-shell dependency from the listener service
- Use PYTHONPATH to make evdev available to system python312
- More permanent and reliable than nix-shell approach
2026-07-22 23:55:22 -04:00
Harley e72deef39c fix: add overload_tap_timeout=500 to keyd for Caps+grave reliability 2026-07-22 23:43:10 -04:00
Harley ce70e287c3 nixify: TV power toggle listener as proper NixOS package
- Move tv-power-listener.py into Nix store via writeShellScriptBin
- Remove external wrapper script and manual systemd user service file
- Service now fully managed by NixOS configuration
2026-07-22 23:38:40 -04:00
Harley 0fcea60e69 feat: TV power toggle via Caps+grave -> F14 keyd mapping + evdev listener
- keyd: map grave=f14 in nav layer (Caps+grave sends F14)
- tv-power-listener: systemd user service that listens for F14 on keyd virtual keyboard
- toggle-power.sh: sends Power keypress + switches to HDMI1 input
- Uses nix-shell wrapper for python312 + evdev dependencies
2026-07-22 23:33:08 -04:00
Harley da112c61cb fix: use F14 instead of F13 to avoid KDE interception 2026-07-22 23:03:30 -04:00
Harley 6c91adedb1 fix: clean up tv-power-listener.py 2026-07-22 23:02:04 -04:00
Harley 4ca2eb24b8 feat: add Caps+grave -> F13 keyd mapping for TV power toggle 2026-07-22 23:01:37 -04:00
Harley ffe276f7f5 fix: change TV power toggle to Ctrl+Meta+Alt+grave 2026-07-22 22:58:08 -04:00
Harley 6b18e03ff7 fix: change TV power toggle from Caps+F12 to Caps+grave 2026-07-22 22:56:03 -04:00
Harley 182fdc1f8d feat: add TV power toggle key listener service (Caps+F12) 2026-07-22 22:53:40 -04:00
Harley 4bc681a893 revert: remove unsupported f12 exec from keyd config 2026-07-22 22:47:40 -04:00
Harley 96e390ea89 remove accidental .save backup 2026-07-22 22:45:33 -04:00
Harley ef8c1c6153 keyd: add F12 to toggle TV power + switch to Computer input 2026-07-22 22:44:55 -04:00
Harley 6dce4c7304 Enable virtiofsd for Arch VM shared storage 2026-07-22 14:42:56 -04:00
Harley 24f5af3ebd Use 192.168.2.4 for Arch VM WireGuard 2026-07-22 12:42:28 -04:00
Harley e589c72bc2 Add WireGuard peer for Arch VM 2026-07-22 12:39:59 -04:00
Harley 8f58290b98 Enable KVM libvirt for Arch VM 2026-07-22 12:35:16 -04:00
Harley 4bd583c450 Open active Sunshine streaming ports 2026-07-21 16:54:31 -04:00
Harley 034b3033c9 Fix Sunshine mDNS interface advertisement 2026-07-21 16:46:27 -04:00
Harley b2b7bae2ef revert to 44dce202 config (before this morning's changes) 2026-07-20 21:44:34 -04:00
Harley 3a2c38d38c avahi: restrict to eno1+lo only, enable nssmdns — prevents Docker bridge IPs in mDNS (fixes Moonlight iPad connection timeout) 2026-07-20 21:10:23 -04:00
Harley e6b7234632 remove atomic ephemeral user 2026-07-20 20:50:47 -04:00
Harley 2b741e12aa fix sddm atomic user selection 2026-07-20 19:45:45 -04:00