From a7dd6284d894639fe816e96b4f7398f04177b067 Mon Sep 17 00:00:00 2001 From: Danny Date: Sun, 3 May 2026 06:25:54 +0200 Subject: [PATCH 01/72] phantom-ship: add Caddy + shelfish FastAPI service 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) --- nixos/hosts/phantom-ship.nix | 46 ++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index a298360..123c6e9 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -248,6 +248,52 @@ in }; }; + # Caddy reverse proxy in front of the local HTTP services (shelfish, etc). + # Auto-Let's-Encrypt; ACME requires the subdomain to A-record to this host. + networking.firewall.allowedTCPPorts = [ 80 443 ]; + services.caddy = { + enable = true; + email = "powerhouseplayer@gmail.com"; + virtualHosts."shelfish.dannydannydanny.me".extraConfig = '' + reverse_proxy 127.0.0.1:8081 + ''; + }; + + # Shelfish — Goodreads-flavoured book club Mini App. + # Code deployed out-of-band via rsync to /home/danny/shelfish/ + # (staying in-tree in ~/python-projects/27_shelfish/ until spun out). + # Auth: validates Telegram WebApp initData against shipyard's bot token + # (the bot that publishes shelfish via shipyard's project list). + # DB lives outside the rsynced code dir so deploys don't clobber state. + systemd.tmpfiles.rules = (lib.mkAfter [ + "d /home/danny/.local/share/shelfish 0755 danny users - -" + ]); + systemd.services.shelfish = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + fastapi + uvicorn + httpx + python-telegram-bot + ]); + in { + description = "Shelfish FastAPI server (book club Mini App)"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv ]; + environment = { + SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; + SH_DB_PATH = "/home/danny/.local/share/shelfish/shelfish.db"; + }; + serviceConfig = { + WorkingDirectory = "/home/danny/shelfish"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host 127.0.0.1 --port 8081"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + # Auto-rebuild service/timer + safe.directory provided by the # shared dotfiles-rebuild NixOS module (see nixos/modules/dotfiles-rebuild.nix). } From d787b0ea4836e8ab2acd27ffc7697a7dab787a8e Mon Sep 17 00:00:00 2001 From: Danny Date: Sun, 3 May 2026 06:27:21 +0200 Subject: [PATCH 02/72] phantom-ship: merge shelfish data dir into existing tmpfiles rules Fixes nixos-rebuild error: systemd.tmpfiles.rules was set twice. Co-Authored-By: Claude Opus 4.7 (1M context) --- nixos/hosts/phantom-ship.nix | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 123c6e9..ea44e0e 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -160,10 +160,12 @@ in }; }; - # OpenClaw gateway needs write access to its config dir and repo clones. + # OpenClaw gateway needs write access to its config dir and repo clones; + # shelfish wants its DB outside the rsynced code dir. systemd.tmpfiles.rules = [ "d /etc/openclaw 0775 root openclaw - -" "d /var/lib/openclaw/repos 0750 openclaw openclaw - -" + "d /home/danny/.local/share/shelfish 0755 danny users - -" ]; # Hara Gmail MCP server (path 1: IMAP+SMTP). Replaced by an OAuth2 @@ -265,9 +267,7 @@ in # Auth: validates Telegram WebApp initData against shipyard's bot token # (the bot that publishes shelfish via shipyard's project list). # DB lives outside the rsynced code dir so deploys don't clobber state. - systemd.tmpfiles.rules = (lib.mkAfter [ - "d /home/danny/.local/share/shelfish 0755 danny users - -" - ]); + # (tmpfiles rule for the DB dir is bundled into the OpenClaw block above.) systemd.services.shelfish = let pythonEnv = pkgs.python3.withPackages (ps: with ps; [ fastapi From 2aec4d4d5ecf8c1cefd98cc0c1b40f8a734ae001 Mon Sep 17 00:00:00 2001 From: Danny Date: Sun, 3 May 2026 06:29:48 +0200 Subject: [PATCH 03/72] shelfish: front via vps-relay (don't expose phantom-ship public IP) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- nixos/hosts/phantom-ship.nix | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index ea44e0e..f870929 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -250,18 +250,10 @@ in }; }; - # Caddy reverse proxy in front of the local HTTP services (shelfish, etc). - # Auto-Let's-Encrypt; ACME requires the subdomain to A-record to this host. - networking.firewall.allowedTCPPorts = [ 80 443 ]; - services.caddy = { - enable = true; - email = "powerhouseplayer@gmail.com"; - virtualHosts."shelfish.dannydannydanny.me".extraConfig = '' - reverse_proxy 127.0.0.1:8081 - ''; - }; - # Shelfish — Goodreads-flavoured book club Mini App. + # Public traffic comes through vps-relay's Caddy → ZeroTier → here. + # See vps-relay.nix for the public-facing virtualHost. We never expose + # this host's IP directly. # Code deployed out-of-band via rsync to /home/danny/shelfish/ # (staying in-tree in ~/python-projects/27_shelfish/ until spun out). # Auth: validates Telegram WebApp initData against shipyard's bot token From 0b20c375b51c332dd84427457d77d05ece75e93e Mon Sep 17 00:00:00 2001 From: Danny Date: Sun, 3 May 2026 06:30:07 +0200 Subject: [PATCH 04/72] =?UTF-8?q?vps-relay:=20add=20shelfish.dannydannydan?= =?UTF-8?q?ny.me=20vhost=20=E2=86=92=20phantom-ship=20ZT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nixos/hosts/vps-relay.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nixos/hosts/vps-relay.nix b/nixos/hosts/vps-relay.nix index f7bf7b0..6ace54b 100644 --- a/nixos/hosts/vps-relay.nix +++ b/nixos/hosts/vps-relay.nix @@ -101,6 +101,10 @@ "bbbot.dannydannydanny.me".extraConfig = '' reverse_proxy http://[fdd5:53a2:de33:d269:6499:93d5:53a2:de33]:8080 ''; + # Shelfish — phantom-ship's ZT IPv6. + "shelfish.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8081 + ''; }; }; From f599a76aba1a7924a09f1d4e357bfea787643455 Mon Sep 17 00:00:00 2001 From: Danny Date: Sun, 3 May 2026 06:39:57 +0200 Subject: [PATCH 05/72] phantom-ship: open shelfish (:8081) on ZT iface, bind 0.0.0.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- nixos/hosts/phantom-ship.nix | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index f870929..87efe78 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -48,6 +48,11 @@ in }; networking.firewall.trustedInterfaces = [ "enp0s31f6" ]; + # Shelfish HTTP (8081) is reachable only over the ZeroTier mesh — the + # vps-relay Caddy reverse-proxies into it. Same pattern as sunken-ship's + # bbbot. Not in global allowedTCPPorts, so the WAN side stays closed. + networking.firewall.interfaces."zt+".allowedTCPPorts = [ 8081 ]; + hardware.enableRedistributableFirmware = true; # iwlwifi (Intel 8260) + GPU + BT firmware boot.kernelParams = [ "consoleblank=60" ]; # blank TTY after 60s to reduce burn-in @@ -279,7 +284,7 @@ in }; serviceConfig = { WorkingDirectory = "/home/danny/shelfish"; - ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host 127.0.0.1 --port 8081"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host 0.0.0.0 --port 8081"; Restart = "on-failure"; RestartSec = 10; User = "danny"; From 8056e510c58122568d1f218347c871d9d77e5cd0 Mon Sep 17 00:00:00 2001 From: Danny Date: Sun, 3 May 2026 06:41:04 +0200 Subject: [PATCH 06/72] phantom-ship: bind shelfish to '::' so it listens on both IPv4 and IPv6 ZT mesh addresses are IPv6; uvicorn on 0.0.0.0 only listens on IPv4 so vps-relay's caddy got 'connection refused' over the mesh. --- nixos/hosts/phantom-ship.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 87efe78..9df8375 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -284,7 +284,7 @@ in }; serviceConfig = { WorkingDirectory = "/home/danny/shelfish"; - ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host 0.0.0.0 --port 8081"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8081"; Restart = "on-failure"; RestartSec = 10; User = "danny"; From 4d2e40455d412a71acae8af0657492042635942d Mon Sep 17 00:00:00 2001 From: Hara Date: Sun, 3 May 2026 07:14:42 +0200 Subject: [PATCH 07/72] hara-gmail-mcp: add mark_read and archive tools (v0.2.0) 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 --- nixos/pkgs/hara-gmail-mcp/default.nix | 2 +- nixos/pkgs/hara-gmail-mcp/pyproject.toml | 2 +- .../src/hara_gmail_mcp/imap_client.py | 28 +++++++++++++ .../src/hara_gmail_mcp/server.py | 39 +++++++++++++++++-- 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/nixos/pkgs/hara-gmail-mcp/default.nix b/nixos/pkgs/hara-gmail-mcp/default.nix index e82523e..6d62d10 100644 --- a/nixos/pkgs/hara-gmail-mcp/default.nix +++ b/nixos/pkgs/hara-gmail-mcp/default.nix @@ -6,7 +6,7 @@ python3Packages.buildPythonApplication { pname = "hara-gmail-mcp"; - version = "0.1.0"; + version = "0.2.0"; pyproject = true; src = ./.; nativeBuildInputs = [ python3Packages.setuptools ]; diff --git a/nixos/pkgs/hara-gmail-mcp/pyproject.toml b/nixos/pkgs/hara-gmail-mcp/pyproject.toml index b2a985e..fb1db6d 100644 --- a/nixos/pkgs/hara-gmail-mcp/pyproject.toml +++ b/nixos/pkgs/hara-gmail-mcp/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "hara-gmail-mcp" -version = "0.1.0" +version = "0.2.0" description = "Gmail MCP server for Hara (IMAP+SMTP, throwaway pre-OAuth2)" requires-python = ">=3.11" dependencies = [ diff --git a/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/imap_client.py b/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/imap_client.py index 34f2e29..de204e6 100644 --- a/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/imap_client.py +++ b/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/imap_client.py @@ -153,6 +153,34 @@ def read_email( ) +def mark_read( + store: AccountStore, + email_addr: str, + uid: str, + mailbox: str = "INBOX", +) -> None: + """Mark a message as read by adding the \\Seen flag.""" + account = store.get(email_addr) + password = store.password_for(email_addr) + with _open(account, password, mailbox) as conn: + conn.uid("store", uid, "+FLAGS", r"(\Seen)") + + +def archive( + store: AccountStore, + email_addr: str, + uid: str, + mailbox: str = "INBOX", +) -> None: + """Archive a message: copy to All Mail then delete from INBOX.""" + account = store.get(email_addr) + password = store.password_for(email_addr) + with _open(account, password, mailbox) as conn: + conn.uid("copy", uid, "[Gmail]/All Mail") + conn.uid("store", uid, "+FLAGS", r"(\Deleted)") + conn.expunge() + + def _fetch_summary(conn: imaplib.IMAP4_SSL, uid: str) -> MessageSummary: typ, data = conn.uid( "fetch", diff --git a/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/server.py b/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/server.py index 797d41c..0310786 100644 --- a/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/server.py +++ b/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/server.py @@ -1,14 +1,15 @@ """Hara Gmail MCP server. -Exposes a small toolset for reading and (later) replying to mail across -the configured Gmail accounts. v1 ships read-only tools; reply/archive/label -follow once Hara is using these reliably. +Exposes a small toolset for reading and writing mail across the configured +Gmail accounts. Tools: list_accounts() list configured accounts list_inbox(email, limit) recent messages from an account search(email, query, limit) IMAP SEARCH wrapper read_email(email, uid) full body of one message + mark_read(email, uid) mark a message as read + archive(email, uid) archive a message (remove from INBOX) """ from __future__ import annotations @@ -21,7 +22,7 @@ from dataclasses import asdict from mcp.server.fastmcp import FastMCP from .accounts import AccountStore -from .imap_client import list_inbox, read_email, search +from .imap_client import archive, list_inbox, mark_read, read_email, search logger = logging.getLogger("hara_gmail_mcp") @@ -92,6 +93,36 @@ def gmail_read_email(email: str, uid: str) -> str: return json.dumps(asdict(msg), ensure_ascii=False) +@mcp.tool() +def gmail_mark_read(email: str, uid: str) -> str: + """Mark a message as read (sets the \\Seen flag). + + Args: + email: which configured account + uid: the message UID (returned by gmail_list_inbox or gmail_search) + + Returns: + JSON object with ok and uid. + """ + mark_read(_get_store(), email, uid=uid) + return json.dumps({"ok": True, "uid": uid}) + + +@mcp.tool() +def gmail_archive(email: str, uid: str) -> str: + """Archive a message (copies to All Mail, removes from INBOX). + + Args: + email: which configured account + uid: the message UID (returned by gmail_list_inbox or gmail_search) + + Returns: + JSON object with ok and uid. + """ + archive(_get_store(), email, uid=uid) + return json.dumps({"ok": True, "uid": uid}) + + def main() -> None: logging.basicConfig( level=os.environ.get("HARA_GMAIL_LOG_LEVEL", "INFO"), From 6d9ccf5d4e47d0b760cf49d41d632e3759a75ec2 Mon Sep 17 00:00:00 2001 From: Danny Date: Sun, 3 May 2026 07:26:17 +0200 Subject: [PATCH 08/72] phantom-ship + vps-relay: add scuttle service + vhost MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- nixos/hosts/phantom-ship.nix | 42 ++++++++++++++++++++++++++++++++---- nixos/hosts/vps-relay.nix | 5 +++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 9df8375..3a673ca 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -48,10 +48,11 @@ in }; networking.firewall.trustedInterfaces = [ "enp0s31f6" ]; - # Shelfish HTTP (8081) is reachable only over the ZeroTier mesh — the - # vps-relay Caddy reverse-proxies into it. Same pattern as sunken-ship's - # bbbot. Not in global allowedTCPPorts, so the WAN side stays closed. - networking.firewall.interfaces."zt+".allowedTCPPorts = [ 8081 ]; + # Shelfish (:8081) and Scuttle (:8082) are reachable only over the + # ZeroTier mesh — the vps-relay Caddy reverse-proxies into them. Same + # pattern as sunken-ship's bbbot. Not in global allowedTCPPorts, so + # the WAN side stays closed. + networking.firewall.interfaces."zt+".allowedTCPPorts = [ 8081 8082 ]; hardware.enableRedistributableFirmware = true; # iwlwifi (Intel 8260) + GPU + BT firmware @@ -171,6 +172,7 @@ in "d /etc/openclaw 0775 root openclaw - -" "d /var/lib/openclaw/repos 0750 openclaw openclaw - -" "d /home/danny/.local/share/shelfish 0755 danny users - -" + "d /home/danny/.local/share/scuttle 0755 danny users - -" ]; # Hara Gmail MCP server (path 1: IMAP+SMTP). Replaced by an OAuth2 @@ -291,6 +293,38 @@ in }; }; + # Scuttle — topdown tilt-to-move multiplayer Mini App. + # Same vps-relay-fronted ZT path as shelfish; binds to :: so the + # ZeroTier IPv6 address can reach it. + # Code rsync'd from ~/python-projects/26_scuttle/ to /home/danny/scuttle/ + # DB at ~/.local/share/scuttle/scuttle.db. + systemd.services.scuttle = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + fastapi + uvicorn + httpx + websockets + python-telegram-bot + ]); + in { + description = "Scuttle FastAPI + WebSocket game server"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv ]; + environment = { + SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; + SC_DB_PATH = "/home/danny/.local/share/scuttle/scuttle.db"; + }; + serviceConfig = { + WorkingDirectory = "/home/danny/scuttle"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8082"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + # Auto-rebuild service/timer + safe.directory provided by the # shared dotfiles-rebuild NixOS module (see nixos/modules/dotfiles-rebuild.nix). } diff --git a/nixos/hosts/vps-relay.nix b/nixos/hosts/vps-relay.nix index 6ace54b..bedf32e 100644 --- a/nixos/hosts/vps-relay.nix +++ b/nixos/hosts/vps-relay.nix @@ -105,6 +105,11 @@ "shelfish.dannydannydanny.me".extraConfig = '' reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8081 ''; + # Scuttle — same backend, different port. WebSocket upgrade is + # transparent under reverse_proxy. + "scuttle.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8082 + ''; }; }; From 08495161aea0bb05ea4098c79cdf05ea6c910ea2 Mon Sep 17 00:00:00 2001 From: Danny Date: Sun, 3 May 2026 07:58:12 +0200 Subject: [PATCH 09/72] phantom-ship + vps-relay: add bananasimulator service + vhost MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- nixos/hosts/phantom-ship.nix | 41 +++++++++++++++++++++++++++++++----- nixos/hosts/vps-relay.nix | 4 ++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 3a673ca..1d138f1 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -48,11 +48,11 @@ in }; networking.firewall.trustedInterfaces = [ "enp0s31f6" ]; - # Shelfish (:8081) and Scuttle (:8082) are reachable only over the - # ZeroTier mesh — the vps-relay Caddy reverse-proxies into them. Same - # pattern as sunken-ship's bbbot. Not in global allowedTCPPorts, so - # the WAN side stays closed. - networking.firewall.interfaces."zt+".allowedTCPPorts = [ 8081 8082 ]; + # Shelfish (:8081), Scuttle (:8082), Bananasimulator (:8083) are + # reachable only over the ZeroTier mesh — the vps-relay Caddy + # reverse-proxies into them. Same pattern as sunken-ship's bbbot. + # Not in global allowedTCPPorts, so the WAN side stays closed. + networking.firewall.interfaces."zt+".allowedTCPPorts = [ 8081 8082 8083 ]; hardware.enableRedistributableFirmware = true; # iwlwifi (Intel 8260) + GPU + BT firmware @@ -173,6 +173,7 @@ in "d /var/lib/openclaw/repos 0750 openclaw openclaw - -" "d /home/danny/.local/share/shelfish 0755 danny users - -" "d /home/danny/.local/share/scuttle 0755 danny users - -" + "d /home/danny/.local/share/bananasimulator 0755 danny users - -" ]; # Hara Gmail MCP server (path 1: IMAP+SMTP). Replaced by an OAuth2 @@ -325,6 +326,36 @@ in }; }; + # Bananasimulator — the actual project at https://bananasimulator.dannydannydanny.me + # (was a placeholder in shipyard's apps.json for ages). You ARE a banana. + # Code rsync'd from ~/python-projects/26_bananasimulator/ to /home/danny/bananasimulator/ + systemd.services.bananasimulator = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + fastapi + uvicorn + httpx + python-telegram-bot + ]); + in { + description = "Bananasimulator FastAPI server"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv ]; + environment = { + SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; + BS_DB_PATH = "/home/danny/.local/share/bananasimulator/bananasimulator.db"; + BS_RIPE_MIN_PER_STAGE = "2"; # 2 min/stage → 30 min to compost in production + }; + serviceConfig = { + WorkingDirectory = "/home/danny/bananasimulator"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8083"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + # Auto-rebuild service/timer + safe.directory provided by the # shared dotfiles-rebuild NixOS module (see nixos/modules/dotfiles-rebuild.nix). } diff --git a/nixos/hosts/vps-relay.nix b/nixos/hosts/vps-relay.nix index bedf32e..70ec646 100644 --- a/nixos/hosts/vps-relay.nix +++ b/nixos/hosts/vps-relay.nix @@ -110,6 +110,10 @@ "scuttle.dannydannydanny.me".extraConfig = '' reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8082 ''; + # Bananasimulator — same backend, port 8083. + "bananasimulator.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8083 + ''; }; }; From f419fed7ebcd0d8cf5f040b48c7d9f261b75f997 Mon Sep 17 00:00:00 2001 From: Danny Date: Sun, 3 May 2026 18:56:52 +0200 Subject: [PATCH 10/72] phantom-ship + vps-relay: KomTolk service + vhost (was translate-platform) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- nixos/hosts/phantom-ship.nix | 37 ++++++++++++++++++++++++++++++++---- nixos/hosts/vps-relay.nix | 4 ++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 1d138f1..bef7db3 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -48,11 +48,11 @@ in }; networking.firewall.trustedInterfaces = [ "enp0s31f6" ]; - # Shelfish (:8081), Scuttle (:8082), Bananasimulator (:8083) are - # reachable only over the ZeroTier mesh — the vps-relay Caddy - # reverse-proxies into them. Same pattern as sunken-ship's bbbot. + # KomTolk (:8080), Shelfish (:8081), Scuttle (:8082), Bananasimulator + # (:8083) are reachable only over the ZeroTier mesh — the vps-relay + # Caddy reverse-proxies into them. Same pattern as sunken-ship's bbbot. # Not in global allowedTCPPorts, so the WAN side stays closed. - networking.firewall.interfaces."zt+".allowedTCPPorts = [ 8081 8082 8083 ]; + networking.firewall.interfaces."zt+".allowedTCPPorts = [ 8080 8081 8082 8083 ]; hardware.enableRedistributableFirmware = true; # iwlwifi (Intel 8260) + GPU + BT firmware @@ -174,6 +174,7 @@ in "d /home/danny/.local/share/shelfish 0755 danny users - -" "d /home/danny/.local/share/scuttle 0755 danny users - -" "d /home/danny/.local/share/bananasimulator 0755 danny users - -" + "d /home/danny/.local/share/komtolk 0755 danny users - -" ]; # Hara Gmail MCP server (path 1: IMAP+SMTP). Replaced by an OAuth2 @@ -356,6 +357,34 @@ in }; }; + # KomTolk (formerly translate-platform) — Copenhagen translation gigs Mini App. + # Code rsync'd from ~/python-projects/26_komtolk/ to /home/danny/komtolk/ + systemd.services.komtolk = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + fastapi + uvicorn + httpx + python-telegram-bot + ]); + in { + description = "KomTolk FastAPI server (Copenhagen translation gigs)"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv ]; + environment = { + SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; + KT_DB_PATH = "/home/danny/.local/share/komtolk/komtolk.db"; + }; + serviceConfig = { + WorkingDirectory = "/home/danny/komtolk"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8080"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + # Auto-rebuild service/timer + safe.directory provided by the # shared dotfiles-rebuild NixOS module (see nixos/modules/dotfiles-rebuild.nix). } diff --git a/nixos/hosts/vps-relay.nix b/nixos/hosts/vps-relay.nix index 70ec646..e94b116 100644 --- a/nixos/hosts/vps-relay.nix +++ b/nixos/hosts/vps-relay.nix @@ -114,6 +114,10 @@ "bananasimulator.dannydannydanny.me".extraConfig = '' reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8083 ''; + # KomTolk (formerly translate-platform) — same backend, port 8080. + "komtolk.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8080 + ''; }; }; From 3604c086507dc94b204f2179a1d3606f987a2392 Mon Sep 17 00:00:00 2001 From: Danny Date: Sun, 3 May 2026 19:22:28 +0200 Subject: [PATCH 11/72] phantom-ship: scuttle gets SC_TILES_DIR + tmpfiles for OSM tile cache --- nixos/hosts/phantom-ship.nix | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index bef7db3..7caccf9 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -175,6 +175,7 @@ in "d /home/danny/.local/share/scuttle 0755 danny users - -" "d /home/danny/.local/share/bananasimulator 0755 danny users - -" "d /home/danny/.local/share/komtolk 0755 danny users - -" + "d /home/danny/.local/share/scuttle/tiles 0755 danny users - -" ]; # Hara Gmail MCP server (path 1: IMAP+SMTP). Replaced by an OAuth2 @@ -309,7 +310,7 @@ in python-telegram-bot ]); in { - description = "Scuttle FastAPI + WebSocket game server"; + description = "Scuttle FastAPI + WebSocket game server (geo: Østerbro)"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; @@ -317,6 +318,7 @@ in environment = { SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; SC_DB_PATH = "/home/danny/.local/share/scuttle/scuttle.db"; + SC_TILES_DIR = "/home/danny/.local/share/scuttle/tiles"; }; serviceConfig = { WorkingDirectory = "/home/danny/scuttle"; From 69d982d0fa9400f757fc8fa7fc7814bc5bbe34e6 Mon Sep 17 00:00:00 2001 From: Hara Date: Mon, 4 May 2026 12:51:33 +0200 Subject: [PATCH 12/72] hara: add morning heartbeat systemd service + timer Daily 08:07 CEST oneshot: runs claude -p with Gmail MCP to check email, sends a morning Telegram ping via Bot API. Persistent timer survives reboots. --- nixos/hosts/phantom-ship.nix | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 7caccf9..13a8dce 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -387,6 +387,48 @@ in }; }; + # Hara morning heartbeat — daily email check + Telegram good-morning ping. + # Runs claude in print mode with the Gmail MCP, then sends output via Bot API. + # Token lives in ~/.claude/channels/telegram/.env (managed by the telegram plugin). + systemd.services.hara-heartbeat = { + description = "Hara morning heartbeat (email check + Telegram ping)"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + path = [ pkgs.claude-code pkgs.curl pkgs.jq ]; + environment = { + HOME = "/home/danny"; + }; + serviceConfig = { + Type = "oneshot"; + User = "danny"; + Group = "users"; + WorkingDirectory = "/home/danny"; + EnvironmentFile = "/etc/claude-channels/env"; + }; + script = '' + set -euo pipefail + CHAT_ID="66070351" + BOT_TOKEN=$(grep '^TELEGRAM_BOT_TOKEN=' /home/danny/.claude/channels/telegram/.env | cut -d= -f2-) + MSG=$(${pkgs.claude-code}/bin/claude -p \ + "You are Hara, a concise cat-energy AI assistant. Read ~/.hara/HEARTBEAT.md. Check Gmail for both accounts (danielth95 and powerhouseplayer) for urgent unread emails — security alerts, invoices, anything requiring a decision; skip newsletters and marketing. Compose a short morning message for Danny: flag urgent emails if any, otherwise just say good morning. One message, very short, cat energy." \ + --mcp-config /etc/hara/mcp-servers.json \ + 2>/dev/null) + ${pkgs.curl}/bin/curl -sf -X POST \ + "https://api.telegram.org/bot$BOT_TOKEN/sendMessage" \ + -H "Content-Type: application/json" \ + -d "{\"chat_id\": $CHAT_ID, \"text\": $(echo "$MSG" | ${pkgs.jq}/bin/jq -Rs .)}" \ + > /dev/null + ''; + }; + + systemd.timers.hara-heartbeat = { + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "Europe/Copenhagen *-*-* 08:07:00"; + Persistent = true; + }; + }; + # Auto-rebuild service/timer + safe.directory provided by the # shared dotfiles-rebuild NixOS module (see nixos/modules/dotfiles-rebuild.nix). } From 9ad8d71f6194609786b0e8d81f489ceeaaffcced Mon Sep 17 00:00:00 2001 From: Danny Date: Mon, 4 May 2026 18:25:19 +0200 Subject: [PATCH 13/72] phantom-ship: set SHIPYARD_OWNER_ID for owner-only /admin commands --- nixos/hosts/phantom-ship.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 13a8dce..10aa133 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -250,6 +250,8 @@ in path = [ pythonEnv ]; environment = { SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; + # Owner-only commands (/admin, /grant, /revoke) — anyone else gets ignored. + SHIPYARD_OWNER_ID = "66070351"; # @DannyDannyDanny }; serviceConfig = { WorkingDirectory = "/home/danny/shipyard"; From c04b463ad002388f8166b217a14ac135427e3f56 Mon Sep 17 00:00:00 2001 From: Hara Date: Mon, 4 May 2026 18:26:57 +0200 Subject: [PATCH 14/72] hara-heartbeat: fix OnCalendar timezone syntax, fire every 4h (08/12/16/20) --- nixos/hosts/phantom-ship.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 10aa133..886b9b1 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -426,7 +426,8 @@ in systemd.timers.hara-heartbeat = { wantedBy = [ "timers.target" ]; timerConfig = { - OnCalendar = "Europe/Copenhagen *-*-* 08:07:00"; + OnCalendar = "08,12,16,20:07"; + Timezone = "Europe/Copenhagen"; Persistent = true; }; }; From e952667623840c15a3e434c281cd06dda4853258 Mon Sep 17 00:00:00 2001 From: Hara Date: Mon, 4 May 2026 18:28:00 +0200 Subject: [PATCH 15/72] hara-heartbeat: shift schedule to 06/10/14/18 Copenhagen --- nixos/hosts/phantom-ship.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 886b9b1..91b3d2e 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -426,7 +426,7 @@ in systemd.timers.hara-heartbeat = { wantedBy = [ "timers.target" ]; timerConfig = { - OnCalendar = "08,12,16,20:07"; + OnCalendar = "06,10,14,18:07"; Timezone = "Europe/Copenhagen"; Persistent = true; }; From a9bb775b7d8564ee2c11e57d892c36763f3762e4 Mon Sep 17 00:00:00 2001 From: Hara Date: Mon, 4 May 2026 18:56:38 +0200 Subject: [PATCH 16/72] hara-heartbeat: check all 3 Gmail accounts (add wildstylewarrior) --- nixos/hosts/phantom-ship.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 91b3d2e..b0698da 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -412,7 +412,7 @@ in CHAT_ID="66070351" BOT_TOKEN=$(grep '^TELEGRAM_BOT_TOKEN=' /home/danny/.claude/channels/telegram/.env | cut -d= -f2-) MSG=$(${pkgs.claude-code}/bin/claude -p \ - "You are Hara, a concise cat-energy AI assistant. Read ~/.hara/HEARTBEAT.md. Check Gmail for both accounts (danielth95 and powerhouseplayer) for urgent unread emails — security alerts, invoices, anything requiring a decision; skip newsletters and marketing. Compose a short morning message for Danny: flag urgent emails if any, otherwise just say good morning. One message, very short, cat energy." \ + "You are Hara, a concise cat-energy AI assistant. Read ~/.hara/HEARTBEAT.md. Check Gmail for all three accounts (danielth95, powerhouseplayer, wildstylewarrior) for urgent unread emails — security alerts, invoices, anything requiring a decision; skip newsletters and marketing. Compose a short message for Danny: flag urgent emails if any, otherwise just a brief check-in. One message, very short, cat energy." \ --mcp-config /etc/hara/mcp-servers.json \ 2>/dev/null) ${pkgs.curl}/bin/curl -sf -X POST \ From d0e9b3f9072df17afd58100d68bf1f5592ae1c1c Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Mon, 4 May 2026 21:34:13 +0200 Subject: [PATCH 17/72] phantom-ship + vps-relay: Forgejo on git.dannydannydanny.me MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- nixos/hosts/phantom-ship.nix | 39 ++++++++++++++++++++++++++++++++---- nixos/hosts/vps-relay.nix | 5 +++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index b0698da..bab6b2d 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -49,10 +49,11 @@ in networking.firewall.trustedInterfaces = [ "enp0s31f6" ]; # KomTolk (:8080), Shelfish (:8081), Scuttle (:8082), Bananasimulator - # (:8083) are reachable only over the ZeroTier mesh — the vps-relay - # Caddy reverse-proxies into them. Same pattern as sunken-ship's bbbot. - # Not in global allowedTCPPorts, so the WAN side stays closed. - networking.firewall.interfaces."zt+".allowedTCPPorts = [ 8080 8081 8082 8083 ]; + # (:8083), Forgejo (:3000) are reachable only over the ZeroTier mesh — + # the vps-relay Caddy reverse-proxies into them. Same pattern as + # sunken-ship's bbbot. Not in global allowedTCPPorts, so the WAN side + # stays closed. + networking.firewall.interfaces."zt+".allowedTCPPorts = [ 3000 8080 8081 8082 8083 ]; hardware.enableRedistributableFirmware = true; # iwlwifi (Intel 8260) + GPU + BT firmware @@ -331,6 +332,7 @@ in }; }; +<<<<<<< HEAD # Bananasimulator — the actual project at https://bananasimulator.dannydannydanny.me # (was a placeholder in shipyard's apps.json for ages). You ARE a banana. # Code rsync'd from ~/python-projects/26_bananasimulator/ to /home/danny/bananasimulator/ @@ -429,6 +431,35 @@ in OnCalendar = "06,10,14,18:07"; Timezone = "Europe/Copenhagen"; Persistent = true; +======= + # Forgejo — self-hosted Git forge. Phase 1 of the de-platform-from-GitHub + # roadmap (vimwiki/diary/2026-05-03.md). Public URL git.dannydannydanny.me + # is fronted by Caddy on vps-relay reverse-proxying over ZT to :3000 here. + # Auth for now: HTTPS + PAT (osxkeychain credential helper on the Mac). + # SSH disabled in Phase 1; revisit if push-via-https gets annoying. + # Backups: TODO — snapshot /var/lib/forgejo/ once it's up. + services.forgejo = { + enable = true; + database.type = "sqlite3"; # personal scale; one user, plenty + lfs.enable = true; + settings = { + DEFAULT.APP_NAME = "git.dannydannydanny.me"; + server = { + DOMAIN = "git.dannydannydanny.me"; + ROOT_URL = "https://git.dannydannydanny.me/"; + # Bind to all interfaces — firewall above scopes inbound to ZT. + HTTP_ADDR = "0.0.0.0"; + HTTP_PORT = 3000; + DISABLE_SSH = true; + }; + service = { + DISABLE_REGISTRATION = true; # admin-bootstrapped only + REQUIRE_SIGNIN_VIEW = true; # no anonymous browsing + }; + session.COOKIE_SECURE = true; + log.LEVEL = "Info"; + repository.DEFAULT_BRANCH = "main"; +>>>>>>> 0a9124e (phantom-ship + vps-relay: Forgejo on git.dannydannydanny.me) }; }; diff --git a/nixos/hosts/vps-relay.nix b/nixos/hosts/vps-relay.nix index e94b116..4f40143 100644 --- a/nixos/hosts/vps-relay.nix +++ b/nixos/hosts/vps-relay.nix @@ -118,6 +118,11 @@ "komtolk.dannydannydanny.me".extraConfig = '' reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8080 ''; + # Forgejo on phantom-ship — Phase 1 of the de-platform-from-GitHub + # roadmap (vimwiki/diary/2026-05-03.md). + "git.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:3000 + ''; }; }; From 4600a8e5ca63c305ef8c037b3d19dbd7b497e52a Mon Sep 17 00:00:00 2001 From: Hara Date: Mon, 4 May 2026 23:23:57 +0200 Subject: [PATCH 18/72] escape-hormuz: add service (port 8090) + escapehormuz.dannydannydanny.me vhost --- nixos/hosts/phantom-ship.nix | 38 ++++++++++++++++++++++++++++++++---- nixos/hosts/vps-relay.nix | 4 ++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index bab6b2d..0465ba0 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -53,7 +53,7 @@ in # the vps-relay Caddy reverse-proxies into them. Same pattern as # sunken-ship's bbbot. Not in global allowedTCPPorts, so the WAN side # stays closed. - networking.firewall.interfaces."zt+".allowedTCPPorts = [ 3000 8080 8081 8082 8083 ]; + networking.firewall.interfaces."zt+".allowedTCPPorts = [ 3000 8080 8081 8082 8083 8090 ]; hardware.enableRedistributableFirmware = true; # iwlwifi (Intel 8260) + GPU + BT firmware @@ -176,6 +176,7 @@ in "d /home/danny/.local/share/scuttle 0755 danny users - -" "d /home/danny/.local/share/bananasimulator 0755 danny users - -" "d /home/danny/.local/share/komtolk 0755 danny users - -" + "d /home/danny/.local/share/escape_hormuz 0755 danny users - -" "d /home/danny/.local/share/scuttle/tiles 0755 danny users - -" ]; @@ -332,7 +333,6 @@ in }; }; -<<<<<<< HEAD # Bananasimulator — the actual project at https://bananasimulator.dannydannydanny.me # (was a placeholder in shipyard's apps.json for ages). You ARE a banana. # Code rsync'd from ~/python-projects/26_bananasimulator/ to /home/danny/bananasimulator/ @@ -391,6 +391,35 @@ in }; }; + # Escape Hormuz — turn-based boat racing Mini App through the Strait of Hormuz. + # Code rsync'd from ~/python-projects/28_escape_hormuz/ to /home/danny/escape_hormuz/ + # DB at ~/.local/share/escape_hormuz/escape_hormuz.db. + systemd.services.escape-hormuz = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + fastapi + uvicorn + python-telegram-bot + ]); + in { + description = "Escape Hormuz FastAPI server (turn-based boat race)"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv ]; + environment = { + SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; + DB_PATH = "/home/danny/.local/share/escape_hormuz/escape_hormuz.db"; + MINIAPP_URL = "https://escapehormuz.dannydannydanny.me"; + }; + serviceConfig = { + WorkingDirectory = "/home/danny/escape_hormuz"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8090"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + # Hara morning heartbeat — daily email check + Telegram good-morning ping. # Runs claude in print mode with the Gmail MCP, then sends output via Bot API. # Token lives in ~/.claude/channels/telegram/.env (managed by the telegram plugin). @@ -431,7 +460,9 @@ in OnCalendar = "06,10,14,18:07"; Timezone = "Europe/Copenhagen"; Persistent = true; -======= + }; + }; + # Forgejo — self-hosted Git forge. Phase 1 of the de-platform-from-GitHub # roadmap (vimwiki/diary/2026-05-03.md). Public URL git.dannydannydanny.me # is fronted by Caddy on vps-relay reverse-proxying over ZT to :3000 here. @@ -459,7 +490,6 @@ in session.COOKIE_SECURE = true; log.LEVEL = "Info"; repository.DEFAULT_BRANCH = "main"; ->>>>>>> 0a9124e (phantom-ship + vps-relay: Forgejo on git.dannydannydanny.me) }; }; diff --git a/nixos/hosts/vps-relay.nix b/nixos/hosts/vps-relay.nix index 4f40143..d9b3edf 100644 --- a/nixos/hosts/vps-relay.nix +++ b/nixos/hosts/vps-relay.nix @@ -123,6 +123,10 @@ "git.dannydannydanny.me".extraConfig = '' reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:3000 ''; + # Escape Hormuz — turn-based boat race, port 8090. + "escapehormuz.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8090 + ''; }; }; From 8a91f3db883e48be95c54886877e41a24a8e4488 Mon Sep 17 00:00:00 2001 From: Danny Date: Tue, 5 May 2026 09:39:07 +0200 Subject: [PATCH 19/72] phantom-ship + vps-relay: declare escape-hormuz service + vhost MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- nixos/hosts/phantom-ship.nix | 38 ++++++++++++++++++++++++++++++++---- nixos/hosts/vps-relay.nix | 2 +- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 0465ba0..d78345f 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -49,10 +49,10 @@ in networking.firewall.trustedInterfaces = [ "enp0s31f6" ]; # KomTolk (:8080), Shelfish (:8081), Scuttle (:8082), Bananasimulator - # (:8083), Forgejo (:3000) are reachable only over the ZeroTier mesh — - # the vps-relay Caddy reverse-proxies into them. Same pattern as - # sunken-ship's bbbot. Not in global allowedTCPPorts, so the WAN side - # stays closed. + # (:8083), Forgejo (:3000), Escape Hormuz (:8090) are reachable only + # over the ZeroTier mesh — the vps-relay Caddy reverse-proxies into + # them. Same pattern as sunken-ship's bbbot. Not in global + # allowedTCPPorts, so the WAN side stays closed. networking.firewall.interfaces."zt+".allowedTCPPorts = [ 3000 8080 8081 8082 8083 8090 ]; hardware.enableRedistributableFirmware = true; # iwlwifi (Intel 8260) + GPU + BT firmware @@ -178,6 +178,7 @@ in "d /home/danny/.local/share/komtolk 0755 danny users - -" "d /home/danny/.local/share/escape_hormuz 0755 danny users - -" "d /home/danny/.local/share/scuttle/tiles 0755 danny users - -" + "d /home/danny/.local/share/escape_hormuz 0755 danny users - -" ]; # Hara Gmail MCP server (path 1: IMAP+SMTP). Replaced by an OAuth2 @@ -363,6 +364,35 @@ in }; }; + # Escape Hormuz — turn-based boat-race Mini App (Hara's first build). + # Code lives at /home/danny/escape_hormuz/. Same vps-relay-fronted ZT path + # as the others; binds :: so the ZeroTier IPv6 address is reachable. + systemd.services.escape-hormuz = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + fastapi + uvicorn + python-telegram-bot + ]); + in { + description = "Escape Hormuz FastAPI server (turn-based boat race)"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv ]; + environment = { + SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; + DB_PATH = "/home/danny/.local/share/escape_hormuz/escape_hormuz.db"; + MINIAPP_URL = "https://escapehormuz.dannydannydanny.me"; + }; + serviceConfig = { + WorkingDirectory = "/home/danny/escape_hormuz"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8090"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + # KomTolk (formerly translate-platform) — Copenhagen translation gigs Mini App. # Code rsync'd from ~/python-projects/26_komtolk/ to /home/danny/komtolk/ systemd.services.komtolk = let diff --git a/nixos/hosts/vps-relay.nix b/nixos/hosts/vps-relay.nix index d9b3edf..24c4d73 100644 --- a/nixos/hosts/vps-relay.nix +++ b/nixos/hosts/vps-relay.nix @@ -123,7 +123,7 @@ "git.dannydannydanny.me".extraConfig = '' reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:3000 ''; - # Escape Hormuz — turn-based boat race, port 8090. + # Escape Hormuz — turn-based boat-race Mini App, port 8090. "escapehormuz.dannydannydanny.me".extraConfig = '' reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8090 ''; From 4e01e62cc0f1cf16630c81744210cb3fc1e2f8f7 Mon Sep 17 00:00:00 2001 From: Danny Date: Tue, 5 May 2026 09:41:17 +0200 Subject: [PATCH 20/72] phantom-ship: dedupe escape-hormuz tmpfiles + service block (rebase artifact) --- nixos/hosts/phantom-ship.nix | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index d78345f..6c81646 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -178,7 +178,6 @@ in "d /home/danny/.local/share/komtolk 0755 danny users - -" "d /home/danny/.local/share/escape_hormuz 0755 danny users - -" "d /home/danny/.local/share/scuttle/tiles 0755 danny users - -" - "d /home/danny/.local/share/escape_hormuz 0755 danny users - -" ]; # Hara Gmail MCP server (path 1: IMAP+SMTP). Replaced by an OAuth2 @@ -421,35 +420,6 @@ in }; }; - # Escape Hormuz — turn-based boat racing Mini App through the Strait of Hormuz. - # Code rsync'd from ~/python-projects/28_escape_hormuz/ to /home/danny/escape_hormuz/ - # DB at ~/.local/share/escape_hormuz/escape_hormuz.db. - systemd.services.escape-hormuz = let - pythonEnv = pkgs.python3.withPackages (ps: with ps; [ - fastapi - uvicorn - python-telegram-bot - ]); - in { - description = "Escape Hormuz FastAPI server (turn-based boat race)"; - after = [ "network-online.target" ]; - wants = [ "network-online.target" ]; - wantedBy = [ "multi-user.target" ]; - path = [ pythonEnv ]; - environment = { - SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; - DB_PATH = "/home/danny/.local/share/escape_hormuz/escape_hormuz.db"; - MINIAPP_URL = "https://escapehormuz.dannydannydanny.me"; - }; - serviceConfig = { - WorkingDirectory = "/home/danny/escape_hormuz"; - ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8090"; - Restart = "on-failure"; - RestartSec = 10; - User = "danny"; - }; - }; - # Hara morning heartbeat — daily email check + Telegram good-morning ping. # Runs claude in print mode with the Gmail MCP, then sends output via Bot API. # Token lives in ~/.claude/channels/telegram/.env (managed by the telegram plugin). From 7f8badf1d10f04360f11fe344200d358e9213ffa Mon Sep 17 00:00:00 2001 From: Hara Date: Tue, 5 May 2026 14:10:57 +0200 Subject: [PATCH 21/72] =?UTF-8?q?hara-heartbeat:=20plain=20text=20only=20p?= =?UTF-8?q?rompt=20=E2=80=94=20no=20markdown=20asterisks=20in=20Telegram?= =?UTF-8?q?=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nixos/hosts/phantom-ship.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 6c81646..de500da 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -443,7 +443,7 @@ in CHAT_ID="66070351" BOT_TOKEN=$(grep '^TELEGRAM_BOT_TOKEN=' /home/danny/.claude/channels/telegram/.env | cut -d= -f2-) MSG=$(${pkgs.claude-code}/bin/claude -p \ - "You are Hara, a concise cat-energy AI assistant. Read ~/.hara/HEARTBEAT.md. Check Gmail for all three accounts (danielth95, powerhouseplayer, wildstylewarrior) for urgent unread emails — security alerts, invoices, anything requiring a decision; skip newsletters and marketing. Compose a short message for Danny: flag urgent emails if any, otherwise just a brief check-in. One message, very short, cat energy." \ + "You are Hara, a concise cat-energy AI assistant. Read ~/.hara/HEARTBEAT.md. Check Gmail for all three accounts (danielth95, powerhouseplayer, wildstylewarrior) for urgent unread emails — security alerts, invoices, anything requiring a decision; skip newsletters and marketing. Compose a short message for Danny: flag urgent emails if any, otherwise just a brief check-in. One message, very short, cat energy. IMPORTANT: use plain text only — no markdown, no asterisks, no bold syntax. The message is sent via Telegram Bot API plain text mode." \ --mcp-config /etc/hara/mcp-servers.json \ 2>/dev/null) ${pkgs.curl}/bin/curl -sf -X POST \ From 3de1747e92a41f8aae9809ccbde7f6e0c535245c Mon Sep 17 00:00:00 2001 From: Hara Date: Tue, 5 May 2026 14:22:26 +0200 Subject: [PATCH 22/72] hara-heartbeat: strip markdown asterisks/underscores via sed before sending --- nixos/hosts/phantom-ship.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index de500da..5e267b6 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -427,7 +427,7 @@ in description = "Hara morning heartbeat (email check + Telegram ping)"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; - path = [ pkgs.claude-code pkgs.curl pkgs.jq ]; + path = [ pkgs.claude-code pkgs.curl pkgs.jq pkgs.gnused ]; environment = { HOME = "/home/danny"; }; @@ -443,9 +443,9 @@ in CHAT_ID="66070351" BOT_TOKEN=$(grep '^TELEGRAM_BOT_TOKEN=' /home/danny/.claude/channels/telegram/.env | cut -d= -f2-) MSG=$(${pkgs.claude-code}/bin/claude -p \ - "You are Hara, a concise cat-energy AI assistant. Read ~/.hara/HEARTBEAT.md. Check Gmail for all three accounts (danielth95, powerhouseplayer, wildstylewarrior) for urgent unread emails — security alerts, invoices, anything requiring a decision; skip newsletters and marketing. Compose a short message for Danny: flag urgent emails if any, otherwise just a brief check-in. One message, very short, cat energy. IMPORTANT: use plain text only — no markdown, no asterisks, no bold syntax. The message is sent via Telegram Bot API plain text mode." \ + "You are Hara, a concise cat-energy AI assistant. Read ~/.hara/HEARTBEAT.md. Check Gmail for all three accounts (danielth95, powerhouseplayer, wildstylewarrior) for urgent unread emails — security alerts, invoices, anything requiring a decision; skip newsletters and marketing. Compose a short message for Danny: flag urgent emails if any, otherwise just a brief check-in. One message, very short, cat energy." \ --mcp-config /etc/hara/mcp-servers.json \ - 2>/dev/null) + 2>/dev/null | ${pkgs.gnused}/bin/sed 's/\*\*//g; s/\*//g; s/__//g; s/_//g') ${pkgs.curl}/bin/curl -sf -X POST \ "https://api.telegram.org/bot$BOT_TOKEN/sendMessage" \ -H "Content-Type: application/json" \ From 1744d776e29aaf4a3b569efd8a1922e93afe496d Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Tue, 5 May 2026 21:10:49 +0200 Subject: [PATCH 23/72] sunken-ship: mulbo-server systemd service + pull timer + ZT port 8091 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- nixos/hosts/sunken-ship.nix | 76 ++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/nixos/hosts/sunken-ship.nix b/nixos/hosts/sunken-ship.nix index e305c03..663a242 100644 --- a/nixos/hosts/sunken-ship.nix +++ b/nixos/hosts/sunken-ship.nix @@ -95,7 +95,10 @@ networking.firewall = { allowedTCPPorts = [ 7000 7001 7100 4533 ]; allowedUDPPorts = [ 5353 6000 6001 7011 ]; - interfaces."zt+".allowedTCPPorts = [ 8080 ]; + # 8080: bbbot HTTP backend. 8091: mulbo-server companion service. + # Both ZT-only — see vps-relay.nix for reverse proxy if exposing + # publicly later. + interfaces."zt+".allowedTCPPorts = [ 8080 8091 ]; }; # Navidrome — self-hosted music streaming server (Subsonic API). @@ -200,6 +203,77 @@ timerConfig.RandomizedDelaySec = "2min"; }; + # Mulbo companion service (Phase 5: uploads + dedup index + folders). + # Wire spec: ~danny/python-projects/20_mulbo/SERVER_API.md. + # Bootstrap (one-time): git clone git@github.com:DannyDannyDanny/python-projects.git /home/danny/python-projects + # (uses sunken-ship's id_ed25519 as a read-only deploy key on the repo) + # ZT-only via the firewall rule above (port 8091). Runs as `danny` so + # writes go through to /home/danny/music/mulbo-uploads, which Navidrome + # reads via the existing /srv/music ro bind-mount with no mount changes. + systemd.tmpfiles.rules = [ + "d /home/danny/music/mulbo-uploads 0755 danny users -" + ]; + + systemd.services.mulbo-server = let + pythonEnv = pkgs.python312.withPackages (ps: with ps; [ + fastapi + uvicorn + python-multipart + ]); + in { + description = "Mulbo companion service (uploads, dedup, folders)"; + after = [ "network-online.target" "navidrome.service" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + environment = { + MULBO_UPLOADS_DIR = "/home/danny/music/mulbo-uploads"; + MULBO_INDEX_DB = "/var/lib/mulbo-server/index.db"; + MULBO_NAVIDROME_URL = "http://localhost:4533"; + MULBO_BIND_HOST = "::"; + MULBO_BIND_PORT = "8091"; + PYTHONUNBUFFERED = "1"; # immediate journal output + }; + serviceConfig = { + WorkingDirectory = "/home/danny/python-projects/20_mulbo"; + ExecStart = "${pythonEnv}/bin/python mulbo_server/app.py"; + Restart = "on-failure"; + RestartSec = 5; + User = "danny"; + StateDirectory = "mulbo-server"; # /var/lib/mulbo-server, owned by danny + }; + }; + + # Pull mulbo (python-projects repo) and restart service if repo changed. + # Repo lives at /home/danny/python-projects (must be cloned manually first + # — see bootstrap note above). DBs/state live in /var/lib/mulbo-server, + # not in the repo, so they survive pulls. + systemd.services.mulbo-pull = { + description = "Pull mulbo repo and restart mulbo-server if changed"; + path = with pkgs; [ git systemd ]; + environment = { + GIT_CONFIG_COUNT = "1"; + GIT_CONFIG_KEY_0 = "safe.directory"; + GIT_CONFIG_VALUE_0 = "/home/danny/python-projects"; + }; + script = '' + set -euo pipefail + cd /home/danny/python-projects + git fetch origin + if [ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ]; then + exit 0 + fi + git pull origin main + systemctl restart mulbo-server + ''; + serviceConfig.Type = "oneshot"; + }; + + systemd.timers.mulbo-pull = { + wantedBy = [ "timers.target" ]; + timerConfig.OnCalendar = "*-*-* *:11/15:00"; # every 15 min, offset from fitness-bot-pull and dotfiles-rebuild + timerConfig.RandomizedDelaySec = "2min"; + }; + # Auto-rebuild service/timer + safe.directory provided by the # shared dotfiles-rebuild NixOS module (see nixos/modules/dotfiles-rebuild.nix). } From 4debab6f69e097ca3b5a96cbcb9298cd3c1ebafa Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Tue, 5 May 2026 21:22:37 +0200 Subject: [PATCH 24/72] sunken-ship: mulbo-server creds via EnvironmentFile + MULBO_MUSIC_ROOT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- nixos/hosts/sunken-ship.nix | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nixos/hosts/sunken-ship.nix b/nixos/hosts/sunken-ship.nix index 663a242..840062b 100644 --- a/nixos/hosts/sunken-ship.nix +++ b/nixos/hosts/sunken-ship.nix @@ -228,6 +228,7 @@ environment = { MULBO_UPLOADS_DIR = "/home/danny/music/mulbo-uploads"; MULBO_INDEX_DB = "/var/lib/mulbo-server/index.db"; + MULBO_MUSIC_ROOT = "/srv/music"; # for /folders fs walk MULBO_NAVIDROME_URL = "http://localhost:4533"; MULBO_BIND_HOST = "::"; MULBO_BIND_PORT = "8091"; @@ -240,6 +241,12 @@ RestartSec = 5; User = "danny"; StateDirectory = "mulbo-server"; # /var/lib/mulbo-server, owned by danny + # Navidrome credentials — file format: KEY=value lines. + # Required keys: MULBO_NAVIDROME_USER, MULBO_NAVIDROME_PASS. + # Created manually on sunken-ship (mode 600, owned by danny): + # echo -e "MULBO_NAVIDROME_USER=DannyDannyDanny\nMULBO_NAVIDROME_PASS=..." > ~/.secrets/mulbo-server-navidrome + # chmod 600 ~/.secrets/mulbo-server-navidrome + EnvironmentFile = "/home/danny/.secrets/mulbo-server-navidrome"; }; }; From 73d4225f9b24e47117656b6817b27ba22f367dd6 Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Wed, 6 May 2026 15:06:51 +0200 Subject: [PATCH 25/72] sunken-ship: grant mulbo-server read on navidrome.db 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) --- nixos/hosts/sunken-ship.nix | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/nixos/hosts/sunken-ship.nix b/nixos/hosts/sunken-ship.nix index 840062b..c05f12b 100644 --- a/nixos/hosts/sunken-ship.nix +++ b/nixos/hosts/sunken-ship.nix @@ -113,6 +113,13 @@ }; }; + # Navidrome's Subsonic API path field is tag-virtual; only the internal + # SQLite has real fs paths. mulbo-server reads navidrome.db ro to + # power /folders + POST /tracks resolution. UMask=0027 makes new DB + # files (and WAL rotations) group-readable; the tmpfile rule fixes the + # existing files written under the previous 0600 umask. + systemd.services.navidrome.serviceConfig.UMask = lib.mkForce "0027"; + # Persist the bind mount so navidrome can read music outside ProtectHome. fileSystems."/srv/music" = { device = "/home/danny/music"; @@ -212,6 +219,12 @@ # reads via the existing /srv/music ro bind-mount with no mount changes. systemd.tmpfiles.rules = [ "d /home/danny/music/mulbo-uploads 0755 danny users -" + # One-time fix for the existing navidrome.db (+ WAL/SHM) created + # under the old 0600 umask. UMask=0027 above keeps future writes + # group-readable. + "z /var/lib/navidrome/navidrome.db 0640 navidrome navidrome -" + "z /var/lib/navidrome/navidrome.db-wal 0640 navidrome navidrome -" + "z /var/lib/navidrome/navidrome.db-shm 0640 navidrome navidrome -" ]; systemd.services.mulbo-server = let @@ -235,18 +248,21 @@ PYTHONUNBUFFERED = "1"; # immediate journal output }; serviceConfig = { - WorkingDirectory = "/home/danny/python-projects/20_mulbo"; - ExecStart = "${pythonEnv}/bin/python mulbo_server/app.py"; - Restart = "on-failure"; - RestartSec = 5; - User = "danny"; - StateDirectory = "mulbo-server"; # /var/lib/mulbo-server, owned by danny + WorkingDirectory = "/home/danny/python-projects/20_mulbo"; + ExecStart = "${pythonEnv}/bin/python mulbo_server/app.py"; + Restart = "on-failure"; + RestartSec = 5; + User = "danny"; + # Read-only access to navidrome.db (+WAL/SHM) — see UMask override + # on the navidrome service above. + SupplementaryGroups = [ "navidrome" ]; + StateDirectory = "mulbo-server"; # /var/lib/mulbo-server, owned by danny # Navidrome credentials — file format: KEY=value lines. # Required keys: MULBO_NAVIDROME_USER, MULBO_NAVIDROME_PASS. # Created manually on sunken-ship (mode 600, owned by danny): # echo -e "MULBO_NAVIDROME_USER=DannyDannyDanny\nMULBO_NAVIDROME_PASS=..." > ~/.secrets/mulbo-server-navidrome # chmod 600 ~/.secrets/mulbo-server-navidrome - EnvironmentFile = "/home/danny/.secrets/mulbo-server-navidrome"; + EnvironmentFile = "/home/danny/.secrets/mulbo-server-navidrome"; }; }; From 082529dac95d4e05b070b9abd4dbf5ee0ca8af58 Mon Sep 17 00:00:00 2001 From: Danny Date: Thu, 7 May 2026 22:12:03 +0200 Subject: [PATCH 26/72] phantom-ship + vps-relay: declare bon service + vhost (port 8091) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bon — receipt scanner Mini App. Snap a receipt with the device camera, upload, list. MVP only captures + stores; OCR/categorization later. phantom-ship.nix - systemd.services.bon on port 8091, binds :: for ZT - 8091 added to zt+ allowedTCPPorts - tmpfiles for /home/danny/.local/share/bon/{,images} - python env adds python-multipart (form upload) + pillow (image validate + downscale to 2400px JPEG) vps-relay.nix - Caddy vhost bon.dannydannydanny.me → ZT [::]:8091 --- nixos/hosts/phantom-ship.nix | 43 +++++++++++++++++++++++++++++++----- nixos/hosts/vps-relay.nix | 5 +++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 5e267b6..9bed76e 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -49,11 +49,11 @@ in networking.firewall.trustedInterfaces = [ "enp0s31f6" ]; # KomTolk (:8080), Shelfish (:8081), Scuttle (:8082), Bananasimulator - # (:8083), Forgejo (:3000), Escape Hormuz (:8090) are reachable only - # over the ZeroTier mesh — the vps-relay Caddy reverse-proxies into - # them. Same pattern as sunken-ship's bbbot. Not in global - # allowedTCPPorts, so the WAN side stays closed. - networking.firewall.interfaces."zt+".allowedTCPPorts = [ 3000 8080 8081 8082 8083 8090 ]; + # (:8083), Forgejo (:3000), Escape Hormuz (:8090), bon (:8091) are + # reachable only over the ZeroTier mesh — the vps-relay Caddy + # reverse-proxies into them. Same pattern as sunken-ship's bbbot. Not + # in global allowedTCPPorts, so the WAN side stays closed. + networking.firewall.interfaces."zt+".allowedTCPPorts = [ 3000 8080 8081 8082 8083 8090 8091 ]; hardware.enableRedistributableFirmware = true; # iwlwifi (Intel 8260) + GPU + BT firmware @@ -178,6 +178,8 @@ in "d /home/danny/.local/share/komtolk 0755 danny users - -" "d /home/danny/.local/share/escape_hormuz 0755 danny users - -" "d /home/danny/.local/share/scuttle/tiles 0755 danny users - -" + "d /home/danny/.local/share/bon 0755 danny users - -" + "d /home/danny/.local/share/bon/images 0755 danny users - -" ]; # Hara Gmail MCP server (path 1: IMAP+SMTP). Replaced by an OAuth2 @@ -392,6 +394,37 @@ in }; }; + # bon — receipt scanner Mini App (camera capture + gallery). + # Code rsync'd from ~/python-projects/26_bon/ to /home/danny/bon/ + # Images on disk under /home/danny/.local/share/bon/images// + systemd.services.bon = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + fastapi + uvicorn + python-telegram-bot + python-multipart + pillow + ]); + in { + description = "bon FastAPI server (receipt scanner)"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv ]; + environment = { + SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; + BON_DB_PATH = "/home/danny/.local/share/bon/bon.db"; + BON_IMAGES_DIR = "/home/danny/.local/share/bon/images"; + }; + serviceConfig = { + WorkingDirectory = "/home/danny/bon"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8091"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + # KomTolk (formerly translate-platform) — Copenhagen translation gigs Mini App. # Code rsync'd from ~/python-projects/26_komtolk/ to /home/danny/komtolk/ systemd.services.komtolk = let diff --git a/nixos/hosts/vps-relay.nix b/nixos/hosts/vps-relay.nix index 24c4d73..3d34a1a 100644 --- a/nixos/hosts/vps-relay.nix +++ b/nixos/hosts/vps-relay.nix @@ -127,6 +127,11 @@ "escapehormuz.dannydannydanny.me".extraConfig = '' reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8090 ''; + # bon — receipt scanner Mini App, port 8091. Camera capture in + # the WebView needs HTTPS, which Caddy terminates here. + "bon.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8091 + ''; }; }; From 4525e73f1a6ae03b5e50761ef1dc99cb52f7623e Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Thu, 7 May 2026 22:30:05 +0200 Subject: [PATCH 27/72] sunken-ship: mulbo-server-backfill systemd oneshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- nixos/hosts/sunken-ship.nix | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/nixos/hosts/sunken-ship.nix b/nixos/hosts/sunken-ship.nix index c05f12b..d1714bf 100644 --- a/nixos/hosts/sunken-ship.nix +++ b/nixos/hosts/sunken-ship.nix @@ -297,6 +297,38 @@ timerConfig.RandomizedDelaySec = "2min"; }; + # One-shot backfill: walks Navidrome's media_file, computes + # (sha256, chromaprint) per file, populates mulbo-server's tracks_index + # with the corresponding navidrome_track_id. Idempotent — existing rows + # left alone. Without this, /tracks/by-hash misses for every existing + # offshore track and `mulbo reconcile-local` duplicates content. + # + # Trigger manually: sudo systemctl start mulbo-server-backfill + # Follow progress: journalctl -fu mulbo-server-backfill + systemd.services.mulbo-server-backfill = let + pythonEnv = pkgs.python312.withPackages (ps: with ps; [ ]); + in { + description = "Backfill mulbo-server tracks_index from Navidrome catalog"; + after = [ "mulbo-server.service" ]; + requires = [ "mulbo-server.service" ]; + path = [ pkgs.chromaprint ]; # provides fpcalc + environment = { + MULBO_INDEX_DB = "/var/lib/mulbo-server/index.db"; + MULBO_NAVIDROME_DB = "/var/lib/navidrome/navidrome.db"; + MULBO_MUSIC_ROOT = "/srv/music"; + PYTHONUNBUFFERED = "1"; + }; + serviceConfig = { + Type = "oneshot"; + WorkingDirectory = "/home/danny/python-projects/20_mulbo"; + ExecStart = "${pythonEnv}/bin/python mulbo_server/backfill.py"; + User = "danny"; + SupplementaryGroups = [ "navidrome" ]; # ro access to navidrome.db + StateDirectory = "mulbo-server"; # so /var/lib/mulbo-server/index.db stays writable + TimeoutSec = "8h"; # full backfill on 274 GB ≈ 1h, leave headroom + }; + }; + # Auto-rebuild service/timer + safe.directory provided by the # shared dotfiles-rebuild NixOS module (see nixos/modules/dotfiles-rebuild.nix). } From 647d748d30fca06d674c6616c8b079973700c025 Mon Sep 17 00:00:00 2001 From: Danny Date: Fri, 8 May 2026 06:57:06 +0200 Subject: [PATCH 28/72] phantom-ship: add tesseract to bon service for OCR --- nixos/hosts/phantom-ship.nix | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 9bed76e..ad9169e 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -394,9 +394,10 @@ in }; }; - # bon — receipt scanner Mini App (camera capture + gallery). + # bon — receipt scanner Mini App (camera capture + gallery + OCR). # Code rsync'd from ~/python-projects/26_bon/ to /home/danny/bon/ # Images on disk under /home/danny/.local/share/bon/images// + # OCR via tesseract (binary on PATH; server uses subprocess directly). systemd.services.bon = let pythonEnv = pkgs.python3.withPackages (ps: with ps; [ fastapi @@ -405,12 +406,18 @@ in python-multipart pillow ]); + # English-only for now — Danish receipts in DK are mostly English chars + # plus prices, which `eng` handles fine. Add more languages later if + # vyscul or other testers report missed text. + tesseractEng = pkgs.tesseract.override { + enableLanguages = [ "eng" ]; + }; in { description = "bon FastAPI server (receipt scanner)"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; - path = [ pythonEnv ]; + path = [ pythonEnv tesseractEng ]; environment = { SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; BON_DB_PATH = "/home/danny/.local/share/bon/bon.db"; From 327bdc11fe7e01c4b688e667ffd6916a3161fbb7 Mon Sep 17 00:00:00 2001 From: Danny Date: Fri, 8 May 2026 07:23:08 +0200 Subject: [PATCH 29/72] phantom-ship: services.ollama + qwen2.5:3b-instruct for bon extraction --- nixos/hosts/phantom-ship.nix | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index ad9169e..90df6de 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -394,10 +394,22 @@ in }; }; - # bon — receipt scanner Mini App (camera capture + gallery + OCR). + # Ollama — local LLM runtime, used by bon's structured-data extraction + # step. Listens on 127.0.0.1:11434 only (not exposed over ZT). The + # qwen2.5:3b-instruct model is pre-pulled at boot via loadModels. + services.ollama = { + enable = true; + host = "127.0.0.1"; + port = 11434; + # ~2.5 GB on disk after Q4_K_M quantization. Phantom-ship has plenty. + loadModels = [ "qwen2.5:3b-instruct" ]; + }; + + # bon — receipt scanner Mini App (camera capture + gallery + OCR + extract). # Code rsync'd from ~/python-projects/26_bon/ to /home/danny/bon/ # Images on disk under /home/danny/.local/share/bon/images// # OCR via tesseract (binary on PATH; server uses subprocess directly). + # Structured extraction via local Ollama (qwen2.5:3b-instruct). systemd.services.bon = let pythonEnv = pkgs.python3.withPackages (ps: with ps; [ fastapi @@ -405,6 +417,7 @@ in python-telegram-bot python-multipart pillow + httpx # for the Ollama HTTP call from extract.py ]); # English-only for now — Danish receipts in DK are mostly English chars # plus prices, which `eng` handles fine. Add more languages later if @@ -414,7 +427,7 @@ in }; in { description = "bon FastAPI server (receipt scanner)"; - after = [ "network-online.target" ]; + after = [ "network-online.target" "ollama.service" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; path = [ pythonEnv tesseractEng ]; @@ -422,6 +435,8 @@ in SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; BON_DB_PATH = "/home/danny/.local/share/bon/bon.db"; BON_IMAGES_DIR = "/home/danny/.local/share/bon/images"; + BON_OLLAMA_URL = "http://127.0.0.1:11434"; + BON_OLLAMA_MODEL = "qwen2.5:3b-instruct"; }; serviceConfig = { WorkingDirectory = "/home/danny/bon"; From eee28d3e9a160e7325581c2ccc3c7297b88e4cae Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Fri, 8 May 2026 07:23:09 +0200 Subject: [PATCH 30/72] phantom-ship + vps-relay: declare notes service + vhosts (port 8092) 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) --- nixos/hosts/phantom-ship.nix | 38 +++++++++++++++++++++++++++++++----- nixos/hosts/vps-relay.nix | 8 ++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 90df6de..b6b269f 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -49,11 +49,12 @@ in networking.firewall.trustedInterfaces = [ "enp0s31f6" ]; # KomTolk (:8080), Shelfish (:8081), Scuttle (:8082), Bananasimulator - # (:8083), Forgejo (:3000), Escape Hormuz (:8090), bon (:8091) are - # reachable only over the ZeroTier mesh — the vps-relay Caddy - # reverse-proxies into them. Same pattern as sunken-ship's bbbot. Not - # in global allowedTCPPorts, so the WAN side stays closed. - networking.firewall.interfaces."zt+".allowedTCPPorts = [ 3000 8080 8081 8082 8083 8090 8091 ]; + # (:8083), Forgejo (:3000), Escape Hormuz (:8090), bon (:8091), + # notes (:8092) are reachable only over the ZeroTier mesh — the + # vps-relay Caddy reverse-proxies into them. Same pattern as + # sunken-ship's bbbot. Not in global allowedTCPPorts, so the WAN side + # stays closed. + networking.firewall.interfaces."zt+".allowedTCPPorts = [ 3000 8080 8081 8082 8083 8090 8091 8092 ]; hardware.enableRedistributableFirmware = true; # iwlwifi (Intel 8260) + GPU + BT firmware @@ -475,6 +476,33 @@ in }; }; + # notes — tiny markdown blog + apex landing page. + # One service serves two hostnames via Host-header switch: + # notes.dannydannydanny.me → blog + # dannydannydanny.me → landing + # Code rsync'd from ~/python-projects/26_notes/ to /home/danny/notes/ + systemd.services.notes = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + fastapi + uvicorn + markdown + jinja2 + ]); + in { + description = "notes — markdown blog + landing page"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv ]; + serviceConfig = { + WorkingDirectory = "/home/danny/notes"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8092"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + # Hara morning heartbeat — daily email check + Telegram good-morning ping. # Runs claude in print mode with the Gmail MCP, then sends output via Bot API. # Token lives in ~/.claude/channels/telegram/.env (managed by the telegram plugin). diff --git a/nixos/hosts/vps-relay.nix b/nixos/hosts/vps-relay.nix index 3d34a1a..306c127 100644 --- a/nixos/hosts/vps-relay.nix +++ b/nixos/hosts/vps-relay.nix @@ -132,6 +132,14 @@ "bon.dannydannydanny.me".extraConfig = '' reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8091 ''; + # notes — markdown blog (notes.X) + apex landing (X). Same backend + # service on phantom :8092 routes by Host header. + "notes.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8092 + ''; + "dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8092 + ''; }; }; From ccf9eb28595018043cf61debd831605dd47dca60 Mon Sep 17 00:00:00 2001 From: Danny Date: Fri, 8 May 2026 15:28:52 +0200 Subject: [PATCH 31/72] phantom-ship: bon switches to qwen2.5:7b-instruct for extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- nixos/hosts/phantom-ship.nix | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index b6b269f..138633c 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -396,14 +396,18 @@ in }; # Ollama — local LLM runtime, used by bon's structured-data extraction - # step. Listens on 127.0.0.1:11434 only (not exposed over ZT). The - # qwen2.5:3b-instruct model is pre-pulled at boot via loadModels. + # step. Listens on 127.0.0.1:11434 only (not exposed over ZT). + # We pre-pull both 3B and 7B Qwen2.5; bon currently runs 7B for better + # column-parsing accuracy on receipts (3B mis-conflates qty/price + # columns and over-eagerly nominates line items as merchants). services.ollama = { enable = true; host = "127.0.0.1"; port = 11434; - # ~2.5 GB on disk after Q4_K_M quantization. Phantom-ship has plenty. - loadModels = [ "qwen2.5:3b-instruct" ]; + loadModels = [ + "qwen2.5:3b-instruct" # ~2.5 GB — kept as fast fallback + "qwen2.5:7b-instruct" # ~4.7 GB — current default, slower but better + ]; }; # bon — receipt scanner Mini App (camera capture + gallery + OCR + extract). @@ -437,7 +441,7 @@ in BON_DB_PATH = "/home/danny/.local/share/bon/bon.db"; BON_IMAGES_DIR = "/home/danny/.local/share/bon/images"; BON_OLLAMA_URL = "http://127.0.0.1:11434"; - BON_OLLAMA_MODEL = "qwen2.5:3b-instruct"; + BON_OLLAMA_MODEL = "qwen2.5:7b-instruct"; }; serviceConfig = { WorkingDirectory = "/home/danny/bon"; From 814993e66bcebb0f614f2fd0199a8aa4dc3a863d Mon Sep 17 00:00:00 2001 From: Danny Date: Fri, 8 May 2026 20:39:31 +0200 Subject: [PATCH 32/72] phantom-ship: revert bon to 3B model (7B too slow on CPU) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- nixos/hosts/phantom-ship.nix | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 138633c..90886cc 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -397,16 +397,17 @@ in # Ollama — local LLM runtime, used by bon's structured-data extraction # step. Listens on 127.0.0.1:11434 only (not exposed over ZT). - # We pre-pull both 3B and 7B Qwen2.5; bon currently runs 7B for better - # column-parsing accuracy on receipts (3B mis-conflates qty/price - # columns and over-eagerly nominates line items as merchants). + # 3B is bon's default — 7B was tested but ran ~3.6 min/receipt vs ~30s + # for 3B on phantom-ship CPU, with no real accuracy gain (still picked + # line items as merchant on header-less OCR; that's an OCR problem, + # not a model problem). Both kept loaded so we can A/B without a pull. services.ollama = { enable = true; host = "127.0.0.1"; port = 11434; loadModels = [ - "qwen2.5:3b-instruct" # ~2.5 GB — kept as fast fallback - "qwen2.5:7b-instruct" # ~4.7 GB — current default, slower but better + "qwen2.5:3b-instruct" # ~2.5 GB — current default + "qwen2.5:7b-instruct" # ~4.7 GB — A/B testing only ]; }; @@ -441,7 +442,7 @@ in BON_DB_PATH = "/home/danny/.local/share/bon/bon.db"; BON_IMAGES_DIR = "/home/danny/.local/share/bon/images"; BON_OLLAMA_URL = "http://127.0.0.1:11434"; - BON_OLLAMA_MODEL = "qwen2.5:7b-instruct"; + BON_OLLAMA_MODEL = "qwen2.5:3b-instruct"; }; serviceConfig = { WorkingDirectory = "/home/danny/bon"; From c5cabe7531e74cad5824beda30b795bb3f97fbb0 Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Sat, 9 May 2026 22:43:40 +0200 Subject: [PATCH 33/72] sunken-ship: MULBO_MUSIC_WRITE_ROOT for mulbo-server dedup /srv/music is RO bind-mount; deletes/quarantines have to go through the underlying /home/danny/music. New env var separates the read-side (MUSIC_ROOT, used for hashing) from the write-side (MUSIC_WRITE_ROOT, used for unlink + move-to-quarantine). Co-Authored-By: Claude Opus 4.7 (1M context) --- nixos/hosts/sunken-ship.nix | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nixos/hosts/sunken-ship.nix b/nixos/hosts/sunken-ship.nix index d1714bf..4c0f13b 100644 --- a/nixos/hosts/sunken-ship.nix +++ b/nixos/hosts/sunken-ship.nix @@ -239,9 +239,10 @@ wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; environment = { - MULBO_UPLOADS_DIR = "/home/danny/music/mulbo-uploads"; - MULBO_INDEX_DB = "/var/lib/mulbo-server/index.db"; - MULBO_MUSIC_ROOT = "/srv/music"; # for /folders fs walk + MULBO_UPLOADS_DIR = "/home/danny/music/mulbo-uploads"; + MULBO_INDEX_DB = "/var/lib/mulbo-server/index.db"; + MULBO_MUSIC_ROOT = "/srv/music"; # ro view via bind-mount; reads + hashing + MULBO_MUSIC_WRITE_ROOT = "/home/danny/music"; # underlying rw path; deletes + quarantines MULBO_NAVIDROME_URL = "http://localhost:4533"; MULBO_BIND_HOST = "::"; MULBO_BIND_PORT = "8091"; From fb99ef3cffc111c8172c4eb75ebea8d3955c5bd0 Mon Sep 17 00:00:00 2001 From: Danny Date: Sun, 10 May 2026 12:48:26 +0200 Subject: [PATCH 34/72] sunken-ship: add fitness-bot-shipyard staging instance Mirrors the prod fitness-bot setup but watches origin/staging, runs in /home/danny/tg_fitness_bot_shipyard, listens on port 8081, and loads its bot token from /home/danny/.secrets/bigbiggerbiggestbot-shipyard.env via EnvironmentFile (separate from prod's secrets file). ConditionPathExists keeps the service from start-looping until the secrets file is written. No WEBAPP_URL set, so start.py boots an ephemeral cloudflared Quick Tunnel; the bot updates its Telegram menu button to that URL on every start (same as prod was originally). Pull-timer fires every 15 min on the :13/28/43/58 offset to spread load against the existing fitness-bot-pull (:07/15) and mulbo-server-pull (:11/15) timers. Co-Authored-By: Claude Opus 4.7 (1M context) --- nixos/hosts/sunken-ship.nix | 65 +++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/nixos/hosts/sunken-ship.nix b/nixos/hosts/sunken-ship.nix index 4c0f13b..4188a41 100644 --- a/nixos/hosts/sunken-ship.nix +++ b/nixos/hosts/sunken-ship.nix @@ -210,6 +210,71 @@ timerConfig.RandomizedDelaySec = "2min"; }; + # ── Shipyard staging — second instance for verifying changes pre-prod ─ + # Working dir: /home/danny/tg_fitness_bot_shipyard (separate clone of the same repo). + # Branch: origin/staging (push there to deploy here; push to origin/main for prod). + # Bot token (separate from prod): /home/danny/.secrets/bigbiggerbiggestbot-shipyard.env + # File contents: BOT_TOKEN= + # Service won't start until this file exists (ConditionPathExists). + # Mini App URL: ephemeral cloudflared Quick Tunnel (no VPS Caddy). + # Workflow: git push origin :staging → wait ~15 min → /start the + # shipyard bot in Telegram → test → git push origin :main. + systemd.services.fitness-bot-shipyard = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + python-telegram-bot + python-dotenv + aiohttp + ]); + in { + description = "BigBiggerBiggestBot — SHIPYARD STAGING instance"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv pkgs.cloudflared ]; + environment.API_HOST = "::"; + environment.API_PORT = "8081"; + # No WEBAPP_URL — start.py spins up its own ephemeral cloudflared tunnel. + unitConfig.ConditionPathExists = "/home/danny/.secrets/bigbiggerbiggestbot-shipyard.env"; + serviceConfig = { + WorkingDirectory = "/home/danny/tg_fitness_bot_shipyard"; + EnvironmentFile = "/home/danny/.secrets/bigbiggerbiggestbot-shipyard.env"; + ExecStart = "${pythonEnv}/bin/python start.py"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + + systemd.services.fitness-bot-shipyard-pull = { + description = "Pull shipyard fitness bot from origin/staging and restart if changed"; + path = with pkgs; [ git systemd ]; + environment.GIT_CONFIG_COUNT = "1"; + environment.GIT_CONFIG_KEY_0 = "safe.directory"; + environment.GIT_CONFIG_VALUE_0 = "/home/danny/tg_fitness_bot_shipyard"; + script = '' + set -euo pipefail + if [ ! -d /home/danny/tg_fitness_bot_shipyard/.git ]; then + echo "Shipyard working dir not bootstrapped yet — skipping pull." + exit 0 + fi + cd /home/danny/tg_fitness_bot_shipyard + git fetch origin + if [ "$(git rev-parse HEAD)" = "$(git rev-parse origin/staging)" ]; then + exit 0 + fi + git pull origin staging + systemctl restart fitness-bot-shipyard + ''; + serviceConfig.Type = "oneshot"; + }; + + systemd.timers.fitness-bot-shipyard-pull = { + wantedBy = [ "timers.target" ]; + # Offset from prod (07/15), mulbo (11/15), and dotfiles-rebuild. + timerConfig.OnCalendar = "*-*-* *:13/15:00"; + timerConfig.RandomizedDelaySec = "2min"; + }; + # Mulbo companion service (Phase 5: uploads + dedup index + folders). # Wire spec: ~danny/python-projects/20_mulbo/SERVER_API.md. # Bootstrap (one-time): git clone git@github.com:DannyDannyDanny/python-projects.git /home/danny/python-projects From 851ee8ea1df311cac2b87b48b0b87bb6ec2b6727 Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Sun, 10 May 2026 13:01:05 +0200 Subject: [PATCH 35/72] sunken-ship: mulbo-server-enrich oneshot (Phase 7.5) Companion oneshot for mulbo-server. python312 env adds mutagen (tag writeback); pkgs.yt-dlp on PATH for SoundCloud lookups. Same User/SupplementaryGroups/EnvironmentFile/StateDirectory as mulbo-server-backfill. TimeoutSec=8h covers a full library pass. Trigger: sudo systemctl start mulbo-server-enrich Follow: journalctl -fu mulbo-server-enrich Add MULBO_ACOUSTID_KEY to /home/danny/.secrets/mulbo-server-navidrome to enable the AcoustID source; the yt-dlp + filename sources need no keys. Co-Authored-By: Claude Opus 4.7 (1M context) --- nixos/hosts/sunken-ship.nix | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/nixos/hosts/sunken-ship.nix b/nixos/hosts/sunken-ship.nix index 4188a41..c66010f 100644 --- a/nixos/hosts/sunken-ship.nix +++ b/nixos/hosts/sunken-ship.nix @@ -395,6 +395,45 @@ }; }; + # Phase 7.5 enrichment one-shot. For tracks where Navidrome's tags + # are empty/Unknown, runs three sources (filename heuristics, yt-dlp + # for SoundCloud `[]` patterns, AcoustID+MusicBrainz), votes the + # results, and writes back via mutagen with strict-replacement + # (never touches user-set tags). + # + # Trigger: sudo systemctl start mulbo-server-enrich + # Follow progress: journalctl -fu mulbo-server-enrich + systemd.services.mulbo-server-enrich = let + pythonEnv = pkgs.python312.withPackages (ps: with ps; [ + mutagen # tag writeback + ]); + in { + description = "Enrich Navidrome tracks with empty/Unknown metadata"; + after = [ "mulbo-server.service" ]; + requires = [ "mulbo-server.service" ]; + path = with pkgs; [ yt-dlp ]; # provides yt-dlp for SoundCloud lookups + environment = { + MULBO_INDEX_DB = "/var/lib/mulbo-server/index.db"; + MULBO_NAVIDROME_DB = "/var/lib/navidrome/navidrome.db"; + MULBO_MUSIC_ROOT = "/srv/music"; + MULBO_MUSIC_WRITE_ROOT = "/home/danny/music"; + PYTHONUNBUFFERED = "1"; + }; + serviceConfig = { + Type = "oneshot"; + WorkingDirectory = "/home/danny/python-projects/20_mulbo"; + ExecStart = "${pythonEnv}/bin/python mulbo_server/enrich.py"; + User = "danny"; + SupplementaryGroups = [ "navidrome" ]; + StateDirectory = "mulbo-server"; + # Add MULBO_ACOUSTID_KEY to the secrets file to enable the + # AcoustID source. yt-dlp source needs no key. Filename source + # needs nothing. + EnvironmentFile = "/home/danny/.secrets/mulbo-server-navidrome"; + TimeoutSec = "8h"; + }; + }; + # Auto-rebuild service/timer + safe.directory provided by the # shared dotfiles-rebuild NixOS module (see nixos/modules/dotfiles-rebuild.nix). } From 067bab125b77a1dfb9db7cdc099908d7621684f9 Mon Sep 17 00:00:00 2001 From: Danny Date: Sun, 10 May 2026 13:12:09 +0200 Subject: [PATCH 36/72] sunken-ship: shipyard staging uses shipyard_poc_bot token shipyard_poc_bot is the shared "POC slot" Telegram bot that hosts whatever experiment is currently being staged; B3Bot staging is just the current tenant. Repoint EnvironmentFile and ConditionPathExists at /home/danny/.secrets/shipyard_poc_bot.env. Co-Authored-By: Claude Opus 4.7 (1M context) --- nixos/hosts/sunken-ship.nix | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/nixos/hosts/sunken-ship.nix b/nixos/hosts/sunken-ship.nix index c66010f..39e54bf 100644 --- a/nixos/hosts/sunken-ship.nix +++ b/nixos/hosts/sunken-ship.nix @@ -213,12 +213,14 @@ # ── Shipyard staging — second instance for verifying changes pre-prod ─ # Working dir: /home/danny/tg_fitness_bot_shipyard (separate clone of the same repo). # Branch: origin/staging (push there to deploy here; push to origin/main for prod). - # Bot token (separate from prod): /home/danny/.secrets/bigbiggerbiggestbot-shipyard.env - # File contents: BOT_TOKEN= + # Bot: shipyard_poc_bot — the shared "POC slot" Telegram bot. While B3Bot + # staging is the active POC, shipyard_poc_bot polls into this service. + # Token file: /home/danny/.secrets/shipyard_poc_bot.env + # File contents: BOT_TOKEN= # Service won't start until this file exists (ConditionPathExists). # Mini App URL: ephemeral cloudflared Quick Tunnel (no VPS Caddy). - # Workflow: git push origin :staging → wait ~15 min → /start the - # shipyard bot in Telegram → test → git push origin :main. + # Workflow: git push origin :staging → wait ~15 min → /start + # shipyard_poc_bot in Telegram → test → git push origin :main. systemd.services.fitness-bot-shipyard = let pythonEnv = pkgs.python3.withPackages (ps: with ps; [ python-telegram-bot @@ -234,10 +236,10 @@ environment.API_HOST = "::"; environment.API_PORT = "8081"; # No WEBAPP_URL — start.py spins up its own ephemeral cloudflared tunnel. - unitConfig.ConditionPathExists = "/home/danny/.secrets/bigbiggerbiggestbot-shipyard.env"; + unitConfig.ConditionPathExists = "/home/danny/.secrets/shipyard_poc_bot.env"; serviceConfig = { WorkingDirectory = "/home/danny/tg_fitness_bot_shipyard"; - EnvironmentFile = "/home/danny/.secrets/bigbiggerbiggestbot-shipyard.env"; + EnvironmentFile = "/home/danny/.secrets/shipyard_poc_bot.env"; ExecStart = "${pythonEnv}/bin/python start.py"; Restart = "on-failure"; RestartSec = 10; From 83dd92d7384c775f3072e41abc063c8872fc4614 Mon Sep 17 00:00:00 2001 From: Danny Date: Sun, 10 May 2026 14:00:39 +0200 Subject: [PATCH 37/72] shipyard staging gets a stable URL: b3.dannydannydanny.me MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the cloudflared Quick Tunnel (URL changed on every restart → unworkable for shipyard's apps.json). Move to the same pattern every other tenant uses: - vps-relay Caddy: new virtualHost b3.dannydannydanny.me → reverse_proxy to sunken-ship's ZT IPv6 :8081. - sunken-ship: open port 8081 on the zt+ firewall interface (was 8080 + 8091, now 8080 + 8081 + 8091). - fitness-bot-shipyard service: set WEBAPP_URL=https://b3... so start.py skips its own tunnel attempt; drop pkgs.cloudflared from path now that nothing in the unit needs it. Co-Authored-By: Claude Opus 4.7 (1M context) --- nixos/hosts/sunken-ship.nix | 33 ++++++++++++++++++++------------- nixos/hosts/vps-relay.nix | 5 +++++ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/nixos/hosts/sunken-ship.nix b/nixos/hosts/sunken-ship.nix index 39e54bf..75ddc1d 100644 --- a/nixos/hosts/sunken-ship.nix +++ b/nixos/hosts/sunken-ship.nix @@ -95,10 +95,10 @@ networking.firewall = { allowedTCPPorts = [ 7000 7001 7100 4533 ]; allowedUDPPorts = [ 5353 6000 6001 7011 ]; - # 8080: bbbot HTTP backend. 8091: mulbo-server companion service. - # Both ZT-only — see vps-relay.nix for reverse proxy if exposing - # publicly later. - interfaces."zt+".allowedTCPPorts = [ 8080 8091 ]; + # 8080: bbbot HTTP backend. 8081: bbbot SHIPYARD STAGING (B3Bot beta). + # 8091: mulbo-server companion service. All ZT-only — see vps-relay.nix + # for the reverse proxies that expose them publicly. + interfaces."zt+".allowedTCPPorts = [ 8080 8081 8091 ]; }; # Navidrome — self-hosted music streaming server (Subsonic API). @@ -210,17 +210,22 @@ timerConfig.RandomizedDelaySec = "2min"; }; - # ── Shipyard staging — second instance for verifying changes pre-prod ─ - # Working dir: /home/danny/tg_fitness_bot_shipyard (separate clone of the same repo). + # ── Shipyard staging — B3Bot beta tenant under shipyard_poc_bot ────── + # Mini-App-only HTTP server (no Telegram polling — shipyard_poc_bot on + # phantom-ship owns the polling loop; this service only validates Telegram + # WebApp initData HMACs against the shared bot token). + # + # Working dir: /home/danny/tg_fitness_bot_shipyard (separate clone of the + # same repo, gitignored workouts.db kept across pulls). # Branch: origin/staging (push there to deploy here; push to origin/main for prod). - # Bot: shipyard_poc_bot — the shared "POC slot" Telegram bot. While B3Bot - # staging is the active POC, shipyard_poc_bot polls into this service. # Token file: /home/danny/.secrets/shipyard_poc_bot.env # File contents: BOT_TOKEN= # Service won't start until this file exists (ConditionPathExists). - # Mini App URL: ephemeral cloudflared Quick Tunnel (no VPS Caddy). - # Workflow: git push origin :staging → wait ~15 min → /start - # shipyard_poc_bot in Telegram → test → git push origin :main. + # Mini App URL: https://b3.dannydannydanny.me (vps-relay Caddy → + # ZT IPv6 → here:8081). Stable across restarts — listed in + # ~/python-projects/26_shipyard/apps.json. + # Workflow: git push origin :staging → wait ~15 min → tap B3Bot + # beta in shipyard_poc_bot's launcher → test → git push :main. systemd.services.fitness-bot-shipyard = let pythonEnv = pkgs.python3.withPackages (ps: with ps; [ python-telegram-bot @@ -232,10 +237,12 @@ after = [ "network-online.target" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; - path = [ pythonEnv pkgs.cloudflared ]; + path = [ pythonEnv ]; environment.API_HOST = "::"; environment.API_PORT = "8081"; - # No WEBAPP_URL — start.py spins up its own ephemeral cloudflared tunnel. + # Stable URL fronted by vps-relay's Caddy → ZT → here:8081. + # WEBAPP_URL set tells start.py to skip cloudflared entirely. + environment.WEBAPP_URL = "https://b3.dannydannydanny.me"; unitConfig.ConditionPathExists = "/home/danny/.secrets/shipyard_poc_bot.env"; serviceConfig = { WorkingDirectory = "/home/danny/tg_fitness_bot_shipyard"; diff --git a/nixos/hosts/vps-relay.nix b/nixos/hosts/vps-relay.nix index 306c127..1914698 100644 --- a/nixos/hosts/vps-relay.nix +++ b/nixos/hosts/vps-relay.nix @@ -101,6 +101,11 @@ "bbbot.dannydannydanny.me".extraConfig = '' reverse_proxy http://[fdd5:53a2:de33:d269:6499:93d5:53a2:de33]:8080 ''; + # B3Bot beta — bbbot's staging tenant under shipyard_poc_bot. + # Same backend host as bbbot prod, port 8081. + "b3.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:93d5:53a2:de33]:8081 + ''; # Shelfish — phantom-ship's ZT IPv6. "shelfish.dannydannydanny.me".extraConfig = '' reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8081 From 40cc62f65bb04acc305377d5cde52ee57c38b847 Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Sun, 10 May 2026 16:02:42 +0200 Subject: [PATCH 38/72] sunken-ship: chromaprint on PATH for mulbo-server-enrich AcoustID needs fpcalc -plain output (re-fingerprinted on-demand since tracks_index stores -raw for dedup). chromaprint added alongside the existing yt-dlp. Co-Authored-By: Claude Opus 4.7 (1M context) --- nixos/hosts/sunken-ship.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/hosts/sunken-ship.nix b/nixos/hosts/sunken-ship.nix index 75ddc1d..095b986 100644 --- a/nixos/hosts/sunken-ship.nix +++ b/nixos/hosts/sunken-ship.nix @@ -420,7 +420,7 @@ description = "Enrich Navidrome tracks with empty/Unknown metadata"; after = [ "mulbo-server.service" ]; requires = [ "mulbo-server.service" ]; - path = with pkgs; [ yt-dlp ]; # provides yt-dlp for SoundCloud lookups + path = with pkgs; [ yt-dlp chromaprint ]; # yt-dlp for SC/YT lookups, chromaprint for AcoustID's -plain fingerprint environment = { MULBO_INDEX_DB = "/var/lib/mulbo-server/index.db"; MULBO_NAVIDROME_DB = "/var/lib/navidrome/navidrome.db"; From 3b6f4545b4d72cfe62c4b41782f8d120aa2983ff Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Sun, 10 May 2026 16:12:08 +0200 Subject: [PATCH 39/72] monitoring: prometheus + alertmanager + grafana on sunken-ship node_exporter on all three hosts (port 9100, ZT-only). Prometheus server scrapes via the clan ZT IPv6s. Alertmanager routes alerts to @HarakatBot (chat 66070351); critical repeats every 1h, others 4h. Starter rule: HostDown when up==0 for 5m. Grafana on :3000 over ZT, provisioned with the local Prometheus as default datasource. Manual secrets on sunken-ship: /etc/alertmanager/telegram-token and /etc/grafana/secret-key. Co-Authored-By: Claude Opus 4.7 (1M context) --- flake-modules/clan.nix | 4 + flake-modules/nixos-modules.nix | 2 + modules/monitoring-node-exporter.nix | 12 ++ modules/monitoring-prometheus-server.nix | 134 +++++++++++++++++++++++ 4 files changed, 152 insertions(+) create mode 100644 modules/monitoring-node-exporter.nix create mode 100644 modules/monitoring-prometheus-server.nix diff --git a/flake-modules/clan.nix b/flake-modules/clan.nix index f8b1293..6b4a5d0 100644 --- a/flake-modules/clan.nix +++ b/flake-modules/clan.nix @@ -127,6 +127,8 @@ in { ../nixos/hosts/sunken-ship.nix config.flake.nixosModules.dotfiles-rebuild config.flake.nixosModules.server-debug-tools + config.flake.nixosModules.monitoring-node-exporter + config.flake.nixosModules.monitoring-prometheus-server inputs.home-manager.nixosModules.home-manager (hmModule { user = "danny"; @@ -146,6 +148,7 @@ in { } clanHostsModule ../nixos/hosts/vps-relay.nix + config.flake.nixosModules.monitoring-node-exporter inputs.home-manager.nixosModules.home-manager (hmModule { user = "danny"; @@ -167,6 +170,7 @@ in { ../nixos/hosts/phantom-ship.nix config.flake.nixosModules.dotfiles-rebuild config.flake.nixosModules.server-debug-tools + config.flake.nixosModules.monitoring-node-exporter inputs.home-manager.nixosModules.home-manager (hmModule { user = "danny"; diff --git a/flake-modules/nixos-modules.nix b/flake-modules/nixos-modules.nix index a466a58..3f6bf96 100644 --- a/flake-modules/nixos-modules.nix +++ b/flake-modules/nixos-modules.nix @@ -5,4 +5,6 @@ { ... }: { flake.nixosModules.dotfiles-rebuild = ../modules/dotfiles-rebuild.nix; flake.nixosModules.server-debug-tools = ../modules/server-debug-tools.nix; + flake.nixosModules.monitoring-node-exporter = ../modules/monitoring-node-exporter.nix; + flake.nixosModules.monitoring-prometheus-server = ../modules/monitoring-prometheus-server.nix; } diff --git a/modules/monitoring-node-exporter.nix b/modules/monitoring-node-exporter.nix new file mode 100644 index 0000000..402f44d --- /dev/null +++ b/modules/monitoring-node-exporter.nix @@ -0,0 +1,12 @@ +# Prometheus node_exporter — exposes host metrics on :9100, scoped to the +# ZeroTier mesh so only sunken-ship (the Prometheus server) can scrape it. +{ ... }: { + services.prometheus.exporters.node = { + enable = true; + port = 9100; + listenAddress = "::"; + enabledCollectors = [ "systemd" ]; + }; + + networking.firewall.interfaces."zt+".allowedTCPPorts = [ 9100 ]; +} diff --git a/modules/monitoring-prometheus-server.nix b/modules/monitoring-prometheus-server.nix new file mode 100644 index 0000000..6b02d14 --- /dev/null +++ b/modules/monitoring-prometheus-server.nix @@ -0,0 +1,134 @@ +# Prometheus + Alertmanager + Grafana on sunken-ship. +# +# Scrape targets are the clan ZeroTier IPv6s — kept in sync with +# vars/per-machine//zerotier/zerotier-ip/value. +# +# Telegram receiver uses the existing @HarakatBot. Drop the bot token at +# /etc/alertmanager/telegram-token (mode 0400, root) before rebuild — same +# manual-secret pattern as the other Telegram bots in the repo. +# +# Routing: critical alerts repeat every 1h, everything else every 4h. +{ ... }: +let + sunkenShipZTv6 = "fdd5:53a2:de33:d269:6499:93d5:53a2:de33"; + phantomShipZTv6 = "fdd5:53a2:de33:d269:6499:936c:48a:bbdc"; + vpsRelayZTv6 = "fdd5:53a2:de33:d269:6499:9305:339f:2ed3"; + + target = ip: "[${ip}]:9100"; +in { + services.prometheus = { + enable = true; + port = 9090; + listenAddress = "[::1]"; + + globalConfig = { + scrape_interval = "30s"; + evaluation_interval = "30s"; + }; + + scrapeConfigs = [{ + job_name = "node"; + static_configs = [{ + targets = [ + (target sunkenShipZTv6) + (target phantomShipZTv6) + (target vpsRelayZTv6) + ]; + labels.job = "node"; + }]; + }]; + + ruleFiles = [ + (builtins.toFile "host-rules.yml" (builtins.toJSON { + groups = [{ + name = "hosts"; + rules = [{ + alert = "HostDown"; + expr = ''up{job="node"} == 0''; + for = "5m"; + labels.severity = "critical"; + annotations = { + summary = "{{ $labels.instance }} is down"; + description = "{{ $labels.instance }} has been unreachable for 5 minutes."; + }; + }]; + }]; + })) + ]; + + alertmanagers = [{ + static_configs = [{ targets = [ "127.0.0.1:9093" ]; }]; + }]; + + alertmanager = { + enable = true; + port = 9093; + listenAddress = "[::1]"; + configuration = { + route = { + receiver = "telegram-default"; + group_by = [ "alertname" ]; + group_wait = "30s"; + group_interval = "5m"; + repeat_interval = "4h"; + routes = [{ + matchers = [ ''severity="critical"'' ]; + receiver = "telegram-critical"; + group_wait = "10s"; + group_interval = "1m"; + repeat_interval = "1h"; + }]; + }; + receivers = [ + { + name = "telegram-default"; + telegram_configs = [{ + bot_token_file = "/etc/alertmanager/telegram-token"; + chat_id = 66070351; + api_url = "https://api.telegram.org"; + parse_mode = ""; + }]; + } + { + name = "telegram-critical"; + telegram_configs = [{ + bot_token_file = "/etc/alertmanager/telegram-token"; + chat_id = 66070351; + api_url = "https://api.telegram.org"; + parse_mode = ""; + message = '' + CRITICAL: {{ .CommonLabels.alertname }} + {{ range .Alerts }}{{ .Annotations.summary }} + {{ .Annotations.description }} + {{ end }}''; + }]; + } + ]; + }; + }; + }; + + services.grafana = { + enable = true; + settings.server = { + http_addr = "::"; + http_port = 3000; + domain = "sunken-ship.clan"; + }; + # Drop a random 32+ char string at /etc/grafana/secret-key (mode 0400, + # owned by grafana:grafana) before rebuild — same manual-secret pattern + # as /etc/alertmanager/telegram-token. Used to encrypt secrets stored + # in Grafana's DB; nothing to rotate on a fresh install. + settings.security.secret_key = "$__file{/etc/grafana/secret-key}"; + provision.datasources.settings.datasources = [{ + name = "Prometheus"; + type = "prometheus"; + url = "http://[::1]:9090"; + isDefault = true; + }]; + }; + + # Grafana on the ZeroTier mesh only. Prometheus + Alertmanager bind to + # localhost so they're not reachable off-host. + networking.firewall.interfaces."zt+".allowedTCPPorts = [ 3000 ]; +} From dc7895e3b26528312aac0a366b6d7a307fe96884 Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Sun, 10 May 2026 16:17:28 +0200 Subject: [PATCH 40/72] monitoring: bracket IPv6 listenAddress for node_exporter The NixOS module concatenates listenAddress and port as `${a}:${p}`, so "::" became ":::9100" and node_exporter rejected it ("too many colons in address"). Use "[::]" so the result is "[::]:9100". Co-Authored-By: Claude Opus 4.7 (1M context) --- modules/monitoring-node-exporter.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/monitoring-node-exporter.nix b/modules/monitoring-node-exporter.nix index 402f44d..7e08ae0 100644 --- a/modules/monitoring-node-exporter.nix +++ b/modules/monitoring-node-exporter.nix @@ -4,7 +4,7 @@ services.prometheus.exporters.node = { enable = true; port = 9100; - listenAddress = "::"; + listenAddress = "[::]"; enabledCollectors = [ "systemd" ]; }; From e8158e6c0f525e9324ebd61e95d81100fbbef374 Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Sun, 10 May 2026 16:47:37 +0200 Subject: [PATCH 41/72] =?UTF-8?q?monitoring:=20fix=20prometheus=20?= =?UTF-8?q?=E2=86=92=20alertmanager=20loopback=20(IPv4=20vs=20IPv6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Alertmanager binds [::1]:9093 but Prometheus was dialing 127.0.0.1:9093 — connection refused, so alerts fired internally but never reached Alertmanager. Switch the target to [::1]:9093 to match the bind. Co-Authored-By: Claude Opus 4.7 (1M context) --- modules/monitoring-prometheus-server.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/monitoring-prometheus-server.nix b/modules/monitoring-prometheus-server.nix index 6b02d14..9aedc14 100644 --- a/modules/monitoring-prometheus-server.nix +++ b/modules/monitoring-prometheus-server.nix @@ -57,7 +57,7 @@ in { ]; alertmanagers = [{ - static_configs = [{ targets = [ "127.0.0.1:9093" ]; }]; + static_configs = [{ targets = [ "[::1]:9093" ]; }]; }]; alertmanager = { From fc9894c32f29dab890ac53fc722f7849c25e6922 Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Tue, 12 May 2026 10:13:11 +0200 Subject: [PATCH 42/72] feat: install zed-editor :sparkles: --- nixos/home/danny/home.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/home/danny/home.nix b/nixos/home/danny/home.nix index 16d9adf..739abc2 100644 --- a/nixos/home/danny/home.nix +++ b/nixos/home/danny/home.nix @@ -228,7 +228,7 @@ # alacritty # TODO: configured via programs.alacritty above, so not needed here # warp-terminal # TODO: Bloat # vscodium # TODO: Bloat - # zed-editor # TODO: Bloat + zed-editor code-cursor cursor-cli dfu-util # USB DFU firmware flasher (Flipper Zero etc.) From 4fab9a28a20fe6a19c05df5a40fb425dbf0ca11b Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Tue, 12 May 2026 13:57:36 +0200 Subject: [PATCH 43/72] chore: update flake.lock :arrow_up: --- flake.lock | 157 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 92 insertions(+), 65 deletions(-) diff --git a/flake.lock b/flake.lock index 106a124..c524fd2 100644 --- a/flake.lock +++ b/flake.lock @@ -13,11 +13,11 @@ "treefmt-nix": "treefmt-nix" }, "locked": { - "lastModified": 1776708356, - "narHash": "sha256-Smv2algQmojsu0m9EEXs+Oy0Tg/SjwI5WN66u/BaxYs=", + "lastModified": 1778267301, + "narHash": "sha256-/SEnX0wGQuvQ78EKWTIDA8nCUaJhCD0nVbtP5evFdSQ=", "ref": "fix/dm-pull-deploy-hyphen-hostnames", - "rev": "796ee625b60941bb959039924bfc39e5d13481cc", - "revCount": 46, + "rev": "bd2f9c63ed5613eb52a03116df88b06275171f55", + "revCount": 47, "type": "git", "url": "https://git.clan.lol/dannydannydanny/clan-community.git" }, @@ -44,11 +44,11 @@ "treefmt-nix": "treefmt-nix_2" }, "locked": { - "lastModified": 1776557977, - "narHash": "sha256-j+UWg3fR6jWKPqkPoqRf1a6nR1b/AnZXDuh04H+voUc=", - "rev": "e9ced950bedc726492e5cb52139bf5f17258dc69", + "lastModified": 1778462753, + "narHash": "sha256-/9qWZbrwoVWP0YWuC1Z5HMEb/oy6rNsjypUKTuk1PB4=", + "rev": "09551fdb27a7e5712bef371e9271034d503242ed", "type": "tarball", - "url": "https://git.clan.lol/api/v1/repos/clan/clan-core/archive/e9ced950bedc726492e5cb52139bf5f17258dc69.tar.gz" + "url": "https://git.clan.lol/api/v1/repos/clan/clan-core/archive/09551fdb27a7e5712bef371e9271034d503242ed.tar.gz" }, "original": { "type": "tarball", @@ -71,11 +71,11 @@ ] }, "locked": { - "lastModified": 1776506822, - "narHash": "sha256-WlxAhXEoDHbkfFw3uNYra0CXce7pBk314x9chPu7ycE=", - "rev": "c3f48f5931b27bb9cc58de8799d36ecefb867d98", + "lastModified": 1776654564, + "narHash": "sha256-5bpzOOXsaAr4g25/ghtKdYO17xg0l+MieCcWgqx24eY=", + "rev": "ad23733ebc47284dc1158db43218cf4027824aee", "type": "tarball", - "url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/c3f48f5931b27bb9cc58de8799d36ecefb867d98.tar.gz" + "url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/ad23733ebc47284dc1158db43218cf4027824aee.tar.gz" }, "original": { "type": "tarball", @@ -90,11 +90,11 @@ ] }, "locked": { - "lastModified": 1773889306, - "narHash": "sha256-PAqwnsBSI9SVC2QugvQ3xeYCB0otOwCacB1ueQj2tgw=", + "lastModified": 1776613567, + "narHash": "sha256-gC9Cp5ibBmGD5awCA9z7xy6MW6iJufhazTYJOiGlCUI=", "owner": "nix-community", "repo": "disko", - "rev": "5ad85c82cc52264f4beddc934ba57f3789f28347", + "rev": "32f4236bfc141ae930b5ba2fb604f561fed5219d", "type": "github" }, "original": { @@ -110,11 +110,11 @@ ] }, "locked": { - "lastModified": 1773889306, - "narHash": "sha256-PAqwnsBSI9SVC2QugvQ3xeYCB0otOwCacB1ueQj2tgw=", + "lastModified": 1777713215, + "narHash": "sha256-8GzXDOXckDWwST8TY5DbwYFjdvQLlP7K9CLSVx6iTTo=", "owner": "nix-community", "repo": "disko", - "rev": "5ad85c82cc52264f4beddc934ba57f3789f28347", + "rev": "63b4e7e6cf75307c1d26ac3762b886b5b0247267", "type": "github" }, "original": { @@ -167,11 +167,11 @@ ] }, "locked": { - "lastModified": 1775087534, - "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", + "lastModified": 1777988971, + "narHash": "sha256-qIoWPDs+0/8JecyYgE3gpKQxW/4bLW/gp45vow9ioCQ=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", + "rev": "0678d8986be1661af6bb555f3489f2fdfc31f6ff", "type": "github" }, "original": { @@ -223,11 +223,11 @@ ] }, "locked": { - "lastModified": 1776184304, - "narHash": "sha256-No6QGBmIv5ChiwKCcbkxjdEQ/RO2ZS1gD7SFy6EZ7rc=", + "lastModified": 1778444552, + "narHash": "sha256-f18pIiR9q/p1vHY93gmAum7aHhQOG49oGvAB9+lptRo=", "owner": "nix-community", "repo": "home-manager", - "rev": "3c7524c68348ef79ce48308e0978611a050089b2", + "rev": "dcebe66f958673729896eec2de4abfd86ef22d21", "type": "github" }, "original": { @@ -265,11 +265,11 @@ ] }, "locked": { - "lastModified": 1774991950, - "narHash": "sha256-kScKj3qJDIWuN9/6PMmgy5esrTUkYinrO5VvILik/zw=", + "lastModified": 1777594677, + "narHash": "sha256-h90sHwoRJLRvaTpZroTvU2JRHDFj0czUafM8eqLe1RI=", "owner": "nix-community", "repo": "home-manager", - "rev": "f2d3e04e278422c7379e067e323734f3e8c585a7", + "rev": "899c08a15beae5da51a5cecd6b2b994777a948da", "type": "github" }, "original": { @@ -321,11 +321,11 @@ ] }, "locked": { - "lastModified": 1775037210, - "narHash": "sha256-KM2WYj6EA7M/FVZVCl3rqWY+TFV5QzSyyGE2gQxeODU=", + "lastModified": 1777780666, + "narHash": "sha256-8wURyQMdDkGUarSTKOGdCuFfYiwa3HbzwscUfn3STDE=", "owner": "nix-darwin", "repo": "nix-darwin", - "rev": "06648f4902343228ce2de79f291dd5a58ee12146", + "rev": "8c62fba0854ba15c8917aed18894dbccb48a3777", "type": "github" }, "original": { @@ -339,17 +339,18 @@ "inputs": { "flake-utils": "flake-utils", "home-manager": "home-manager_2", - "nix-steipete-tools": "nix-steipete-tools", + "nix-openclaw-tools": "nix-openclaw-tools", "nixpkgs": [ "nixpkgs" - ] + ], + "qmd": "qmd" }, "locked": { - "lastModified": 1776183358, - "narHash": "sha256-uRWaRXGhkyGWMbNgQcmx0+RPzPLenVGopkNHgAEfmBQ=", + "lastModified": 1778353239, + "narHash": "sha256-g0yC+loN19X3Xyn6RuBHeWzevH7Qymt0REW+kyGuCLY=", "owner": "openclaw", "repo": "nix-openclaw", - "rev": "53aac0dce0810c40c75793fdad3d41b0f7e7baaf", + "rev": "e2ea91056fdd0836bef96326a2b687277dbe3e1c", "type": "github" }, "original": { @@ -358,6 +359,24 @@ "type": "github" } }, + "nix-openclaw-tools": { + "inputs": { + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1778060041, + "narHash": "sha256-tXWkN1VnwFG8XlRqW/e7VwbKnUfyU9tB7YDm9QHJXTY=", + "owner": "openclaw", + "repo": "nix-openclaw-tools", + "rev": "4c1cee3c7eaf68f9de0f756be1484534f5bb5f34", + "type": "github" + }, + "original": { + "owner": "openclaw", + "repo": "nix-openclaw-tools", + "type": "github" + } + }, "nix-select": { "locked": { "lastModified": 1763303120, @@ -371,35 +390,17 @@ "url": "https://git.clan.lol/clan/nix-select/archive/main.tar.gz" } }, - "nix-steipete-tools": { - "inputs": { - "nixpkgs": "nixpkgs" - }, - "locked": { - "lastModified": 1773561580, - "narHash": "sha256-wT0bKTp45YnMkc4yXQvk943Zz/rksYiIjEXGdWzxnic=", - "owner": "openclaw", - "repo": "nix-steipete-tools", - "rev": "cd4c429ff3b3aaef9f92e59812cf2baf5704b86f", - "type": "github" - }, - "original": { - "owner": "openclaw", - "repo": "nix-steipete-tools", - "type": "github" - } - }, "nixos-wsl": { "inputs": { "flake-compat": "flake-compat", "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1776255237, - "narHash": "sha256-LQjlc0VEn55WAT4BiI8sIsokb/2FNlcbBD+Xr3MTE24=", + "lastModified": 1777732699, + "narHash": "sha256-2uX/XtOWZ/oy2rerRynVhqVA//ZXZ3Fo60PikLHEPQc=", "owner": "nix-community", "repo": "NixOS-WSL", - "rev": "9a8c2a85f1ffdcecfb0f9c52c5a73c49ceb43911", + "rev": "5482f113fd31ebac131d1ebeb2ae90bf0d5e41f5", "type": "github" }, "original": { @@ -427,11 +428,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1773734432, - "narHash": "sha256-IF5ppUWh6gHGHYDbtVUyhwy/i7D261P7fWD1bPefOsw=", + "lastModified": 1776169885, + "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "cda48547b432e8d3b18b4180ba07473762ec8558", + "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", "type": "github" }, "original": { @@ -443,11 +444,11 @@ }, "nixpkgs_3": { "locked": { - "lastModified": 1776255774, - "narHash": "sha256-psVTpH6PK3q1htMJpmdz1hLF5pQgEshu7gQWgKO6t6Y=", + "lastModified": 1778274207, + "narHash": "sha256-I4puXmX1iovcCHZlRmztO3vW0mAbbRvq4F8wgIMQ1MM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "566acc07c54dc807f91625bb286cb9b321b5f42a", + "rev": "b3da656039dc7a6240f27b2ef8cc6a3ef3bccae7", "type": "github" }, "original": { @@ -471,6 +472,32 @@ "type": "indirect" } }, + "qmd": { + "inputs": { + "flake-utils": [ + "nix-openclaw", + "flake-utils" + ], + "nixpkgs": [ + "nix-openclaw", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1775429264, + "narHash": "sha256-bqIVaNRTa8H5vrw3RwsD7QdtTa0xNvRuEVzlzE1hIBQ=", + "owner": "tobi", + "repo": "qmd", + "rev": "65cd1b3fd02891d1ee0eefa751620918664fa321", + "type": "github" + }, + "original": { + "owner": "tobi", + "ref": "v2.1.0", + "repo": "qmd", + "type": "github" + } + }, "root": { "inputs": { "clan-community": "clan-community", @@ -638,11 +665,11 @@ ] }, "locked": { - "lastModified": 1776317517, - "narHash": "sha256-JP1XVRabZquf7pnXvRUjp7DV+EBrB6Qmp3+vG3HMy/k=", + "lastModified": 1778394798, + "narHash": "sha256-/jR8bModWv0ji305ecMgAB+2eaXLZiYdH+9Z4JIRkuA=", "owner": "0xc000022070", "repo": "zen-browser-flake", - "rev": "0a7be59e988bb2cb452080f59aaabae70bc415ae", + "rev": "45bc54456044b96492923739bfae633e1a4352e1", "type": "github" }, "original": { From 0f34d2508de2299794bada37ee7f195aabb61e0b Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Sun, 17 May 2026 12:55:58 +0200 Subject: [PATCH 44/72] feat: add kf.dannydannydanny.me portfolio vhost :sparkles: Routes the new subdomain to the existing notes service on phantom-ship :8092 (Host-header routed). Serves Kyranna Fardi's architecture portfolio. Co-Authored-By: Claude Opus 4.7 (1M context) --- nixos/hosts/vps-relay.nix | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nixos/hosts/vps-relay.nix b/nixos/hosts/vps-relay.nix index 1914698..bd93b25 100644 --- a/nixos/hosts/vps-relay.nix +++ b/nixos/hosts/vps-relay.nix @@ -145,6 +145,11 @@ "dannydannydanny.me".extraConfig = '' reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8092 ''; + # kf — Kyranna Fardi architecture portfolio. Same notes service on + # phantom :8092, routed by Host header (PORTFOLIO_HOST). + "kf.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8092 + ''; }; }; From 5d4f2048a63ddae41c3a92d3fe37435c7a13983b Mon Sep 17 00:00:00 2001 From: Hara Date: Wed, 20 May 2026 15:37:31 +0200 Subject: [PATCH 45/72] hara: heartbeat timer reduced to once daily at 06:07 --- nixos/hosts/phantom-ship.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 90886cc..4e71c54 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -545,7 +545,7 @@ in systemd.timers.hara-heartbeat = { wantedBy = [ "timers.target" ]; timerConfig = { - OnCalendar = "06,10,14,18:07"; + OnCalendar = "06:07"; Timezone = "Europe/Copenhagen"; Persistent = true; }; From 0c11628f734e2c83e330247ad6edefd83605fcbb Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Wed, 20 May 2026 18:44:51 +0200 Subject: [PATCH 46/72] phantom-ship: Catppuccin theme for Forgejo (mocha + mauve) Adds catppuccin flake input and wires its NixOS module into phantom-ship's imports via clan.nix. Enables catppuccin.forgejo with mocha flavor + mauve accent on the running Forgejo instance. Module ref: https://nix.catppuccin.com/options/main/nixos/catppuccin.forgejo/ --- flake-modules/clan.nix | 1 + flake.lock | 21 +++++++++++++++++++++ flake.nix | 6 ++++++ nixos/hosts/phantom-ship.nix | 8 ++++++++ 4 files changed, 36 insertions(+) diff --git a/flake-modules/clan.nix b/flake-modules/clan.nix index 6b4a5d0..d8fe760 100644 --- a/flake-modules/clan.nix +++ b/flake-modules/clan.nix @@ -167,6 +167,7 @@ in { } clanHostsModule inputs.nix-openclaw.nixosModules.openclaw-gateway + inputs.catppuccin.nixosModules.catppuccin ../nixos/hosts/phantom-ship.nix config.flake.nixosModules.dotfiles-rebuild config.flake.nixosModules.server-debug-tools diff --git a/flake.lock b/flake.lock index c524fd2..6ae35fc 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,25 @@ { "nodes": { + "catppuccin": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1779125773, + "narHash": "sha256-F34zmAgMQXHwvFb9SpCilX4cAIfF4+KvpzrJqnkNLJE=", + "owner": "catppuccin", + "repo": "nix", + "rev": "86996e2c4ee6a091fddb10de56dd21a1a5972bcb", + "type": "github" + }, + "original": { + "owner": "catppuccin", + "repo": "nix", + "type": "github" + } + }, "clan-community": { "inputs": { "clan-core": [ @@ -500,6 +520,7 @@ }, "root": { "inputs": { + "catppuccin": "catppuccin", "clan-community": "clan-community", "clan-core": "clan-core", "disko": "disko_2", diff --git a/flake.nix b/flake.nix index 30e7d71..b0b6579 100644 --- a/flake.nix +++ b/flake.nix @@ -25,6 +25,12 @@ nix-openclaw.url = "github:openclaw/nix-openclaw"; nix-openclaw.inputs.nixpkgs.follows = "nixpkgs"; + # Catppuccin NixOS module — used for theming Forgejo (and any future + # NixOS-level services). Home-manager/neovim/alacritty Catppuccin lives + # outside this input (separate nixpkgs packages). + catppuccin.url = "github:catppuccin/nix"; + catppuccin.inputs.nixpkgs.follows = "nixpkgs"; + clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz"; clan-core.inputs.nixpkgs.follows = "nixpkgs"; clan-core.inputs.flake-parts.follows = "flake-parts"; diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 90886cc..d3ebf6f 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -581,6 +581,14 @@ in }; }; + # Catppuccin theme for Forgejo — module wired in via clan.nix's + # phantom-ship.imports (inputs.catppuccin.nixosModules.catppuccin). + catppuccin.forgejo = { + enable = true; + flavor = "mocha"; + accent = "mauve"; + }; + # Auto-rebuild service/timer + safe.directory provided by the # shared dotfiles-rebuild NixOS module (see nixos/modules/dotfiles-rebuild.nix). } From 2e9441f367e92f12b35851778e1957b385c08c42 Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Wed, 20 May 2026 19:31:22 +0200 Subject: [PATCH 47/72] Retire dotfiles-rebuild, switch to dm-pull-deploy push timer - Drop modules/dotfiles-rebuild.nix and its imports in clan.nix; sunken-ship + phantom-ship no longer ship the legacy 15-min rebuild-from-git timer. - Add dm-pull-deploy-push systemd timer on sunken-ship: every 15min runs dm-send-deploy to announce origin/main rev via data-mesher gossip (sunken is the dm-pull-deploy push node). - Fix mulbo-pull service path: add openssh so 'git fetch' over an SSH remote stops failing with 'cannot run ssh'. - vps-relay authorized_keys: rename Mac key comment to mac-admin, add sunken-ship's actual ed25519 key for ZT mesh debugging. - home.nix: add cinny-desktop (Matrix client). - neovim: enable cursorline. --- flake-modules/clan.nix | 2 -- flake-modules/nixos-modules.nix | 3 +-- modules/dotfiles-rebuild.nix | 44 --------------------------------- nixos/home/danny/home.nix | 1 + nixos/hosts/sunken-ship.nix | 40 +++++++++++++++++++++++++++--- nixos/hosts/vps-relay.nix | 9 +++++-- nixos/neovim.nix | 1 + 7 files changed, 46 insertions(+), 54 deletions(-) delete mode 100644 modules/dotfiles-rebuild.nix diff --git a/flake-modules/clan.nix b/flake-modules/clan.nix index d8fe760..2207db4 100644 --- a/flake-modules/clan.nix +++ b/flake-modules/clan.nix @@ -125,7 +125,6 @@ in { } clanHostsModule ../nixos/hosts/sunken-ship.nix - config.flake.nixosModules.dotfiles-rebuild config.flake.nixosModules.server-debug-tools config.flake.nixosModules.monitoring-node-exporter config.flake.nixosModules.monitoring-prometheus-server @@ -169,7 +168,6 @@ in { inputs.nix-openclaw.nixosModules.openclaw-gateway inputs.catppuccin.nixosModules.catppuccin ../nixos/hosts/phantom-ship.nix - config.flake.nixosModules.dotfiles-rebuild config.flake.nixosModules.server-debug-tools config.flake.nixosModules.monitoring-node-exporter inputs.home-manager.nixosModules.home-manager diff --git a/flake-modules/nixos-modules.nix b/flake-modules/nixos-modules.nix index 3f6bf96..3dd7929 100644 --- a/flake-modules/nixos-modules.nix +++ b/flake-modules/nixos-modules.nix @@ -1,9 +1,8 @@ # Expose reusable NixOS modules via `flake.nixosModules`. # # Consume from a host's flake-module via: -# modules = [ config.flake.nixosModules.dotfiles-rebuild ]; +# modules = [ config.flake.nixosModules.server-debug-tools ]; { ... }: { - flake.nixosModules.dotfiles-rebuild = ../modules/dotfiles-rebuild.nix; flake.nixosModules.server-debug-tools = ../modules/server-debug-tools.nix; flake.nixosModules.monitoring-node-exporter = ../modules/monitoring-node-exporter.nix; flake.nixosModules.monitoring-prometheus-server = ../modules/monitoring-prometheus-server.nix; diff --git a/modules/dotfiles-rebuild.nix b/modules/dotfiles-rebuild.nix deleted file mode 100644 index de6ac87..0000000 --- a/modules/dotfiles-rebuild.nix +++ /dev/null @@ -1,44 +0,0 @@ -# Shared auto-rebuild-from-git service for homelab hosts. -# -# Every 15 min: git fetch origin, fast-forward main, and if there were any -# new commits run nixos-rebuild switch against `#`. -# -# Assumes /etc/dotfiles is an already-cloned checkout of the dotfiles repo. -{ config, lib, pkgs, ... }: -let - dotfilesDir = "/etc/dotfiles"; - flakeRef = "${dotfilesDir}#${config.networking.hostName}"; -in { - environment.systemPackages = [ pkgs.git ]; - - # Trust /etc/dotfiles as root even though it's owned by `danny`. - # nix/libgit2 reads safe.directory from /etc/gitconfig; the GIT_CONFIG_* - # env vars on the service only affect the git CLI, not nix. - programs.git.enable = true; - programs.git.config.safe.directory = [ dotfilesDir ]; - - systemd.services.dotfiles-rebuild = { - description = "Pull dotfiles and run nixos-rebuild if repo changed"; - path = with pkgs; [ git nix nixos-rebuild ]; - environment.GIT_CONFIG_COUNT = "1"; - environment.GIT_CONFIG_KEY_0 = "safe.directory"; - environment.GIT_CONFIG_VALUE_0 = dotfilesDir; - script = '' - set -euo pipefail - cd ${dotfilesDir} - git fetch origin - if [ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ]; then - exit 0 - fi - git pull origin main - exec nixos-rebuild switch --flake ${flakeRef} - ''; - serviceConfig.Type = "oneshot"; - }; - - systemd.timers.dotfiles-rebuild = { - wantedBy = [ "timers.target" ]; - timerConfig.OnCalendar = "*-*-* *:00/15:00"; # every 15 minutes - timerConfig.RandomizedDelaySec = "2min"; - }; -} diff --git a/nixos/home/danny/home.nix b/nixos/home/danny/home.nix index 739abc2..c110a83 100644 --- a/nixos/home/danny/home.nix +++ b/nixos/home/danny/home.nix @@ -231,6 +231,7 @@ zed-editor code-cursor cursor-cli + cinny-desktop # Matrix client (Tauri wrapper around the Cinny web app) dfu-util # USB DFU firmware flasher (Flipper Zero etc.) discord mapscii diff --git a/nixos/hosts/sunken-ship.nix b/nixos/hosts/sunken-ship.nix index 095b986..fd14e1b 100644 --- a/nixos/hosts/sunken-ship.nix +++ b/nixos/hosts/sunken-ship.nix @@ -72,7 +72,7 @@ # x86_64-linux builds here via ssh-ng://danny@sunken-ship-zt). nix.settings.trusted-users = [ "root" "danny" ]; environment.systemPackages = with pkgs; [ - git # clone/bootstrap and dotfiles-rebuild timer + git # clone/bootstrap, repo-pull timers, dm-pull-deploy push brightnessctl # manual backlight; replaces removed `light` from nixpkgs uxplay # AirPlay mirroring receiver alsa-utils # aplay, amixer, arecord for audio debugging @@ -347,7 +347,10 @@ # not in the repo, so they survive pulls. systemd.services.mulbo-pull = { description = "Pull mulbo repo and restart mulbo-server if changed"; - path = with pkgs; [ git systemd ]; + # openssh: `git fetch origin` over an SSH remote forks `ssh`; without + # it git dies with "cannot run ssh: No such file or directory" and the + # unit fails (shows up as system `degraded`). + path = with pkgs; [ git openssh systemd ]; environment = { GIT_CONFIG_COUNT = "1"; GIT_CONFIG_KEY_0 = "safe.directory"; @@ -372,6 +375,33 @@ timerConfig.RandomizedDelaySec = "2min"; }; + # dm-pull-deploy push automation. sunken-ship is the push node for the + # clan dm-pull-deploy instance (wired in flake-modules/clan.nix), but + # the upstream module only ships a manual `dm-send-deploy` binary — no + # scheduler. This timer announces the latest origin/main rev over + # data-mesher gossip; the watchers (dm-pull-deploy.path on sunken + + # phantom) compare and only rebuild when the rev actually changes, so + # re-announcing the same rev is a cheap no-op. This is the replacement + # for the legacy dotfiles-rebuild pull timer (being retired). + # + # dm-send-deploy self-discovers the rev via `git ls-remote` and signs + # with /run/secrets/vars/dm-pull-deploy-signing-key — needs root. + systemd.services.dm-pull-deploy-push = { + description = "Announce latest origin/main rev via data-mesher (dm-pull-deploy push)"; + serviceConfig = { + Type = "oneshot"; + ExecStart = "/run/current-system/sw/bin/dm-send-deploy"; + User = "root"; + }; + }; + + systemd.timers.dm-pull-deploy-push = { + wantedBy = [ "timers.target" ]; + timerConfig.OnCalendar = "*-*-* *:04/15:00"; # every 15 min, offset from the other pull timers + timerConfig.RandomizedDelaySec = "2min"; + timerConfig.Persistent = true; + }; + # One-shot backfill: walks Navidrome's media_file, computes # (sha256, chromaprint) per file, populates mulbo-server's tracks_index # with the corresponding navidrome_track_id. Idempotent — existing rows @@ -443,6 +473,8 @@ }; }; - # Auto-rebuild service/timer + safe.directory provided by the - # shared dotfiles-rebuild NixOS module (see nixos/modules/dotfiles-rebuild.nix). + # Deploys now flow through clan dm-pull-deploy: the dm-pull-deploy-push + # timer above announces origin/main, and the dm-pull-deploy.path watcher + # rebuilds on change. The legacy pull-based dotfiles-rebuild module was + # retired 2026-05-19. } diff --git a/nixos/hosts/vps-relay.nix b/nixos/hosts/vps-relay.nix index bd93b25..85ed329 100644 --- a/nixos/hosts/vps-relay.nix +++ b/nixos/hosts/vps-relay.nix @@ -46,8 +46,13 @@ isNormalUser = true; extraGroups = [ "wheel" ]; openssh.authorizedKeys.keys = [ - # Same pubkey used to reach sunken-ship; set at install via clan. - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKW/akfIiVU5o63YrTAJVZhMj7kXfYHOnXDtlpVFW7pf danny@sunken-ship" + # Mac admin key (~/.ssh/id_ed25519_sunken_ship on the laptop — the + # key the Mac uses to reach the fleet). Used for `clan machines + # update vps-relay` from the Mac and at install via clan. + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKW/akfIiVU5o63YrTAJVZhMj7kXfYHOnXDtlpVFW7pf danny@mac-admin" + # sunken-ship's own key, so the push node can SSH into vps-relay + # over ZeroTier for mesh introspection / debugging. + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB9t4YAaoHvVouqp+qyFOq8o3SAtXMiAmjF6J0ldyx4g danny@sunken-ship" ]; }; users.users.root.openssh.authorizedKeys.keys = diff --git a/nixos/neovim.nix b/nixos/neovim.nix index 59a6f85..75b2335 100644 --- a/nixos/neovim.nix +++ b/nixos/neovim.nix @@ -41,6 +41,7 @@ end -- General options + vim.opt.cursorline = true vim.opt.mouse = "a" vim.opt.listchars = { tab = "→ ", space = "·", nbsp = "␣", trail = "•", eol = "¶", precedes = "«", extends = "»" } vim.opt.clipboard:append("unnamedplus") From cbf0defa34acc5965b8230fc08746dfeaf13cc69 Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Wed, 20 May 2026 19:31:22 +0200 Subject: [PATCH 48/72] phantom-ship/forgejo: switch to catppuccin-mauve-auto (light in light mode) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The catppuccin nix module only generates the static flavor+accent combinations and sets DEFAULT_THEME to e.g. catppuccin-mocha-mauve. The auto-switching CSS files (catppuccin--auto) ship in the gitea-theme assets but aren't wired into THEMES. Override DEFAULT_THEME to catppuccin-mauve-auto so the browser's prefers-color-scheme decides — latte (light) in light mode, mocha (dark) in dark mode. Append all auto variants + the four mauve flavor variants to THEMES so users can still pick from the appearance settings. --- nixos/hosts/phantom-ship.nix | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index d91e229..ec3a6b5 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -583,12 +583,37 @@ in # Catppuccin theme for Forgejo — module wired in via clan.nix's # phantom-ship.imports (inputs.catppuccin.nixosModules.catppuccin). + # The module's `flavor` option picks ONE static flavor; the auto + # variants (catppuccin--auto) follow prefers-color-scheme — + # latte in light mode, mocha in dark. We override DEFAULT_THEME + + # THEMES below to use the auto variant. catppuccin.forgejo = { enable = true; - flavor = "mocha"; + flavor = "mocha"; # static fallback; auto-variant takes precedence below accent = "mauve"; }; + # Default to the auto-switching mauve theme (light in light mode, dark + # in dark mode). Replace THEMES with builtins + every catppuccin auto + # variant + the four mauve flavor variants, so the user-level + # appearance picker can still offer them. + services.forgejo.settings.ui = { + DEFAULT_THEME = lib.mkForce "catppuccin-mauve-auto"; + THEMES = lib.mkForce (lib.concatStringsSep "," [ + # Forgejo builtins + "forgejo-auto" "forgejo-light" "forgejo-dark" + # Auto-switching catppuccin (light in light mode, dark in dark mode) + "catppuccin-blue-auto" "catppuccin-flamingo-auto" "catppuccin-green-auto" + "catppuccin-lavender-auto" "catppuccin-maroon-auto" "catppuccin-mauve-auto" + "catppuccin-peach-auto" "catppuccin-pink-auto" "catppuccin-red-auto" + "catppuccin-rosewater-auto" "catppuccin-sapphire-auto" "catppuccin-sky-auto" + "catppuccin-teal-auto" "catppuccin-yellow-auto" + # Static mauve variants for manual override + "catppuccin-latte-mauve" "catppuccin-frappe-mauve" + "catppuccin-macchiato-mauve" "catppuccin-mocha-mauve" + ]); + }; + # Auto-rebuild service/timer + safe.directory provided by the # shared dotfiles-rebuild NixOS module (see nixos/modules/dotfiles-rebuild.nix). } From 9793d5ef7cdecf41985697bc02549b3ee93baf9d Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Wed, 20 May 2026 20:13:44 +0200 Subject: [PATCH 49/72] Revert "phantom-ship/forgejo: switch to catppuccin-mauve-auto (light in light mode)" This reverts commit cbf0defa34acc5965b8230fc08746dfeaf13cc69. --- nixos/hosts/phantom-ship.nix | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index ec3a6b5..d91e229 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -583,37 +583,12 @@ in # Catppuccin theme for Forgejo — module wired in via clan.nix's # phantom-ship.imports (inputs.catppuccin.nixosModules.catppuccin). - # The module's `flavor` option picks ONE static flavor; the auto - # variants (catppuccin--auto) follow prefers-color-scheme — - # latte in light mode, mocha in dark. We override DEFAULT_THEME + - # THEMES below to use the auto variant. catppuccin.forgejo = { enable = true; - flavor = "mocha"; # static fallback; auto-variant takes precedence below + flavor = "mocha"; accent = "mauve"; }; - # Default to the auto-switching mauve theme (light in light mode, dark - # in dark mode). Replace THEMES with builtins + every catppuccin auto - # variant + the four mauve flavor variants, so the user-level - # appearance picker can still offer them. - services.forgejo.settings.ui = { - DEFAULT_THEME = lib.mkForce "catppuccin-mauve-auto"; - THEMES = lib.mkForce (lib.concatStringsSep "," [ - # Forgejo builtins - "forgejo-auto" "forgejo-light" "forgejo-dark" - # Auto-switching catppuccin (light in light mode, dark in dark mode) - "catppuccin-blue-auto" "catppuccin-flamingo-auto" "catppuccin-green-auto" - "catppuccin-lavender-auto" "catppuccin-maroon-auto" "catppuccin-mauve-auto" - "catppuccin-peach-auto" "catppuccin-pink-auto" "catppuccin-red-auto" - "catppuccin-rosewater-auto" "catppuccin-sapphire-auto" "catppuccin-sky-auto" - "catppuccin-teal-auto" "catppuccin-yellow-auto" - # Static mauve variants for manual override - "catppuccin-latte-mauve" "catppuccin-frappe-mauve" - "catppuccin-macchiato-mauve" "catppuccin-mocha-mauve" - ]); - }; - # Auto-rebuild service/timer + safe.directory provided by the # shared dotfiles-rebuild NixOS module (see nixos/modules/dotfiles-rebuild.nix). } From b11add852543b7820c405a21622f62308d82544e Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Wed, 20 May 2026 20:13:44 +0200 Subject: [PATCH 50/72] Revert "Merge add-catppuccin-forgejo: Catppuccin theme on Forgejo" This reverts commit 1b0eb5835ddef99d159b6d6fb20ce9853c30eab5, reversing changes made to 5d4f2048a63ddae41c3a92d3fe37435c7a13983b. --- flake-modules/clan.nix | 1 - flake.lock | 21 --------------------- flake.nix | 6 ------ nixos/hosts/phantom-ship.nix | 8 -------- 4 files changed, 36 deletions(-) diff --git a/flake-modules/clan.nix b/flake-modules/clan.nix index 2207db4..30fe4c9 100644 --- a/flake-modules/clan.nix +++ b/flake-modules/clan.nix @@ -166,7 +166,6 @@ in { } clanHostsModule inputs.nix-openclaw.nixosModules.openclaw-gateway - inputs.catppuccin.nixosModules.catppuccin ../nixos/hosts/phantom-ship.nix config.flake.nixosModules.server-debug-tools config.flake.nixosModules.monitoring-node-exporter diff --git a/flake.lock b/flake.lock index 6ae35fc..c524fd2 100644 --- a/flake.lock +++ b/flake.lock @@ -1,25 +1,5 @@ { "nodes": { - "catppuccin": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1779125773, - "narHash": "sha256-F34zmAgMQXHwvFb9SpCilX4cAIfF4+KvpzrJqnkNLJE=", - "owner": "catppuccin", - "repo": "nix", - "rev": "86996e2c4ee6a091fddb10de56dd21a1a5972bcb", - "type": "github" - }, - "original": { - "owner": "catppuccin", - "repo": "nix", - "type": "github" - } - }, "clan-community": { "inputs": { "clan-core": [ @@ -520,7 +500,6 @@ }, "root": { "inputs": { - "catppuccin": "catppuccin", "clan-community": "clan-community", "clan-core": "clan-core", "disko": "disko_2", diff --git a/flake.nix b/flake.nix index b0b6579..30e7d71 100644 --- a/flake.nix +++ b/flake.nix @@ -25,12 +25,6 @@ nix-openclaw.url = "github:openclaw/nix-openclaw"; nix-openclaw.inputs.nixpkgs.follows = "nixpkgs"; - # Catppuccin NixOS module — used for theming Forgejo (and any future - # NixOS-level services). Home-manager/neovim/alacritty Catppuccin lives - # outside this input (separate nixpkgs packages). - catppuccin.url = "github:catppuccin/nix"; - catppuccin.inputs.nixpkgs.follows = "nixpkgs"; - clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz"; clan-core.inputs.nixpkgs.follows = "nixpkgs"; clan-core.inputs.flake-parts.follows = "flake-parts"; diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index d91e229..4e71c54 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -581,14 +581,6 @@ in }; }; - # Catppuccin theme for Forgejo — module wired in via clan.nix's - # phantom-ship.imports (inputs.catppuccin.nixosModules.catppuccin). - catppuccin.forgejo = { - enable = true; - flavor = "mocha"; - accent = "mauve"; - }; - # Auto-rebuild service/timer + safe.directory provided by the # shared dotfiles-rebuild NixOS module (see nixos/modules/dotfiles-rebuild.nix). } From 3dcbdd408a37143b2c16eb516178c1f388a444bd Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Fri, 22 May 2026 21:15:20 +0200 Subject: [PATCH 51/72] =?UTF-8?q?chore:=20unpin=20clan-community=20now=20t?= =?UTF-8?q?hat=20dm-pull-deploy=20fix=20merged=20=F0=9F=94=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR clan/clan-community#25 (machine.name hyphen sanitization) merged upstream, so swap clan-community.url from the fork branch back to clan/clan-community/archive/main.tar.gz and update flake.lock to upstream rev 81e4c9c. Eval confirms byte-identical host closures. Also finishes the dotfiles-rebuild retirement: phantom-ship.nix still referenced the now-deleted modules/dotfiles-rebuild.nix in comments. --- flake.lock | 43 ++++++++++-------------------------- flake.nix | 7 +++--- nixos/hosts/phantom-ship.nix | 7 +++--- 3 files changed, 19 insertions(+), 38 deletions(-) diff --git a/flake.lock b/flake.lock index c524fd2..59c41ec 100644 --- a/flake.lock +++ b/flake.lock @@ -9,22 +9,18 @@ "nixpkgs": [ "nixpkgs" ], - "systems": "systems", "treefmt-nix": "treefmt-nix" }, "locked": { - "lastModified": 1778267301, - "narHash": "sha256-/SEnX0wGQuvQ78EKWTIDA8nCUaJhCD0nVbtP5evFdSQ=", - "ref": "fix/dm-pull-deploy-hyphen-hostnames", - "rev": "bd2f9c63ed5613eb52a03116df88b06275171f55", - "revCount": 47, - "type": "git", - "url": "https://git.clan.lol/dannydannydanny/clan-community.git" + "lastModified": 1779453564, + "narHash": "sha256-q7iVGGhZYtAwsjf7sIKcYD5IgsTTTobWP/EStaDCUZc=", + "rev": "81e4c9cded645d0384812dd6b8f05bd2475ffe64", + "type": "tarball", + "url": "https://git.clan.lol/api/v1/repos/clan/clan-community/archive/81e4c9cded645d0384812dd6b8f05bd2475ffe64.tar.gz" }, "original": { - "ref": "fix/dm-pull-deploy-hyphen-hostnames", - "type": "git", - "url": "https://git.clan.lol/dannydannydanny/clan-community.git" + "type": "tarball", + "url": "https://git.clan.lol/clan/clan-community/archive/main.tar.gz" } }, "clan-core": { @@ -40,7 +36,7 @@ "nixpkgs" ], "sops-nix": "sops-nix", - "systems": "systems_2", + "systems": "systems", "treefmt-nix": "treefmt-nix_2" }, "locked": { @@ -182,7 +178,7 @@ }, "flake-utils": { "inputs": { - "systems": "systems_3" + "systems": "systems_2" }, "locked": { "lastModified": 1731533236, @@ -200,7 +196,7 @@ }, "flake-utils_2": { "inputs": { - "systems": "systems_4" + "systems": "systems_3" }, "locked": { "lastModified": 1681202837, @@ -536,21 +532,6 @@ } }, "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_2": { "locked": { "lastModified": 1774449309, "narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=", @@ -566,7 +547,7 @@ "type": "github" } }, - "systems_3": { + "systems_2": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", @@ -581,7 +562,7 @@ "type": "github" } }, - "systems_4": { + "systems_3": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", diff --git a/flake.nix b/flake.nix index 30e7d71..2970510 100644 --- a/flake.nix +++ b/flake.nix @@ -29,10 +29,9 @@ clan-core.inputs.nixpkgs.follows = "nixpkgs"; clan-core.inputs.flake-parts.follows = "flake-parts"; - # clan-community: dm-pull-deploy etc. Pinned to our fork's fix branch - # until clan/clan-community#25 (machine.name hyphen sanitization) lands. - # Swap back to `archive/main.tar.gz` when merged. - clan-community.url = "git+https://git.clan.lol/dannydannydanny/clan-community.git?ref=fix/dm-pull-deploy-hyphen-hostnames"; + # clan-community: dm-pull-deploy etc. Back on upstream main since + # clan/clan-community#25 (machine.name hyphen sanitization) merged. + clan-community.url = "https://git.clan.lol/clan/clan-community/archive/main.tar.gz"; clan-community.inputs.nixpkgs.follows = "nixpkgs"; clan-community.inputs.clan-core.follows = "clan-core"; }; diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 4e71c54..f232e63 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -111,7 +111,7 @@ in # Passwordless sudo for wheel. security.sudo.wheelNeedsPassword = false; environment.systemPackages = with pkgs; [ - git # clone/bootstrap and dotfiles-rebuild timer + git # clone/bootstrap and dm-pull-deploy nodejs # npm for openclaw plugin installs python3 # node-gyp dependency for openclaw plugins wakeonlan # wake rusty-anchor: wakeonlan 00:16:cb:87:20:ba @@ -581,6 +581,7 @@ in }; }; - # Auto-rebuild service/timer + safe.directory provided by the - # shared dotfiles-rebuild NixOS module (see nixos/modules/dotfiles-rebuild.nix). + # Deploys flow through clan dm-pull-deploy: the dm-pull-deploy.path + # watcher rebuilds when sunken-ship announces a new origin/main rev. + # The legacy pull-based dotfiles-rebuild module was retired 2026-05-19. } From cda9c4cf0fa99118e425360ac25476753b62071c Mon Sep 17 00:00:00 2001 From: Danny Date: Sat, 23 May 2026 11:51:20 +0200 Subject: [PATCH 52/72] sunken-ship: drop python-telegram-bot from fitness-bot pythonEnvs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bot.py was deleted upstream — neither prod nor shipyard launches a polling bot anymore. server.py only needs python-dotenv + aiohttp. Also refresh the prod section's comment + service description to reflect the Mini-App-only architecture. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/server-installer-usb.md | 37 +++++++++++++++++++++--- flake-modules/installer-iso.nix | 8 +++-- nixos/hosts/sunken-ship.nix | 16 +++++----- scripts/build-installer-iso-on-server.sh | 35 +++++++++++++++++----- 4 files changed, 75 insertions(+), 21 deletions(-) diff --git a/docs/server-installer-usb.md b/docs/server-installer-usb.md index 4c69d53..295f227 100644 --- a/docs/server-installer-usb.md +++ b/docs/server-installer-usb.md @@ -94,16 +94,45 @@ sudo dd if=result/iso/nixos-minimal-*.iso of=/dev/sdX status=progress bs=4M ## Live-system WiFi (optional, custom ISO only) -Create `nixos/installer-wifi.nix` (gitignored): +The minimal installer ISO runs NetworkManager, so live-system WiFi must be a +declarative NetworkManager profile. `networking.wireless` / wpa_supplicant does +**not** work here — NixOS asserts you cannot combine `networking.networkmanager` +with `networking.wireless.networks`. + +Create `nixos/installer-wifi.nix` (gitignored — it holds the PSK): ```nix { - networking.wireless.enable = true; - networking.wireless.networks."YourSSID".psk = "your-password"; + networking.networkmanager.ensureProfiles.profiles.installer-wifi = { + connection = { + id = "installer-wifi"; + type = "wifi"; + }; + wifi = { + mode = "infrastructure"; + ssid = "YourSSID"; + }; + wifi-security = { + auth-alg = "open"; + key-mgmt = "wpa-psk"; + psk = "your-password"; + }; + ipv4.method = "auto"; + ipv6.method = "auto"; + }; } ``` -Add to flake's installer-iso modules, rebuild ISO on Linux. +`flake-modules/installer-iso.nix` auto-includes this file when present (via a +`builtins.pathExists` check) — no flake edit needed. Because the file is +gitignored, the flake only sees it once it is staged: + +- **`build-installer-iso-on-server.sh`** copies the file to the build host and + runs `git add -f` automatically. +- For a **direct `nix build`**, run `git add -f nixos/installer-wifi.nix` first + (staging is enough — never commit it; it contains the PSK). + +Then rebuild the ISO on Linux. ## Installed-system WiFi (optional) diff --git a/flake-modules/installer-iso.nix b/flake-modules/installer-iso.nix index fc18929..03609ab 100644 --- a/flake-modules/installer-iso.nix +++ b/flake-modules/installer-iso.nix @@ -1,9 +1,13 @@ { inputs, self, ... }: { # Custom minimal installer ISO (build with: nix build .#installer-iso). - # Optional: add ./installer-wifi.nix (gitignored) to modules for live WiFi. + # nixos/installer-wifi.nix (gitignored) is auto-included when present, to + # preconfigure live-system WiFi. See docs/server-installer-usb.md. flake.nixosConfigurations.installer-iso = inputs.nixpkgs.lib.nixosSystem { system = "x86_64-linux"; - modules = [ ../nixos/installer-iso.nix ]; + modules = [ ../nixos/installer-iso.nix ] + ++ inputs.nixpkgs.lib.optional + (builtins.pathExists ../nixos/installer-wifi.nix) + ../nixos/installer-wifi.nix; }; flake.packages.x86_64-linux.installer-iso = diff --git a/nixos/hosts/sunken-ship.nix b/nixos/hosts/sunken-ship.nix index fd14e1b..0ab8f33 100644 --- a/nixos/hosts/sunken-ship.nix +++ b/nixos/hosts/sunken-ship.nix @@ -149,23 +149,26 @@ }; }; - # BigBiggerBiggestBot — Telegram fitness tracker with Mini App. + # BigBiggerBiggestBot — Mini App backend (no Telegram polling). # Code: https://github.com/DannyDannyDanny/bigbiggerbiggestbot cloned at /home/danny/tg_fitness_bot - # Bot token: ~danny/.secrets/bigbiggerbiggestbot + # Bot token (used only for validating Telegram WebApp initData HMACs): + # ~danny/.secrets/bigbiggerbiggestbot # Deployment: fitness-bot-pull timer below runs every 15 min, git pulls, restarts service on changes. # # Mini App URL is fronted by Caddy on the vps-relay host at # https://bbbot.dannydannydanny.me (VPS → ZeroTier → localhost:8080). - # The bot's start.py honors WEBAPP_URL to skip starting its own - # cloudflared Quick Tunnel when we've got a stable URL from the VPS. + # start.py honors WEBAPP_URL to skip starting its own cloudflared + # Quick Tunnel when the stable URL from the VPS is already set. + # + # The slash-command bot (bot.py) was removed in May 2026 — the Mini App + # is now the only interface. No python-telegram-bot dependency required. systemd.services.fitness-bot = let pythonEnv = pkgs.python3.withPackages (ps: with ps; [ - python-telegram-bot python-dotenv aiohttp ]); in { - description = "BigBiggerBiggestBot Telegram fitness tracker"; + description = "BigBiggerBiggestBot Mini App backend"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; @@ -228,7 +231,6 @@ # beta in shipyard_poc_bot's launcher → test → git push :main. systemd.services.fitness-bot-shipyard = let pythonEnv = pkgs.python3.withPackages (ps: with ps; [ - python-telegram-bot python-dotenv aiohttp ]); diff --git a/scripts/build-installer-iso-on-server.sh b/scripts/build-installer-iso-on-server.sh index d969b68..e7bd002 100755 --- a/scripts/build-installer-iso-on-server.sh +++ b/scripts/build-installer-iso-on-server.sh @@ -5,12 +5,17 @@ # host: SSH host (default: sunken-ship) # output_dir: where to save the ISO on your Mac (default: .) # Override SSH key: SSH_KEY=~/.ssh/my_key ./scripts/build-installer-iso-on-server.sh +# +# If nixos/installer-wifi.nix exists locally (gitignored), it is copied into +# the build and the ISO gets preconfigured live-system WiFi. flake-modules/ +# installer-iso.nix auto-includes it via a builtins.pathExists check. set -euo pipefail HOST="${1:-sunken-ship}" OUT="${2:-.}" +REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -# Use sunken-ship key if not set (AGENTS.md) +# Default to the sunken-ship SSH key when targeting that host. if [[ -n "${SSH_KEY:-}" ]]; then SSH_OPTS=(-i "$SSH_KEY") elif [[ "$HOST" == "sunken-ship" ]] && [[ -f ~/.ssh/id_ed25519_sunken_ship ]]; then @@ -19,23 +24,37 @@ else SSH_OPTS=() fi -echo "Pushing branch so server can pull..." -git push origin server-installer-usb 2>/dev/null || true +echo "Pushing main so the server can clone the latest..." +git -C "$REPO_ROOT" push origin main 2>/dev/null || true -echo "On $HOST: clone branch, build ISO..." +echo "On $HOST: clone main into ~/dotfiles-iso-build..." ssh "${SSH_OPTS[@]}" "$HOST" 'set -e BUILD_DIR=~/dotfiles-iso-build rm -rf "$BUILD_DIR" - git clone --branch server-installer-usb https://github.com/DannyDannyDanny/dotfiles.git "$BUILD_DIR" - cd "$BUILD_DIR/nixos" + git clone --branch main https://github.com/DannyDannyDanny/dotfiles.git "$BUILD_DIR" +' + +# Optional live-system WiFi: the module is gitignored, so a fresh clone never +# has it. Copy it in and stage it (git add -f) so the flake sees it -- a flake +# build only includes git-tracked files. +if [[ -f "$REPO_ROOT/nixos/installer-wifi.nix" ]]; then + echo "Found nixos/installer-wifi.nix - including live-system WiFi in the ISO." + scp "${SSH_OPTS[@]}" "$REPO_ROOT/nixos/installer-wifi.nix" \ + "$HOST:dotfiles-iso-build/nixos/installer-wifi.nix" + ssh "${SSH_OPTS[@]}" "$HOST" 'cd ~/dotfiles-iso-build && git add -f nixos/installer-wifi.nix' +fi + +echo "On $HOST: build ISO (flake is at the repo root)..." +ssh "${SSH_OPTS[@]}" "$HOST" 'set -e + cd ~/dotfiles-iso-build nix build .#installer-iso ls -la result/iso/ ' -ISO_NAME=$(ssh "${SSH_OPTS[@]}" "$HOST" 'ls ~/dotfiles-iso-build/nixos/result/iso/*.iso 2>/dev/null | head -1') +ISO_NAME=$(ssh "${SSH_OPTS[@]}" "$HOST" 'ls ~/dotfiles-iso-build/result/iso/*.iso 2>/dev/null | head -1') ISO_NAME=$(basename "$ISO_NAME") echo "Copying $ISO_NAME to $OUT ..." -scp "${SSH_OPTS[@]}" "$HOST:~/dotfiles-iso-build/nixos/result/iso/$ISO_NAME" "$OUT/" +scp "${SSH_OPTS[@]}" "$HOST:dotfiles-iso-build/result/iso/$ISO_NAME" "$OUT/" echo "Done. ISO at $OUT/$ISO_NAME" echo "Write to USB: diskutil unmountDisk diskN && sudo dd if=$OUT/$ISO_NAME of=/dev/rdiskN bs=4m" From 1204584ae4f0346b84545bea07473d13b764a2e0 Mon Sep 17 00:00:00 2001 From: Danny Date: Sat, 23 May 2026 12:01:56 +0200 Subject: [PATCH 53/72] fitness-bot: ExecStartPost runs set-bot-presence.py Re-publishes the bot's menu button + description on every restart so @BBBot's chat experience stays in sync with $WEBAPP_URL. Errors are non-fatal. Co-Authored-By: Claude Opus 4.7 (1M context) --- nixos/hosts/sunken-ship.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nixos/hosts/sunken-ship.nix b/nixos/hosts/sunken-ship.nix index 0ab8f33..f25a4c4 100644 --- a/nixos/hosts/sunken-ship.nix +++ b/nixos/hosts/sunken-ship.nix @@ -162,6 +162,9 @@ # # The slash-command bot (bot.py) was removed in May 2026 — the Mini App # is now the only interface. No python-telegram-bot dependency required. + # ExecStartPost re-publishes the bot's chat-side presence (menu button, + # description, cleared command list) every time the service starts. + # Idempotent against the Telegram API. Errors are non-fatal (`-` prefix). systemd.services.fitness-bot = let pythonEnv = pkgs.python3.withPackages (ps: with ps; [ python-dotenv @@ -179,6 +182,7 @@ serviceConfig = { WorkingDirectory = "/home/danny/tg_fitness_bot"; ExecStart = "${pythonEnv}/bin/python start.py"; + ExecStartPost = "-${pythonEnv}/bin/python scripts/set-bot-presence.py"; Restart = "on-failure"; RestartSec = 10; User = "danny"; From 8fcb43f279c6e6d4c6977781d52a1fd0231e7650 Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Sun, 24 May 2026 10:35:07 +0200 Subject: [PATCH 54/72] sunken-ship: navidrome Scanner.PurgeMissing = missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stops `missing=1` rows accumulating in media_file. After Phase 7 dedupe, Navidrome's watcher minted ~4k track IDs for files briefly present in /home/danny/music/.mulbo-quarantine; after rm -rf'ing the quarantine, those rows stayed flagged-missing forever — and Substreamer's cached queue then hit 500s on every play attempt ("Internal Server Error: open /srv/music/.mulbo-quarantine/...: no such file or directory"). Cleaned the 4135 quarantine rows manually via SQL; this config prevents recurrence. Trade-off: missing rows used to preserve play-history across "file disappeared, came back" cycles. We prefer client-cache hygiene. Co-Authored-By: Claude Opus 4.7 (1M context) --- nixos/hosts/sunken-ship.nix | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nixos/hosts/sunken-ship.nix b/nixos/hosts/sunken-ship.nix index f25a4c4..f6faa05 100644 --- a/nixos/hosts/sunken-ship.nix +++ b/nixos/hosts/sunken-ship.nix @@ -110,6 +110,12 @@ Address = "0.0.0.0"; Port = 4533; MusicFolder = "/srv/music"; + # Auto-delete `missing=1` rows during scan so transient files + # (e.g. mulbo dedupe quarantine ones) don't accumulate as stale + # track IDs that Substreamer caches and then 500s on. Without + # this, Navidrome keeps missing rows forever (default behaviour + # preserves play history; we trade that for client-cache hygiene). + Scanner.PurgeMissing = "missing"; }; }; From b2df891b20fc2ff8559b305d46598ed9018d75ef Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Sun, 24 May 2026 10:35:59 +0200 Subject: [PATCH 55/72] sunken-ship: PurgeMissing = always (valid value; 'missing' was rejected by navidrome 0.61.2) Co-Authored-By: Claude Opus 4.7 (1M context) --- nixos/hosts/sunken-ship.nix | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nixos/hosts/sunken-ship.nix b/nixos/hosts/sunken-ship.nix index f6faa05..ad78f70 100644 --- a/nixos/hosts/sunken-ship.nix +++ b/nixos/hosts/sunken-ship.nix @@ -115,7 +115,10 @@ # track IDs that Substreamer caches and then 500s on. Without # this, Navidrome keeps missing rows forever (default behaviour # preserves play history; we trade that for client-cache hygiene). - Scanner.PurgeMissing = "missing"; + # Valid values: never | always | full. `always` purges on every + # scan (selective + full); risk on transient missing is fine + # here (stable local disk). + Scanner.PurgeMissing = "always"; }; }; From ba51b6bcf724783a007dfa401190c61bb04ed5f8 Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Sun, 24 May 2026 20:48:00 +0200 Subject: [PATCH 56/72] tmux: add resurrect + continuum so force-quits don't nuke sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Twice in the last few sessions a Love2D force-quit cascaded into killing the tmux server and losing every window. Resurrect snapshots windows / panes / cwd / pane contents (with capture-pane-contents on) to ~/.local/share/tmux/resurrect/last. Continuum auto-saves every 15 min and auto-restores on tmux server start — so the next force-quit just costs up to 15 min of recent activity, not the whole workspace. Manual save: prefix + Ctrl-s. Manual restore: prefix + Ctrl-r. Co-Authored-By: Claude Opus 4.7 (1M context) --- nixos/home/danny/home.nix | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/nixos/home/danny/home.nix b/nixos/home/danny/home.nix index c110a83..21c10da 100644 --- a/nixos/home/danny/home.nix +++ b/nixos/home/danny/home.nix @@ -88,6 +88,28 @@ catppuccin tmux-fzf extrakto + # tmux-resurrect: prefix + Ctrl-s saves, prefix + Ctrl-r restores. + # Snapshot lives at ~/.local/share/tmux/resurrect/last (window + # layout, working dirs, pane contents if enabled). Survives + # force-quits / reboots / kernel panics. + { + plugin = resurrect; + extraConfig = '' + set -g @resurrect-capture-pane-contents 'on' + set -g @resurrect-strategy-nvim 'session' + ''; + } + # tmux-continuum: auto-saves every 15min and auto-restores on + # tmux server start. With this, the next force-quit just costs + # you up to 15min of recent terminal activity, not the whole + # workspace. + { + plugin = continuum; + extraConfig = '' + set -g @continuum-restore 'on' + set -g @continuum-save-interval '15' + ''; + } ]; }; From 09d25a1899e8574f5891a0f1b94f940aa20e3cee Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Mon, 25 May 2026 19:17:05 +0200 Subject: [PATCH 57/72] sunken-ship: add mutagen to mulbo-server env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /enrich/revert endpoint shipped in 20_mulbo commit 5d4e9466 calls enrich.write_tags, which imports mutagen. The main mulbo-server's pythonEnv only had fastapi/uvicorn/python-multipart — first revert attempt 500'd with "ModuleNotFoundError: No module named 'mutagen'". (The enrich oneshot has its own env with mutagen; that's why batch enrichment worked.) Co-Authored-By: Claude Opus 4.7 (1M context) --- nixos/hosts/sunken-ship.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nixos/hosts/sunken-ship.nix b/nixos/hosts/sunken-ship.nix index ad78f70..768a0df 100644 --- a/nixos/hosts/sunken-ship.nix +++ b/nixos/hosts/sunken-ship.nix @@ -321,6 +321,8 @@ fastapi uvicorn python-multipart + mutagen # tag writeback (enrich.write_tags); needed by the + # /enrich/revert endpoint which reuses enrich.py. ]); in { description = "Mulbo companion service (uploads, dedup, folders)"; From dc7ef476811728321480d100eb6cc71b22ab38b1 Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Thu, 4 Jun 2026 12:35:22 +0200 Subject: [PATCH 58/72] sunken-ship: add numpy to mulbo-server env For FFT-based spectral-rolloff analysis (quality.py) used by the chromaprint-dupe winner picker. Effective bitrate alone can't tell a real lossless file from a re-encoded-128kbps-MP3-saved-as-WAV; spectral rolloff catches the upsampled fakes (rolloff < 17kHz = came from lossy source). Co-Authored-By: Claude Opus 4.7 (1M context) --- nixos/hosts/sunken-ship.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nixos/hosts/sunken-ship.nix b/nixos/hosts/sunken-ship.nix index 768a0df..f7b99b3 100644 --- a/nixos/hosts/sunken-ship.nix +++ b/nixos/hosts/sunken-ship.nix @@ -323,6 +323,8 @@ python-multipart mutagen # tag writeback (enrich.write_tags); needed by the # /enrich/revert endpoint which reuses enrich.py. + numpy # FFT for spectral-rolloff analysis (quality.py); used + # by chromaprint-dupe winner picker in --spectral mode. ]); in { description = "Mulbo companion service (uploads, dedup, folders)"; From e43a5eb8809b9f889e5ab4987a594ebddc576a7f Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Thu, 4 Jun 2026 12:40:45 +0200 Subject: [PATCH 59/72] sunken-ship: add ffmpeg to mulbo-server PATH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit quality.py's spectral-rolloff probe shells out to ffmpeg to extract a 30s PCM clip. Without ffmpeg on PATH, subprocess fails silently and get_or_compute_rolloff returns 0.0 — picker degrades to bitrate ranking (which is what we were trying to fix). Add ffmpeg via systemd unit `path = with pkgs; [ ffmpeg ];`. Co-Authored-By: Claude Opus 4.7 (1M context) --- nixos/hosts/sunken-ship.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nixos/hosts/sunken-ship.nix b/nixos/hosts/sunken-ship.nix index f7b99b3..c929d84 100644 --- a/nixos/hosts/sunken-ship.nix +++ b/nixos/hosts/sunken-ship.nix @@ -331,6 +331,10 @@ after = [ "network-online.target" "navidrome.service" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; + # ffmpeg: PCM extraction for quality.py's spectral-rolloff probe + # (chromaprint-dupe winner picker in --spectral mode). Without it, + # the subprocess silently fails and rolloff returns 0Hz. + path = with pkgs; [ ffmpeg ]; environment = { MULBO_UPLOADS_DIR = "/home/danny/music/mulbo-uploads"; MULBO_INDEX_DB = "/var/lib/mulbo-server/index.db"; From 9283643e07b89525cbcd44573f5c53c45e89f727 Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Fri, 5 Jun 2026 17:18:57 +0200 Subject: [PATCH 60/72] =?UTF-8?q?feat(fish):=20add=20gco=20=E2=80=94=20sma?= =?UTF-8?q?rt=20checkout=20that=20cds=20into=20worktrees=20=F0=9F=8C=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If the target branch is already checked out in another worktree, `gco ` cds there instead of erroring with "already used by worktree at". Falls through to plain `git checkout` otherwise. --- assets/zed/settings.json | 53 ++++++++++++++++++++++++++++++++++++++++ nixos/fish.nix | 32 ++++++++++++++++++++++++ nixos/neovim.nix | 35 ++++++++++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 assets/zed/settings.json diff --git a/assets/zed/settings.json b/assets/zed/settings.json new file mode 100644 index 0000000..e48db6f --- /dev/null +++ b/assets/zed/settings.json @@ -0,0 +1,53 @@ +// Zed settings — tracked in dotfiles, symlinked into ~/.config/zed/settings.json +// by home-manager (xdg.configFile in nixos/home/danny/home.nix). +// +// Because this is a symlink to a nix-store file, editing it from inside Zed +// will fail (read-only). Edit THIS file in dotfiles, commit, and rebuild +// (`darwin-rebuild switch --flake .`). To see Zed's full default settings, +// run `zed: open default settings` from the command palette. +{ + "sticky_scroll": { + "enabled": true + }, + "edit_predictions": { + "provider": "ollama" + }, + "buffer_font_family": "JetBrains Mono", + "cli_default_open_behavior": "existing_window", + "project_panel": { + "dock": "left" + }, + "outline_panel": { + "dock": "left" + }, + "collaboration_panel": { + "dock": "left" + }, + "git_panel": { + "dock": "left" + }, + "agent": { + "dock": "right", + "default_model": { + "provider": "ollama", + "model": "llama3.2:latest" + } + }, + "disable_ai": false, + "minimap": { + "show": "auto" + }, + "telemetry": { + "diagnostics": false, + "metrics": false + }, + "base_keymap": "VSCode", + "vim_mode": true, + "ui_font_size": 16, + "buffer_font_size": 15, + "theme": { + "mode": "system", + "light": "One Light", + "dark": "One Dark" + } +} diff --git a/nixos/fish.nix b/nixos/fish.nix index 9d04f51..1d72d26 100644 --- a/nixos/fish.nix +++ b/nixos/fish.nix @@ -24,6 +24,38 @@ set fish_greeting 🐟: (set_color yellow; date +%T; set_color green; date --iso-8601 2>/dev/null; or date +%F; set_color normal) + # gco: smart `git checkout` — if the branch is checked out in another + # worktree, cd there instead of failing with "already used by worktree at". + function gco --description 'git checkout, but cd into worktree if the branch lives there' + if test (count $argv) -eq 0 + git checkout + return $status + end + + set -l branch $argv[1] + set -l target_ref "refs/heads/$branch" + set -l wt_path "" + set -l current_wt "" + for line in (git worktree list --porcelain 2>/dev/null) + switch $line + case 'worktree *' + set current_wt (string replace -r '^worktree ' "" -- $line) + case "branch $target_ref" + set wt_path $current_wt + break + end + end + + set -l here (git rev-parse --show-toplevel 2>/dev/null) + if test -n "$wt_path"; and test "$wt_path" != "$here" + echo "→ cd $wt_path (branch '$branch' is checked out in another worktree)" + cd $wt_path + return $status + end + + git checkout $argv + end + # Alacritty palette follows macOS appearance; refresh when opening a shell (LaunchAgent also polls). if test (uname -s) = Darwin bash ~/dotfiles/scripts/alacritty-sync-system-theme.sh >/dev/null 2>&1 & diff --git a/nixos/neovim.nix b/nixos/neovim.nix index 75b2335..51ae100 100644 --- a/nixos/neovim.nix +++ b/nixos/neovim.nix @@ -58,6 +58,39 @@ end, }) + -- Treesitter highlighting: parser-driven syntax highlighting (richer + -- than the regex-based default). Leaving `indent` off — it's still + -- buggy in several languages (python, yaml). + require'nvim-treesitter.configs'.setup { + highlight = { enable = true }, + } + + -- Sticky scroll: pin enclosing scopes (functions, classes, YAML keys, + -- etc.) to the top of the window as you scroll deeper. Same idea as + -- Zed/VS Code's "Sticky Scroll". `mode = 'topline'` matches Zed's + -- "scrolled past" feel; switch to 'cursor' if you'd rather it track + -- the cursor instead of the viewport. + require'treesitter-context'.setup { + enable = true, + max_lines = 5, + mode = 'topline', + trim_scope = 'outer', + } + + -- Fish: expand tabs to spaces. Fish renders raw \t in the commandline + -- as the Unicode glyph ␉ (U+2409) and wrap-indents each line to the + -- column of the opening quote, which mangles Alt-E multiline edits. + -- Using spaces sidesteps the issue entirely. + vim.api.nvim_create_autocmd("FileType", { + pattern = "fish", + callback = function() + vim.opt_local.expandtab = true + vim.opt_local.tabstop = 2 + vim.opt_local.shiftwidth = 2 + vim.opt_local.softtabstop = 2 + end, + }) + -- Keymaps vim.keymap.set("n", "S", ":%s//g", { desc = "Replace all" }) vim.keymap.set("n", "w", ":w", { desc = "Save file" }) @@ -73,6 +106,8 @@ catppuccin-nvim # theme goyo-vim # write prose limelight-vim # prose paragraph highlighter + nvim-treesitter.withAllGrammars # parsers (also makes vim.treesitter.foldexpr work for markdown) + nvim-treesitter-context # sticky scroll: pin parent scopes at top of window ]; }; } From 592e989b03465ed413910d272efdca5804392ab7 Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Fri, 5 Jun 2026 17:19:38 +0200 Subject: [PATCH 61/72] =?UTF-8?q?fix(home):=20resurrect=20process=20list?= =?UTF-8?q?=20+=20track=20zed=20settings=20in=20dotfiles=20=F0=9F=8F=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tmux-resurrect only restores programs in its allow-list; nvim/claude/ssh were missing so restored panes came back as bare fish prompts. Adds the three programs with argv-aware restart patterns. Also wires ~/.config/zed/settings.json as an xdg.configFile symlink so Zed config survives machine rebuilds alongside the rest of dotfiles. --- nixos/home/danny/home.nix | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/nixos/home/danny/home.nix b/nixos/home/danny/home.nix index 21c10da..b57def5 100644 --- a/nixos/home/danny/home.nix +++ b/nixos/home/danny/home.nix @@ -89,14 +89,21 @@ tmux-fzf extrakto # tmux-resurrect: prefix + Ctrl-s saves, prefix + Ctrl-r restores. - # Snapshot lives at ~/.local/share/tmux/resurrect/last (window - # layout, working dirs, pane contents if enabled). Survives - # force-quits / reboots / kernel panics. + # Snapshot lives at ~/.tmux/resurrect/last (window layout, working + # dirs, pane contents if enabled). Survives force-quits / reboots + # / kernel panics. + # + # @resurrect-processes: programs to restart on restore. Default + # list covers vim/emacs/less/top/etc. but NOT nvim, claude, or + # ssh. The "~name->cmd" form re-runs the original argv; bare + # names match argv-less invocations. Without this, restored panes + # come back as plain fish prompts in the right directory. { plugin = resurrect; extraConfig = '' set -g @resurrect-capture-pane-contents 'on' set -g @resurrect-strategy-nvim 'session' + set -g @resurrect-processes 'nvim "~nvim->nvim *" claude "~claude->claude --continue" ssh "~ssh->ssh *"' ''; } # tmux-continuum: auto-saves every 15min and auto-restores on @@ -164,6 +171,11 @@ xdg.configFile."alacritty/catppuccin-mocha-colors.toml".source = ../../../assets/alacritty/catppuccin-mocha-colors.toml; + # Zed: settings.json is a read-only symlink to assets/zed/settings.json. + # To change a setting, edit the asset file and rebuild — editing via Zed's + # UI will fail because the target is in the nix store. + xdg.configFile."zed/settings.json".source = ../../../assets/zed/settings.json; + # Alacritty: base config + imported active-colors.toml (updated without rebuild) programs.alacritty = { enable = true; From 680c20483ca4b68e1893a297fe632d60e9e80479 Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Sun, 7 Jun 2026 13:05:55 +0200 Subject: [PATCH 62/72] feat: add map.dannydannydanny.me vhost :world_map: Curated-architecture world map by Kyranna, served by the same notes service on phantom :8092 (routed by Host header, MAP_HOST). Mirrors the kf vhost. Co-Authored-By: Claude Opus 4.8 --- nixos/hosts/vps-relay.nix | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nixos/hosts/vps-relay.nix b/nixos/hosts/vps-relay.nix index 85ed329..9f8c3f5 100644 --- a/nixos/hosts/vps-relay.nix +++ b/nixos/hosts/vps-relay.nix @@ -155,6 +155,11 @@ "kf.dannydannydanny.me".extraConfig = '' reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8092 ''; + # map — curated-architecture world map by Kyranna. Same notes + # service on phantom :8092, routed by Host header (MAP_HOST). + "map.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8092 + ''; }; }; From cc8cc05a08364c230a723ac8667b77d44fa78fdd Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Sun, 7 Jun 2026 13:16:38 +0200 Subject: [PATCH 63/72] phantom-ship/shipyard: add media-processing tools for feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feedback now accepts photos, voice notes, video, documents etc. Phase A captures + stores raw files (Pillow for EXIF strip); Phase B derives OCR text, speech transcripts, poster frames, PDF text — all via subprocess so each tool degrades gracefully if absent. Wire the following into the shipyard service: - python3Packages.pillow → EXIF strip on captured photos - ffmpeg → poster frames + audio→16kHz WAV for whisper - tesseract (eng + rus) → OCR (vyscul writes in Russian) - whisper-cpp → speech-to-text for voice / audio / video - poppler_utils → pdftotext for document attachments Co-Authored-By: Claude Opus 4.7 (1M context) --- flake-modules/distant-shore.nix | 14 +++ nixos/disko-distant-shore.nix | 37 ++++++++ nixos/hosts/distant-shore-hardware.nix | 18 ++++ nixos/hosts/distant-shore.nix | 114 +++++++++++++++++++++++++ nixos/hosts/phantom-ship.nix | 23 ++++- 5 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 flake-modules/distant-shore.nix create mode 100644 nixos/disko-distant-shore.nix create mode 100644 nixos/hosts/distant-shore-hardware.nix create mode 100644 nixos/hosts/distant-shore.nix diff --git a/flake-modules/distant-shore.nix b/flake-modules/distant-shore.nix new file mode 100644 index 0000000..6c5a023 --- /dev/null +++ b/flake-modules/distant-shore.nix @@ -0,0 +1,14 @@ +# Standalone nixosSystem registration for distant-shore. +# Temporary: clan integration (zerotier/data-mesher/dm-pull-deploy) needs +# vars generated via sops on the admin machine. Until that runs, this +# keeps the box buildable without clan deps. Delete this file when +# distant-shore moves into flake-modules/clan.nix. +{ inputs, ... }: { + flake.nixosConfigurations.distant-shore = inputs.nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + inputs.disko.nixosModules.disko + ../nixos/hosts/distant-shore.nix + ]; + }; +} diff --git a/nixos/disko-distant-shore.nix b/nixos/disko-distant-shore.nix new file mode 100644 index 0000000..ab35aac --- /dev/null +++ b/nixos/disko-distant-shore.nix @@ -0,0 +1,37 @@ +# Declarative disk layout for distant-shore (ThinkPad X13 Gen 2 — 256 GB +# SK Hynix NVMe). UEFI/systemd-boot, no encryption: it's a headless, +# WiFi-only server that must reboot unattended (clan dm-pull-deploy), so +# a LUKS passphrase prompt at boot would hang it. Mirrors sunken-ship's +# plain-ext4 choice. Device is wiped + repartitioned at install time by +# clan/nixos-anywhere. +{ + disko.devices = { + disk.main = { + type = "disk"; + device = "/dev/nvme0n1"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "fmask=0022" "dmask=0022" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; +} diff --git a/nixos/hosts/distant-shore-hardware.nix b/nixos/hosts/distant-shore-hardware.nix new file mode 100644 index 0000000..3c52633 --- /dev/null +++ b/nixos/hosts/distant-shore-hardware.nix @@ -0,0 +1,18 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = + [ (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ "xhci_pci" "thunderbolt" "nvme" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-intel" ]; + boot.extraModulePackages = [ ]; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/nixos/hosts/distant-shore.nix b/nixos/hosts/distant-shore.nix new file mode 100644 index 0000000..33a7026 --- /dev/null +++ b/nixos/hosts/distant-shore.nix @@ -0,0 +1,114 @@ +# NixOS server on a ThinkPad X13 Gen 2 (Intel i5-1145G7, 16 GB). +# WiFi-only, headless, unattended auto-rebuild via clan dm-pull-deploy. +# No LUKS (mirrors sunken-ship) so reboots don't block on a passphrase. +# +# Blank-slate server for now — no application services. Give it a purpose +# later (just add services here and let dm-pull-deploy roll it out). +{ config, lib, pkgs, ... }: + +{ + imports = [ + ./distant-shore-hardware.nix + ../disko-distant-shore.nix + ]; + + boot.loader.systemd-boot.enable = true; + # Secure Boot is enforced and the BIOS supervisor password is unknown, so + # we can't enrol our own SB keys. Instead, shim (MS-signed) is placed on + # the ESP and chain-loads systemd-boot; the NVRAM boot entry points at + # shim. We manage that entry imperatively via efibootmgr; letting bootctl + # touch EFI variables would replace it on every rebuild. + boot.loader.efi.canTouchEfiVariables = false; + boot.loader.efi.efiSysMountPoint = "/boot"; # matches disko ESP mountpoint + + # --- Secure Boot via shim + MOK (no firmware key enrolment possible) ------ + # The firmware trusts Microsoft-signed shim; shim trusts our enrolled MOK. + # On every bootloader install we: (1) sign systemd-boot with the MOK and + # drop it where shim chain-loads it (grubx64.efi), (2) install shim as the + # firmware-booted binary (+ MokManager), (3) MOK-sign every kernel image + # systemd-boot will hand off to (shim verifies them via the shim-lock + # protocol). Re-runs on each nixos-rebuild so auto-deployed generations + # stay bootable. Keys + shim live in /etc/secrets (outside the repo). + boot.loader.systemd-boot.extraInstallCommands = '' + # NixOS's bootloader-install systemd unit runs with a minimal PATH that + # doesn't include coreutils, so use absolute paths for cp/mv. + KEY=/etc/secrets/MOK.key + CRT=/etc/secrets/MOK.crt + sb() { ${pkgs.sbsigntool}/bin/sbsign --key "$KEY" --cert "$CRT" --output "$2" "$1"; } + # systemd-boot -> shim's chain-load target + sb /boot/EFI/systemd/systemd-bootx64.efi /boot/EFI/BOOT/grubx64.efi + # shim (MS-signed) is what the firmware boots; MokManager beside it + ${pkgs.coreutils}/bin/cp -f /etc/secrets/shimx64.efi /boot/EFI/BOOT/BOOTX64.EFI + ${pkgs.coreutils}/bin/cp -f /etc/secrets/mmx64.efi /boot/EFI/BOOT/mmx64.efi + # MOK-sign each kernel (skip already-signed; never touch initrds) + for k in /boot/EFI/nixos/*bzImage.efi; do + [ -e "$k" ] || continue + if ! ${pkgs.sbsigntool}/bin/sbverify --cert "$CRT" "$k" >/dev/null 2>&1; then + ${pkgs.sbsigntool}/bin/sbsign --key "$KEY" --cert "$CRT" --output "$k.tmp" "$k" \ + && ${pkgs.coreutils}/bin/mv -f "$k.tmp" "$k" + fi + done + ''; + + networking.hostName = "distant-shore"; + # WiFi via NetworkManager. The wpa_supplicant stack hit two issues on this + # box: (1) it strips CAP_CHOWN so wpa couldn't create its ctrl_interface, + # and (2) dhcpcd didn't grab a lease after the (late) association at boot, + # needing a manual restart — fatal for an unattended headless server. NM + # handles association + DHCP atomically and connected first-try here. + # The PSK stays out of the repo: it's substituted from /etc/secrets/nm.env + # ($PSK_INTENO) into the declared profile at activation. + networking.networkmanager.enable = true; + networking.networkmanager.ensureProfiles.environmentFiles = [ "/etc/secrets/nm.env" ]; + networking.networkmanager.ensureProfiles.profiles."Inteno-89FE" = { + connection = { id = "Inteno-89FE"; type = "wifi"; autoconnect = true; }; + wifi = { ssid = "Inteno-89FE"; mode = "infrastructure"; }; + wifi-security = { key-mgmt = "wpa-psk"; psk = "$PSK_INTENO"; }; + ipv4.method = "auto"; + ipv6.method = "auto"; + }; + hardware.enableRedistributableFirmware = true; # iwlwifi for the Intel AX201 WiFi + time.timeZone = "Europe/Copenhagen"; + + # It's a laptop acting as a server: keep running with the lid shut. + services.logind.settings.Login.HandleLidSwitch = "ignore"; + services.logind.settings.Login.HandleLidSwitchExternalPower = "ignore"; + + # Reduce screen burn-in / power: blank the TTY after a minute. + boot.kernelParams = [ "consoleblank=60" ]; + + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + programs.nix-ld.enable = true; # run dynamically linked binaries (e.g. Claude Code remote CLI) + nix.settings.trusted-users = [ "root" "danny" ]; + system.stateVersion = "25.11"; + + users.users.danny = { + isNormalUser = true; + extraGroups = [ "wheel" "video" "audio" ]; + openssh.authorizedKeys.keys = [ + # Mac admin / fleet key (~/.ssh/id_ed25519_sunken_ship) — the key the + # Mac uses to reach the fleet; clan machines update relies on it. + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKW/akfIiVU5o63YrTAJVZhMj7kXfYHOnXDtlpVFW7pf danny@mac-admin" + # Per-host key (~/.ssh/id_ed25519_distant_shore) — plain `ssh distant-shore`. + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH61JOiOOPrAXekakAwTJg5yCSDfOIjlSvMYkpXrarAf distant-shore" + # sunken-ship (dm-pull-deploy push node) — reach distant-shore over ZT. + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB9t4YAaoHvVouqp+qyFOq8o3SAtXMiAmjF6J0ldyx4g danny@sunken-ship" + ]; + }; + users.users.root.openssh.authorizedKeys.keys = + config.users.users.danny.openssh.authorizedKeys.keys; + + services.openssh = { + enable = true; + settings = { + PasswordAuthentication = false; + KbdInteractiveAuthentication = false; + }; + }; + + security.sudo.wheelNeedsPassword = false; + + # mokutil — manage MOK enrolment for the shim chain; sbsigntool — inspect + # signatures on bootloader/kernel images when debugging. + environment.systemPackages = with pkgs; [ git mokutil sbsigntool ]; +} diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index f232e63..1386fd3 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -241,18 +241,37 @@ in # Code deployed out-of-band via rsync to /home/danny/shipyard/ # (staying in-tree in ~/python-projects/26_shipyard/ until spun out to its own repo). # Bot token (not in repo): ~danny/.secrets/telegram-bot-token-shipyard - # Data (feedback.jsonl, pointer cache): ~danny/.local/share/shipyard/ + # Data (feedback.jsonl, feedback.db, pointer cache, feedback_media/): + # ~danny/.local/share/shipyard/ + # + # Feedback now accepts photos / voice / video / docs / stickers etc. + # Phase A captures + stores raw files; Phase B derives OCR text + # (tesseract), speech transcripts (whisper-cpp), poster frames + # (ffmpeg) and PDF text (pdftotext) — all via subprocess, so each + # tool degrades gracefully if missing. systemd.services.shipyard = let pythonEnv = pkgs.python3.withPackages (ps: with ps; [ python-telegram-bot httpx + pillow # EXIF strip on captured photos ]); + # tesseract with English + Russian tessdata — vyscul writes in + # Russian, screenshots can land in either language. + tesseractWithLangs = pkgs.tesseract.override { + enableLanguages = [ "eng" "rus" ]; + }; in { description = "Shipyard Telegram bot (mini-app launcher + feedback)"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; - path = [ pythonEnv ]; + path = [ + pythonEnv + pkgs.ffmpeg # video/animation posters, sticker decode + tesseractWithLangs # photo OCR + pkgs.whisper-cpp # voice/audio transcription + pkgs.poppler_utils # pdftotext (document handling) + ]; environment = { SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; # Owner-only commands (/admin, /grant, /revoke) — anyone else gets ignored. From 05896f6d3bd75d8005f904a5b52d9bea47276faf Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Sun, 7 Jun 2026 13:17:33 +0200 Subject: [PATCH 64/72] =?UTF-8?q?phantom-ship/shipyard:=20rename=20poppler?= =?UTF-8?q?=5Futils=20=E2=86=92=20poppler-utils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nixpkgs renamed it; the old attr is now an error alias. Co-Authored-By: Claude Opus 4.7 (1M context) --- nixos/hosts/phantom-ship.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 1386fd3..e018a73 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -270,7 +270,7 @@ in pkgs.ffmpeg # video/animation posters, sticker decode tesseractWithLangs # photo OCR pkgs.whisper-cpp # voice/audio transcription - pkgs.poppler_utils # pdftotext (document handling) + pkgs.poppler-utils # pdftotext (document handling) ]; environment = { SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; From 09f191d10b9b48b31c4353a350747478ccf24abe Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Sun, 7 Jun 2026 15:25:16 +0200 Subject: [PATCH 65/72] feat: add studio.dannydannydanny.me vhost :art: Kyranna's private art-learning archive ("Studio"), served by the same notes service on phantom :8092 (routed by Host header, STUDIO_HOST). Mirrors the map/kf vhosts. Co-Authored-By: Claude Opus 4.8 --- nixos/hosts/vps-relay.nix | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nixos/hosts/vps-relay.nix b/nixos/hosts/vps-relay.nix index 9f8c3f5..3387aa5 100644 --- a/nixos/hosts/vps-relay.nix +++ b/nixos/hosts/vps-relay.nix @@ -160,6 +160,11 @@ "map.dannydannydanny.me".extraConfig = '' reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8092 ''; + # studio — Kyranna's private art-learning archive. Same notes + # service on phantom :8092, routed by Host header (STUDIO_HOST). + "studio.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8092 + ''; }; }; From bbe05c971d7a2e094ec9be6952d999b9e31c34d3 Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Sat, 6 Jun 2026 16:23:09 +0200 Subject: [PATCH 66/72] =?UTF-8?q?feat(distant-shore):=20add=20X13=20Gen=20?= =?UTF-8?q?2=20as=20clan=20machine=20w/=20shim+MOK=20secure=20boot=20?= =?UTF-8?q?=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ThinkPad X13 Gen 2, BIOS-locked + Secure Boot enforced. Boots NixOS via Microsoft-signed shim chain-loading MOK-signed systemd-boot + kernel (re-signed each rebuild). WiFi via NetworkManager. Migrated from the standalone install module into clan (zerotier/data-mesher/dm-pull-deploy). --- flake-modules/clan.nix | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/flake-modules/clan.nix b/flake-modules/clan.nix index 30fe4c9..3b9b45c 100644 --- a/flake-modules/clan.nix +++ b/flake-modules/clan.nix @@ -47,6 +47,7 @@ in { inventory.machines.sunken-ship = { }; inventory.machines.phantom-ship = { }; inventory.machines.vps-relay = { }; + inventory.machines.distant-shore = { }; # ZeroTier mesh VPN. sunken-ship is the controller (manages network # membership); phantom-ship is a peer. The mac joins manually as an @@ -58,6 +59,7 @@ in { roles.peer.machines.phantom-ship = { }; roles.peer.machines.sunken-ship = { }; roles.peer.machines.vps-relay = { }; + roles.peer.machines.distant-shore = { }; }; # data-mesher — signed-file gossip protocol over libp2p (port 7946). @@ -70,6 +72,7 @@ in { module.input = "clan-core"; roles.default.machines.sunken-ship = { }; roles.default.machines.phantom-ship = { }; + roles.default.machines.distant-shore = { }; roles.bootstrap.machines.sunken-ship = { }; }; @@ -87,6 +90,7 @@ in { }; roles.default.machines.sunken-ship.settings.action = "switch"; roles.default.machines.phantom-ship.settings.action = "switch"; + roles.default.machines.distant-shore.settings.action = "switch"; }; # `clan machines update` connection target. Priority 2000 > ZT's 900 @@ -111,6 +115,12 @@ in { host = "89.167.39.251"; user = "danny"; }; + # distant-shore: LAN IP for the first update (not yet on ZT). Swap to + # its generated ZT IPv6 after it joins the mesh, like the others. + roles.default.machines.distant-shore.settings = { + host = "192.168.1.182"; + user = "danny"; + }; }; # Preserve current network / init stack (no systemd-networkd/resolved, @@ -157,6 +167,29 @@ in { ]; }; + # distant-shore — ThinkPad X13 Gen 2, WiFi, Secure Boot via shim+MOK + # (installed standalone, then migrated into clan). targetHost is the LAN + # IP for the first `clan machines update`; switch to its ZT IPv6 once the + # mesh is up. Builds on the box itself (it has nix + internet). + machines.distant-shore = { + imports = [ + { + clan.core.enableRecommendedDefaults = false; + clan.core.networking.targetHost = "danny@192.168.1.182"; + clan.core.networking.buildHost = "danny@192.168.1.182"; + } + clanHostsModule + ../nixos/hosts/distant-shore.nix + config.flake.nixosModules.monitoring-node-exporter + inputs.home-manager.nixosModules.home-manager + (hmModule { + user = "danny"; + homeDirectory = "/home/danny"; + stateVersion = "25.11"; + }) + ]; + }; + machines.phantom-ship = { imports = [ { From df18b1cfaf68bfff265798816196b9b0cfefad85 Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Sun, 7 Jun 2026 18:38:30 +0200 Subject: [PATCH 67/72] =?UTF-8?q?feat(distant-shore):=20generate=20clan=20?= =?UTF-8?q?vars=20(zerotier/data-mesher/dm-pull-deploy)=20+=20ZT=20host=20?= =?UTF-8?q?entry=20=F0=9F=94=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- flake-modules/clan.nix | 2 ++ sops/machines/distant-shore/key.json | 6 ++++++ sops/secrets/distant-shore-age.key/secret | 14 ++++++++++++++ sops/secrets/distant-shore-age.key/users/danny | 1 + .../identity.cert/machines/distant-shore | 1 + .../identity.cert/secret | 18 ++++++++++++++++++ .../identity.cert/users/danny | 1 + .../identity.key/machines/distant-shore | 1 + .../identity.key/secret | 18 ++++++++++++++++++ .../identity.key/users/danny | 1 + .../identity.pub/value | 3 +++ .../data-mesher-node-identity/peer.id/value | 1 + .../signing.key/machines/distant-shore | 1 + .../signing.key/secret | 18 ++++++++++++++++++ .../signing.key/users/danny | 1 + .../signing.pub/value | 3 +++ .../machines/distant-shore | 1 + .../zerotier/zerotier-identity-secret/secret | 18 ++++++++++++++++++ .../zerotier-identity-secret/users/danny | 1 + .../distant-shore/zerotier/zerotier-ip/value | 1 + 20 files changed, 111 insertions(+) create mode 100755 sops/machines/distant-shore/key.json create mode 100644 sops/secrets/distant-shore-age.key/secret create mode 120000 sops/secrets/distant-shore-age.key/users/danny create mode 120000 vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/machines/distant-shore create mode 100644 vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/secret create mode 120000 vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/users/danny create mode 120000 vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/machines/distant-shore create mode 100644 vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/secret create mode 120000 vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/users/danny create mode 100644 vars/per-machine/distant-shore/data-mesher-node-identity/identity.pub/value create mode 100644 vars/per-machine/distant-shore/data-mesher-node-identity/peer.id/value create mode 120000 vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/machines/distant-shore create mode 100644 vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/secret create mode 120000 vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/users/danny create mode 100644 vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.pub/value create mode 120000 vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/machines/distant-shore create mode 100644 vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/secret create mode 120000 vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/users/danny create mode 100644 vars/per-machine/distant-shore/zerotier/zerotier-ip/value diff --git a/flake-modules/clan.nix b/flake-modules/clan.nix index 3b9b45c..2e974d5 100644 --- a/flake-modules/clan.nix +++ b/flake-modules/clan.nix @@ -21,6 +21,7 @@ let sunkenShipZTv6 = "fdd5:53a2:de33:d269:6499:93d5:53a2:de33"; phantomShipZTv6 = "fdd5:53a2:de33:d269:6499:936c:48a:bbdc"; vpsRelayZTv6 = "fdd5:53a2:de33:d269:6499:9305:339f:2ed3"; + distantShoreZTv6 = "fdd5:53a2:de33:d269:6499:93b6:ef1a:c3b3"; # Shared across both servers: /etc/hosts entries so data-mesher's # libp2p /dns/.clan/... bootstrap multiaddrs resolve over ZT. @@ -29,6 +30,7 @@ let "${sunkenShipZTv6}" = [ "sunken-ship.clan" ]; "${phantomShipZTv6}" = [ "phantom-ship.clan" ]; "${vpsRelayZTv6}" = [ "vps-relay.clan" ]; + "${distantShoreZTv6}" = [ "distant-shore.clan" ]; }; }; in { diff --git a/sops/machines/distant-shore/key.json b/sops/machines/distant-shore/key.json new file mode 100755 index 0000000..f580056 --- /dev/null +++ b/sops/machines/distant-shore/key.json @@ -0,0 +1,6 @@ +[ + { + "publickey": "age1hjhqyuvcjuh62xh9m5ek3aa2rluaz8c28hgh2pm435jkqtpry9ssdn2l0z", + "type": "age" + } +] \ No newline at end of file diff --git a/sops/secrets/distant-shore-age.key/secret b/sops/secrets/distant-shore-age.key/secret new file mode 100644 index 0000000..27a5780 --- /dev/null +++ b/sops/secrets/distant-shore-age.key/secret @@ -0,0 +1,14 @@ +{ + "data": "ENC[AES256_GCM,data:WTerGWNmve9/q+TLYi8HoGUQI0UgYMZN2zuC/FABX0MC6VuUsz9Doz36X8lsy+MRJzcHNPqdaHmAHopY/hODHLBirfUPLVZjEKI=,iv:ilp+cJivxY2us1jO85dWUHAqLJSsJ7ZKpmYMyi2476I=,tag:H0k2CZDhcH9lYSxz6BAPrg==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZS2ZhQlJacmR4R3JMek5l\nZlFRajM4VllmK2R6NFlRMEkwNUJOL05OUUhzCmpWQ0gxQ1BHUkZOVm80QzRUc1BY\nTDNRZDZOL3EyS1FWK1A4UUd6MTFaTjAKLS0tIEUxU3hBSkZqRmc5d0dXZm0rNTYw\nQ0hrZUF5dDJLN0MvM2RQZlVFZkNPc28Kvq8yV+VwqQIuG1SPI/mMYbGwuD7oUOeR\nCzAuZvqGtludjW7+wX5uIwRzHMudU/yP/iME8vsDC3dL6sf75+arHg==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-07T16:36:36Z", + "mac": "ENC[AES256_GCM,data:g35f5YmoneVewxmTh3E8ECDGAl0OwUj4v/2bjFs9Dd7MaT3in7PHvu30jJ4WHalYC8pkT5IlpBwsp1nVUnKsgh+2V+jN4JiGizlvTwByaYoalOoGZStIyQa+k8XRQqoUDbV3ESdI5q+dwS5PCWYIOH3MoA0o5b42iQPghrViaeY=,iv:v0UUy4LtQ5SRLB01vbcfNpcm8zgs1Vp3KCK552peXlA=,tag:45b8czXYtNh02q7P42FJmQ==,type:str]", + "version": "3.12.2" + } +} diff --git a/sops/secrets/distant-shore-age.key/users/danny b/sops/secrets/distant-shore-age.key/users/danny new file mode 120000 index 0000000..215639b --- /dev/null +++ b/sops/secrets/distant-shore-age.key/users/danny @@ -0,0 +1 @@ +../../../users/danny \ No newline at end of file diff --git a/vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/machines/distant-shore b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/machines/distant-shore new file mode 120000 index 0000000..2f4e8ad --- /dev/null +++ b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/machines/distant-shore @@ -0,0 +1 @@ +../../../../../../sops/machines/distant-shore \ No newline at end of file diff --git a/vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/secret b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/secret new file mode 100644 index 0000000..58ead86 --- /dev/null +++ b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/secret @@ -0,0 +1,18 @@ +{ + "data": "ENC[AES256_GCM,data:esTlopK7VkLLnWvxsLoZtAGgbYKWKfu0XJde2fzxDuOaf9yUCU6NHpnyRAZnChceEZ3frwS7Lh/LWqX9CTKQ1LHTV8HrJERSERDzrQDHbIXFLtDbeF+qN7M1wYFEwCUa8PVAg4XHMN/ZGy6H71+J8UrktcbxcHUr+8L3pj4DZb5930kT3U02rzSoan8zb4zMhGqA0keq9QJ04uNJEN2Bly1kCBvdgc7kVUBNHwS78s+jfsa3PyOiLy5AI4CEbQ5r/xBjNgY/aSEOzRMoZtVWUFlh5Kxc47gz7MlK2x/2iXyCIAw3qeTIxor30GIL,iv:QbSPukR5aMrhBfYOM6lOb0qSEPm4oEqqQp59WDv0p6Y=,tag:KrMyGleLjIhT1LTlS3S63g==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyWlpRS1hhQThqaFUyNE4y\nYU1YVDFtazZnSHpTOWRFQkZYVThsRk9RQ1RZCnI4ZlFacTRRSHlub2hWVTNSSkhN\ndWExR202RG1nZ2dQTzQ5LzBNNW1kc2sKLS0tIHZlZXNhSm9wdElTZzRXZjQxaDAx\nRXpvcEkwK3dMNHZ2M21OSFluWnhDOFEKv0/yC/Llmhsm3+kV3AUJ2PPF817rOyL5\n6GkqTrb/gB8q8jnQabDr2sHUz7AB4w7zlQaNLRSo3Ba8KFbW7GZNRg==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1hjhqyuvcjuh62xh9m5ek3aa2rluaz8c28hgh2pm435jkqtpry9ssdn2l0z", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvcEdVdTBsUThiOE1EVWZW\nYXh2NUNCZTVieUZKbjByY2dSZVU2c0Q4L2hBCm1mNzVrcTRTTFpUTkJDZlArYTBZ\nWXhEMERmd1J3VTYxa2dWTlFxOW45N00KLS0tIHNLVzRCdDJGdWk2K0JoY1dJbzIz\nQU9DR2tXU3l2aU9YMGd1RjBGbUJYM0UKYmdAj535wvaGxN6m2VBBVtWRAD5RzQ7K\nbiJjvf8NH4A0aO9RVTFCevqRXUOKBu7jNIpFFfEyUEGHEUCWOuVSlA==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-07T16:36:36Z", + "mac": "ENC[AES256_GCM,data:QVkNUsAO6BsVoPAL5GG1/DProapF8ryaUGDr8Y8mYPpD1Y2YXAF2sBRJ4FWkFZkWl4L2sp5DLXfqs+z0tpvi6rpG0jfpgJzy3Du2QKnk5W78WENlK+M74tSzAUfCUPn6RodykLJ8ik+EvxR+yxRmfjStAWsS6eqoTYowa4TGeJ0=,iv:qousMcaNKMtl8hGcfiS1WYBe0ftyb9ohHdBG+gqTio0=,tag:j64zgZpB7cmDfPcq4csjMQ==,type:str]", + "version": "3.12.2" + } +} diff --git a/vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/users/danny b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/users/danny new file mode 120000 index 0000000..48e5c60 --- /dev/null +++ b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/users/danny @@ -0,0 +1 @@ +../../../../../../sops/users/danny \ No newline at end of file diff --git a/vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/machines/distant-shore b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/machines/distant-shore new file mode 120000 index 0000000..2f4e8ad --- /dev/null +++ b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/machines/distant-shore @@ -0,0 +1 @@ +../../../../../../sops/machines/distant-shore \ No newline at end of file diff --git a/vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/secret b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/secret new file mode 100644 index 0000000..50edf19 --- /dev/null +++ b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/secret @@ -0,0 +1,18 @@ +{ + "data": "ENC[AES256_GCM,data:kAzaF+nxyux0zwjoqC5QYrx5UyEhMPW0v9hGcYUXExZl6ShMMgCWhKN82al2jY6OnU/CQ7UT9USH6PC+eecimyM6A5YXQ0GvvU3uus0t46GKqXqcGVl4BdgVO6tm8ienIcfjF6ml3LyvMXirjDdIluVkrH/P0vM=,iv:BSQrtg9kgBHRkCV8+nODNyPX3PchkTEjPPTYy5JZrfo=,tag:dPtjxWqDh1Bce9rlW6czyw==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMNkR1YTZNU1FyU2VWaUJR\nbjBOc0RSMW1SL1ZkZ1ozVHRmcVdkS01sdkZnCndTbGJlOVFrdDJHVDUxS1JFUmcy\nZS9jWGhRbWFCeGZOMHQwdzYxTFlrSjgKLS0tIEFaZmFzOXdXVjVOeUJuMDdpQ3hK\ndnRkUytmZk1zaXhUTSt1OTljUkNYK2MKpe6f3GHGCfduiidzYh0qaKEBaKyBZY4s\ne/f5QvZVApMiI4HFkOwFmNITOv6JdjGMQOw+OI6po0nqg0mqVnNIVA==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1hjhqyuvcjuh62xh9m5ek3aa2rluaz8c28hgh2pm435jkqtpry9ssdn2l0z", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpa0p1NCtUeGFLRzJFOFQv\nVXh4MThqOVR4TU9SK3Mrc21Ga1BPdUZrM1c0CmJxQzNyam56aTdUVVB5NVhEenlV\nOTkwb2YyRWdoVXc4K2VEaXhwZXM3TEUKLS0tIEFBajAycEQzelNoR2tCU3l6cVJo\nc3lHbWJZQWFQTkVxd0lBamxlQStZWlkKopG1Z2E0Smt/z/y1+cQeTKUKyJKBXzZr\nCQNkGfi1Dk/7n/WeKwePHWVF/19WqVfOIZW0E3tOKOIqDQZa0Io1Nw==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-07T16:36:36Z", + "mac": "ENC[AES256_GCM,data:B6UAFOrK0QIngkf5OA3+BnLAouBvsr0AbW8lKI8RH7ylGQNOyXfnN06fYshi+jQyu5EAZBqovfSZzgcTDm7MDRAjzzmTToT5ekHPZnquleU/F7pF/D7JF78M6rQyw3uG0nwhnJcRqlCAXy+56++kTJhoKEW+B5fUsbvlHTmxwLk=,iv:BXDbLObPBXsL3Uj+TRwIFtNDRzWYJeM0mJyDDluz70s=,tag:eTANaLmNaUUSYBNcIhuIFQ==,type:str]", + "version": "3.12.2" + } +} diff --git a/vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/users/danny b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/users/danny new file mode 120000 index 0000000..48e5c60 --- /dev/null +++ b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/users/danny @@ -0,0 +1 @@ +../../../../../../sops/users/danny \ No newline at end of file diff --git a/vars/per-machine/distant-shore/data-mesher-node-identity/identity.pub/value b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.pub/value new file mode 100644 index 0000000..8f2058b --- /dev/null +++ b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.pub/value @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAABhcRTNvFEyWkyRBX17KkM5nDuqOvR1xTY5vDqTygvk= +-----END PUBLIC KEY----- diff --git a/vars/per-machine/distant-shore/data-mesher-node-identity/peer.id/value b/vars/per-machine/distant-shore/data-mesher-node-identity/peer.id/value new file mode 100644 index 0000000..f748b68 --- /dev/null +++ b/vars/per-machine/distant-shore/data-mesher-node-identity/peer.id/value @@ -0,0 +1 @@ +12D3KooW9pjiKnqmnHSwGRhgyUqKeFydDUE8RvYJDAqHb5PZvzue \ No newline at end of file diff --git a/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/machines/distant-shore b/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/machines/distant-shore new file mode 120000 index 0000000..2f4e8ad --- /dev/null +++ b/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/machines/distant-shore @@ -0,0 +1 @@ +../../../../../../sops/machines/distant-shore \ No newline at end of file diff --git a/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/secret b/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/secret new file mode 100644 index 0000000..7c824e7 --- /dev/null +++ b/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/secret @@ -0,0 +1,18 @@ +{ + "data": "ENC[AES256_GCM,data:tLR5iZ7Iro3BuBJlpvkKO7RrA9X2pO2H9Isi6jc8y8krh+a89Eug0PCNb4U/aSASjQgDfZgwg9+SU1y4iIoc3qC4sxw3f4uTdjCWRDEgfAvY3DVWiWI/EbWcfX7bVvl/GCQtHSwBW5z3KwhJV2McLK6Fpblx6fM=,iv:exFXncN3SA9zqSTFxX6o3kstwMGL9y8x0IOqJVNqK+I=,tag:dEkDG3meaWoq74hkRHbplg==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3YWliNzFKWnZqdnZYeHRs\nVy8wdk1ZZWpJTlg2WWc4eEpUM2diZEFoamg0CnJnVDZJT3lWaUZlV3NHY1NpN0tW\nMXdRTnNGSjBhSFpLY0xvaER6UDI5RlEKLS0tIDloRHJFV2I1RVN6TXh6dmd5dzV0\nZ1AvZmpOM0VkaVZjNHlFdFBNd0FhTVkKEVFjtN66i+8f7P03ODYgoWZsTUiEcPiL\nYaV4UZKbjnp3SKTAeWk1P/lEj5DkicW3hq0ONQf2xrYriCpAc3/gKw==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1hjhqyuvcjuh62xh9m5ek3aa2rluaz8c28hgh2pm435jkqtpry9ssdn2l0z", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0VG1wZE52RFQ2ekl1R1ZY\nN2pmYWd1cStGZzU0Tk1LTmNuMnc5c1UwMnlVCmJrMjB6Qzc5ZUE3aXhmUmVuTTN1\neVFWbGhOUUNYUFJJVHF0OGtaR1FvVG8KLS0tIFp4aHgwN1QvWVVnaTNDVG42SXVB\nYVlLRndmQ1ovQjFMcHZYU0dqNS9ML2sKtHjmgLODafDcmrpYQyXRc/ajAR2saGs8\nlh4NVYYYwoXE6sNKSXwzgXXSjGEXGTVLxVvp9OKnSloI5/LsbrxANQ==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-07T16:36:37Z", + "mac": "ENC[AES256_GCM,data:20RiSc6b3o3xy23NDQRw4pcSf/akdcUMO6ciSFSZMQrhreYPBEa+Tb85qqqZ0dqQHRQFanzE3Usomp+Ux4FhFfSsCxljxdOjkQCAfkQKrg+GQ7/NOUhgdVQtep2+gT7MFrEzo5Jv8kctNuT18kqUjv5CwCOR35QJ98yHeAUULoo=,iv:ocUDNN4vhOX9pCUJKqQiBRhjTHbdRdw96csN6EWFdUg=,tag:Lps0aJy0ctWU5ilCUn9Uww==,type:str]", + "version": "3.12.2" + } +} diff --git a/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/users/danny b/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/users/danny new file mode 120000 index 0000000..48e5c60 --- /dev/null +++ b/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/users/danny @@ -0,0 +1 @@ +../../../../../../sops/users/danny \ No newline at end of file diff --git a/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.pub/value b/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.pub/value new file mode 100644 index 0000000..ea47b41 --- /dev/null +++ b/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.pub/value @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAPVF7m/+s1YroGdvSMxPwKmenJjk4yNrP8tNtZGHEhJI= +-----END PUBLIC KEY----- diff --git a/vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/machines/distant-shore b/vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/machines/distant-shore new file mode 120000 index 0000000..2f4e8ad --- /dev/null +++ b/vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/machines/distant-shore @@ -0,0 +1 @@ +../../../../../../sops/machines/distant-shore \ No newline at end of file diff --git a/vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/secret b/vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/secret new file mode 100644 index 0000000..ed43256 --- /dev/null +++ b/vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/secret @@ -0,0 +1,18 @@ +{ + "data": "ENC[AES256_GCM,data:9BN/+IBbsAmgABYuTEZvgB3cJOwiZ1aKu5GqcBEvCBoY3K4T5lDPqHrwdH48msu9/KD435SSz336+Stq8bQB87AXdfDMEhVIUwi8SV/CQg3urXvyqp0+lkbbrP9xyFzcH16L7NDmfD/SlZeFXQoPA3YHLvoYSsWnfjzHqrt0600IhAgq0TK+c+5hCzke9k89pgOrO6ypueHV+6GMx0g4JMcwq17bqT3fOQZ+hHSp9uOWDP1kJrO2TktwR/9AWAN+IG1sjUcaKYg+W34pG4XDkNPnp30NPfXSGMXjrM++MkIxyow1zFeSRI+bP5iLQEFpm1AvFFRdYIGN66hQVCgv0kxaOEJknlrG4QT4TyEJ,iv:MUsdjMEBvuaFkJJ6t3NNDrgECjheLJ0FtdrBsztOKZ8=,tag:lTcmyWAoKYPUhDjkHTd+Iw==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLQ0Y3Y1N5aEo5ZDQwZ2g4\neXBDMldtWU42cUFaaTBmS1B6YW5QWktNcVZvCjBMYmNKWjR6cmVIRjhNK2Y2aWg5\nM093ZFhFWW0yZnVrOUxGQ3MzSGY5UkUKLS0tICtTbHFTMUtGQWEycGNDNFlXcTBS\nWmNWbDZSNE5sWUpzQ0dTNTgyemhNdzgKdPZIFY/m3IpEMH1PGsYToyLe9Qzj6LpW\nJhOTJbT9L0dTfE3OzdaG8BkwCkb8XCWxzveLPTLPCOvbP8DmOpjjHA==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1hjhqyuvcjuh62xh9m5ek3aa2rluaz8c28hgh2pm435jkqtpry9ssdn2l0z", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvcXQ4NjE4d00ramdsemhI\nb1dxcldHS040TkVyL2lxUjdxL3J1WUlCdEZNCnExMDRqcmh5MGUxNFpJd3k4MzZT\nMXljSW5ncWxlSGRsYlJBdkoxQjIyZHMKLS0tIEhUSkRpeXhOM3BnTEsrNEpDb1I2\nUlhvZzFjRVNCcng2c3lsYS8vZHVHN00KFMMGm6BJY7/cn5WSP/RgjK6bVo4r7ps2\nkMcPoyMyenPiZrzWdL4iIb5azFB3CI8DAQS84Mt6KPR/wkYNoErxJg==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-07T16:36:38Z", + "mac": "ENC[AES256_GCM,data:Cy3KGFXu58LAWSCUYJGpMeJxBboQxEPS1TzoK8iCFUyTT7Xfak9M9omaBd2r2fEel61iuSDVoDvQbZgNy2RwuiG0HhTXliMXR6G4oOheQIsSQix81tOWoPipu77qoeVkOSUDRhBzHdQVQQmiN7VJvw1kHvCq20u2ZM0057vf91g=,iv:uAmwqd0gpCD7pTFWwgKdkKjjxVadnHeRYUEv+vUgvL8=,tag:iDbx80+08AqhvdZIXJzdgQ==,type:str]", + "version": "3.12.2" + } +} diff --git a/vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/users/danny b/vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/users/danny new file mode 120000 index 0000000..48e5c60 --- /dev/null +++ b/vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/users/danny @@ -0,0 +1 @@ +../../../../../../sops/users/danny \ No newline at end of file diff --git a/vars/per-machine/distant-shore/zerotier/zerotier-ip/value b/vars/per-machine/distant-shore/zerotier/zerotier-ip/value new file mode 100644 index 0000000..1de93b8 --- /dev/null +++ b/vars/per-machine/distant-shore/zerotier/zerotier-ip/value @@ -0,0 +1 @@ +fdd5:53a2:de33:d269:6499:93b6:ef1a:c3b3 \ No newline at end of file From 0cdb4b8697b814895525fbe8fe2ac3fb08ae086d Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Sun, 7 Jun 2026 18:46:31 +0200 Subject: [PATCH 68/72] =?UTF-8?q?fix(distant-shore):=20build=20on=20sunken?= =?UTF-8?q?-ship=20(avoids=20self-SSH=20on=20closure=20copy)=20?= =?UTF-8?q?=F0=9F=94=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- flake-modules/clan.nix | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/flake-modules/clan.nix b/flake-modules/clan.nix index 2e974d5..cc4b8fe 100644 --- a/flake-modules/clan.nix +++ b/flake-modules/clan.nix @@ -172,13 +172,15 @@ in { # distant-shore — ThinkPad X13 Gen 2, WiFi, Secure Boot via shim+MOK # (installed standalone, then migrated into clan). targetHost is the LAN # IP for the first `clan machines update`; switch to its ZT IPv6 once the - # mesh is up. Builds on the box itself (it has nix + internet). + # mesh is up. buildHost = sunken-ship: it's an x86_64 builder whose key is + # already in distant-shore's authorized_keys, so the closure copy works + # (building on distant-shore itself needs a fragile self-SSH). machines.distant-shore = { imports = [ { clan.core.enableRecommendedDefaults = false; clan.core.networking.targetHost = "danny@192.168.1.182"; - clan.core.networking.buildHost = "danny@192.168.1.182"; + clan.core.networking.buildHost = "danny@sunken-ship"; } clanHostsModule ../nixos/hosts/distant-shore.nix From 610454f0d2488a9627c47845c3be9afa24fb69c3 Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Sun, 7 Jun 2026 20:27:34 +0200 Subject: [PATCH 69/72] =?UTF-8?q?fix(distant-shore):=20drop=20duplicate=20?= =?UTF-8?q?standalone=20flake-module=20(clan-managed=20now)=20=F0=9F=A9=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- flake-modules/distant-shore.nix | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 flake-modules/distant-shore.nix diff --git a/flake-modules/distant-shore.nix b/flake-modules/distant-shore.nix deleted file mode 100644 index 6c5a023..0000000 --- a/flake-modules/distant-shore.nix +++ /dev/null @@ -1,14 +0,0 @@ -# Standalone nixosSystem registration for distant-shore. -# Temporary: clan integration (zerotier/data-mesher/dm-pull-deploy) needs -# vars generated via sops on the admin machine. Until that runs, this -# keeps the box buildable without clan deps. Delete this file when -# distant-shore moves into flake-modules/clan.nix. -{ inputs, ... }: { - flake.nixosConfigurations.distant-shore = inputs.nixpkgs.lib.nixosSystem { - system = "x86_64-linux"; - modules = [ - inputs.disko.nixosModules.disko - ../nixos/hosts/distant-shore.nix - ]; - }; -} From e2cf93e7d64504d965755730b734b264ffdc378a Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Sun, 7 Jun 2026 21:43:28 +0200 Subject: [PATCH 70/72] feat(foreign-port): add WiFi-only laptop as clan machine :sparkles: Mirrors the distant-shore pattern: clan-managed (no standalone flake-module), wired into zerotier/data-mesher/dm-pull-deploy with the generated vars. WiFi via NetworkManager (PSK from /etc/secrets/nm.env); locally-signed boot chain (shim chain-loads sbsign-signed systemd-boot + kernel, refreshed every nixos-rebuild). targetHost is the LAN IP for the first push, switch to ZT IPv6 once on the mesh. buildHost = sunken-ship to avoid self-SSH on the closure copy. --- flake-modules/clan.nix | 36 ++++++ nixos/disko-foreign-port.nix | 36 ++++++ nixos/hosts/foreign-port-hardware.nix | 18 +++ nixos/hosts/foreign-port.nix | 111 ++++++++++++++++++ sops/machines/foreign-port/key.json | 6 + sops/secrets/foreign-port-age.key/secret | 14 +++ sops/secrets/foreign-port-age.key/users/danny | 1 + .../identity.cert/machines/foreign-port | 1 + .../identity.cert/secret | 18 +++ .../identity.cert/users/danny | 1 + .../identity.key/machines/foreign-port | 1 + .../identity.key/secret | 18 +++ .../identity.key/users/danny | 1 + .../identity.pub/value | 3 + .../data-mesher-node-identity/peer.id/value | 1 + .../signing.key/machines/foreign-port | 1 + .../signing.key/secret | 18 +++ .../signing.key/users/danny | 1 + .../signing.pub/value | 3 + .../machines/foreign-port | 1 + .../zerotier/zerotier-identity-secret/secret | 18 +++ .../zerotier-identity-secret/users/danny | 1 + .../foreign-port/zerotier/zerotier-ip/value | 1 + 23 files changed, 310 insertions(+) create mode 100644 nixos/disko-foreign-port.nix create mode 100644 nixos/hosts/foreign-port-hardware.nix create mode 100644 nixos/hosts/foreign-port.nix create mode 100755 sops/machines/foreign-port/key.json create mode 100644 sops/secrets/foreign-port-age.key/secret create mode 120000 sops/secrets/foreign-port-age.key/users/danny create mode 120000 vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/machines/foreign-port create mode 100644 vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/secret create mode 120000 vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/users/danny create mode 120000 vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/machines/foreign-port create mode 100644 vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/secret create mode 120000 vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/users/danny create mode 100644 vars/per-machine/foreign-port/data-mesher-node-identity/identity.pub/value create mode 100644 vars/per-machine/foreign-port/data-mesher-node-identity/peer.id/value create mode 120000 vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/machines/foreign-port create mode 100644 vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/secret create mode 120000 vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/users/danny create mode 100644 vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.pub/value create mode 120000 vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/machines/foreign-port create mode 100644 vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/secret create mode 120000 vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/users/danny create mode 100644 vars/per-machine/foreign-port/zerotier/zerotier-ip/value diff --git a/flake-modules/clan.nix b/flake-modules/clan.nix index cc4b8fe..e4a7944 100644 --- a/flake-modules/clan.nix +++ b/flake-modules/clan.nix @@ -22,6 +22,7 @@ let phantomShipZTv6 = "fdd5:53a2:de33:d269:6499:936c:48a:bbdc"; vpsRelayZTv6 = "fdd5:53a2:de33:d269:6499:9305:339f:2ed3"; distantShoreZTv6 = "fdd5:53a2:de33:d269:6499:93b6:ef1a:c3b3"; + foreignPortZTv6 = "fdd5:53a2:de33:d269:6499:9389:9b18:6c52"; # Shared across both servers: /etc/hosts entries so data-mesher's # libp2p /dns/.clan/... bootstrap multiaddrs resolve over ZT. @@ -31,6 +32,7 @@ let "${phantomShipZTv6}" = [ "phantom-ship.clan" ]; "${vpsRelayZTv6}" = [ "vps-relay.clan" ]; "${distantShoreZTv6}" = [ "distant-shore.clan" ]; + "${foreignPortZTv6}" = [ "foreign-port.clan" ]; }; }; in { @@ -50,6 +52,7 @@ in { inventory.machines.phantom-ship = { }; inventory.machines.vps-relay = { }; inventory.machines.distant-shore = { }; + inventory.machines.foreign-port = { }; # ZeroTier mesh VPN. sunken-ship is the controller (manages network # membership); phantom-ship is a peer. The mac joins manually as an @@ -62,6 +65,7 @@ in { roles.peer.machines.sunken-ship = { }; roles.peer.machines.vps-relay = { }; roles.peer.machines.distant-shore = { }; + roles.peer.machines.foreign-port = { }; }; # data-mesher — signed-file gossip protocol over libp2p (port 7946). @@ -75,6 +79,7 @@ in { roles.default.machines.sunken-ship = { }; roles.default.machines.phantom-ship = { }; roles.default.machines.distant-shore = { }; + roles.default.machines.foreign-port = { }; roles.bootstrap.machines.sunken-ship = { }; }; @@ -93,6 +98,7 @@ in { roles.default.machines.sunken-ship.settings.action = "switch"; roles.default.machines.phantom-ship.settings.action = "switch"; roles.default.machines.distant-shore.settings.action = "switch"; + roles.default.machines.foreign-port.settings.action = "switch"; }; # `clan machines update` connection target. Priority 2000 > ZT's 900 @@ -123,6 +129,12 @@ in { host = "192.168.1.182"; user = "danny"; }; + # foreign-port: WiFi-only LAN IP for the first update; swap to its + # generated ZT IPv6 after it joins the mesh. + roles.default.machines.foreign-port.settings = { + host = "192.168.1.223"; + user = "danny"; + }; }; # Preserve current network / init stack (no systemd-networkd/resolved, @@ -194,6 +206,30 @@ in { ]; }; + # foreign-port — WiFi-only laptop server, locally-signed boot chain + # (installed standalone, migrated into clan). targetHost is the LAN IP + # for the first `clan machines update`; switch to its ZT IPv6 once the + # mesh is up. buildHost = sunken-ship for the closure copy (avoids + # self-SSH). + machines.foreign-port = { + imports = [ + { + clan.core.enableRecommendedDefaults = false; + clan.core.networking.targetHost = "danny@192.168.1.223"; + clan.core.networking.buildHost = "danny@sunken-ship"; + } + clanHostsModule + ../nixos/hosts/foreign-port.nix + config.flake.nixosModules.monitoring-node-exporter + inputs.home-manager.nixosModules.home-manager + (hmModule { + user = "danny"; + homeDirectory = "/home/danny"; + stateVersion = "25.11"; + }) + ]; + }; + machines.phantom-ship = { imports = [ { diff --git a/nixos/disko-foreign-port.nix b/nixos/disko-foreign-port.nix new file mode 100644 index 0000000..a928620 --- /dev/null +++ b/nixos/disko-foreign-port.nix @@ -0,0 +1,36 @@ +# Declarative disk layout for distant-shore. UEFI/systemd-boot, no +# encryption: it's a headless, WiFi-only server that must reboot +# unattended (clan dm-pull-deploy), so a LUKS passphrase prompt at boot +# would hang it. Mirrors sunken-ship's plain-ext4 choice. Device is wiped +# + repartitioned at install time by clan/nixos-anywhere. +{ + disko.devices = { + disk.main = { + type = "disk"; + device = "/dev/nvme0n1"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "fmask=0022" "dmask=0022" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; +} diff --git a/nixos/hosts/foreign-port-hardware.nix b/nixos/hosts/foreign-port-hardware.nix new file mode 100644 index 0000000..3c52633 --- /dev/null +++ b/nixos/hosts/foreign-port-hardware.nix @@ -0,0 +1,18 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = + [ (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ "xhci_pci" "thunderbolt" "nvme" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-intel" ]; + boot.extraModulePackages = [ ]; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/nixos/hosts/foreign-port.nix b/nixos/hosts/foreign-port.nix new file mode 100644 index 0000000..9705b6e --- /dev/null +++ b/nixos/hosts/foreign-port.nix @@ -0,0 +1,111 @@ +# NixOS laptop server. WiFi-only, headless, unattended auto-rebuild via +# clan dm-pull-deploy. No LUKS (mirrors sunken-ship) so reboots don't +# block on a passphrase. +# +# Blank-slate server for now — no application services. Give it a purpose +# later (just add services here and let dm-pull-deploy roll it out). +{ config, lib, pkgs, ... }: + +{ + imports = [ + ./foreign-port-hardware.nix + ../disko-foreign-port.nix + ]; + + boot.loader.systemd-boot.enable = true; + # Firmware-locked Secure Boot: we can't enrol our own keys into the + # firmware key DB, so a vendor-signed shim is the firmware-booted binary + # and chain-loads a locally-signed systemd-boot + kernel. The NVRAM + # entry points at shim; bootctl is kept away from EFI variables so + # rebuilds don't clobber the entry. + boot.loader.efi.canTouchEfiVariables = false; + boot.loader.efi.efiSysMountPoint = "/boot"; # matches disko ESP mountpoint + + # --- Locally-signed boot chain -------------------------------------------- + # On every bootloader install: re-sign systemd-boot and every kernel + # image, refresh the shim binary on the ESP, and place the helper binary + # beside it. Re-runs on each nixos-rebuild so auto-deployed generations + # stay bootable. Signing material lives in /etc/secrets, never the repo. + boot.loader.systemd-boot.extraInstallCommands = '' + # NixOS's bootloader-install systemd unit runs with a minimal PATH that + # doesn't include coreutils, so use absolute paths for cp/mv. + KEY=/etc/secrets/MOK.key + CRT=/etc/secrets/MOK.crt + sb() { ${pkgs.sbsigntool}/bin/sbsign --key "$KEY" --cert "$CRT" --output "$2" "$1"; } + # systemd-boot -> shim's chain-load target + sb /boot/EFI/systemd/systemd-bootx64.efi /boot/EFI/BOOT/grubx64.efi + # shim is the firmware-booted binary; helper binary sits beside it + ${pkgs.coreutils}/bin/cp -f /etc/secrets/shimx64.efi /boot/EFI/BOOT/BOOTX64.EFI + ${pkgs.coreutils}/bin/cp -f /etc/secrets/mmx64.efi /boot/EFI/BOOT/mmx64.efi + # sign each kernel (skip if already signed; leave initrds untouched) + for k in /boot/EFI/nixos/*bzImage.efi; do + [ -e "$k" ] || continue + if ! ${pkgs.sbsigntool}/bin/sbverify --cert "$CRT" "$k" >/dev/null 2>&1; then + ${pkgs.sbsigntool}/bin/sbsign --key "$KEY" --cert "$CRT" --output "$k.tmp" "$k" \ + && ${pkgs.coreutils}/bin/mv -f "$k.tmp" "$k" + fi + done + ''; + + networking.hostName = "foreign-port"; + # WiFi via NetworkManager. The wpa_supplicant stack hit two issues on this + # box: (1) it strips CAP_CHOWN so wpa couldn't create its ctrl_interface, + # and (2) dhcpcd didn't grab a lease after the (late) association at boot, + # needing a manual restart — fatal for an unattended headless server. NM + # handles association + DHCP atomically and connected first-try here. + # The PSK stays out of the repo: it's substituted from /etc/secrets/nm.env + # ($PSK_INTENO) into the declared profile at activation. + networking.networkmanager.enable = true; + networking.networkmanager.ensureProfiles.environmentFiles = [ "/etc/secrets/nm.env" ]; + networking.networkmanager.ensureProfiles.profiles."Inteno-89FE-5GHz" = { + connection = { id = "Inteno-89FE-5GHz"; type = "wifi"; autoconnect = true; }; + wifi = { ssid = "Inteno-89FE-5GHz"; mode = "infrastructure"; }; + wifi-security = { key-mgmt = "wpa-psk"; psk = "$PSK_INTENO"; }; + ipv4.method = "auto"; + ipv6.method = "auto"; + }; + hardware.enableRedistributableFirmware = true; # WiFi firmware blobs + time.timeZone = "Europe/Copenhagen"; + + # It's a laptop acting as a server: keep running with the lid shut. + services.logind.settings.Login.HandleLidSwitch = "ignore"; + services.logind.settings.Login.HandleLidSwitchExternalPower = "ignore"; + + # Reduce screen burn-in / power: blank the TTY after a minute. + boot.kernelParams = [ "consoleblank=60" ]; + + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + programs.nix-ld.enable = true; # run dynamically linked binaries (e.g. Claude Code remote CLI) + nix.settings.trusted-users = [ "root" "danny" ]; + system.stateVersion = "25.11"; + + users.users.danny = { + isNormalUser = true; + extraGroups = [ "wheel" "video" "audio" ]; + openssh.authorizedKeys.keys = [ + # Mac admin / fleet key (~/.ssh/id_ed25519_sunken_ship) — the key the + # Mac uses to reach the fleet; clan machines update relies on it. + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKW/akfIiVU5o63YrTAJVZhMj7kXfYHOnXDtlpVFW7pf danny@mac-admin" + # TODO: add a per-host key (~/.ssh/id_ed25519_foreign_port) for + # plain `ssh foreign-port`. Generate when convenient. + # sunken-ship (dm-pull-deploy push node) — reach foreign-port over ZT. + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB9t4YAaoHvVouqp+qyFOq8o3SAtXMiAmjF6J0ldyx4g danny@sunken-ship" + ]; + }; + users.users.root.openssh.authorizedKeys.keys = + config.users.users.danny.openssh.authorizedKeys.keys; + + services.openssh = { + enable = true; + settings = { + PasswordAuthentication = false; + KbdInteractiveAuthentication = false; + }; + }; + + security.sudo.wheelNeedsPassword = false; + + # mokutil + sbsigntool — manage the shim trust chain and inspect signed + # bootloader/kernel images when debugging. + environment.systemPackages = with pkgs; [ git mokutil sbsigntool ]; +} diff --git a/sops/machines/foreign-port/key.json b/sops/machines/foreign-port/key.json new file mode 100755 index 0000000..6aa3307 --- /dev/null +++ b/sops/machines/foreign-port/key.json @@ -0,0 +1,6 @@ +[ + { + "publickey": "age1lwl2z6ymqjshknr79277qnr7hvffcc8n7qdqt98sz3t709a5yutq8d7gka", + "type": "age" + } +] \ No newline at end of file diff --git a/sops/secrets/foreign-port-age.key/secret b/sops/secrets/foreign-port-age.key/secret new file mode 100644 index 0000000..2ba1f0f --- /dev/null +++ b/sops/secrets/foreign-port-age.key/secret @@ -0,0 +1,14 @@ +{ + "data": "ENC[AES256_GCM,data:MH/ib8WAbzucbm2dhhoo6ESSSLKtKMWmjUwtpAOZhU7KyhOoechpJRSkBBmFV4LzbSP1qeaFbid6USJBnRsxkoz6XvhMzP0kzS0=,iv:9sPwc/JIlo5mzxelNzLCB26k2f+n2C9tB8Y/HEdPvHw=,tag:hJBhzTMsTWd9PDydS4aosg==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6L2JGejlzQlhiNFhES1lT\nNTZsNFFMT1NzZEV0T28rNDA3STJ1UXNRcUZFCkxoenpFYWJicHpGVDhtMUdwNXBo\nS29EazVsRGFST2ZodDJMTkxQN2I1RjAKLS0tIFpib0RoOTJ6bkU0b1F6NnRaV3lF\nVHhvYjNOUUtMbGF5ejdaVk5WdGt2d1kKNU5JR1nIYPQLALUM3wRO945Sk6GLxJpn\nTVmVUEgXcwHcSij10a/cQOyPXNNnsfIC+WJFMJcjHfsjBnwS5W/Bgw==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-07T19:41:18Z", + "mac": "ENC[AES256_GCM,data:mVobAhXUhbs49+g0bXfi4TjPG667F7pM8Kk518a7kRZ/HtN2kLYcSyl3XpspTosAs4x3QbFQUbFCgsBgqx+gS6xlw3OAJXM3iG2fNu2qoj9Q7viAEHoWVHwT+ftjA0qVTUf0BDD1r4ow6BNhe6kQy5bQqVu0MhjDfsK9BNTXAu4=,iv:aFHo3bQKgr1XSnwGUajkSFa4UftTWdZbPtXY05N7qOM=,tag:VymYJf4XFLaEGvxQmvF6rA==,type:str]", + "version": "3.12.2" + } +} diff --git a/sops/secrets/foreign-port-age.key/users/danny b/sops/secrets/foreign-port-age.key/users/danny new file mode 120000 index 0000000..215639b --- /dev/null +++ b/sops/secrets/foreign-port-age.key/users/danny @@ -0,0 +1 @@ +../../../users/danny \ No newline at end of file diff --git a/vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/machines/foreign-port b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/machines/foreign-port new file mode 120000 index 0000000..96f5ba3 --- /dev/null +++ b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/machines/foreign-port @@ -0,0 +1 @@ +../../../../../../sops/machines/foreign-port \ No newline at end of file diff --git a/vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/secret b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/secret new file mode 100644 index 0000000..768ab41 --- /dev/null +++ b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/secret @@ -0,0 +1,18 @@ +{ + "data": "ENC[AES256_GCM,data:wnNPCB0+f3dcxMW1/pcFZFauUVYTC1mfWoWBV2EJmyRzZS3Uux5Un3R/GbYQeDSFZDLzLH+zCZFaxq3mpb3NGTTUzF8vnGMk/OnjlolA8OjAfiODI0mahTiQA7WcWSk1hkkZ15Ri1o+uyumx9hmvJU3dIsKIJe7AizCzwP5bHg1jgRhG2wPKKyIDWKoh4JTlR6SxK6/tOaUPx2gb2ddz2Lk56Xdw7GCbb/9I9D6sRwxdWMCoWFKdTllLsdsD48b8Jfq4ewD+LudYEtiVByk5SpyOjQoAmMLYaGlD+nxFgZz53hePRIXnp0fL0pm4,iv:fA607yxD/yHJatEiGh1SVGDcqKxB+EFeyCUQeF/Z5hA=,tag:glaq+MBCp6ptKqDsw4RM/Q==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0MklDUkpWbEdFcFgxTVJZ\ndEs1OTJtZFhVaEsyb2pobGlUOGhtcTY4RWpVCjFDV3lqRmNGclZMbTR3UXlhcjJv\nVEY1Tjk1YWR4Tmt0SmgvR3laZnNIRUkKLS0tIHB1TURnYmVzZW4xSERMR0ZrRXl5\nbWVJbW1keGkyUkhuQXE0MEFTaXFsS1EKHlsS3FDr9RuMBRU5r4T3bCZWZn38V3k+\nfLUfuZK2IF+xyD7kEiBuATB57wwfd8RzZ1lBwz4fD4jlb+fz0BXoJQ==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1lwl2z6ymqjshknr79277qnr7hvffcc8n7qdqt98sz3t709a5yutq8d7gka", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6TnNqc284WkZqdXNLVFBU\nRTJndEFmNjY0Q1YyUnRPLy9jWllpSy9ZaFNFCkFkNmpYenQ2dk1Fb2dRZTNvM0Jl\nemNqUmdjQmpJQUF4M3ZNRmo4UEhXOHcKLS0tIFp4OTZJTGR1algxTEVWemdkQTB5\nME4xTTdlelN6bXJiTGRSM1VSWG5vZUEKOYc71rLx7RTq4DR6ZggrtgllK58sYJ6h\ngw156OTQl3fKWxlrKDd1l4o72M1qmfAIQ1z5YJJ+CfNPk/iMz/R3rQ==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-07T19:41:18Z", + "mac": "ENC[AES256_GCM,data:AkcOoNTxMNkpF0SrwFlNujBrB8fxL1diu+mGq/kbsiWIj6UqvVD+dimDSvTgVqvnU4HF7/7b9zKriC6SbG42Kz8zScFv7m3idD2tHr+7SE/iR7CowDQs70CRMo1b85wLq8WAxhfQb93NHdum6I2biNVIf0ZXs1+kZ2iNBxtjqfQ=,iv:kWOCWCe953ekq0n0HLe3S2JprIBnBe9QXwIzDFyQMH8=,tag:tLz7VZwj7RrbpJ7QTrBqcg==,type:str]", + "version": "3.12.2" + } +} diff --git a/vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/users/danny b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/users/danny new file mode 120000 index 0000000..48e5c60 --- /dev/null +++ b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/users/danny @@ -0,0 +1 @@ +../../../../../../sops/users/danny \ No newline at end of file diff --git a/vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/machines/foreign-port b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/machines/foreign-port new file mode 120000 index 0000000..96f5ba3 --- /dev/null +++ b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/machines/foreign-port @@ -0,0 +1 @@ +../../../../../../sops/machines/foreign-port \ No newline at end of file diff --git a/vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/secret b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/secret new file mode 100644 index 0000000..a79bb25 --- /dev/null +++ b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/secret @@ -0,0 +1,18 @@ +{ + "data": "ENC[AES256_GCM,data:1Hq98rN3U+8DcxIFJpYkvv31gUpSm0WBjfZxivYn7/ZkH6zbJ57fzeU+9PH9SRF6QBuekZKZNIBup3fteI5VqQ/moEyQE9aSvnqGCrkcamDwDQfN5GwKX+rb7W96atESRm/VqhgDWC2KTc3892515gBPpkDG+nc=,iv:tAlghG1jpDPcYgTvEzAlnB2upAetl8mz8IIQercHe4k=,tag:mz3fvVlKolg5JzrjhBNPaw==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlaEk2Z1NMZnVtNlJPdXgw\nTWNaTFBCRXo3T2JRUEY2Q2hBY0xpMVV6ckE0ClJOVUpKNDZTcEhGS2RzQm1tSjNp\ndmxQWjl5aHord0RUMHRvTlhyMkVqc1UKLS0tIHlDRXlReUgzZVdLcE9kMFhsTDRq\nOGxpZE9KcUR0VEhyOE9VUkVUVlIyRlEKsnU17famN/qr2M8BdvVpRl5bSWseegrZ\nnB9yljvm+pxsE55xM1WyguNfUwXtHj0YTiVgBl5PIUolj3/J8R76sg==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1lwl2z6ymqjshknr79277qnr7hvffcc8n7qdqt98sz3t709a5yutq8d7gka", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjTGdDVlB0RGlTcng3M3pE\nNzEvNFpBUzF0aDJDaUJFTFFGWlB3bEVVdHhFClZOZGNDanlMTkxIMk9lbzVGRzAv\nZG93NUFFL3NIM3Z0TlhucFlMTTYwc3MKLS0tIFFQcTIwekNEM0k0MElGZys2QldS\nMDZpRVk5OVNZYVVWSWJDTFZqVFdiRWcKgwuwZgKhKx1PiQwH2CgMoCl0WUQR5Rv9\nx4mpZgkoD5pkEx896117CyAy2BRzrDWo+4SsjEijSMlDynYsbxLReA==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-07T19:41:18Z", + "mac": "ENC[AES256_GCM,data:DX9+9MH8ZPtc6sPbYSc+54soAIXJWWEoEWBZdbJ6gT5RhVdzUjMHuEbmb9eMcb+nVu4KSUCoXiJOT9XActSU2dcTNIIiLX1lqpw0aWRS2sAWM+Go4hT4/P98z/0vcsdN/uQOBl3cDlygqKhN9GSoPfJTMT+QTSZsVjxwYxW1pPM=,iv:B9RiMMX+yS1Y+3E1ifTJI30pvLrah5SCPwW6CZKZGNU=,tag:MA007hv+nMIMutOdl5ewkQ==,type:str]", + "version": "3.12.2" + } +} diff --git a/vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/users/danny b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/users/danny new file mode 120000 index 0000000..48e5c60 --- /dev/null +++ b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/users/danny @@ -0,0 +1 @@ +../../../../../../sops/users/danny \ No newline at end of file diff --git a/vars/per-machine/foreign-port/data-mesher-node-identity/identity.pub/value b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.pub/value new file mode 100644 index 0000000..b450f2b --- /dev/null +++ b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.pub/value @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAZqy+mwYOfJy3GSHfeC80TFn1c0kYte5zzzbwrP8xww0= +-----END PUBLIC KEY----- diff --git a/vars/per-machine/foreign-port/data-mesher-node-identity/peer.id/value b/vars/per-machine/foreign-port/data-mesher-node-identity/peer.id/value new file mode 100644 index 0000000..f9982a8 --- /dev/null +++ b/vars/per-machine/foreign-port/data-mesher-node-identity/peer.id/value @@ -0,0 +1 @@ +12D3KooWGjAXheQGEfy13JQJP8pSrwcivxoXw5ijRzesfXVDFuyW \ No newline at end of file diff --git a/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/machines/foreign-port b/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/machines/foreign-port new file mode 120000 index 0000000..96f5ba3 --- /dev/null +++ b/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/machines/foreign-port @@ -0,0 +1 @@ +../../../../../../sops/machines/foreign-port \ No newline at end of file diff --git a/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/secret b/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/secret new file mode 100644 index 0000000..cd27ea5 --- /dev/null +++ b/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/secret @@ -0,0 +1,18 @@ +{ + "data": "ENC[AES256_GCM,data:dDO6hu8prxHvoP41Oxky0mGGbrwqcCcrrkg0tbr/Sv8K16gNoQaX2wvaRDExOmt0BZkv5Oe8p5pvKudmm5JN0AS7oaPexW0lE+vFJ+zrRpq01c5BbCYZ0SuuafJ3VmRS/dlYU0/SZ4MyK3eijLzX3rGHPOi3b0g=,iv:hbh49ExGMYyshxcus/5sTIs/ZcOL9pod/3H/oHG1Qs8=,tag:fjHnl2uunGEU0i2FtgZB+g==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZNTlkTWVleld5K3Q5Vklm\nMlphdVduQ0RKY0pEVGdVTm5scHRWR0lNVjAwClV4V3drQnFLUkhpUVk1ZElGcFM1\ncit3UTdURExTRDVjVW1ZdklTZzRINDAKLS0tIHFMYnNycmh1Y0h4OC9UNUtHUmMw\nVXdpVk9QWHlBYmtCS3FOam9SWnRFZG8KDnggBRH/wSh1tfiCGOn1sF/Fdfxkf1us\n7Lzxexrmh+lllns/KY2of9L2HUgDavp+ju/5QVFfT7O3SuSTB6aoow==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1lwl2z6ymqjshknr79277qnr7hvffcc8n7qdqt98sz3t709a5yutq8d7gka", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5eVpNUmV5QlllaXlPaGgr\ndXZYMURzT3I4UWxWSHBSbnAyZVNsOWNaZ2xJClhkRmZ2ejBYVCtkTVBZZE82YXE5\nWkdZWFJFM0lVQXFFYm5rYnRVZDFEdlkKLS0tIHZ5OUgzcFRLZnFWK3pDUUtWUUJj\nWFF4Zk5IeDl5VFNQWlVsTk1lQWlLQmMKJzaOm0cwOshmwoO+eHovf6i6mGkezjIP\ncXJlDaJyxfPKJxc36XlJ5KT9c4RqTX7WFOifHoKRh4EN58KnvtFj+A==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-07T19:41:18Z", + "mac": "ENC[AES256_GCM,data:UX7265pubBBssugQk4pZsQH5WedsmnqFa77bJQZwu2ixNUTkO9VfR8r9CUiugDOmbDj9Y7TJtoN4JR+v6hBmDOnjHO5w0WO5dONNJebGmO+pGU7r/K6WwSGi5nPANiYjGuHqYZwq7PJe8ZCF/vu/ZI8q7iJijw6xGWuGHaP/Gvw=,iv:Ezo1z5n+pHPdhjh9l+HvmsgElEwJR4eoMPtZKdDhHAI=,tag:57yLRXReSRz098sDxyiQZQ==,type:str]", + "version": "3.12.2" + } +} diff --git a/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/users/danny b/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/users/danny new file mode 120000 index 0000000..48e5c60 --- /dev/null +++ b/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/users/danny @@ -0,0 +1 @@ +../../../../../../sops/users/danny \ No newline at end of file diff --git a/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.pub/value b/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.pub/value new file mode 100644 index 0000000..6131304 --- /dev/null +++ b/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.pub/value @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEA6xYjcIT5B5NDduIARf2EAoE+vsnZK+NWcyiI0fQc0Fg= +-----END PUBLIC KEY----- diff --git a/vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/machines/foreign-port b/vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/machines/foreign-port new file mode 120000 index 0000000..96f5ba3 --- /dev/null +++ b/vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/machines/foreign-port @@ -0,0 +1 @@ +../../../../../../sops/machines/foreign-port \ No newline at end of file diff --git a/vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/secret b/vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/secret new file mode 100644 index 0000000..5fd8d79 --- /dev/null +++ b/vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/secret @@ -0,0 +1,18 @@ +{ + "data": "ENC[AES256_GCM,data:PO0Thn6D7kcIGWr7MwmS8H58+9JYSDDGQZlx28B7T6noXTA6tWqMJlqY4aMn1dXJ1CKAqV4q5VZpd/kP9KQvSL4DRnRrFteRe0C+k/mlLfwsWVqLGFY7eqoG1QTZwc4w8cw3FB7R0YUfxRlHq3mIyrbf+8POX2Rq2r5L5GNWVkGTKZOPRtNawPxTrUgfVM4B9ksc1vtTZeWn1GymSwevnt4KPX/8efFAgIclTUHh+Eh+F9xSU9efnkT+Phsh3QLf+3+UHiXQXlpMgwuKrvBJdHWLxJz/3aTpU2+nByqv0IANhGhR8ut0EbFXr8Zr1pIYrt4mWCAyYJvnwxR6iljQ1zyhI0GXUNAHJPQ7wRYq,iv:yDOBYu2+HK/KfS/hbR5QgOi2QHp9RzGPiKxojQX2s8c=,tag:q6s6LemFyoFBEq+ojd4D6A==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1bEkwSUhxR0JQR2psZ3Uy\nUlFpWi93NTBhZ0s0TlpkQ2VkVUdDWHIyNmhFClBhVFBnb1h0c2o2cm9OODZpZWMy\nQTB2YmxnWmN5Ylo4M1JHMVVVdklWeWMKLS0tIDBSY2NQdmRTZnA1QUtnaHloUFJJ\nb0VvZGlwSko0UitTa2t6TDZ4bnhsSWMKt5awUoFdny/Qg5krgUAzHeqIoIhprPmF\nBNleiSJdAvSsK53a7CT2rGInnl3dcrtpkEWluK7WJlFTJBdekMwQuA==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1lwl2z6ymqjshknr79277qnr7hvffcc8n7qdqt98sz3t709a5yutq8d7gka", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6TFZjRjk4Wm8vcEcxN0ZP\na05yd282UmR3NllXM2MyeUpSb0VuWURvTHp3CmJzL0cwcU5WWGJuME1KcmtxSFVw\nL1lFdzg3Z2t4TXBiaWduZ2tSZXc3bjAKLS0tIEp6NWpIMlhoSEtvQ3IyNXJNVnE1\nb1lSczR2eG1JY1NScnkyNWMxWWN0aWcKrnfv9dGrWpmBjt8u+FdtwojU5hLDyV/Z\n6vgaW35SvFYLYR53Zo18MPkYbqGcaNldyr68qbYMLxqVdQUJwv3LSg==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-07T19:41:19Z", + "mac": "ENC[AES256_GCM,data:joT4cUsVDxTVJqF9OJyETkC0lxQ6sT3XonBIjy80/PZ6cs7lcEyboWWSVuBcG+CTPzcUv1uXmdNjUBNc/TDdF8P0vEGnMBgmNRnSrxb0OwENW+c08GOB+c4AJev58H+V1wmzmyr9NJAKxpvQaE/cWIS1wS7c5QdiKAj8HsYd2ns=,iv:H2xSAU0jTH0bKS+P5W+FwbOtzl/Wb5xTfirkZMmtPq8=,tag:o+b9ESO3d8XnIU/bcH09zw==,type:str]", + "version": "3.12.2" + } +} diff --git a/vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/users/danny b/vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/users/danny new file mode 120000 index 0000000..48e5c60 --- /dev/null +++ b/vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/users/danny @@ -0,0 +1 @@ +../../../../../../sops/users/danny \ No newline at end of file diff --git a/vars/per-machine/foreign-port/zerotier/zerotier-ip/value b/vars/per-machine/foreign-port/zerotier/zerotier-ip/value new file mode 100644 index 0000000..79b3db0 --- /dev/null +++ b/vars/per-machine/foreign-port/zerotier/zerotier-ip/value @@ -0,0 +1 @@ +fdd5:53a2:de33:d269:6499:9389:9b18:6c52 \ No newline at end of file From f8a873bd06b106a69e5d62eb1b4bd0efac968143 Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Mon, 8 Jun 2026 22:27:32 +0200 Subject: [PATCH 71/72] nixos: add tdpixi service (port 8093) + vps-relay vhost Idle Tower Defence Mini App by @plasmagoat forked from github.com/plasmagoat/TDPixi. Pure static FastAPI serve, no DB. Proxied at tdpixi.dannydannydanny.me. Co-Authored-By: Claude Sonnet 4.6 --- nixos/hosts/phantom-ship.nix | 29 ++++++++++++++++++++++++++--- nixos/hosts/vps-relay.nix | 4 ++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index e018a73..99bec0e 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -50,11 +50,11 @@ in # KomTolk (:8080), Shelfish (:8081), Scuttle (:8082), Bananasimulator # (:8083), Forgejo (:3000), Escape Hormuz (:8090), bon (:8091), - # notes (:8092) are reachable only over the ZeroTier mesh — the - # vps-relay Caddy reverse-proxies into them. Same pattern as + # notes (:8092), TDPixi (:8093) are reachable only over the ZeroTier mesh — + # the vps-relay Caddy reverse-proxies into them. Same pattern as # sunken-ship's bbbot. Not in global allowedTCPPorts, so the WAN side # stays closed. - networking.firewall.interfaces."zt+".allowedTCPPorts = [ 3000 8080 8081 8082 8083 8090 8091 8092 ]; + networking.firewall.interfaces."zt+".allowedTCPPorts = [ 3000 8080 8081 8082 8083 8090 8091 8092 8093 ]; hardware.enableRedistributableFirmware = true; # iwlwifi (Intel 8260) + GPU + BT firmware @@ -527,6 +527,29 @@ in }; }; + # TDPixi — Idle Tower Defence Telegram Mini App by @plasmagoat. + # Pure static serve, no DB. Code rsync'd to /home/danny/tdpixi/. + # Upstream: https://github.com/plasmagoat/TDPixi + systemd.services.tdpixi = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + fastapi + uvicorn + ]); + in { + description = "tdpixi — Idle Tower Defence Mini App"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv ]; + serviceConfig = { + WorkingDirectory = "/home/danny/tdpixi"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8093"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + # Hara morning heartbeat — daily email check + Telegram good-morning ping. # Runs claude in print mode with the Gmail MCP, then sends output via Bot API. # Token lives in ~/.claude/channels/telegram/.env (managed by the telegram plugin). diff --git a/nixos/hosts/vps-relay.nix b/nixos/hosts/vps-relay.nix index 3387aa5..207e8b8 100644 --- a/nixos/hosts/vps-relay.nix +++ b/nixos/hosts/vps-relay.nix @@ -142,6 +142,10 @@ "bon.dannydannydanny.me".extraConfig = '' reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8091 ''; + # TDPixi — Idle Tower Defence Mini App by @plasmagoat, port 8093. + "tdpixi.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8093 + ''; # notes — markdown blog (notes.X) + apex landing (X). Same backend # service on phantom :8092 routes by Host header. "notes.dannydannydanny.me".extraConfig = '' From 0eab0d47ae1c7c3ff3fbc5f058df653a40a636bb Mon Sep 17 00:00:00 2001 From: DannyDannyDanny Date: Mon, 8 Jun 2026 23:25:34 +0200 Subject: [PATCH 72/72] nixos: add bananasimulator-beta service + vhost MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cheat instance for the bananasim project. Mirrors the production service on phantom-ship but: - own DB at /home/danny/.local/share/bananasimulator-beta/ - own working dir /home/danny/bananasimulator-beta/ - port 8084 (added to zt+ firewall allowlist + new vps-relay vhost at bananasimulator-beta.dannydannydanny.me) - BS_RIPE_MIN_PER_STAGE=0.2 so a banana cycles in ~3 min (testable) - BS_BETA_MODE=1 so the server exposes /api/cheat/* + sets beta:true in /api/me, which makes the frontend render the 🧪 cheat menu Same code base; deploy with the same tar-over-ssh ritual into the sibling dir. apps.json gets a private 'bananasim (beta)' entry that only my user sees. Co-Authored-By: Claude Opus 4.7 (1M context) --- nixos/hosts/phantom-ship.nix | 37 +++++++++++++++++++++++++++++++++++- nixos/hosts/vps-relay.nix | 5 +++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index 99bec0e..26b3e90 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -54,7 +54,7 @@ in # the vps-relay Caddy reverse-proxies into them. Same pattern as # sunken-ship's bbbot. Not in global allowedTCPPorts, so the WAN side # stays closed. - networking.firewall.interfaces."zt+".allowedTCPPorts = [ 3000 8080 8081 8082 8083 8090 8091 8092 8093 ]; + networking.firewall.interfaces."zt+".allowedTCPPorts = [ 3000 8080 8081 8082 8083 8084 8090 8091 8092 8093 ]; hardware.enableRedistributableFirmware = true; # iwlwifi (Intel 8260) + GPU + BT firmware @@ -176,6 +176,7 @@ in "d /home/danny/.local/share/shelfish 0755 danny users - -" "d /home/danny/.local/share/scuttle 0755 danny users - -" "d /home/danny/.local/share/bananasimulator 0755 danny users - -" + "d /home/danny/.local/share/bananasimulator-beta 0755 danny users - -" "d /home/danny/.local/share/komtolk 0755 danny users - -" "d /home/danny/.local/share/escape_hormuz 0755 danny users - -" "d /home/danny/.local/share/scuttle/tiles 0755 danny users - -" @@ -385,6 +386,40 @@ in }; }; + # Bananasimulator BETA — cheat-instance for testing the full progression + # end-to-end. Separate DB, exposes /api/cheat/* (gated by BS_BETA_MODE=1) + # so the frontend cheat menu can seed canonical states and reset. + # Faster ripening (0.2 min/stage = ~3 min to compost) so cycles are + # testable in real time. Same code base; deploy to a sibling dir. + # vhost in vps-relay.nix → bananasimulator-beta.dannydannydanny.me. + systemd.services.bananasimulator-beta = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + fastapi + uvicorn + httpx + python-telegram-bot + ]); + in { + description = "Bananasimulator BETA (cheat instance) FastAPI server"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv ]; + environment = { + SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; + BS_DB_PATH = "/home/danny/.local/share/bananasimulator-beta/bananasimulator.db"; + BS_RIPE_MIN_PER_STAGE = "0.2"; # ~3 min to compost — testable in real time + BS_BETA_MODE = "1"; # exposes /api/cheat/* + flips beta=true in /api/me + }; + serviceConfig = { + WorkingDirectory = "/home/danny/bananasimulator-beta"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8084"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + # Escape Hormuz — turn-based boat-race Mini App (Hara's first build). # Code lives at /home/danny/escape_hormuz/. Same vps-relay-fronted ZT path # as the others; binds :: so the ZeroTier IPv6 address is reachable. diff --git a/nixos/hosts/vps-relay.nix b/nixos/hosts/vps-relay.nix index 207e8b8..cedcbfa 100644 --- a/nixos/hosts/vps-relay.nix +++ b/nixos/hosts/vps-relay.nix @@ -124,6 +124,11 @@ "bananasimulator.dannydannydanny.me".extraConfig = '' reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8083 ''; + # Bananasimulator BETA — separate service on port 8084 with + # BS_BETA_MODE=1 (cheat menu + faster ripening for testing). + "bananasimulator-beta.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8084 + ''; # KomTolk (formerly translate-platform) — same backend, port 8080. "komtolk.dannydannydanny.me".extraConfig = '' reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8080