A/B-tested 7B vs 3B on a real NETTO receipt. 7B took 3.6 min/receipt
vs ~30s for 3B. Accuracy gain was minimal — 7B still picked a line
item ('ARLA SEOMELK 1.') as merchant when the OCR header was missing,
just a different one than 3B picked ('REJESALAT'). The merchant
problem isn't a model-size problem; it's an OCR problem (Tesseract
missed the NETTO logo entirely on this receipt).
Keeping both models in loadModels so we can flip back via env var
without a fresh pull.
3B was making column-parsing mistakes on real receipts (conflating
qty/price, nominating line items as merchant). 7B Q4_K_M is ~3x slower
on phantom-ship CPU (~5min vs ~1.5min per receipt) but materially
better at structured extraction. Background task — speed isn't critical.
Keep 3B in loadModels as a fallback knob (BON_OLLAMA_MODEL env).
notes serves both notes.dannydannydanny.me (blog) and
dannydannydanny.me (apex landing) from the same FastAPI process,
switching on Host header. Source rsync'd from ~/python-projects/26_notes/
to /home/danny/notes/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion oneshot for mulbo-server: populates the dedup index
(tracks_index) from Navidrome's existing 15k tracks. Without it,
GET /tracks/by-hash misses for every existing offshore track and
the upload path duplicates content.
Inherits same User/SupplementaryGroups as the running service.
chromaprint added to PATH for fpcalc. TimeoutSec=8h covers full
274 GB hashing run with headroom.
Triggered manually — not auto-scheduled:
sudo systemctl start mulbo-server-backfill
journalctl -fu mulbo-server-backfill
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mulbo-server's /folders endpoint reads navidrome.db directly because
the Subsonic API's path field is tag-virtual (not real fs paths).
Three pieces:
- services.navidrome UMask = 0027 (force) so future DB writes are
group-readable; default was 0077.
- tmpfiles z-rules to chmod 0640 the existing navidrome.db, -wal, -shm
(created under the old umask).
- mulbo-server gets SupplementaryGroups=[navidrome] so the unit's
process can read those files.
Trade-off: couples mulbo-server to Navidrome's schema (specifically
media_file.id + media_file.path). Acceptable given Navidrome 0.61.1
has been stable on these columns; we'll catch breakage at the /health
navidrome_db_readable probe.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds:
- MULBO_MUSIC_ROOT=/srv/music (for the /folders fs walk)
- EnvironmentFile=/home/danny/.secrets/mulbo-server-navidrome (creds
for Subsonic API calls — file is mode 600, owned by danny, not in
source control)
Required for the new /folders endpoint and the upcoming POST /tracks
which needs to call search3.view + startScan.view.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 5 of the mulbo Navidrome-pivot — companion HTTP service co-
located with Navidrome that owns uploads + the dedup index + the
real on-disk folder layout (which Navidrome's tag-virtual API can't
expose). Wire spec lives in the mulbo repo at 20_mulbo/SERVER_API.md.
Runs as `danny` so writes pass through to /home/danny/music/mulbo-
uploads via the existing /srv/music ro bind-mount — no mount changes
needed. Bound to [::]:8091 (8090 was taken by escape-hormuz on
phantom-ship); firewall scopes it to the ZT mesh, same trick bbbot
uses on 8080.
Pulls the python-projects repo via SSH using sunken-ship's id_ed25519
(registered as a read-only deploy key on the repo). Auto-pull timer
runs every 15 min, offset from fitness-bot-pull and dotfiles-rebuild.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hara (openclaw) shipped escape_hormuz imperatively — service runs but
firewall + Caddy vhost weren't declared, so the public URL didn't
resolve and the firewall rule would've been wiped on next
dotfiles-rebuild. Bring it under nix:
phantom-ship.nix
- systemd.services.escape-hormuz on port 8090, binds :: for ZT
- 8090 added to zt+ allowedTCPPorts
- tmpfiles entry for /home/danny/.local/share/escape_hormuz
vps-relay.nix
- Caddy vhost escapehormuz.dannydannydanny.me → ZT [::]:8090
Phase 1 of the de-platform-from-GitHub roadmap (vimwiki/diary/2026-05-03.md).
- phantom-ship: services.forgejo bound to 0.0.0.0:3000, sqlite, lfs on,
registration disabled, sign-in required.
- phantom-ship: add :3000 to the existing zt+ allowedTCPPorts list
(joins shelfish/scuttle — never exposed on WAN/Wi-Fi).
- vps-relay: Caddy vhost git.dannydannydanny.me reverse-proxies over
ZT to phantom-ship:3000.
Manual steps before reachable:
1. GoDaddy A record git.dannydannydanny.me -> 89.167.39.251
2. clan machines update phantom-ship && clan machines update vps-relay
3. On phantom-ship: bootstrap admin (registration is disabled)
KomTolk is the rebranded translate-platform — same Copenhagen
translation gigs Mini App, new name. Service on port 8080, mirrors
shelfish/scuttle/banana setup. New tmpfiles dir + zt+ firewall
opening + caddy vhost.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bananasimulator.service mirrors shelfish/scuttle (fastapi + uvicorn
+ httpx + python-telegram-bot). Port 8083. ENV BS_RIPE_MIN_PER_STAGE=2
in prod (30 min total banana lifetime); preview uses 0.5 for fast
testing.
vps-relay gets a fifth vhost (bananasimulator.dannydannydanny.me)
reverse-proxying to phantom-ship over ZeroTier. The shipyard manifest
has been pointing at this URL as a placeholder since day one — now
it's actually live.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
scuttle.service mirrors shelfish — fastapi/uvicorn/httpx/python-telegram-bot
plus websockets, runs uvicorn --host :: --port 8082, DB at
~/.local/share/scuttle/scuttle.db (tmpfiles rule + zt+ firewall port
added alongside shelfish's).
vps-relay gets a fourth virtualHost (scuttle.dannydannydanny.me)
reverse-proxying to phantom-ship over ZeroTier. WebSocket upgrade is
transparent under Caddy's reverse_proxy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two write-capable IMAP tools:
- gmail_mark_read: sets \Seen flag on a message
- gmail_archive: copies to [Gmail]/All Mail and removes from INBOX
The IMAP connection already used SELECT (read-write mode); this just
exposes the mutation surface through MCP.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
shelfish was only listening on 127.0.0.1 — vps-relay's Caddy
couldn't reach it over the ZT mesh. Bind 0.0.0.0 and allow 8081
inbound on \`zt+\` interfaces (not the global firewall — same
pattern sunken-ship uses for bbbot).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Original commit added Caddy directly on phantom-ship and opened
ports 80/443 — that would have exposed the home connection's
public IP via DNS. Reverting that and using the existing relay
pattern instead: vps-relay (Hetzner) terminates public TLS and
reverse-proxies over ZeroTier to phantom-ship's ZT IPv6 on 8081.
phantom-ship now just runs shelfish.service bound to 127.0.0.1:8081;
it accepts connections only from the ZT mesh interface (since
caddy/firewall changes are gone, the only listeners are the
existing trusted-interface ones plus this loopback).
vps-relay gets a third virtualHost alongside navidrome and bbbot.
DNS: shelfish.dannydannydanny.me → 89.167.39.251 (vps-relay public IP),
NOT phantom-ship's home IP.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Caddy fronts 80/443 with auto-Let's-Encrypt; reverse-proxies
shelfish.dannydannydanny.me to the local shelfish service on
127.0.0.1:8081. ACME issues the cert once the subdomain A-records
to this host's static IP.
Shelfish service mirrors shipyard's pattern: nix-built python env,
SHIPYARD_BOT_TOKEN_FILE pointed at the existing secret, DB stored
outside the rsynced code dir at ~/.local/share/shelfish/ so deploys
don't clobber state.
Code itself is rsync'd from ~/python-projects/27_shelfish/ to
/home/danny/shelfish/ (same convention as shipyard).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an MCP server exposing read tools (list_inbox, search, read_email)
across three personal Gmail accounts using existing app passwords in
/etc/openclaw/. Wired into claude-channels via --mcp-config. Slated for
replacement by an OAuth2 Gmail+Calendar server in path 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VPS public SSH: enable fail2ban with bantime-increment so brute-force
probers get evicted with exponential backoff (1h → 4h → 16h → 2.7d →
10.7d, capped at 30d). Default jail covers sshd; maxretry=5 in 10m.
server-debug-tools: htop, tcpdump, dnsutils, jq, curl. Imported by
sunken-ship + phantom-ship via flake.nixosModules.server-debug-tools.
These are the practical bits we'd otherwise pick up by enabling
clan.core.enableRecommendedDefaults — but the full clan defaults flip
systemd-networkd/resolved on, which broke dnsmasq + navidrome's resolv
.conf bind-mount on the homelab servers, so we cherry-pick instead.
Move the imperative SSH-key-related scars accumulated during the
clan/VPS rollout into nix config so future installs and rebuilds
reproduce the same state:
- danny@sunken-ship + danny@phantom-ship: trust the mac admin key
(id_ed25519_<host> on Daniel-Macbook-Air) and the host's own
self-loopback key (used by clan ssh-ng:// nix-copy-closure back
to the same host during `clan machines update`).
- root@sunken-ship + root@phantom-ship: trust the mac admin key so
`clan machines update` can run its SOPS-key upload step that
SSHes to root@<host> to write /var/lib/sops-nix/key.txt.
Existing key files (~/.ssh/id_ed25519 on each host) stay where they
are; the keypair was generated once during initial bootstrap and the
public side is now declared above. Reinstalls would regenerate and
need the pubkey re-pinned here.
Stage 4.5: declare a Hetzner-hosted reverse-proxy VPS as a clan machine.
- nixos/hosts/vps-relay.nix: Debian→NixOS cx23 in hel1. Caddy at public
80/443 reverse-proxies navidrome.dannydannydanny.me and
bbbot.dannydannydanny.me over ZT to sunken-ship.
- nixos/disko-cloud.nix: simple GPT + ext4 root, no LUKS — cloud provider
has physical disk anyway and there's no operator at boot.
- flake-modules/clan.nix: register vps-relay as an inventory machine,
zerotier peer, internet networking target at its clan-generated ZT
IPv6, and add vps-relay.clan to clanHostsModule /etc/hosts.
- sunken-ship fitness-bot: drop pkgs.cloudflared from PATH + set
WEBAPP_URL=https://bbbot.dannydannydanny.me. Paired with the bbbot
upstream patch (start.py honors env WEBAPP_URL and skips cloudflared
when set) — once the 15-min fitness-bot-pull timer pulls that change,
bbbot will stop churning trycloudflare.com URLs.
Vars (zerotier identity/ip + sops machine key) generated on sunken-ship
because clan's hermetic sandbox on macOS fails to run the zerotier
identity generator (same workaround as for data-mesher earlier).
VPS install flow: Hetzner-created Debian box, then `clan machines
install vps-relay --target-host root@<public-ipv4>` reinstalls to
NixOS; subsequent updates go over ZT.
Stage 4f cleanup. The flake moved from ~/dotfiles/nixos/ to ~/dotfiles/
in 88c5139; docs and install scripts hadn't been refreshed. Point all
rebuild / flake references at the new root:
- AGENTS.md, README.md, server-quickstart.md, docs/server-installer-usb.md,
docs/sunken-ship-wifi.md, nixos/readme.md — rebuild command paths.
- scripts/nixos-server-install.sh — auto-detect now looks for flake.nix
at repo root (was nixos/flake.nix).
- scripts/post-install-provision.sh — first-rebuild hint path.
`nixos/hosts/<host>-hardware.nix` and friends stay where they are —
host-specific NixOS modules still live under nixos/; only the flake
entry-points + sops/ + vars/ + lib/ + modules/ + flake-modules/ moved.
nixos/readme.md rewritten to reflect the split (flake at root, per-host
modules under nixos/).
Stage 4e, take 2. Point the clan-community input at our fork's branch
that sanitizes machine.name for data-mesher's file-name validator
(upstream PR: clan/clan-community#25). Revisit this pin once merged.
- flake.nix: clan-community.url → fork + fix branch
- flake-modules/clan.nix: re-adds meta.domain = "clan",
inventory.instances.data-mesher (sunken-ship bootstrap, both default),
inventory.instances.dm-pull-deploy (sunken-ship push, both default
action="switch"), and clanHostsModule that puts /etc/hosts entries
for <host>.clan → each machine's ZT IPv6 so libp2p multiaddr
resolution works without a clan-domain DNS server.
- Generator vars for data-mesher + dm-pull-deploy signing keys were
regenerated on sunken-ship (data-mesher isn't packaged for
aarch64-darwin, so clan vars generate runs on Linux).