Open work only. Completed work is recorded in CHANGELOG.md; the unified-layout design rationale is in UNIFICATION.md.
Setup migrates install methods but never uninstalls the old copy, so each live run leaves shadowed binaries to reconcile.
- Mac mini live-run cleanup (from the 2026-06
brew leavesaudit):brew uninstallthe testing leftoversforgejo,tea, andpython@3.12(project Pythons come from pyenv/uv), pluszsh-autosuggestions/zsh-syntax-highlighting/powerlevel10k(antidote manages them now), andbrew uninstall --cask claude-code(repo installs it via curl) - MBP live-run cleanup (same audit):
brew uninstall tea python-tk@3.11 python@3.11 zsh-autosuggestions zsh-syntax-highlighting powerlevel10k; pre-existing casks (anki, ghostty, obsidian) get picked up by the cask--adoptflag - Dropped when PR #38 auto-closed #34: track the claude-hud display config
(
~/.claude/plugins/claude-hud/config.json) underagentic-ai/Claude/and symlink it frominstall.sh(#34 task 2). Task 3 — the statusLine/usr/bin/nodehardcode — is fixed on this branch (runtimecommand -v nodewith an nvm-glob fallback) - Ubuntu desktop leftover:
sudo apt remove micro— the stale apt 2.0.13 still shadows the snap (/usr/binprecedes/snap/binin PATH) - Caveat for the remaining live runs (CachyOS, both Macs): setup migrates
install methods but never uninstalls the old copy — after each run,
command -vevery migrated tool to catch shadowed binaries - Later: consider base + per-platform overlay for zshrc (desktop vs server vs macOS)
linux-server/ups/ monitors the EcoFlow River 3 Plus over NUT, but ups.load
shows empty. Root cause: the EcoFlow firmware exposes no load percentage over
USB HID (confirmed by the EcoFlow HID subdriver author and EcoFlow support —
see NUT PR #2837). The fix is NUT's CDC serial companion
(ecoflow-hid-aux-cdc.c), which derives ups.load from
output_power / rated_output_power, plus per-outlet power, frequency,
temperature, and AC input telemetry. It is enabled by ecoflow_cdc_port in
ups.conf.
This is not available in any packaged NUT. Ubuntu 26.04 ships 2.8.4, which
the subdriver driver.version.data: EcoFlow HID 0.01 reflects — it rejects the
variable with Fatal error: 'ecoflow_cdc_port' is not a valid variable name.
Even the latest stable 2.8.5 lacks it; the CDC feature landed post-2.8.5 in
NUT master. So the only way to get ups.load is to build NUT from git master.
State before this was attempted (don't skip these notes):
/dev/serial/by-id/usb-EcoFlow_EF-UPS_RIVER_3_Plus_...-if01→/dev/ttyACM0already exists (the unit exposes the CDC ACM interface; groupdialout).ups.confintentionally does not carryecoflow_cdc_portyet — adding it to the 2.8.4 driver makesnut-driver@ecoflowdie in a restart loop (result 'protocol'). It must be added after the master build replaces the driver.
# 1. Build deps (autotools + libusb + ssl for the NUT build)
apt install -y git build-essential autoconf automake libtool pkg-config \
libtool-bin libusb-1.0-0-dev libssl-dev
# 2. Clone master (depth-1 is fine)
git clone --depth 1 https://github.com/networkupstools/nut /usr/local/src/nut
cd /usr/local/src/nut
# 3. Generate configure + build flags tailored to the Ubuntu usrmerge layout
./autogen.sh
./configure \
--with-usb=yes \
--with-serial=yes \
--with-statepath=/run/nut \
--with-pidpath=/run/nut \
--with-altpidpath=/var/run/nut \
--prefix=/usr \
--sysconfdir=/etc \
--localstatedir=/var \
--with-drvpath=/usr/libexec/nut \
--with-cgipath=/usr/lib/cgi-bin/nut \
--with-udev-dir=/lib/udev \
--with-confdir=/etc/nut
# 4. Build (parallel; -j to taste)
make -j"$(nproc)"
# 5. Back up the distro binaries in case apt re-packages later
cp -a /usr/sbin/upsd{,,.distro} && cp -a /usr/sbin/upsdrvctl{,,.distro} \
&& cp -a /usr/sbin/upsmon{,,.distro} && cp -a /usr/bin/upsc{,,.distro} \
&& cp -a /usr/libexec/nut/usbhid-ups{,,.distro}
# 6. Install (overwrites the distro binaries; configs stay in /etc/nut)
make install
ldconfig
# 7. Confirm the new driver now understands the CDC variable
upsdrvctl -h | grep -i cdc # or: /usr/libexec/nut/usbhid-ups -h | grep cdc
# 8. Add the CDC port to the ecoflow stanza in ups.conf, then redeploy
# (this deferred change is tracked right below)
cd ~/github/Computer-Setup/linux-server/ups
sudo bash setup.sh # restarts nut-driver@ecoflow
upsc ecoflow ups.load # expect a percentage, e.g. 0 - 100Caveats:
- Master is development code. If something's wrong, restore the
.distrobinaries (mv ....distro back into place) —/etc/nutconfigs are untouched either way. - An
apt upgradeof thenutpackage would overwrite the source build; the.distrobackups make the rollback obvious. - If the CDC poll fails, HID monitoring stays authoritative (by design); only
the enriched fields (
ups.load,input.*,outlet.*, temperatures) go stale — seeecoflow-hid-aux-cdc.c(ecoflow_cdc_poll).
Add to the [ecoflow] stanza in linux-server/ups/ups.conf (substitute the
actual serial suffix from your unit):
ecoflow_cdc_port = /dev/serial/by-id/usb-EcoFlow_EF-UPS_RIVER_3_Plus_<serial>-if01
Use the persistent by-id path, not the transient /dev/ttyACM0. Then commit
the ups.conf change. (Deliberately not committed yet: the 2.8.4 driver in
current service rejects it and enters a restart loop.)
- Re-run every benchmark suite end-to-end on one Mac and confirm the result
JSON has no unexpected
nullfields before treating the measurements as validated. The review fixes landed, but no completed post-fix suite run is recorded yet.
Config uses mlx_lm.server with Qwen 3.5 9B (4bit, MLX) on the Mac Mini M4.
opencode-local script auto-discovers models in ~/.models/, starts the
server, and launches OpenCode.
Still to explore:
- Test tool-calling quality with Qwen 3.5 9B (does it work well for agentic coding?)
- Set up on CachyOS/AMD R9700 with Gemma 4 and Qwen 3.6 (via llama.cpp or lemonade)
- Add CachyOS provider config once the model/runtime is chosen
- Consider
small_modelfor lightweight tasks (title gen, etc.) - Install
opencode-localvia install.sh and verify PATH
Core Arch/CachyOS support shipped in PR #18 (see CHANGELOG). Remaining:
- Test
--personalflag end-to-end - Create PR for CachyOS support
Every tailnet-facing service is converted (see CHANGELOG); the non-tailnet edge is what's left. Pattern and full rollout table in ../linux-server/HTTPS.md.
- Set up the NPM trusted-HTTPS edge (domain
ulises-c.me, already owned): NPM wildcard Let's Encrypt cert for*.home.ulises-c.mevia DNS-01, AdGuard rewrite*.home.ulises-c.me→ LAN IP, then per-service proxy hosts. Not started — documented in HTTPS.md to pick up later. - Update Homepage hrefs to HTTPS as each service converts; a service's widget
url:must move to the HTTPS domain too (localhost stops resolving once the host port is dropped)
Server observability & hardening (post-HTTPS rollout) — #49
Improvements identified once every service was wired up with a Tailscale sidecar.
Watchtower has no native history UI, and its /v1/metrics endpoint (now monitored
by Uptime Kuma) is only cumulative counters (watchtower_containers_updated /
_failed / _scanned, watchtower_scans_total) — no container names or image
versions. So the "what was actually updated" has to come from notifications or
logs, not metrics. Build it up in layers:
- Tier 1 — ntfy notifications (quick win, reuses the existing ntfy). On the
watchtower service set
WATCHTOWER_NOTIFICATION_URLto a shoutrrr ntfy URL pointing at our ntfy instance (dedicated topic, e.g.watchtower) andWATCHTOWER_NOTIFICATION_REPORT=truefor a per-run report (which containers updated/failed/skipped, old→new image). Gives a timestamped, persistent history in ntfy + a phone push — directly answers "what & when." Lowest effort. - Tier 2 — Prometheus + Grafana on the existing
/v1/metrics. Scrape the counters, dashboard the update/scan trend, alert onwatchtower_containers_failed > 0. Counts only (no names) — pairs with Tier 1 for the "what." Heavier (new stack); also becomes the home for other metrics (glances, node-exporter, cAdvisor). - Tier 3 (optional) — dedicated update tracker with a UI. Evaluate What's Up Docker (WUD) or Diun, which show per-container available/applied updates in a UI. Could complement or take over watchtower's notification role.
- Pin the Tailscale sidecar image. All 22 sidecars run
tailscale/tailscale:latestand watchtower auto-updates them — a bad release could drop every HTTPS front door at once. Pin a stable tag (bump deliberately) or exclude the sidecars from watchtower. Cheap, high-value. - DRY the sidecar boilerplate. 22 near-identical
<svc>-tsblocks +ts-serve.json(differ only by hostname/port). Use Composeextendsfrom a shared base so a global change (the image pin above,TS_EXTRA_ARGS) is one edit, not 13. Medium effort — touches all stacks, needs live re-verify. - One shared
TS_AUTHKEY. The same OAuth secret is copied into 22.envfiles; rotation/rebuild means editing all of them. Share one env file. - Validation script for the server stacks (CI, like
dryrun-smoke.sh): assert everylinux-server/*/has matching compose +ts-serve.json+.env.example, valid YAML/JSON, serve port == container port,ts-state/gitignored. Catches the drift that bit us mid-rollout (wrong port, stale config). - Tighten the Tailscale ACL — least-privilege for the
tag:containernodes (currently default allow-all). - Forward-auth for the NPM public edge (Authelia/Authentik) — bundle with the
*.home.ulises-c.meNPM setup, since services like filebrowser/glances have weak/no auth once exposed off-tailnet.
The scheduled-maintenance outage took the whole LAN's DNS down and it couldn't self-heal — the server ran the only resolver, and a latent bootstrap deadlock kept the primary AdGuard from recovering.
- Fix the bootstrap deadlock. The primary AdGuard rides its Tailscale
sidecar's netns, and the sidecar's OAuth bootstrap needs DNS — so a cold
start deadlocked (sidecar needs DNS → DNS needs the sidecar). Pinned static
resolvers on
adguard-ts(dns: [9.9.9.10, 1.1.1.1]) so bootstrap never depends on AdGuard —linux-server/adguard. - Secondary DNS on the Pi. Kill the single point of failure: a backup
AdGuard on
<pi-hostname>, host-networked (independent of Tailscale) and config-synced from the primary, handed out as secondary DNS by the router —linux-pi/adguard+linux-pi/adguardhome-sync. - Secondary DHCP. DHCP is still single-homed on the server; a server outage means no new leases. Add a secondary scope (Pi/router) or long leases.
linux-server/qbittorrent currently runs without a VPN (fine for academic/legal
torrents only). Before broader use, route all torrent traffic through a VPN.
- Add a
qmcgaw/gluetunsidecar; set qBittorrent tonetwork_mode: service:gluetun(move the6881+ web UI port mappings onto the gluetun service, add a kill-switch) - Pick a provider — evaluate free Cloudflare WARP vs a paid WireGuard provider
- Add the provider creds to
.env.example/.env
Docker Compose service stacks now live under linux-pi/; base OS provisioning is
still separate from the unified Ubuntu Server profile.
- Secondary AdGuard Home with config sync
- Pi Homepage dashboard and Tailscale front doors
- MotionEye, CUPS, and backup service configuration
- Add a Debian/arm64 Pi platform to the root provisioning engine (no snap/PPA)
- Add the shared headless zsh/Tailscale/Docker/SSH base without duplicating
platforms/server.sh - Run and record the complete provisioning and service verification on Pi hardware