Compare commits

..

188 commits

Author SHA1 Message Date
DannyDannyDanny
0eab0d47ae nixos: add bananasimulator-beta service + vhost
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) <noreply@anthropic.com>
2026-06-08 23:25:34 +02:00
DannyDannyDanny
f8a873bd06 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 <noreply@anthropic.com>
2026-06-08 22:27:32 +02:00
DannyDannyDanny
e2cf93e7d6 feat(foreign-port): add WiFi-only laptop as clan machine
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.
2026-06-07 21:44:14 +02:00
DannyDannyDanny
610454f0d2 fix(distant-shore): drop duplicate standalone flake-module (clan-managed now) 🩹 2026-06-07 20:27:34 +02:00
DannyDannyDanny
0cdb4b8697 fix(distant-shore): build on sunken-ship (avoids self-SSH on closure copy) 🔧 2026-06-07 20:25:09 +02:00
DannyDannyDanny
df18b1cfaf feat(distant-shore): generate clan vars (zerotier/data-mesher/dm-pull-deploy) + ZT host entry 🔐 2026-06-07 20:25:09 +02:00
DannyDannyDanny
bbe05c971d feat(distant-shore): add X13 Gen 2 as clan machine w/ shim+MOK secure boot
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).
2026-06-07 20:25:09 +02:00
09f191d10b feat: add studio.dannydannydanny.me vhost 🎨
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 <noreply@anthropic.com>
2026-06-07 15:25:16 +02:00
DannyDannyDanny
05896f6d3b phantom-ship/shipyard: rename poppler_utils → poppler-utils
nixpkgs renamed it; the old attr is now an error alias.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 13:17:33 +02:00
DannyDannyDanny
cc8cc05a08 phantom-ship/shipyard: add media-processing tools for feedback
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) <noreply@anthropic.com>
2026-06-07 13:16:57 +02:00
680c20483c feat: add map.dannydannydanny.me vhost 🗺️
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 <noreply@anthropic.com>
2026-06-07 13:05:55 +02:00
DannyDannyDanny
592e989b03 fix(home): resurrect process list + track zed settings in dotfiles 🏠
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.
2026-06-05 17:19:38 +02:00
DannyDannyDanny
9283643e07 feat(fish): add gco — smart checkout that cds into worktrees 🌿
If the target branch is already checked out in another worktree,
`gco <branch>` cds there instead of erroring with "already used by
worktree at". Falls through to plain `git checkout` otherwise.
2026-06-05 17:18:57 +02:00
DannyDannyDanny
e43a5eb880 sunken-ship: add ffmpeg to mulbo-server PATH
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) <noreply@anthropic.com>
2026-06-04 12:40:45 +02:00
DannyDannyDanny
dc7ef47681 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) <noreply@anthropic.com>
2026-06-04 12:35:22 +02:00
DannyDannyDanny
09d25a1899 sunken-ship: add mutagen to mulbo-server env
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) <noreply@anthropic.com>
2026-05-25 19:17:05 +02:00
DannyDannyDanny
ba51b6bcf7 tmux: add resurrect + continuum so force-quits don't nuke sessions
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) <noreply@anthropic.com>
2026-05-24 20:48:00 +02:00
DannyDannyDanny
b2df891b20 sunken-ship: PurgeMissing = always (valid value; 'missing' was rejected by navidrome 0.61.2)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:35:59 +02:00
DannyDannyDanny
8fcb43f279 sunken-ship: navidrome Scanner.PurgeMissing = missing
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) <noreply@anthropic.com>
2026-05-24 10:35:07 +02:00
Danny
1204584ae4 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) <noreply@anthropic.com>
2026-05-23 12:01:56 +02:00
Danny
cda9c4cf0f sunken-ship: drop python-telegram-bot from fitness-bot pythonEnvs
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) <noreply@anthropic.com>
2026-05-23 11:51:20 +02:00
DannyDannyDanny
3dcbdd408a chore: unpin clan-community now that dm-pull-deploy fix merged 🔧
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.
2026-05-22 21:15:20 +02:00
DannyDannyDanny
b11add8525 Revert "Merge add-catppuccin-forgejo: Catppuccin theme on Forgejo"
This reverts commit 1b0eb5835d, reversing
changes made to 5d4f2048a6.
2026-05-20 20:13:44 +02:00
DannyDannyDanny
9793d5ef7c Revert "phantom-ship/forgejo: switch to catppuccin-mauve-auto (light in light mode)"
This reverts commit cbf0defa34.
2026-05-20 20:13:44 +02:00
DannyDannyDanny
cbf0defa34 phantom-ship/forgejo: switch to catppuccin-mauve-auto (light in light mode)
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-<accent>-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.
2026-05-20 19:31:22 +02:00
DannyDannyDanny
2e9441f367 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.
2026-05-20 19:31:22 +02:00
DannyDannyDanny
1b0eb5835d Merge add-catppuccin-forgejo: Catppuccin theme on Forgejo 2026-05-20 18:46:57 +02:00
DannyDannyDanny
0c11628f73 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/
2026-05-20 18:44:51 +02:00
Hara
5d4f2048a6 hara: heartbeat timer reduced to once daily at 06:07 2026-05-20 15:37:39 +02:00
DannyDannyDanny
0f34d2508d feat: add kf.dannydannydanny.me portfolio vhost
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) <noreply@anthropic.com>
2026-05-17 12:55:58 +02:00
DannyDannyDanny
4fab9a28a2 chore: update flake.lock ⬆️ 2026-05-12 13:57:36 +02:00
DannyDannyDanny
fc9894c32f feat: install zed-editor 2026-05-12 10:13:11 +02:00
DannyDannyDanny
e8158e6c0f monitoring: fix prometheus → alertmanager loopback (IPv4 vs IPv6)
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) <noreply@anthropic.com>
2026-05-10 16:47:37 +02:00
DannyDannyDanny
dc7895e3b2 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) <noreply@anthropic.com>
2026-05-10 16:17:28 +02:00
DannyDannyDanny
3b6f4545b4 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) <noreply@anthropic.com>
2026-05-10 16:12:08 +02:00
DannyDannyDanny
40cc62f65b 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) <noreply@anthropic.com>
2026-05-10 16:02:42 +02:00
Danny
83dd92d738 shipyard staging gets a stable URL: b3.dannydannydanny.me
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) <noreply@anthropic.com>
2026-05-10 14:00:39 +02:00
Danny
067bab125b 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) <noreply@anthropic.com>
2026-05-10 13:12:09 +02:00
DannyDannyDanny
851ee8ea1d 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) <noreply@anthropic.com>
2026-05-10 13:01:05 +02:00
Danny
fb99ef3cff 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) <noreply@anthropic.com>
2026-05-10 12:48:26 +02:00
DannyDannyDanny
c5cabe7531 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) <noreply@anthropic.com>
2026-05-09 22:43:46 +02:00
814993e66b phantom-ship: revert bon to 3B model (7B too slow on CPU)
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.
2026-05-08 20:39:31 +02:00
ccf9eb2859 phantom-ship: bon switches to qwen2.5:7b-instruct for extraction
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).
2026-05-08 15:28:52 +02:00
DannyDannyDanny
eee28d3e9a 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) <noreply@anthropic.com>
2026-05-08 07:23:17 +02:00
327bdc11fe phantom-ship: services.ollama + qwen2.5:3b-instruct for bon extraction 2026-05-08 07:23:08 +02:00
647d748d30 phantom-ship: add tesseract to bon service for OCR 2026-05-08 06:57:06 +02:00
DannyDannyDanny
4525e73f1a sunken-ship: mulbo-server-backfill systemd oneshot
Companion oneshot for mulbo-server: populates the dedup index
(tracks_index) from Navidrome's existing 15k tracks. Without it,
GET /tracks/by-hash misses for every existing offshore track and
the upload path duplicates content.

Inherits same User/SupplementaryGroups as the running service.
chromaprint added to PATH for fpcalc. TimeoutSec=8h covers full
274 GB hashing run with headroom.

Triggered manually — not auto-scheduled:
  sudo systemctl start mulbo-server-backfill
  journalctl -fu mulbo-server-backfill

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 22:30:10 +02:00
082529dac9 phantom-ship + vps-relay: declare bon service + vhost (port 8091)
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
2026-05-07 22:12:03 +02:00
DannyDannyDanny
73d4225f9b 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) <noreply@anthropic.com>
2026-05-06 15:06:51 +02:00
DannyDannyDanny
4debab6f69 sunken-ship: mulbo-server creds via EnvironmentFile + MULBO_MUSIC_ROOT
Adds:
- MULBO_MUSIC_ROOT=/srv/music (for the /folders fs walk)
- EnvironmentFile=/home/danny/.secrets/mulbo-server-navidrome (creds
  for Subsonic API calls — file is mode 600, owned by danny, not in
  source control)

Required for the new /folders endpoint and the upcoming POST /tracks
which needs to call search3.view + startScan.view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:22:37 +02:00
DannyDannyDanny
1744d776e2 sunken-ship: mulbo-server systemd service + pull timer + ZT port 8091
Phase 5 of the mulbo Navidrome-pivot — companion HTTP service co-
located with Navidrome that owns uploads + the dedup index + the
real on-disk folder layout (which Navidrome's tag-virtual API can't
expose). Wire spec lives in the mulbo repo at 20_mulbo/SERVER_API.md.

Runs as `danny` so writes pass through to /home/danny/music/mulbo-
uploads via the existing /srv/music ro bind-mount — no mount changes
needed. Bound to [::]:8091 (8090 was taken by escape-hormuz on
phantom-ship); firewall scopes it to the ZT mesh, same trick bbbot
uses on 8080.

Pulls the python-projects repo via SSH using sunken-ship's id_ed25519
(registered as a read-only deploy key on the repo). Auto-pull timer
runs every 15 min, offset from fitness-bot-pull and dotfiles-rebuild.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:11:05 +02:00
Hara
3de1747e92 hara-heartbeat: strip markdown asterisks/underscores via sed before sending 2026-05-05 14:22:26 +02:00
Hara
7f8badf1d1 hara-heartbeat: plain text only prompt — no markdown asterisks in Telegram output 2026-05-05 14:11:03 +02:00
4e01e62cc0 phantom-ship: dedupe escape-hormuz tmpfiles + service block (rebase artifact) 2026-05-05 09:41:17 +02:00
8a91f3db88 phantom-ship + vps-relay: declare escape-hormuz service + vhost
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
2026-05-05 09:40:11 +02:00
Hara
4600a8e5ca escape-hormuz: add service (port 8090) + escapehormuz.dannydannydanny.me vhost 2026-05-04 23:25:00 +02:00
DannyDannyDanny
d0e9b3f907 phantom-ship + vps-relay: Forgejo on git.dannydannydanny.me
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)
2026-05-04 21:35:03 +02:00
Hara
a9bb775b7d hara-heartbeat: check all 3 Gmail accounts (add wildstylewarrior) 2026-05-04 18:56:38 +02:00
Hara
e952667623 hara-heartbeat: shift schedule to 06/10/14/18 Copenhagen 2026-05-04 18:28:00 +02:00
Hara
c04b463ad0 hara-heartbeat: fix OnCalendar timezone syntax, fire every 4h (08/12/16/20) 2026-05-04 18:27:01 +02:00
9ad8d71f61 phantom-ship: set SHIPYARD_OWNER_ID for owner-only /admin commands 2026-05-04 18:26:20 +02:00
Hara
69d982d0fa 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.
2026-05-04 12:51:33 +02:00
Danny
3604c08650 phantom-ship: scuttle gets SC_TILES_DIR + tmpfiles for OSM tile cache 2026-05-03 19:22:28 +02:00
Danny
f419fed7eb phantom-ship + vps-relay: KomTolk service + vhost (was translate-platform)
KomTolk is the rebranded translate-platform — same Copenhagen
translation gigs Mini App, new name. Service on port 8080, mirrors
shelfish/scuttle/banana setup. New tmpfiles dir + zt+ firewall
opening + caddy vhost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:56:52 +02:00
Danny
08495161ae phantom-ship + vps-relay: add bananasimulator service + vhost
bananasimulator.service mirrors shelfish/scuttle (fastapi + uvicorn
+ httpx + python-telegram-bot). Port 8083. ENV BS_RIPE_MIN_PER_STAGE=2
in prod (30 min total banana lifetime); preview uses 0.5 for fast
testing.

vps-relay gets a fifth vhost (bananasimulator.dannydannydanny.me)
reverse-proxying to phantom-ship over ZeroTier. The shipyard manifest
has been pointing at this URL as a placeholder since day one — now
it's actually live.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:58:12 +02:00
Danny
6d9ccf5d4e phantom-ship + vps-relay: add scuttle service + vhost
scuttle.service mirrors shelfish — fastapi/uvicorn/httpx/python-telegram-bot
plus websockets, runs uvicorn --host :: --port 8082, DB at
~/.local/share/scuttle/scuttle.db (tmpfiles rule + zt+ firewall port
added alongside shelfish's).

vps-relay gets a fourth virtualHost (scuttle.dannydannydanny.me)
reverse-proxying to phantom-ship over ZeroTier. WebSocket upgrade is
transparent under Caddy's reverse_proxy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:26:17 +02:00
Hara
4d2e40455d 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 <noreply@anthropic.com>
2026-05-03 07:14:42 +02:00
Danny
8056e510c5 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.
2026-05-03 06:41:04 +02:00
Danny
f599a76aba phantom-ship: open shelfish (:8081) on ZT iface, bind 0.0.0.0
shelfish was only listening on 127.0.0.1 — vps-relay's Caddy
couldn't reach it over the ZT mesh. Bind 0.0.0.0 and allow 8081
inbound on \`zt+\` interfaces (not the global firewall — same
pattern sunken-ship uses for bbbot).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 06:39:57 +02:00
Danny
0b20c375b5 vps-relay: add shelfish.dannydannydanny.me vhost → phantom-ship ZT 2026-05-03 06:30:07 +02:00
Danny
2aec4d4d5e shelfish: front via vps-relay (don't expose phantom-ship public IP)
Original commit added Caddy directly on phantom-ship and opened
ports 80/443 — that would have exposed the home connection's
public IP via DNS. Reverting that and using the existing relay
pattern instead: vps-relay (Hetzner) terminates public TLS and
reverse-proxies over ZeroTier to phantom-ship's ZT IPv6 on 8081.

phantom-ship now just runs shelfish.service bound to 127.0.0.1:8081;
it accepts connections only from the ZT mesh interface (since
caddy/firewall changes are gone, the only listeners are the
existing trusted-interface ones plus this loopback).

vps-relay gets a third virtualHost alongside navidrome and bbbot.

DNS: shelfish.dannydannydanny.me → 89.167.39.251 (vps-relay public IP),
NOT phantom-ship's home IP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 06:29:48 +02:00
Danny
d787b0ea48 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) <noreply@anthropic.com>
2026-05-03 06:27:21 +02:00
Danny
a7dd6284d8 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) <noreply@anthropic.com>
2026-05-03 06:25:54 +02:00
DannyDannyDanny
af9f735abc feat(phantom-ship): hara-gmail-mcp server (path 1, IMAP+SMTP) 📬
Adds an MCP server exposing read tools (list_inbox, search, read_email)
across three personal Gmail accounts using existing app passwords in
/etc/openclaw/. Wired into claude-channels via --mcp-config. Slated for
replacement by an OAuth2 Gmail+Calendar server in path 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:15:10 +02:00
DannyDannyDanny
771cc58076 feat: vps fail2ban + shared server-debug-tools module 🛡️
VPS public SSH: enable fail2ban with bantime-increment so brute-force
probers get evicted with exponential backoff (1h → 4h → 16h → 2.7d →
10.7d, capped at 30d). Default jail covers sshd; maxretry=5 in 10m.

server-debug-tools: htop, tcpdump, dnsutils, jq, curl. Imported by
sunken-ship + phantom-ship via flake.nixosModules.server-debug-tools.
These are the practical bits we'd otherwise pick up by enabling
clan.core.enableRecommendedDefaults — but the full clan defaults flip
systemd-networkd/resolved on, which broke dnsmasq + navidrome's resolv
.conf bind-mount on the homelab servers, so we cherry-pick instead.
2026-04-25 13:51:19 +02:00
DannyDannyDanny
b8bc17f385 feat(servers): declare SSH authorizedKeys + root mac admin trust 🔑
Move the imperative SSH-key-related scars accumulated during the
clan/VPS rollout into nix config so future installs and rebuilds
reproduce the same state:

- danny@sunken-ship + danny@phantom-ship: trust the mac admin key
  (id_ed25519_<host> on Daniel-Macbook-Air) and the host's own
  self-loopback key (used by clan ssh-ng:// nix-copy-closure back
  to the same host during `clan machines update`).
- root@sunken-ship + root@phantom-ship: trust the mac admin key so
  `clan machines update` can run its SOPS-key upload step that
  SSHes to root@<host> to write /var/lib/sops-nix/key.txt.

Existing key files (~/.ssh/id_ed25519 on each host) stay where they
are; the keypair was generated once during initial bootstrap and the
public side is now declared above. Reinstalls would regenerate and
need the pubkey re-pinned here.
2026-04-25 13:30:40 +02:00
DannyDannyDanny
644420481e fix(sunken-ship): bbbot 8080 only allowed on ZT interface 🔐 2026-04-25 13:26:37 +02:00
DannyDannyDanny
3b5288a48c feat(sunken-ship): bbbot bind dual-stack so VPS Caddy reaches it via ZT IPv6 🪢 2026-04-25 13:17:27 +02:00
DannyDannyDanny
bce34985eb feat(sunken-ship): open firewall :8080 for bbbot via vps-relay 🔓 2026-04-25 13:15:59 +02:00
DannyDannyDanny
4332dfcbb5 chore(clan): point vps-relay at public IPv4 while ZT identity bootstraps 🎯 2026-04-24 17:48:52 +02:00
DannyDannyDanny
ba277b3f49 fix(vps-relay): grub config force-override to resolve dup in mirroredBoots 🐞 2026-04-24 17:43:00 +02:00
DannyDannyDanny
244988d52d fix(vps-relay): switch to GRUB/BIOS — Hetzner Cloud is not UEFI 🧷 2026-04-24 16:05:27 +02:00
DannyDannyDanny
f4738584c3 fix(vps-relay): add virtio modules to initrd so it boots on Hetzner 🛰️ 2026-04-24 14:51:41 +02:00
DannyDannyDanny
914a825587 feat(sunken-ship): trust danny for nix remote builds 🏗️ 2026-04-24 13:47:38 +02:00
DannyDannyDanny
7141582f75 Merge remote-tracking branch 'origin/main' into unruffled-tharp 2026-04-24 13:43:37 +02:00
DannyDannyDanny
47fc658523 feat(clan): add vps-relay + strip bbbot cloudflared 🚢
Stage 4.5: declare a Hetzner-hosted reverse-proxy VPS as a clan machine.

- nixos/hosts/vps-relay.nix: Debian→NixOS cx23 in hel1. Caddy at public
  80/443 reverse-proxies navidrome.dannydannydanny.me and
  bbbot.dannydannydanny.me over ZT to sunken-ship.
- nixos/disko-cloud.nix: simple GPT + ext4 root, no LUKS — cloud provider
  has physical disk anyway and there's no operator at boot.
- flake-modules/clan.nix: register vps-relay as an inventory machine,
  zerotier peer, internet networking target at its clan-generated ZT
  IPv6, and add vps-relay.clan to clanHostsModule /etc/hosts.
- sunken-ship fitness-bot: drop pkgs.cloudflared from PATH + set
  WEBAPP_URL=https://bbbot.dannydannydanny.me. Paired with the bbbot
  upstream patch (start.py honors env WEBAPP_URL and skips cloudflared
  when set) — once the 15-min fitness-bot-pull timer pulls that change,
  bbbot will stop churning trycloudflare.com URLs.

Vars (zerotier identity/ip + sops machine key) generated on sunken-ship
because clan's hermetic sandbox on macOS fails to run the zerotier
identity generator (same workaround as for data-mesher earlier).

VPS install flow: Hetzner-created Debian box, then `clan machines
install vps-relay --target-host root@<public-ipv4>` reinstalls to
NixOS; subsequent updates go over ZT.
2026-04-24 13:43:21 +02:00
Hara
6ef7112ae0 revert: remove danny from openclaw group
Widened access unnecessarily - use sudo -n instead for one-off secret reads.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 11:24:27 +02:00
Hara
e8d4bbf24b Add danny to openclaw group for secret file access 2026-04-24 10:38:00 +02:00
DannyDannyDanny
b0c8664f5c docs: update stale dotfiles/nixos flake paths 📝
Stage 4f cleanup. The flake moved from ~/dotfiles/nixos/ to ~/dotfiles/
in 88c5139; docs and install scripts hadn't been refreshed. Point all
rebuild / flake references at the new root:

- AGENTS.md, README.md, server-quickstart.md, docs/server-installer-usb.md,
  docs/sunken-ship-wifi.md, nixos/readme.md — rebuild command paths.
- scripts/nixos-server-install.sh — auto-detect now looks for flake.nix
  at repo root (was nixos/flake.nix).
- scripts/post-install-provision.sh — first-rebuild hint path.

`nixos/hosts/<host>-hardware.nix` and friends stay where they are —
host-specific NixOS modules still live under nixos/; only the flake
entry-points + sops/ + vars/ + lib/ + modules/ + flake-modules/ moved.

nixos/readme.md rewritten to reflect the split (flake at root, per-host
modules under nixos/).
2026-04-20 20:28:05 +02:00
DannyDannyDanny
754cb0d274 chore(flake): bump clan-community fork (dm-send-deploy narHash skip) 🔖 2026-04-20 20:06:08 +02:00
DannyDannyDanny
22808f39fa feat(clan): re-enable dm-pull-deploy via forked clan-community 🌊
Stage 4e, take 2. Point the clan-community input at our fork's branch
that sanitizes machine.name for data-mesher's file-name validator
(upstream PR: clan/clan-community#25). Revisit this pin once merged.

- flake.nix: clan-community.url → fork + fix branch
- flake-modules/clan.nix: re-adds meta.domain = "clan",
  inventory.instances.data-mesher (sunken-ship bootstrap, both default),
  inventory.instances.dm-pull-deploy (sunken-ship push, both default
  action="switch"), and clanHostsModule that puts /etc/hosts entries
  for <host>.clan → each machine's ZT IPv6 so libp2p multiaddr
  resolution works without a clan-domain DNS server.
- Generator vars for data-mesher + dm-pull-deploy signing keys were
  regenerated on sunken-ship (data-mesher isn't packaged for
  aarch64-darwin, so clan vars generate runs on Linux).
2026-04-20 19:58:16 +02:00
DannyDannyDanny
1d4c6c8f4f Revert "Reapply "feat(clan): data-mesher + dm-pull-deploy wiring 🌊""
This reverts commit d184064bfd.
2026-04-20 14:29:39 +02:00
DannyDannyDanny
d184064bfd Reapply "feat(clan): data-mesher + dm-pull-deploy wiring 🌊"
This reverts commit c4c40e80d5.
2026-04-20 14:28:12 +02:00
DannyDannyDanny
c4c40e80d5 Revert "feat(clan): data-mesher + dm-pull-deploy wiring 🌊"
This reverts commit 6846faa5f1.
2026-04-20 11:40:07 +02:00
DannyDannyDanny
6846faa5f1 feat(clan): data-mesher + dm-pull-deploy wiring 🌊
Stage 4e-a of the clan migration. Set up signed-file gossip
(data-mesher, experimental, clan-core) and pull-based NixOS deploy
(dm-pull-deploy, experimental, clan-community) across both servers.

- sunken-ship is the data-mesher bootstrap node + dm-pull-deploy push
  role; phantom-ship joins via /dns/sunken-ship.clan/tcp/7946/... — the
  hostname resolves via /etc/hosts (clanHostsModule) to sunken-ship's
  ZT IPv6 since we don't run a DNS server for the clan domain.
- Both machines run the dm-pull-deploy default role with
  action="switch": they watch /var/lib/data-mesher/files/home/
  dm_pull_deploy/target and nixos-rebuild switch against the pushed
  git+…?rev=…&narHash=… flake ref on each change.
- Signing keys (shared + per-host status) generated via clan vars
  generate, ran on sunken-ship because data-mesher isn't packaged for
  aarch64-darwin.

The legacy dotfiles-rebuild timer stays installed as a fallback until
dm-pull-deploy is proven; a smart push timer on sunken-ship (calls
dm-send-deploy only when origin/main moves) comes next.
2026-04-20 11:38:01 +02:00
DannyDannyDanny
41b3d217f8 feat(clan): use ZT IPv6 as clan networking target 🛰️
clan-cli's upload / build / copy steps each resolve the SSH target
independently. With `internet.host = "sunken-ship"` (bare hostname),
off-LAN / missing-mDNS cases broke \`clan machines update\` because the
mac couldn't resolve the hostname. Pin both the inventory internet
instance's host AND clan.core.networking.{target,build}Host to each
machine's stable ZT IPv6, so every update path works regardless of
LAN DNS state — and the mac reaches the servers the same way it does
for ssh sunken-ship-zt / phantom-ship-zt.
2026-04-20 10:39:24 +02:00
DannyDannyDanny
0cd4947282 feat(sunken-ship): retire Cloudflare Tunnel for navidrome ☁️💥
Stage 4d of the clan migration. Navidrome is now reachable only over
the ZeroTier mesh (port 4533 on sunken-ship's ZT IPv6 address, or via
the sunken-ship-zt SSH alias). Dropped:

- systemd.services.cloudflare-tunnel
- clan.core.vars.generators.cloudflare-tunnel
- cloudflared from environment.systemPackages
- vars/per-machine/sunken-ship/cloudflare-tunnel/

Manual follow-ups still needed on sunken-ship:
- rm /home/danny/.secrets/cloudflare-tunnel-token  (old unmanaged token)
- delete the tunnel itself in the Cloudflare Zero Trust dashboard
- unlink the DNS record music.dannydannydanny.me if it was separate
2026-04-20 10:36:15 +02:00
DannyDannyDanny
b66dd1d30c fix(ssh): phantom-ship-zt needs the dedicated identity key 🔑 2026-04-20 10:28:34 +02:00
DannyDannyDanny
32cb3b7510 feat(clan): add internet networking instance for LAN reachability 🛣️
clan-cli preferred the zerotier networking export (priority 900, user
defaulted to root@) over our clan.core.networking.targetHost setting,
which broke \`clan machines update\` with "Host key verification failed"
against the ZT IPv6 address as root@. Declaring an inventory.instances
.internet instance with priority 2000 makes clan-cli prefer the LAN
hostname and explicit danny@ user, so updates go over the LAN (ZT
stays available for SSH aliases and service-level use).
2026-04-19 21:09:37 +02:00
DannyDannyDanny
84da9ed8f5 feat(ssh): add zerotier host aliases on mac 🕸️
Home-manager now writes a drop-in at ~/.ssh/config.d/zerotier with
sunken-ship-zt and phantom-ship-zt aliases pointing at the ZT IPv6
addresses. Useful when off the LAN — the aliases route over the
ZeroTier mesh. Requires a one-time \`Include ~/.ssh/config.d/*\` at
the top of ~/.ssh/config.
2026-04-19 21:07:02 +02:00
DannyDannyDanny
7d3fd2d8cf feat(sunken-ship): migrate cloudflare-tunnel-token to clan vars 🔐
Declare a clan.core.vars.generators.cloudflare-tunnel generator that
prompts for the tunnel token on first run and stores it SOPS-encrypted
under vars/per-machine/sunken-ship/cloudflare-tunnel/tunnel-token.
systemd.services.cloudflare-tunnel ExecStart now reads the decrypted
secret at runtime from \${config.clan.core.vars...path} (lives at
/run/secrets/vars/...) instead of the unmanaged
/home/danny/.secrets/cloudflare-tunnel-token file.

Stage 4c of the clan migration. The tunnel itself is slated for
retirement in 4d — ZeroTier-only access after that. Cloudflare token
was rotated during this migration; old value no longer valid.
2026-04-19 21:07:02 +02:00
DannyDannyDanny
c6cb19eff6 vars: update via generator cloudflare-tunnel (machine: sunken-ship) 2026-04-19 21:05:26 +02:00
DannyDannyDanny
88c51399d0 refactor(nix): move flake to repo root 🚚
clan-cli silently ignores the `?dir=` URL parameter when resolving a
flake source, so with the flake at nixos/flake.nix `clan machines
update` fails with "flake.nix does not exist". Move the flake tree up
so the repo root contains flake.nix, flake.lock, flake-modules/, lib/,
modules/, sops/, and vars/. Host-specific NixOS modules stay in
nixos/{hosts,home,fish.nix,neovim.nix,…}; flake-module paths updated
accordingly.

- dotfiles-rebuild flakeRef is now "${dotfilesDir}#<host>" (was
  "${dotfilesDir}/nixos#<host>").
- CLAUDE.md build commands + clan section updated. nixupdate fish alias
  updated. sunken-ship hostsfile comment updated.
- Existing /etc/dotfiles checkouts on the servers will pick up the new
  layout on the next `dotfiles-rebuild` timer tick; the rebuild service
  was pre-updated via rsync so its flakeRef matches before the pull.

Also includes 4b follow-through: zerotier identities are now live on
both servers (sunken-ship=d553a2de33 controller, phantom-ship=6c048abbdc
peer) and IPv6 ping across the ZT mesh works.
2026-04-19 15:19:59 +02:00
DannyDannyDanny
9921a7f9f1 feat(nix): zerotier overlay via clan inventory + mac ZT client 🕸️
Stage 4b of the clan migration. Declares a clan.inventory.instances.zerotier
instance with sunken-ship as controller and phantom-ship as peer (controller
is also listed as a peer so it joins its own network). Generates the network
ID, controller identity, and per-peer identities via `clan vars generate`;
all secrets are SOPS-encrypted to the user's age key and the per-machine
age keys.

- nixos/sops/ — clan-managed SOPS state (user + per-machine age keys).
- nixos/vars/ — shared + per-machine zerotier vars; *-identity-secret
  files are SOPS-encrypted, *.value files are plain public data.
- clan.core.networking.{targetHost,buildHost} = "danny@<host>" on both
  servers so `clan machines update` knows where to push and build.
- mac gets `zerotier-one` installed as a homebrew cask; authorization
  on the controller happens manually by node-ID in a follow-up step.

Known rough edges (to chase in later stages):
- zerotier-inventory-autoaccept.service races zerotierone.service on
  first activation (connection refused against the local API). Retrying
  the unit succeeds; clan upstream bug.
- Deployment must go through `clan machines update`, not plain
  nixos-rebuild, or the per-host SOPS age key isn't uploaded and
  zerotier-one can't decrypt its identity.
2026-04-19 14:43:29 +02:00
DannyDannyDanny
29ff1c9be7 feat(nix): bootstrap clan-core for sunken-ship + phantom-ship 🏴‍☠️
Stage 4a of the dendritic + clan migration. Both servers now live under
clan.machines (via nixos/flake-modules/clan.nix) and clan-core generates
their nixosConfigurations for us; the previous per-host flake-modules
are removed.

Notes:
- clan.core.enableRecommendedDefaults = false on both machines so we
  keep the existing dhcpcd / non-networkd / non-resolved stack. Services
  like dnsmasq, navidrome, and the existing wireless setup break with
  the clan defaults on.
- dotfiles-rebuild timer is untouched (safety net). Replacing it with
  clan machines update / dm-pull-deploy comes in 4e.
- mac stays outside the clan as admin only.

Verified: `clan machines list --flake path:…/nixos` returns both hosts;
both servers rebuild cleanly and all services (navidrome, cloudflare-
tunnel, fitness-bot, dnsmasq, openclaw-gateway, sshd) stay active.
2026-04-19 13:54:44 +02:00
DannyDannyDanny
663be7872a fix(neovim): set withRuby and withPython3 explicitly to false 🔇 2026-04-19 13:48:25 +02:00
DannyDannyDanny
c3742db32e feat(phantom-ship): add shipyard systemd service 🚢
Telegram bot hub that lists mini-apps and collects feedback via ForceReply.
Code deployed via rsync to /home/danny/shipyard/; token at
~danny/.secrets/telegram-bot-token-shipyard.
2026-04-19 13:20:27 +02:00
Hara
14e60ca839 phantom-ship: add openai-whisper + ffmpeg for voice transcription 2026-04-18 23:05:24 +02:00
DannyDannyDanny
9566986ade fix: move permission bypass to settings.json to avoid warning dialog 🔧 2026-04-18 22:47:21 +02:00
DannyDannyDanny
7f40280700 feat: skip permission prompts for claude-channels unattended use 🤖 2026-04-18 22:45:03 +02:00
DannyDannyDanny
6500ad39bf fix: gate openclaw-gateway hardening on enable flag 🔧 2026-04-18 22:28:32 +02:00
DannyDannyDanny
40627405f7 feat: add claude-channels systemd service on phantom-ship 🤖
Claude Code Channels replaces OpenClaw for the @HarakatBot Telegram
bridge. Uses claude.ai subscription auth via long-lived OAuth token
at /etc/claude-channels/env — sidesteps the API rate limits OpenClaw
was hitting.

Runs as danny since plugin + pairing state lives in ~/.claude.
Wraps claude in script(1) because claude needs a PTY for its
interactive session mode.

OpenClaw service disabled but config kept for easy rollback during
validation. Will be fully removed once Channels is proven stable.
Her workspace (SOUL/MEMORY/IDENTITY/etc) is preserved in
vimwiki/openclaw/workspace/.
2026-04-18 22:27:28 +02:00
DannyDannyDanny
975b2a3ee9 refactor(nix): auto-load flake-modules + extract shared dotfiles-rebuild 🌳
- Add import-tree input; flake.nix now auto-loads every file under
  ./flake-modules so new hosts/features drop in without editing flake.nix.
- Extract the duplicated dotfiles-rebuild service, timer, and
  safe.directory wiring into nixos/modules/dotfiles-rebuild.nix, exposed
  via flake.nixosModules.dotfiles-rebuild.
- sunken-ship and phantom-ship now pull it in from their flake-modules;
  hostname-specific flakeRef is derived from config.networking.hostName.
2026-04-18 18:00:54 +02:00
DannyDannyDanny
5e7b76bdcf fix(servers): declare safe.directory in /etc/gitconfig 🔒
The dotfiles-rebuild service runs as root, but /etc/dotfiles is owned
by `danny`. The GIT_CONFIG_* env vars in the service unit only affect
the git CLI — nix/libgit2 reads safe.directory from /etc/gitconfig.
After a recent nixpkgs bump libgit2 now enforces this strictly, so the
service was failing to evaluate the flake.

Enable programs.git and set programs.git.config.safe.directory =
[ dotfilesDir ] on both sunken-ship and phantom-ship so the trust is
persistent and Nix-managed.
2026-04-18 17:29:11 +02:00
DannyDannyDanny
c69c7c9b11 refactor(nix): dedupe home-manager wiring across hosts ♻️
Extract the per-host home-manager block (useGlobalPkgs, useUserPackages,
backupFileExtension, users.<name> with username/homeDirectory/optional
stateVersion/optional imports) into nixos/lib/home-manager-user.nix.
Each flake-module now imports it with its per-host parameters, removing
~40 lines of boilerplate across the four hosts.
2026-04-18 17:20:51 +02:00
DannyDannyDanny
00ab64d83c Merge remote-tracking branch 'origin/main' into unruffled-tharp 2026-04-18 17:00:26 +02:00
DannyDannyDanny
c434a479a5 refactor(nix): migrate to flake-parts, drop specialArgs ♻️
- Convert flake.nix to flake-parts.lib.mkFlake; split each host into
  its own module under nixos/flake-modules/.
- Replace zen-browser specialArgs plumbing with a nixpkgs overlay so
  home.nix can just reference pkgs.zen-browser.
2026-04-18 17:00:19 +02:00
DannyDannyDanny
af486e8a33 fix: allow unfree claude-code package on phantom-ship 🔓 2026-04-18 16:48:58 +02:00
DannyDannyDanny
7ad82a41b1 fix: permit openclaw 2026.4.12 on phantom-ship 🔓
Nixpkgs bumped openclaw version; keep both permitted so rebuild works
until we fully cut over to channels and remove the input.
2026-04-18 16:48:33 +02:00
DannyDannyDanny
d0d25160c8 feat: add bun + claude-code to phantom-ship for channels migration 🚀
Claude Code Channels will replace OpenClaw for the Telegram bot.
Channels uses claude.ai subscription auth instead of pay-as-you-go
API, sidestepping the rate limits Hara has been hitting.
2026-04-18 16:48:10 +02:00
DannyDannyDanny
a36b90e656 fix(sunken-ship): set fsType=none on /srv/music bind mount
nixos-rebuild was failing with "fsType accessed but has no value
defined" on newer nixpkgs. Bind mounts need fsType=none explicitly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 14:35:27 +02:00
DannyDannyDanny
5db45664ab feat(sunken-ship): auto-pull fitness bot from GitHub every 15 min
New fitness-bot-pull service + timer, modeled on dotfiles-rebuild.
Checks origin/main for new commits, pulls + restarts the service if
the HEAD moved. Offset by 7 min from dotfiles-rebuild to avoid
overlap.

Code now lives at github.com/DannyDannyDanny/bigbiggerbiggestbot
(cloned to /home/danny/tg_fitness_bot). workouts.db is gitignored
so it's preserved across pulls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 14:33:15 +02:00
DannyDannyDanny
b667f7c247 feat: add feishin + disable uhk 2026-04-16 09:46:30 +02:00
DannyDannyDanny
d1b0742f32 chore(disable): UHK - x86_64-linux only 2026-04-16 09:45:05 +02:00
DannyDannyDanny
72d8714e51 feat(neovim): add markdown folding by heading via Treesitter 📝 2026-04-10 12:05:34 +02:00
DannyDannyDanny
be6dde6f0a feat(sunken-ship): add cloudflare tunnel for external access 🌐
Exposes navidrome via music.dannydannydanny.me.
Bypasses CGNAT — no port forwarding needed.
Token stored outside repo at ~/.secrets/cloudflare-tunnel-token.
2026-04-06 21:19:38 +02:00
DannyDannyDanny
76f63f0ae3 fix(sunken-ship): move navidrome music folder to /srv/music — ProtectHome bypass 🎵 2026-04-06 15:36:07 +02:00
DannyDannyDanny
c31ca7d473 feat(sunken-ship): add navidrome user to users group for music dir access 🎵 2026-04-06 14:46:24 +02:00
DannyDannyDanny
f0d52aed04 feat(darwin): add uhk-agent to homebrew casks 🎹 2026-04-06 13:40:37 +02:00
DannyDannyDanny
300849b8c6 fix: neovim extraLuaConfig→initLua, remove uhk-agent (linux-only) 🔧
- programs.neovim.extraLuaConfig renamed to initLua in nixpkgs unstable
- uhk-agent is x86_64-linux only, removed from darwin home config;
  macOS: download .dmg from ultimatehackingkeyboard.com
2026-04-06 12:15:10 +02:00
DannyDannyDanny
4bccb6e6a8 fix(sunken-ship): add audioconvert to uxplay pipeline — fixes ALAC format error 🎵
feat(home): add uhk-agent for UHK keyboard configuration 🎹
2026-04-06 11:55:06 +02:00
DannyDannyDanny
1c7794e904 fix: remove rusty-anchor Mac dependency from alacritty-sync-system-theme 🧹
rusty-anchor now switches themes independently via systemd timer + sunrise-sunset
API — no longer needs the Mac to push changes over SSH
2026-04-06 10:37:38 +02:00
DannyDannyDanny
74eb3a9c40 feat: rusty-anchor WoL, auto dark/light VT theme, wakeonlan on phantom-ship 🦀
- Enable Wake-on-LAN (magic packet) on rusty-anchor enp2s0 via systemd service
- Add vt-theme script to rusty-anchor: switches between Catppuccin Latte/Mocha
- Theme state persisted in /etc/vt-theme, applied on login via profile.d
- alacritty-sync-system-theme.sh now SSHes to rusty-anchor and pushes the
  macOS light/dark change (best-effort, non-blocking, skips if unchanged)
- Add wakeonlan to phantom-ship packages (wakeonlan 00:16:cb:87:20:ba)
2026-04-04 21:18:44 +02:00
OpenClaw Bot
0985503002 phantom-ship: add openai-whisper to openclaw service path 2026-04-04 14:14:12 +02:00
DannyDannyDanny
3813206a3e feat: add nodejs and python3 to phantom-ship for openclaw plugins 📦 2026-04-04 13:38:04 +02:00
DannyDannyDanny
52649f500a feat: add git/nodejs to openclaw, configure GitHub PAT credential helper 🔑
Adds git and nodejs to openclaw-gateway service PATH. Configures a
git credential helper that reads a fine-grained PAT from
/etc/openclaw/github-token. Creates /var/lib/openclaw/repos for
repo clones.
2026-04-04 12:06:08 +02:00
DannyDannyDanny
369e96cbd7 security: harden openclaw-gateway systemd service 🛡️
ProtectSystem=strict, ProtectHome=read-only, PrivateTmp,
NoNewPrivileges. Only /var/lib/openclaw and /etc/openclaw
are writable.
2026-04-04 11:27:05 +02:00
DannyDannyDanny
4544635ad6 security: remove initialPassword from phantom-ship config 🔒
Password is locked in shadow and SSH is key-only, so the
initialPassword served no purpose and was a minor security concern.
2026-04-04 11:26:54 +02:00
DannyDannyDanny
4d6b64dee9 fix: add nixos-rebuild to dotfiles-rebuild PATH on sunken-ship 🔧 2026-04-04 11:26:09 +02:00
DannyDannyDanny
8ce36f8726 feat: add Flipper Zero tools (dfu-util + qFlipper) 🐬 2026-04-04 11:25:54 +02:00
DannyDannyDanny
f3854af82a fix: grant openclaw write access to config dir 🔧 2026-04-03 14:38:03 +02:00
DannyDannyDanny
0de86837df fix: set gateway.mode=local for openclaw on phantom-ship 🔧 2026-04-03 14:36:57 +02:00
DannyDannyDanny
49165590a6 feat: add fitness bot systemd service to sunken-ship
Code deployed separately via rsync (private repo, not referenced here).
Expects code at /home/danny/tg_fitness_bot/ and token at
~/.secrets/bigbiggerbiggestbot.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:15:51 +02:00
DannyDannyDanny
cfa2834516 fix: permit insecure openclaw package on phantom-ship 🔓 2026-04-03 12:05:42 +02:00
DannyDannyDanny
b04b53f9c4 feat: add OpenClaw gateway to phantom-ship 🤖
Telegram bot via nix-openclaw NixOS module. Secrets (API key,
bot token) loaded from /etc/openclaw/ at runtime. Telegram user
ID read from gitignored openclaw-allow-from.nix.
2026-04-03 12:02:02 +02:00
DannyDannyDanny
d1ab7d9a69 feat: blank phantom-ship display after 60s idle 🖥️
Same consoleblank + backlight-off pattern as sunken-ship.
2026-04-02 09:16:08 +02:00
DannyDannyDanny
5fcb54cc63 feat: NAT + DHCP on phantom-ship ethernet for rusty-anchor install 🌐
Shares WiFi internet to rusty-anchor over ethernet via dnsmasq DHCP
and iptables NAT. Rusty-anchor gets DHCP on 10.0.0.x with phantom-ship
as gateway and DNS.
2026-04-01 13:04:19 +02:00
DannyDannyDanny
14c29945eb chore: add server alerting to TODO 🔔
phantom-ship lost power unnoticed; want alerting when servers go down.
2026-04-01 10:23:36 +02:00
DannyDannyDanny
a5f0d36d82 chore: claim rusty-anchor as next hostname 🦀
Old iMac G4 / Power Mac G4 (PowerPC) — will run OpenBSD.
2026-03-31 17:19:45 +02:00
DannyDannyDanny
c43cd0ee17 fix: enable redistributable firmware on phantom-ship 📡
iwlwifi (Intel 8260 WiFi), GPU, and Bluetooth firmware were missing.
2026-03-31 15:41:33 +02:00
DannyDannyDanny
1bfd96c0d0 feat: enable WiFi on phantom-ship 📶
Uses /etc/wpa_supplicant.conf for credentials (outside repo),
same pattern as sunken-ship.
2026-03-31 15:36:34 +02:00
DannyDannyDanny
9f73571f55 fix: restore bootloader config in phantom-ship hardware nix 🥾
Accidentally stripped systemd-boot config when cleaning up duplicate
fileSystems entries.
2026-03-31 15:33:23 +02:00
DannyDannyDanny
420f3881b5 feat: add phantom-ship real hardware config 🖥️
Generated by nixos-generate-config during install; cleaned up
duplicate bind-mount entries from chroot detection.
2026-03-31 14:52:57 +02:00
DannyDannyDanny
92593c7d0a fix: add initialPassword fallback for phantom-ship console login 🔑
No password was set, locking out console access. initialPassword gives
a known fallback until SSH key is installed and password is changed.
2026-03-31 14:46:19 +02:00
DannyDannyDanny
245eb912a9 fix: find git/nix in PATH before cloning dotfiles in provisioning 🔧
Live installer strips PATH under sudo; extend PATH to include nix
profile dirs. Prefer git directly if available, fall back to nix run.
No chroot involved.
2026-03-31 14:30:25 +02:00
DannyDannyDanny
ef6e303a60 fix: run git from live system instead of chroot in provisioning 🔧
chroot had no nix in PATH; clone directly into /mnt/etc/dotfiles
from the live installer environment instead.
2026-03-31 14:28:35 +02:00
DannyDannyDanny
f327b8e868 feat: add post-install provisioning script 🛠️
Standalone script for completing provisioning after disko-install
(mounts installed system, clones dotfiles, installs SSH key, generates
hardware config). Run via curl for single-command provisioning.
2026-03-31 14:26:46 +02:00
DannyDannyDanny
c7793b68ea fix: detect already-open LUKS device in post-install provisioning 🔐
disko-install leaves the LUKS device open; re-opening failed with
"Device crypted already exists". Now detects the open mapper and
skips the redundant cryptsetup open call.
2026-03-31 14:24:58 +02:00
DannyDannyDanny
d4dbd73a8c feat(nixos): add phantom-ship host and streamline server installer
- New host config: phantom-ship.nix (SSH, auto-rebuild, nix-ld, Ethernet)
- Hardware stub: phantom-ship-hardware.nix (replaced by install script)
- Add phantom-ship to flake.nix with home-manager
- Install script now auto-provisions post-install:
  - Clones dotfiles to /etc/dotfiles
  - Installs SSH public key (SSH_PUBKEY_FILE env var)
  - Generates hardware config
  - Supports INSTALLER_HOSTNAME and INSTALLER_DISK env vars
- Fix bootstrap-install.sh default branch to main
- Update CLAUDE.md and server-installer-usb.md
2026-03-31 11:37:15 +02:00
DannyDannyDanny
2c9cf1e8b4 docs: restore USB installer and encryption TODOs 📝
Sunken-ship is not actually encrypted (plain ext4). USB installer
workflow still needs refinement.
2026-03-31 10:10:04 +02:00
DannyDannyDanny
42462f57a2 docs: replace completed TODOs with Tailscale investigation 📝 2026-03-31 10:08:30 +02:00
DannyDannyDanny
33e2e327b5 fix(nixos): increase UxPlay ALSA buffer to reduce audio chop 🐛
WiFi jitter causes underruns with default buffer. Set buffer-time
to 200ms for smoother playback.
2026-03-31 10:01:22 +02:00
DannyDannyDanny
384b84fec2 fix(nixos): enable nix-ld on sunken-ship for Claude Code 🐛
The Claude Code remote CLI is a dynamically linked binary that
fails on NixOS without a standard ld-linux stub. nix-ld provides it.
2026-03-30 23:52:03 +02:00
DannyDannyDanny
81c510ca16 docs: deduplicate AGENTS.md, defer to CLAUDE.md 📝
Remove rebuild protocol, repo rules, SSH key strategy, and server
bootstrap info that was duplicated from CLAUDE.md. Keep only
agent-specific operational details and learnings.
2026-03-30 23:44:38 +02:00
DannyDannyDanny
d7bd99744c docs: update nixos/readme.md with current host targets 📝
Replace stale #macbookair example with current macOS, WSL, and
sunken-ship rebuild commands.
2026-03-30 23:44:18 +02:00
DannyDannyDanny
d9e569d477 docs: remove stale macbookair/tmux refs from CLAUDE.md 📝 2026-03-30 23:44:02 +02:00
DannyDannyDanny
ee4c2db93f refactor: consolidate tmux config into home-manager 🎨
Remove system-level tmux.nix; home.nix is now the single source.
Port resize-pane shortcuts (H/J/K/L) from the old config.
2026-03-30 18:12:41 +02:00
DannyDannyDanny
e997a83c93 refactor(neovim): migrate to extraLuaConfig 🎨
Move Lua config out of VimScript heredoc into proper extraLuaConfig.
Use vim.opt and vim.keymap.set instead of legacy set/nnoremap.
Keep VimScript only for settings that are simpler in vim (colorscheme,
netrw, let g: vars).
2026-03-30 18:03:37 +02:00
DannyDannyDanny
533e5810a9 refactor(wsl): move user packages to home-manager 🎨
Enable home-manager on WSL, importing the shared home.nix config.
Remove duplicate packages and env vars from wsl.nix that are now
provided by home-manager (git, ripgrep, fzf, direnv, etc.).
2026-03-30 18:03:11 +02:00
DannyDannyDanny
6c057d945e chore: gitignore result symlink and openclaw dir 🙈 2026-03-30 18:02:27 +02:00
DannyDannyDanny
e44ef1fdcc chore: remove legacy macbookair host config 🔥
Superseded by daniel-macbook-air.nix (nix-darwin) and wsl.nix.
Also removes its orphaned hardware-configuration.nix.
2026-03-30 18:02:03 +02:00
DannyDannyDanny
69e07dbc14 chore: remove unused uxplay.nix 🔥
AirPlay config is inline in sunken-ship.nix; this file was never
imported.
2026-03-30 18:01:23 +02:00
DannyDannyDanny
657e250f75 fix(nixos): quote UxPlay alsasink GStreamer pipeline arg 🔧 2026-03-30 16:17:14 +02:00
DannyDannyDanny
cea6913cf3 fix(nixos): route UxPlay audio directly to ALSA on sunken-ship 🔊
Drop PipeWire (WirePlumber fails to detect ALSA cards without a
graphical session). Use GStreamer alsasink with plughw:USB,0 to
output directly to the Scarlett Solo.
2026-03-30 16:16:18 +02:00
DannyDannyDanny
84715596f5 feat(nixos): add PipeWire and fix UxPlay audio on sunken-ship 🔊
Enable PipeWire with ALSA/PulseAudio compat so GStreamer can output
audio. Move UxPlay to a user service with linger so it can reach
PipeWire. Add danny to audio group, add alsa-utils for debugging.
2026-03-30 15:49:35 +02:00
DannyDannyDanny
ee2fa1e5f1 feat(nixos): add UxPlay systemd service on sunken-ship 🔊
Audio-only AirPlay receiver that starts at boot, advertises as
"sunken-ship", and auto-restarts on failure.
2026-03-30 15:44:00 +02:00
DannyDannyDanny
e2b820aac0 feat(nixos): add UxPlay AirPlay receiver to sunken-ship 📡
Enable Avahi for mDNS discovery and open firewall ports for
AirPlay mirroring (TCP 7000-7100, UDP 5353/6000-6001/7011).
2026-03-30 15:38:27 +02:00
DannyDannyDanny
d8e5cbe26a fix(nixos): add safe.directory for dotfiles-rebuild service 🔧
Git refuses to operate on /etc/dotfiles owned by danny when the
service runs as root. Pass safe.directory via environment variables.
2026-03-30 14:20:31 +02:00
DannyDannyDanny
afbc87be2b fix(macos): sync Neovim Catppuccin with system appearance
Write ~/.local/share/nvim_color_scheme from the same macOS Appearance probe as Alacritty; trim the nvim theme line read for robustness.

Made-with: Cursor
2026-03-25 14:51:31 +01:00
DannyDannyDanny
bded1b359d fix(macos): install disk-inventory-x via Homebrew cask
Use the Homebrew cask on Apple Silicon because the nixpkgs package is x86_64-darwin only, and document the reason inline to prevent future regressions.

Made-with: Cursor
2026-03-25 10:54:41 +01:00
DannyDannyDanny
309d97c708 feat(nixos): add scheduled garbage collection and optimization for Nix 🎨
Implement launchd daemons for automatic Nix garbage collection and store optimization on a weekly schedule. The configuration includes intervals for both tasks to ensure efficient management of Nix store resources.
2026-03-25 10:49:37 +01:00
DannyDannyDanny
be4233a53b feat(macos): install Google Chrome via Homebrew cask
Enable declarative Homebrew cask management on the macOS host so Google Chrome is installed during darwin activation and stale Homebrew items are cleaned up with zap.

Made-with: Cursor
2026-03-24 15:17:19 +01:00
DannyDannyDanny
463249961e fix(nixos): replace removed light option and harden char-count script
Restore flake checks by removing deprecated `programs.light` from sunken-ship and switching to brightnessctl guidance. Also clean up flake formatting and make the Raycast char-count script safer for empty input.

Made-with: Cursor
2026-03-24 12:58:40 +01:00
DannyDannyDanny
befe2f8a5b chore: remove unused Alacritty duplicates and dead script
Clean up legacy Alacritty theme files and an unreferenced theme-detection script, and fix README links to existing setup docs.

Made-with: Cursor
2026-03-24 10:20:10 +01:00
DannyDannyDanny
f9edde90e4 fix(macos): make Alacritty system-theme sync robust
New setup follow-up: ensure activation seeds a writable active-colors file and make theme sync always enforce the current system appearance.

Made-with: Cursor
2026-03-24 10:19:41 +01:00
DannyDannyDanny
82ce5a7fe8 Rename macOS nix-darwin host to daniel-macbook-air.nix
Match hostname Daniel-Macbook-Air; update flake and docs.

Made-with: Cursor
2026-03-23 19:36:39 +01:00
DannyDannyDanny
ca0d38316f docs(agents): link macOS Alacritty system-theme notes from AGENTS.md
Made-with: Cursor
2026-03-23 19:16:22 +01:00
DannyDannyDanny
b311e21d5b feat(macos): Alacritty follows system light/dark appearance
New setup — due for review after you run darwin-rebuild switch and
live with it for a few days. See CLAUDE.md (Alacritty) and
assets/alacritty/README.md.

- HM: import active-colors.toml + Catppuccin latte/mocha fragments
- nix-darwin: launchd.user.agents.alacritty-system-theme + PATH helper
- fish: background sync on Darwin; theme.sh no longer rebuilds for Alacritty
- Remove switch-alacritty-theme.sh (sed + darwin-rebuild per toggle)

Made-with: Cursor
2026-03-23 19:16:05 +01:00
168 changed files with 5005 additions and 1505 deletions

9
.gitignore vendored
View file

@ -11,3 +11,12 @@ env/
# Installer ISO live WiFi (SSID/PSK); see docs/server-installer-usb.md # Installer ISO live WiFi (SSID/PSK); see docs/server-installer-usb.md
nixos/installer-wifi.nix nixos/installer-wifi.nix
# Nix build output symlink
result
# OpenClaw: Telegram user ID (not committed to public repo)
nixos/hosts/openclaw-allow-from.nix
# Archived / local-only directories
openclaw-documents-repo/

View file

@ -1,53 +1,24 @@
# Agent Instructions # Agent Instructions
## Nix/Darwin Rebuilds See **CLAUDE.md** for build commands, rebuild protocol, flake architecture, repo rules, and SSH key strategy. This file covers agent-specific operational details.
**IMPORTANT**: When making changes to Nix configuration files (e.g., `nixos/home/danny/home.nix`, `nixos/flake.nix`, etc.), **always ask the user to rebuild** before assuming packages are available. ## Running commands on sunken-ship
To rebuild: From the Mac, agents can SSH to sunken-ship:
```bash
cd ~/dotfiles/nixos
darwin-rebuild switch --flake .
```
Do not automatically run rebuild commands - ask the user first.
## Repo is public
No keys, tokens, or identifying secrets in the repo. Prefer `scp` or config outside the repo.
## SSH keys (one key per purpose)
We use **one key per purpose**, not one per machine: separate keys for server access, GitHub, Forgejo (and other forges if needed). Benefits: limit blast radius if a key is compromised; clear revocation; clear which key is for what.
- **Key names:** e.g. `id_ed25519_github`, `id_ed25519_forgejo`, `id_ed25519_servers` (Ed25519 preferred).
- **Config:** Use `~/.ssh/config` with `IdentityFile` and `IdentitiesOnly yes` per host so the right key is used. Keys and sensitive config stay outside the repo.
- **Server / NixOS:** Use actual key names on the machine (e.g. `id_ed25519_github`), not a generic `id_ed25519` (see Learnings below).
## Server installer USB (new machines only)
- Build: from **Linux** `cd ~/dotfiles/nixos && nix build .#installer-iso` (ISO is x86_64-linux only; cannot build on macOS). Or use official NixOS minimal ISO, write to USB, boot server, clone repo, run [scripts/nixos-server-install.sh](scripts/nixos-server-install.sh). See [docs/server-installer-usb.md](docs/server-installer-usb.md). Optional live WiFi: add `nixos/installer-wifi.nix` (gitignored) when building custom ISO on Linux.
## Learnings (NixOS server)
- Minimal ISO: use Ethernet or the graphical installer (WiFi on minimal is fiddly).
- Server hardware: stub in repo; user replaces with `nixos-generate-config --show-hardware-config` from the server.
- Root password: console only; set dannys password as root once for sudo.
- SSH keys: use actual key names on the machine (e.g. `id_ed25519_github`), not assumed `id_ed25519`.
## Server (sunken-ship)
- **Commit and push** before testing on the server; it clones/pulls from origin.
- Bootstrap: server has no git until first rebuild. Use `nix run --extra-experimental-features "nix-command flakes" nixpkgs#git` to clone. Enable flakes in the daemon via `server-configuration-with-flakes.nix`: scp to server `/tmp/configuration.nix`, on server `sudo cp` to `/etc/nixos/configuration.nix`, then `sudo nixos-rebuild switch`. Then build flake and run `switch-to-configuration switch` (see nixos/readme.md).
- Auto-rebuild timer (`dotfiles-rebuild`) only runs after the system has been switched to the flake config. Check with `systemctl is-active dotfiles-rebuild.timer` on the server.
### Running commands on sunken-ship
From the Mac (where the dotfiles workspace lives), agents can SSH to sunken-ship to run commands. Use the sunken-ship key and the host alias or IP the user has configured (e.g. `ssh -i ~/.ssh/id_ed25519_sunken_ship danny@sunken-ship` or `danny@192.168.1.x`). Example:
```bash ```bash
ssh -i ~/.ssh/id_ed25519_sunken_ship danny@sunken-ship 'hostname; ip addr' ssh -i ~/.ssh/id_ed25519_sunken_ship danny@sunken-ship 'hostname; ip addr'
``` ```
Rebuild on the server (flake is in `nixos/`): `ssh ... 'cd /etc/dotfiles/nixos && sudo nixos-rebuild switch --flake .#sunken-ship'`. The server has WiFi (see [docs/sunken-ship-wifi.md](docs/sunken-ship-wifi.md)); it remains reachable when ethernet is unplugged. Rebuild on the server: `ssh ... 'cd /etc/dotfiles && sudo nixos-rebuild switch --flake .#sunken-ship'`. The server has WiFi; it remains reachable when ethernet is unplugged. Preferred from the mac: `nix run git+https://git.clan.lol/clan/clan-core#clan-cli -- machines update sunken-ship --flake ~/dotfiles`.
## Server installer USB (new machines only)
Build from **Linux**: `cd ~/dotfiles && nix build .#installer-iso` (x86_64-linux only; cannot build on macOS). Or use official NixOS minimal ISO, write to USB, boot server, clone repo, run [scripts/nixos-server-install.sh](scripts/nixos-server-install.sh). See [docs/server-installer-usb.md](docs/server-installer-usb.md). Optional live WiFi: add `nixos/installer-wifi.nix` (gitignored) when building custom ISO on Linux.
## Learnings (NixOS server)
- Minimal ISO: use Ethernet or the graphical installer (WiFi on minimal is fiddly).
- Server hardware: stub in repo; user replaces with `nixos-generate-config --show-hardware-config` from the server.
- Root password: console only; set danny's password as root once for sudo.
- SSH keys: use actual key names on the machine (e.g. `id_ed25519_github`), not assumed `id_ed25519`.

View file

@ -2,21 +2,28 @@
## Build commands ## Build commands
The flake lives at the repo root (`~/dotfiles/flake.nix`) — clan-cli doesn't handle flakes in subdirs.
```bash ```bash
# macOS (from ~/dotfiles/nixos) # macOS (from ~/dotfiles)
darwin-rebuild switch --flake . darwin-rebuild switch --flake .
# NixOS server (SSH from mac, or on server) # NixOS servers (SSH from mac, or on server)
sudo nixos-rebuild switch --flake .#sunken-ship sudo nixos-rebuild switch --flake .#sunken-ship
sudo nixos-rebuild switch --flake .#phantom-ship
# WSL # WSL
sudo nixos-rebuild switch --flake ~/dotfiles/nixos#wsl sudo nixos-rebuild switch --flake ~/dotfiles#wsl
# Update flake + rebuild (fish alias: nixupdate) # Update flake + rebuild (fish alias: nixupdate)
cd ~/dotfiles/nixos && sudo nix flake update && sudo darwin-rebuild switch --flake ~/dotfiles/nixos#Daniel-Macbook-Air cd ~/dotfiles && sudo nix flake update && sudo darwin-rebuild switch --flake ~/dotfiles#Daniel-Macbook-Air
# Installer ISO (Linux only, cannot build on macOS) # Installer ISO (Linux only, cannot build on macOS)
cd ~/dotfiles/nixos && nix build .#installer-iso cd ~/dotfiles && nix build .#installer-iso
# Clan push update (from mac; builds on target so aarch64-darwin → x86_64-linux works)
nix run git+https://git.clan.lol/clan/clan-core#clan-cli -- \
machines update sunken-ship --flake ~/dotfiles
``` ```
## Rebuild protocol ## Rebuild protocol
@ -28,13 +35,13 @@ cd ~/dotfiles/nixos && nix build .#installer-iso
- **Flake:** `nixos/flake.nix` — single flake for all hosts - **Flake:** `nixos/flake.nix` — single flake for all hosts
- **Inputs:** nixpkgs-unstable, nix-darwin, home-manager, nixos-wsl, disko, zen-browser - **Inputs:** nixpkgs-unstable, nix-darwin, home-manager, nixos-wsl, disko, zen-browser
- **Host configs** in `nixos/hosts/`: - **Host configs** in `nixos/hosts/`:
- `macos.nix` — Apple Silicon MacBook Air (aarch64-darwin, nix-darwin) - `daniel-macbook-air.nix` — hostname `Daniel-Macbook-Air` (aarch64-darwin, nix-darwin)
- `sunken-ship.nix` — NixOS home server (x86_64-linux) - `sunken-ship.nix` — NixOS home server (x86_64-linux, WiFi + AirPlay)
- `phantom-ship.nix` — NixOS home server (x86_64-linux, Ethernet)
- `wsl.nix` — WSL (x86_64-linux) - `wsl.nix` — WSL (x86_64-linux)
- `macbookair.nix` — old MacBook Air NixOS/WSL config - `server-install.nix` — disko-install target (LUKS)
- `server-install.nix` — disko-install target (LUKS + WiFi) - **Home Manager:** integrated on macOS, WSL, and sunken-ship; user config in `nixos/home/danny/home.nix`
- **Home Manager:** integrated via `home-manager.darwinModules.home-manager` on macOS; user config in `nixos/home/danny/home.nix` - **Shared modules:** `nixos/fish.nix` (fish + bash), `nixos/ollama.nix`
- **Shared modules:** `nixos/fish.nix` (fish + bash), `nixos/tmux.nix`, `nixos/ollama.nix`
- **Darwin config name:** `Daniel-Macbook-Air` (must match in rebuild commands) - **Darwin config name:** `Daniel-Macbook-Air` (must match in rebuild commands)
## Repo rules ## Repo rules
@ -46,13 +53,39 @@ cd ~/dotfiles/nixos && nix build .#installer-iso
## Server (sunken-ship) ## Server (sunken-ship)
- SSH: `ssh -i ~/.ssh/id_ed25519_sunken_ship danny@sunken-ship` - SSH: `ssh -i ~/.ssh/id_ed25519_sunken_ship danny@sunken-ship`
- Remote rebuild: `ssh ... 'cd /etc/dotfiles/nixos && sudo nixos-rebuild switch --flake .#sunken-ship'` - Remote rebuild: `ssh ... 'cd /etc/dotfiles && sudo nixos-rebuild switch --flake .#sunken-ship'`
- Auto-rebuild timer: `dotfiles-rebuild` — only active after flake config switch. Check with `systemctl is-active dotfiles-rebuild.timer`. - Auto-rebuild timer: `dotfiles-rebuild` — every 15 min. Check with `systemctl is-active dotfiles-rebuild.timer`.
- Server has WiFi; stays reachable when ethernet is unplugged. - WiFi connected; stays reachable when ethernet is unplugged.
- Services: UxPlay (AirPlay receiver on Scarlett Solo)
## Server (phantom-ship)
- SSH: `ssh danny@phantom-ship`
- Remote rebuild: `ssh ... 'cd /etc/dotfiles && sudo nixos-rebuild switch --flake .#phantom-ship'`
- Auto-rebuild timer: same pattern as sunken-ship.
- Ethernet only (no WiFi).
## Ollama ## Ollama
Custom nix-darwin module at `nixos/ollama.nix` (upstream PR not yet merged). Enabled on macOS via `nixos/hosts/macos.nix`. Runs as a launchd user agent with `ollama serve`. Custom nix-darwin module at `nixos/ollama.nix` (upstream PR not yet merged). Enabled on macOS via `nixos/hosts/daniel-macbook-air.nix`. Runs as a launchd user agent with `ollama serve`.
## Alacritty (macOS)
Terminal colors follow **System Settings → Appearance**: `programs.alacritty` imports `~/.config/alacritty/active-colors.toml`; `scripts/alacritty-sync-system-theme.sh` copies Catppuccin latte/mocha there when the OS mode changes. **nix-darwin** `launchd.user.agents.alacritty-system-theme` polls every 30s; **fish** runs the same script on interactive startup. After changing Nix, one `darwin-rebuild switch`. Details: `assets/alacritty/README.md`.
## clan.lol
**CLI invocation:** clan-cli is not installed globally. Run ad-hoc via:
```bash
nix run git+https://git.clan.lol/clan/clan-core#clan-cli -- machines list --flake ~/dotfiles
```
Flake lives at the repo root (not `nixos/`) — clan-cli silently ignores `?dir=` so a subdir flake breaks `clan machines update`.
**`enableRecommendedDefaults = false`:** we opted out fleet-wide because clan's defaults flip to `systemd-networkd` + `systemd-resolved` + `boot.initrd.systemd`, which breaks dnsmasq (NAT DNS on phantom-ship) and navidrome's resolv.conf bind-mount on sunken-ship. Revisit per-service in a later pass — the defaults also include handy extras (tcpdump, htop, curl, jq, nixos-facter). Option defined in `nixosModules/clanCore/defaults.nix` + `nixosModules/clanCore/networking.nix` inside the `clan-core` flake.
**Deployment:** `dotfiles-rebuild` timer (every 15 min pull) is still the source of truth. `clan machines update` works as a push escape hatch; dm-pull-deploy replaces the timer in a later stage.
## Shell ## Shell

View file

@ -7,7 +7,7 @@ Extension of [dannydannydanny/methodology](https://github.com/DannyDannyDanny/me
## Roadmap ## Roadmap
- [firefox-scrolling](firefox-scrolling.md) via terminal - [firefox-scrolling](firefox-scrolling.md) via terminal
- Server: [server](server.md); NixOS flake and bootstrap [nixos/readme.md](nixos/readme.md). SSH and secrets: [docs/ssh-and-secrets.md](docs/ssh-and-secrets.md). New server install (USB, LUKS, WiFi): [docs/server-installer-usb.md](docs/server-installer-usb.md). - Server: [server-quickstart](server-quickstart.md); NixOS flake and bootstrap [nixos/readme.md](nixos/readme.md). SSH and secrets: [docs/ssh-and-secrets.md](docs/ssh-and-secrets.md). New server install (USB, LUKS, WiFi): [docs/server-installer-usb.md](docs/server-installer-usb.md).
- nvim checkhealth; tmux setup; [fonts](https://www.programmingfonts.org/) / nerdfonts; [HN: home server](https://news.ycombinator.com/item?id=34271167) - nvim checkhealth; tmux setup; [fonts](https://www.programmingfonts.org/) / nerdfonts; [HN: home server](https://news.ycombinator.com/item?id=34271167)
## Windows ## Windows
@ -25,7 +25,7 @@ nix-shell -p gh git
gh auth login gh auth login
gh repo clone dannydannydanny/dotfiles && cd dotfiles gh repo clone dannydannydanny/dotfiles && cd dotfiles
# git checkout <branch> # if needed # git checkout <branch> # if needed
sudo nixos-rebuild switch --flake ~/dotfiles/nixos#wsl sudo nixos-rebuild switch --flake ~/dotfiles#wsl
``` ```
### Clone via SSH ### Clone via SSH
@ -40,9 +40,10 @@ ssh-add ~/.ssh/id_ed25519_github
git clone git@github.com:DannyDannyDanny/dotfiles.git && cd dotfiles git clone git@github.com:DannyDannyDanny/dotfiles.git && cd dotfiles
git config user.name "DannyDannyDanny" git config user.name "DannyDannyDanny"
git config user.email "dth@taiga.ai" git config user.email "dth@taiga.ai"
bash install.sh
``` ```
Apply machine config from `nixos/` (see [CLAUDE.md](CLAUDE.md) for macOS rebuild commands or [nixos/readme.md](nixos/readme.md) for NixOS).
## Good reads ## Good reads
- [TODOs aren't for doing](https://sophiebits.com/2025/07/21/todos-arent-for-doing) - [TODOs aren't for doing](https://sophiebits.com/2025/07/21/todos-arent-for-doing)

View file

@ -1,7 +1,6 @@
# TODO # TODO
1. Create a setup/boot USB that: installs NixOS on the server with encryption and WiFi configured from the start; only required input is the server's name (e.g. sunken-ship). - [ ] **USB installer**: Refine the installer USB workflow (`scripts/nixos-server-install.sh`, `disko-server.nix`, `installer-iso.nix`). Goal: boot USB, provide hostname, get a LUKS-encrypted NixOS server with WiFi ready to go.
* I have a set wifi SSID/PSK, assume servers will start up and be able to reach this wifi. - [ ] **Encrypt sunken-ship**: Currently running on plain ext4. Needs reinstall with LUKS via disko, or in-place migration (backup, reformat, restore).
* I don't know how to go about the rest of this. - [ ] **Tailscale**: Investigate setting up Tailscale mesh VPN across devices (sunken-ship, Mac, iPhone). Would allow SSH, AirPlay, and Claude Code remote sessions from anywhere. Free tier, ~5 lines of NixOS config. See: https://tailscale.com
- [ ] **Server alerting**: Get notified when a server goes down (power loss, crash, etc). Options: simple ping-based cron on Mac sending macOS notifications, or lightweight uptime monitor (Uptime Kuma on one of the servers).
2. Encrypt sunken-ship (LUKS); update hardware/config for encrypted root and boot.

View file

@ -1,102 +1,54 @@
# Unified Theme Switching # Alacritty + system appearance (macOS)
Unified theme switching that works across platforms (WSL and macOS) for Neovim, Alacritty, and Windows Terminal. Alacritty follows **System Settings → Appearance** automatically. No `darwin-rebuild` when you change light/dark.
**This solution uses a single `theme` command that detects the platform and switches themes appropriately.** ## How it works
## How It Works 1. Home Manager installs Catppuccin palettes as `~/.config/alacritty/catppuccin-{latte,mocha}-colors.toml` and a generated `alacritty.toml` that sets `general.import` to `active-colors.toml`.
2. `scripts/alacritty-sync-system-theme.sh` copies the matching palette to `active-colors.toml`. Alacrittys `live_config_reload` picks it up immediately.
3. **nix-darwin** runs that script from a user LaunchAgent every 30s (`nixos/hosts/daniel-macbook-air.nix`: `launchd.user.agents.alacritty-system-theme`). It is also installed on `PATH` as `alacritty-sync-system-theme`.
4. **Fish** runs the same script in the background when you open an interactive shell on Darwin, so changes apply quickly without waiting for the next poll.
1. The `theme` command detects the platform (WSL vs macOS) ## Optional manual LaunchAgent
2. **On WSL:** Updates Neovim, Windows Terminal, and Windows system theme
3. **On macOS:** Updates Neovim and Alacritty themes via Nix configuration
4. Uses the same `nvim_color_scheme` file for Neovim on both platforms
## Setup If you are not using the nix-darwin agent, you can load `assets/launchd/com.user.alacritty-theme-sync.plist` (adjust paths if needed). **Do not** load both the nix-darwin agent and this plist or you will run two pollers.
1. **The configuration is already set up!** The `theme` command is available as a fish alias. If you previously used the old plist label `com.user.alacritty-theme-sync` and switch to nix-darwin only:
2. **To switch themes, use the unified command:**
```bash ```bash
theme light # Switch to light theme launchctl bootout "gui/$(id -u)" ~/Library/LaunchAgents/com.user.alacritty-theme-sync.plist 2>/dev/null || true
theme dark # Switch to dark theme
theme toggle # Toggle between light and dark themes
theme status # Show current theme status
``` ```
## Usage ## `theme` command (Neovim / WSL)
The fish alias `theme` still updates `~/.local/share/nvim_color_scheme` (and Windows Terminal on WSL). On macOS, **Alacritty ignores** `theme light|dark` for terminal colors—it only follows System Settings. Neovim stays on whatever you set with `theme`; the Alacritty sync script does not touch the nvim file.
### Unified Theme Command
```bash ```bash
# Switch to light theme (works on WSL and macOS) theme light # Neovim (+ WSL terminal); macOS Alacritty unchanged (uses Appearance)
theme light
# Switch to dark theme (works on WSL and macOS)
theme dark theme dark
# Toggle between light and dark themes
theme toggle theme toggle
# Show current theme status
theme status theme status
``` ```
### What Gets Updated
**On WSL:**
- Neovim theme (via `~/.local/share/nvim_color_scheme`)
- Windows Terminal settings
- Windows system theme
- Windows sound scheme
**On macOS:**
- Neovim theme (via `~/.local/share/nvim_color_scheme`)
- Alacritty theme (via Nix configuration)
### Manual Configuration (macOS only)
You can also manually edit `nixos/home/danny/home.nix` and change:
```nix
isLightTheme = true; # for light theme
isLightTheme = false; # for dark theme
```
Then run: `cd nixos && sudo darwin-rebuild switch --flake .#Daniel-Macbook-Air`
## Files ## Files
- `scripts/theme.sh` - **Main unified theme switching script** - `assets/alacritty/catppuccin-latte-colors.toml` / `catppuccin-mocha-colors.toml` — palette fragments
- `scripts/switch-alacritty-theme.sh` - Alacritty-specific theme switching (used by theme.sh) - `scripts/alacritty-sync-system-theme.sh` — detect macOS appearance, copy palette, refresh nvim marker
- `scripts/detect-system-theme.sh` - Detects current macOS system theme (for reference) - `scripts/sync-alacritty-theme.sh` — thin wrapper (backwards compatible)
- `nixos/fish.nix` - Contains the `theme` fish alias - `nixos/home/danny/home.nix``programs.alacritty` + `xdg.configFile` for palettes
- `nixos/home/danny/home.nix` - Contains the conditional Alacritty configuration - `nixos/hosts/daniel-macbook-air.nix` — LaunchAgent + `alacritty-sync-system-theme` in `environment.systemPackages`
- `bashscripts/wsl_theme.sh` - Legacy WSL script (replaced by theme.sh) - `nixos/fish.nix` — optional shell-open sync on Darwin
## Theme Colors After changing Nix config, run `darwin-rebuild switch` once (see repo `AGENTS.md`).
## Theme colors
### Catppuccin Latte (Light) ### Catppuccin Latte (Light)
- Background: `#eff1f5` (base) - Background: `#eff1f5` (base)
- Foreground: `#4c4f69` (text) - Foreground: `#4c4f69` (text)
- Accent colors optimized for light backgrounds
### Catppuccin Mocha (Dark) ### Catppuccin Mocha (Dark)
- Background: `#1e1e2e` (base) - Background: `#1e1e2e` (base)
- Foreground: `#cdd6f4` (text) - Foreground: `#cdd6f4` (text)
- Accent colors optimized for dark backgrounds
## Integration with NixOS
The solution uses Nix's conditional configuration in `home.nix`:
```nix
colors = let
isLightTheme = true; # Change this to switch themes
lightColors = { /* Catppuccin Latte colors */ };
darkColors = { /* Catppuccin Mocha colors */ };
in if isLightTheme then lightColors else darkColors;
```
This approach:
- ✅ Works with Spotlight/Applications folder launches
- ✅ No complex file reading or external dependencies
- ✅ Integrates cleanly with NixOS configuration
- ✅ Simple and reliable - just change a boolean and rebuild
- ✅ Easy to understand and maintain

View file

@ -1,28 +0,0 @@
# Catppuccin Mocha (Dark) theme for Alacritty
[colors.primary]
background = "0x1e1e2e" # base
foreground = "0xcdd6f4" # text
[colors.cursor]
text = "0x1e1e2e" # base
cursor = "0xf5e0dc" # rosewater
[colors.normal]
black = "0x45475a" # surface1
red = "0xf38ba8" # red
green = "0xa6e3a1" # green
yellow = "0xf9e2af" # yellow
blue = "0x89b4fa" # blue
magenta = "0xf5c2e7" # pink
cyan = "0x94e2d5" # teal
white = "0xbac2de" # subtext1
[colors.bright]
black = "0x585b70" # surface2
red = "0xf38ba8" # red
green = "0xa6e3a1" # green
yellow = "0xf9e2af" # yellow
blue = "0x89b4fa" # blue
magenta = "0xf5c2e7" # pink
cyan = "0x94e2d5" # teal
white = "0xa6adc8" # subtext0

View file

@ -0,0 +1,29 @@
# Catppuccin Latte — imported by main alacritty.toml; swapped by sync script.
[colors.primary]
background = "#eff1f5"
foreground = "#4c4f69"
[colors.cursor]
text = "#eff1f5"
cursor = "#dc8a78"
[colors.normal]
black = "#5c5f77"
red = "#d20f39"
green = "#40a02b"
yellow = "#df8e1d"
blue = "#1e40af"
magenta = "#ea76cb"
cyan = "#179299"
white = "#acb0be"
[colors.bright]
black = "#6c6f85"
red = "#d20f39"
green = "#40a02b"
yellow = "#df8e1d"
blue = "#1e40af"
magenta = "#ea76cb"
cyan = "#179299"
white = "#bcc0cc"

View file

@ -1,28 +0,0 @@
# Catppuccin Latte (Light) theme for Alacritty
[colors.primary]
background = "0xeff1f5" # base
foreground = "0x4c4f69" # text
[colors.cursor]
text = "0xeff1f5" # base
cursor = "0xdc8a78" # rosewater
[colors.normal]
black = "0x5c5f77" # surface1
red = "0xd20f39" # red
green = "0x40a02b" # green
yellow = "0xdf8e1d" # yellow
blue = "0x1e40af" # blue
magenta = "0xea76cb" # pink
cyan = "0x179299" # teal
white = "0xacb0be" # subtext1
[colors.bright]
black = "0x6c6f85" # surface2
red = "0xd20f39" # red
green = "0x40a02b" # green
yellow = "0xdf8e1d" # yellow
blue = "0x1e40af" # blue
magenta = "0xea76cb" # pink
cyan = "0x179299" # teal
white = "0xbcc0cc" # subtext0

View file

@ -0,0 +1,29 @@
# Catppuccin Mocha — imported by main alacritty.toml; swapped by sync script.
[colors.primary]
background = "#1e1e2e"
foreground = "#cdd6f4"
[colors.cursor]
text = "#1e1e2e"
cursor = "#f5e0dc"
[colors.normal]
black = "#45475a"
red = "#f38ba8"
green = "#a6e3a1"
yellow = "#f9e2af"
blue = "#89b4fa"
magenta = "#f5c2e7"
cyan = "#94e2d5"
white = "#bac2de"
[colors.bright]
black = "#585b70"
red = "#f38ba8"
green = "#a6e3a1"
yellow = "#f9e2af"
blue = "#89b4fa"
magenta = "#f5c2e7"
cyan = "#94e2d5"
white = "#a6adc8"

View file

@ -4,28 +4,18 @@
<dict> <dict>
<key>Label</key> <key>Label</key>
<string>com.user.alacritty-theme-sync</string> <string>com.user.alacritty-theme-sync</string>
<key>ProgramArguments</key> <key>ProgramArguments</key>
<array> <array>
<string>/Users/danny/dotfiles/scripts/sync-alacritty-theme.sh</string> <string>/bin/bash</string>
<string>/Users/danny/dotfiles/scripts/alacritty-sync-system-theme.sh</string>
</array> </array>
<key>StartInterval</key> <key>StartInterval</key>
<integer>30</integer> <integer>30</integer>
<key>RunAtLoad</key> <key>RunAtLoad</key>
<true/> <true/>
<key>StandardOutPath</key> <key>StandardOutPath</key>
<string>/tmp/alacritty-theme-sync.log</string> <string>/tmp/alacritty-theme-sync.log</string>
<key>StandardErrorPath</key> <key>StandardErrorPath</key>
<string>/tmp/alacritty-theme-sync-error.log</string> <string>/tmp/alacritty-theme-sync-error.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
</dict>
</dict> </dict>
</plist> </plist>

53
assets/zed/settings.json Normal file
View file

@ -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"
}
}

View file

@ -2,7 +2,7 @@
Two-word hostnames, non-human / non-specific. Two-word hostnames, non-human / non-specific.
- **Ships / sea:** sunken-ship, phantom-ship, rusty-anchor, salty-wind, stormy-wave, calm-harbor, distant-shore, foreign-port, wooden-hull, anchor-chain - **Ships / sea:** sunken-ship, phantom-ship ✓, **rusty-anchor** (next), salty-wind, stormy-wave, calm-harbor, distant-shore, foreign-port, wooden-hull, anchor-chain
- **Prison / stone:** prison-rock, cold-stone, iron-chain, damp-cell, guard-tower, midnight-bell, stony-corridor, broken-chain - **Prison / stone:** prison-rock, cold-stone, iron-chain, damp-cell, guard-tower, midnight-bell, stony-corridor, broken-chain
- **Secrets / treasure:** buried-treasure, secret-cave, forgotten-tunnel, hidden-key, rusty-sword, faded-parchment, ancient-map, broken-seal, buried-chest - **Secrets / treasure:** buried-treasure, secret-cave, forgotten-tunnel, hidden-key, rusty-sword, faded-parchment, ancient-map, broken-seal, buried-chest
- **Atmosphere:** strange-companion, masked-ball, poison-vial - **Atmosphere:** strange-companion, masked-ball, poison-vial

View file

@ -1,180 +1,163 @@
# Server installer USB (NixOS + LUKS + WiFi) # Server installer USB (NixOS + LUKS)
Bootable USB that installs NixOS on a new server with disk encryption (LUKS) and optional WiFi from first boot. Only required input is the hostname (and LUKS passphrase when disko creates the volume). Existing hosts are not modified. Bootable USB that installs NixOS on a new server with disk encryption (LUKS). The install script handles partitioning, encryption, dotfiles cloning, SSH key setup, and hardware config generation. Only required inputs: hostname, LUKS passphrase, and target disk.
## Quick path: boot USB → WiFi → SSH in → run bootstrap ## Quick path (Ethernet server like phantom-ship)
1. Boot the target machine from the NixOS installer USB. ### Prep (on sunken-ship or any Linux box)
2. On the live system, connect to WiFi (or plug in Ethernet). Check internet (e.g. `ping -c 2 8.8.8.8`).
3. On the **live** system, start SSH and set a password for the `nixos` user so you can log in from your Mac: 1. Download the [NixOS minimal ISO](https://nixos.org/download.html#nixos-iso) on sunken-ship.
2. Plug in USB and write the ISO:
```bash
# Find your USB device (e.g. /dev/sdc)
lsblk
sudo dd if=nixos-minimal-*.iso of=/dev/sdX status=progress bs=4M
sync
```
### Install (on the new server)
3. Boot the new machine from USB, plug in Ethernet, verify connectivity (`ping 8.8.8.8`).
4. Start SSH on the live system so you can paste commands from your Mac:
```bash ```bash
sudo systemctl start sshd sudo systemctl start sshd
sudo passwd nixos sudo passwd nixos
hostname -I hostname -I # note the IP
``` ```
Note the IP from `hostname -I`. 5. From your **Mac**, scp your SSH public key and SSH in:
4. From your **Mac**: `ssh nixos@<IP>` (use the password you set). Now you can paste the bootstrap command instead of typing on the machine.
5. In that SSH session, run the bootstrap (installs NixOS with LUKS; prompts for hostname, disk, **danny password**, LUKS passphrase, then once more LUKS to set the password on disk):
```bash ```bash
curl -sL https://raw.githubusercontent.com/DannyDannyDanny/dotfiles/server-installer-usb/scripts/bootstrap-install.sh | sudo bash scp ~/.ssh/id_ed25519_phantom_ship.pub nixos@<IP>:/tmp/key.pub
ssh nixos@<IP>
``` ```
6. When it finishes, reboot and remove the USB. Unlock LUKS at boot, then log in as **danny** with the password you set during the install. 6. Run the bootstrap (one command):
```bash
curl -sL https://raw.githubusercontent.com/DannyDannyDanny/dotfiles/main/scripts/bootstrap-install.sh | \
INSTALLER_HOSTNAME=phantom-ship SSH_PUBKEY_FILE=/tmp/key.pub sudo -E bash
```
This will prompt for: target disk, optional danny password, confirmation, and LUKS passphrase (twice: once for disko, once for post-install provisioning).
## Option A: Official NixOS ISO (works from macOS) The script automatically:
- Partitions and encrypts the disk (LUKS + ext4)
- Installs NixOS with the hostname
- Clones dotfiles to `/etc/dotfiles`
- Installs your SSH public key
- Generates `phantom-ship-hardware.nix`
You **cannot** build the custom installer ISO on macOS (it is x86_64-linux only and `--system` is restricted). Use the official NixOS minimal ISO instead: 7. Reboot, remove USB, unlock LUKS.
1. Download the [minimal ISO](https://nixos.org/download.html#nixos-iso) (e.g. `nixos-minimal-*-x86_64-linux.iso`). ### After first boot
2. Write it to your USB (on macOS: `diskutil unmountDisk diskN`, then `sudo dd if=path/to/nixos-minimal-*.iso of=/dev/rdiskN bs=4m`).
3. Boot the server from the USB. Attach Ethernet or use the **graphical** ISO if you need WiFi on the live system. 8. SSH in: `ssh danny@phantom-ship`
4. On the live system, clone this repo and run the install script (see [Install on the server](#install-on-the-server) below). The script runs `disko-install` and does LUKS + hostname; no custom ISO needed. 9. First rebuild to switch from generic `server-install` to `phantom-ship` config:
```bash
cd /etc/dotfiles && sudo nixos-rebuild switch --flake .#phantom-ship
```
10. Commit the generated `phantom-ship-hardware.nix` back to the repo.
## Environment variables
All optional; skip interactive prompts or add automation:
| Variable | Description |
|----------|-------------|
| `INSTALLER_HOSTNAME` | Skip hostname prompt |
| `INSTALLER_DISK` | Skip disk prompt (validated as block device) |
| `SSH_PUBKEY_FILE` | Path to `.pub` file; installed to danny's `authorized_keys` |
| `FLAKE_REF` | Override flake reference (default: auto-detect from repo) |
| `INSTALLER_SYSTEM_CONFIG_FILE` | JSON file merged into `--system-config` (e.g. WiFi config) |
## Option A: Official NixOS ISO (recommended)
Cannot build the custom ISO on macOS (x86_64-linux only). Use the official NixOS minimal ISO:
1. Download from [nixos.org](https://nixos.org/download.html#nixos-iso).
2. Write to USB from sunken-ship or any Linux box.
3. Boot, connect Ethernet, run bootstrap.
## Option B: Custom ISO (build on Linux only) ## Option B: Custom ISO (build on Linux only)
The custom ISO adds WiFi kernel modules and optional live WiFi; it must be built on **x86_64-linux** (or with a Nix remote builder configured for that system). Building on macOS will fail. Adds WiFi kernel modules for servers that need WiFi on the live system.
### Build from sunken-ship (one command from your Mac) ### Build from sunken-ship
When the server is on the same network, run from the dotfiles repo:
```bash ```bash
./scripts/build-installer-iso-on-server.sh ./scripts/build-installer-iso-on-server.sh
``` ```
This pushes the branch, SSHs to sunken-ship, clones the repo there, runs `nix build .#installer-iso`, and copies the ISO back to the current directory. Optional: `./scripts/build-installer-iso-on-server.sh sunken-ship /path/to/output`. ### Build directly on Linux
### Build directly on a Linux machine
From a Linux box (or on sunken-ship after SSH in):
```bash ```bash
cd ~/dotfiles/nixos cd ~/dotfiles && nix build .#installer-iso
nix build .#installer-iso # Write to USB:
sudo dd if=result/iso/nixos-minimal-*.iso of=/dev/sdX status=progress bs=4M
``` ```
The image is at `result/iso/nixos-minimal-*.iso`. Write it to a USB stick (replace `sdX` with your device, e.g. `sda`): ## Live-system WiFi (optional, custom ISO only)
```bash The minimal installer ISO runs NetworkManager, so live-system WiFi must be a
# Linux declarative NetworkManager profile. `networking.wireless` / wpa_supplicant does
sudo dd if=result/iso/nixos-minimal-*.iso of=/dev/sdX status=progress **not** work here — NixOS asserts you cannot combine `networking.networkmanager`
sync with `networking.wireless.networks`.
```
On macOS, use the disk number (e.g. `4` for `disk4`): Create `nixos/installer-wifi.nix` (gitignored — it holds the PSK):
```bash
sudo dd if=result/iso/nixos-minimal-*.iso of=/dev/rdisk4 bs=4m
diskutil eject disk4
```
Or adapt [scripts/make-ubuntu-usb.sh](../scripts/make-ubuntu-usb.sh) for the NixOS ISO path.
## Live-system WiFi (optional)
So the live system can reach the network (and fetch the flake) without Ethernet, add WiFi to the ISO at **build time**. Do not put SSID/PSK in the repo.
1. Create **`nixos/installer-wifi.nix`** (gitignored) with your network:
```nix ```nix
{ {
networking.wireless.enable = true; networking.networkmanager.ensureProfiles.profiles.installer-wifi = {
networking.wireless.networks."YourSSID".psk = "your-password"; connection = {
} id = "installer-wifi";
``` type = "wifi";
2. Add it to the flake for the installer ISO only. In `nixos/flake.nix`, change the `installer-iso` modules to:
```nix
installer-iso = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./installer-iso.nix ./installer-wifi.nix ]; # add installer-wifi.nix
}; };
wifi = {
mode = "infrastructure";
ssid = "YourSSID";
};
wifi-security = {
auth-alg = "open";
key-mgmt = "wpa-psk";
psk = "your-password";
};
ipv4.method = "auto";
ipv6.method = "auto";
};
}
``` ```
3. Ensure `nixos/installer-wifi.nix` is in `.gitignore`, then rebuild the ISO. `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:
If you skip this, use Ethernet on the live system or the graphical NixOS installer to join WiFi, then run the install script. - **`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).
## Install on the server Then rebuild the ISO on Linux.
1. Boot the server from the USB. ## Installed-system WiFi (optional)
2. If you did not bake WiFi into the ISO, attach Ethernet or (on graphical installer) join WiFi so the machine has network.
3. Run **one** of the following (shortest first).
**Shortest — fetch and run (no clone step):** Pass a JSON file with wireless config:
Exact URL (watch for typos: **.com** not .con, **usb** not ush, **DannyDannyDanny** with three capital Ds):
```bash ```bash
curl -sL https://raw.githubusercontent.com/DannyDannyDanny/dotfiles/server-installer-usb/scripts/bootstrap-install.sh | sudo bash sudo INSTALLER_SYSTEM_CONFIG_FILE=/path/to/wifi.json INSTALLER_HOSTNAME=my-server ./scripts/nixos-server-install.sh
``` ```
If you see `bash: 404: command not found`, the URL was wrong or the branch doesnt exist. Check the URL, or verify first: `curl -sL "THE_URL_ABOVE" | head -1` should show `#!/bin/bash`, not HTML.
To type less, create a [git.io](https://git.io) short link once (paste the raw URL above), then on the machine run: `curl -sL https://git.io/YOUR_CODE | sudo bash`.
**Alternative — clone then run** (if you prefer not to pipe curl to bash):
```bash
nix run --extra-experimental-features "nix-command flakes" nixpkgs#git -- clone https://github.com/USER/REPO.git /tmp/dotfiles && cd /tmp/dotfiles && git checkout server-installer-usb && sudo ./scripts/nixos-server-install.sh
```
If you see `command not found` when running the script, use `sudo bash ./scripts/nixos-server-install.sh` instead of `sudo ./scripts/...`.
4. When prompted: enter **hostname** (e.g. `phantom-ship`), then **target disk** (default `/dev/sda`), then **y** to proceed. When disko creates the LUKS volume, enter your encryption passphrase.
5. When the script finishes, remove the USB and reboot. The new NixOS system will have LUKS root and the hostname you chose.
## WiFi on the installed system (optional)
To have WiFi configured from first boot (no manual step after reboot):
1. Create a JSON file **outside the repo** with the config to merge (hostname is set by the script from the prompt):
```json
{
"networking": {
"wireless": {
"networks": {
"YourSSID": { "psk": "your-password" }
}
}
}
}
```
2. Copy that file onto the live system (e.g. put it on the USB or scp it). If the script is run with `jq` available and `INSTALLER_SYSTEM_CONFIG_FILE` set to that file, the script will merge it and set the hostname:
```bash
sudo INSTALLER_SYSTEM_CONFIG_FILE=/path/to/wifi-config.json ./scripts/nixos-server-install.sh
```
If you omit this, the installed system still has `networking.wireless.enable = true`. Add credentials after first boot (e.g. [imperative wpa_supplicant config](sunken-ship-wifi.md)).
## Manual install (without the script) ## Manual install (without the script)
You can run disko-install yourself:
```bash ```bash
sudo nix run github:nix-community/disko/latest#disko-install -- \ sudo nix run github:nix-community/disko/latest#disko-install -- \
--flake 'path:/tmp/dotfiles/nixos#server-install' \ --flake 'path:/tmp/dotfiles#server-install' \
--disk main /dev/sda \ --disk main /dev/sda \
--system-config '{"networking":{"hostName":"my-server"}}' --system-config '{"networking":{"hostName":"my-server"}}'
``` ```
Adjust the flake path and `--system-config` (e.g. add WiFi) as needed.
## After install
- Add your SSH key: from your machine `scp ~/.ssh/id_ed25519_servers.pub danny@NEW-SERVER:/tmp/`, then on the server `mkdir -p ~/.ssh; cat /tmp/*.pub >> ~/.ssh/authorized_keys`.
- To switch this machine to another host config in the same flake (e.g. a full server profile), clone the repo on the new system and run `sudo nixos-rebuild switch --flake /path/to/nixos#other-host`.
## Summary ## Summary
| Step | Action | | Step | Action |
|------|--------| |------|--------|
| **From macOS** | Use Option A: download official NixOS minimal ISO, write to USB, boot server, clone repo, run install script. | | **Prep** | Download NixOS minimal ISO on sunken-ship, write to USB |
| **From Linux** | Option B: `nix build .#installer-iso` in `nixos/`, then write `result/iso/*.iso` to USB. | | **Boot** | Boot new server from USB, plug Ethernet |
| Optional live WiFi | (Custom ISO only) Add `installer-wifi.nix` (gitignored), include in flake, rebuild on Linux. | | **Install** | `curl ... \| INSTALLER_HOSTNAME=phantom-ship SSH_PUBKEY_FILE=/tmp/key.pub sudo -E bash` |
| Boot | Boot server from USB | | **Reboot** | Remove USB, unlock LUKS |
| Install | On live system: `curl -sL https://raw.githubusercontent.com/.../server-installer-usb/scripts/bootstrap-install.sh | sudo bash` (or clone then `sudo ./scripts/nixos-server-install.sh`) | | **First rebuild** | `sudo nixos-rebuild switch --flake /etc/dotfiles#phantom-ship` |
| Optional installed WiFi | Set `INSTALLER_SYSTEM_CONFIG_FILE` to a JSON file with wireless config | | **Commit** | Push generated `phantom-ship-hardware.nix` to repo |
| Reboot | Remove USB, reboot; set root password if needed, add SSH keys |

View file

@ -42,10 +42,10 @@ nix shell nixpkgs#wpa_supplicant -c wpa_passphrase "YOUR_SSID" "YOUR_PASSWORD"
## Rebuild (after changing Nix config) ## Rebuild (after changing Nix config)
From the server (flake is in `nixos/`): From the server (flake is at the repo root):
```bash ```bash
cd /etc/dotfiles/nixos && sudo nixos-rebuild switch --flake .#sunken-ship cd /etc/dotfiles && sudo nixos-rebuild switch --flake .#sunken-ship
``` ```
## Verify ## Verify

254
flake-modules/clan.nix Normal file
View file

@ -0,0 +1,254 @@
# clan.lol wiring for the homelab.
#
# Declares `sunken-ship` and `phantom-ship` as clan machines. Each machine's
# `imports` list is the NixOS module set that used to live in its own
# flake-module. clan-core produces `flake.nixosConfigurations.<name>` from
# these, which is why the old per-host flake-modules were removed.
#
# The mac stays outside the clan — admin only, uses `clan machines update`
# to push to the servers.
{ config, inputs, ... }:
let
lib = inputs.nixpkgs.lib;
hmModule = { user, homeDirectory, stateVersion ? null, userImports ? [ ] }:
import ../lib/home-manager-user.nix {
inherit lib user homeDirectory stateVersion userImports;
};
# ZT IPv6 addresses of the two clan machines. Clan publishes these as
# generated vars at vars/per-machine/<host>/zerotier/zerotier-ip/value;
# duplicated here so we can drop them into /etc/hosts at module-eval time.
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";
foreignPortZTv6 = "fdd5:53a2:de33:d269:6499:9389:9b18:6c52";
# Shared across both servers: /etc/hosts entries so data-mesher's
# libp2p /dns/<machine>.clan/... bootstrap multiaddrs resolve over ZT.
clanHostsModule = {
networking.hosts = {
"${sunkenShipZTv6}" = [ "sunken-ship.clan" ];
"${phantomShipZTv6}" = [ "phantom-ship.clan" ];
"${vpsRelayZTv6}" = [ "vps-relay.clan" ];
"${distantShoreZTv6}" = [ "distant-shore.clan" ];
"${foreignPortZTv6}" = [ "foreign-port.clan" ];
};
};
in {
imports = [ inputs.clan-core.flakeModules.default ];
clan = {
meta.name = "homelab";
# data-mesher uses `<machine>.${domain}` as a libp2p /dns/ multiaddr.
# We don't run a DNS server for "clan" — per-machine networking.hosts
# entries (via clanHostsModule) resolve it to the host's ZT IPv6.
meta.domain = "clan";
# Inventory machines — required for `inventory.instances` role bindings
# to resolve. Host-specific NixOS config lives under `machines.<name>`
# below.
inventory.machines.sunken-ship = { };
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
# external ZT client and is authorized on the controller by node ID.
inventory.instances.zerotier = {
module.name = "zerotier";
module.input = "clan-core";
roles.controller.machines.sunken-ship = { };
roles.peer.machines.phantom-ship = { };
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).
# Underpins dm-pull-deploy below. Files are registered + their allowed
# signers managed automatically via clan service exports.
# sunken-ship is the bootstrap node; phantom-ship joins via its
# /dns/sunken-ship.clan/... multiaddr (resolved via /etc/hosts).
inventory.instances.data-mesher = {
module.name = "data-mesher";
module.input = "clan-core";
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 = { };
};
# dm-pull-deploy — pull-based NixOS deploy via data-mesher gossip.
# Our clan-community input is pinned to the branch that sanitizes
# machine.name for the status file name (upstream PR pending).
# sunken-ship is the push node; both servers run the default watcher
# with action="switch".
inventory.instances.dm-pull-deploy = {
module.name = "dm-pull-deploy";
module.input = "clan-community";
roles.push.machines.sunken-ship.settings = {
gitUrl = "https://github.com/DannyDannyDanny/dotfiles.git";
branch = "main";
};
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
# and overrides the ZT service's root@ default. Using the ZT IPv6 as
# the host makes updates work regardless of LAN DNS / mDNS state.
inventory.instances.internet = {
module.name = "internet";
module.input = "clan-core";
roles.default.machines.sunken-ship.settings = {
host = "fdd5:53a2:de33:d269:6499:93d5:53a2:de33";
user = "danny";
};
roles.default.machines.phantom-ship.settings = {
host = "fdd5:53a2:de33:d269:6499:936c:48a:bbdc";
user = "danny";
};
# Using public IPv4 while ZT identity is being bootstrapped on the
# VPS. Swap to ZT IPv6 (fdd5:53a2:de33:d269:6499:9305:339f:2ed3)
# after the first clan update uploads SOPS keys and zerotierone
# restarts with the clan-managed identity.
roles.default.machines.vps-relay.settings = {
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";
};
# 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,
# no boot.initrd.systemd, no extra debug packages). Revisit per-service
# in later stages rather than flipping this fleet-wide.
machines.sunken-ship = {
imports = [
{
clan.core.enableRecommendedDefaults = false;
clan.core.networking.targetHost = "danny@[fdd5:53a2:de33:d269:6499:93d5:53a2:de33]";
clan.core.networking.buildHost = "danny@[fdd5:53a2:de33:d269:6499:93d5:53a2:de33]";
}
clanHostsModule
../nixos/hosts/sunken-ship.nix
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";
homeDirectory = "/home/danny";
stateVersion = "25.11";
})
];
};
machines.vps-relay = {
imports = [
{
clan.core.enableRecommendedDefaults = false;
# Initial install uses --target-host override; subsequent
# updates go over ZT IPv6 (set once generated, via the
# internet instance above).
}
clanHostsModule
../nixos/hosts/vps-relay.nix
config.flake.nixosModules.monitoring-node-exporter
inputs.home-manager.nixosModules.home-manager
(hmModule {
user = "danny";
homeDirectory = "/home/danny";
stateVersion = "25.11";
})
];
};
# 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. 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@sunken-ship";
}
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";
})
];
};
# 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 = [
{
clan.core.enableRecommendedDefaults = false;
clan.core.networking.targetHost = "danny@[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]";
clan.core.networking.buildHost = "danny@[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]";
}
clanHostsModule
inputs.nix-openclaw.nixosModules.openclaw-gateway
../nixos/hosts/phantom-ship.nix
config.flake.nixosModules.server-debug-tools
config.flake.nixosModules.monitoring-node-exporter
inputs.home-manager.nixosModules.home-manager
(hmModule {
user = "danny";
homeDirectory = "/home/danny";
stateVersion = "25.11";
})
];
};
};
}

View file

@ -0,0 +1,22 @@
{ inputs, ... }: {
flake.darwinConfigurations."Daniel-Macbook-Air" = inputs.nix-darwin.lib.darwinSystem {
modules = [
# Overlay: make zen-browser available as pkgs.zen-browser
{ nixpkgs.overlays = [ (final: prev: {
zen-browser = inputs.zen-browser.packages.${final.stdenv.hostPlatform.system}.default;
}) ];
}
../nixos/hosts/daniel-macbook-air.nix
../nixos/fish.nix
inputs.home-manager.darwinModules.home-manager
(import ../lib/home-manager-user.nix {
lib = inputs.nixpkgs.lib;
user = "danny";
homeDirectory = "/Users/danny";
userImports = [ ../nixos/home/danny/home.nix ];
})
];
};
}

View file

@ -0,0 +1,15 @@
{ inputs, self, ... }: {
# Custom minimal installer ISO (build with: nix build .#installer-iso).
# 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 ]
++ inputs.nixpkgs.lib.optional
(builtins.pathExists ../nixos/installer-wifi.nix)
../nixos/installer-wifi.nix;
};
flake.packages.x86_64-linux.installer-iso =
self.nixosConfigurations.installer-iso.config.system.build.isoImage;
}

View file

@ -0,0 +1,9 @@
# Expose reusable NixOS modules via `flake.nixosModules`.
#
# Consume from a host's flake-module via:
# modules = [ config.flake.nixosModules.server-debug-tools ];
{ ... }: {
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;
}

View file

@ -0,0 +1,11 @@
{ inputs, ... }: {
# For disko-install: LUKS + WiFi; hostname/WiFi via --system-config.
flake.nixosConfigurations.server-install = inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
inputs.disko.nixosModules.disko
../nixos/disko-server.nix
../nixos/hosts/server-install.nix
];
};
}

19
flake-modules/wsl.nix Normal file
View file

@ -0,0 +1,19 @@
{ inputs, ... }: {
flake.nixosConfigurations.wsl = inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
inputs.nixos-wsl.nixosModules.default
inputs.vscode-server.nixosModules.default
../nixos/hosts/wsl.nix
../nixos/fish.nix
inputs.home-manager.nixosModules.home-manager
(import ../lib/home-manager-user.nix {
lib = inputs.nixpkgs.lib;
user = "dth";
homeDirectory = "/home/dth";
userImports = [ ../nixos/home/danny/home.nix ];
})
];
};
}

665
flake.lock generated Normal file
View file

@ -0,0 +1,665 @@
{
"nodes": {
"clan-community": {
"inputs": {
"clan-core": [
"clan-core"
],
"flake-parts": "flake-parts",
"nixpkgs": [
"nixpkgs"
],
"treefmt-nix": "treefmt-nix"
},
"locked": {
"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": {
"type": "tarball",
"url": "https://git.clan.lol/clan/clan-community/archive/main.tar.gz"
}
},
"clan-core": {
"inputs": {
"data-mesher": "data-mesher",
"disko": "disko",
"flake-parts": [
"flake-parts"
],
"nix-darwin": "nix-darwin",
"nix-select": "nix-select",
"nixpkgs": [
"nixpkgs"
],
"sops-nix": "sops-nix",
"systems": "systems",
"treefmt-nix": "treefmt-nix_2"
},
"locked": {
"lastModified": 1778462753,
"narHash": "sha256-/9qWZbrwoVWP0YWuC1Z5HMEb/oy6rNsjypUKTuk1PB4=",
"rev": "09551fdb27a7e5712bef371e9271034d503242ed",
"type": "tarball",
"url": "https://git.clan.lol/api/v1/repos/clan/clan-core/archive/09551fdb27a7e5712bef371e9271034d503242ed.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://git.clan.lol/clan/clan-core/archive/main.tar.gz"
}
},
"data-mesher": {
"inputs": {
"flake-parts": [
"clan-core",
"flake-parts"
],
"nixpkgs": [
"clan-core",
"nixpkgs"
],
"treefmt-nix": [
"clan-core",
"treefmt-nix"
]
},
"locked": {
"lastModified": 1776654564,
"narHash": "sha256-5bpzOOXsaAr4g25/ghtKdYO17xg0l+MieCcWgqx24eY=",
"rev": "ad23733ebc47284dc1158db43218cf4027824aee",
"type": "tarball",
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/ad23733ebc47284dc1158db43218cf4027824aee.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://git.clan.lol/clan/data-mesher/archive/main.tar.gz"
}
},
"disko": {
"inputs": {
"nixpkgs": [
"clan-core",
"nixpkgs"
]
},
"locked": {
"lastModified": 1776613567,
"narHash": "sha256-gC9Cp5ibBmGD5awCA9z7xy6MW6iJufhazTYJOiGlCUI=",
"owner": "nix-community",
"repo": "disko",
"rev": "32f4236bfc141ae930b5ba2fb604f561fed5219d",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "disko",
"type": "github"
}
},
"disko_2": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1777713215,
"narHash": "sha256-8GzXDOXckDWwST8TY5DbwYFjdvQLlP7K9CLSVx6iTTo=",
"owner": "nix-community",
"repo": "disko",
"rev": "63b4e7e6cf75307c1d26ac3762b886b5b0247267",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "disko",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"clan-community",
"nixpkgs"
]
},
"locked": {
"lastModified": 1775087534,
"narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1777988971,
"narHash": "sha256-qIoWPDs+0/8JecyYgE3gpKQxW/4bLW/gp45vow9ioCQ=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "0678d8986be1661af6bb555f3489f2fdfc31f6ff",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_3"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1778444552,
"narHash": "sha256-f18pIiR9q/p1vHY93gmAum7aHhQOG49oGvAB9+lptRo=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "dcebe66f958673729896eec2de4abfd86ef22d21",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"home-manager_2": {
"inputs": {
"nixpkgs": [
"nix-openclaw",
"nixpkgs"
]
},
"locked": {
"lastModified": 1767909183,
"narHash": "sha256-u/bcU0xePi5bgNoRsiqSIwaGBwDilKKFTz3g0hqOBAo=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "cd6e96d56ed4b2a779ac73a1227e0bb1519b3509",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"home-manager_3": {
"inputs": {
"nixpkgs": [
"zen-browser",
"nixpkgs"
]
},
"locked": {
"lastModified": 1777594677,
"narHash": "sha256-h90sHwoRJLRvaTpZroTvU2JRHDFj0czUafM8eqLe1RI=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "899c08a15beae5da51a5cecd6b2b994777a948da",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"import-tree": {
"locked": {
"lastModified": 1773693634,
"narHash": "sha256-BtZ2dtkBdSUnFPPFc+n0kcMbgaTxzFNPv2iaO326Ffg=",
"owner": "vic",
"repo": "import-tree",
"rev": "c41e7d58045f9057880b0d85e1152d6a4430dbf1",
"type": "github"
},
"original": {
"owner": "vic",
"repo": "import-tree",
"type": "github"
}
},
"nix-darwin": {
"inputs": {
"nixpkgs": [
"clan-core",
"nixpkgs"
]
},
"locked": {
"lastModified": 1775037210,
"narHash": "sha256-KM2WYj6EA7M/FVZVCl3rqWY+TFV5QzSyyGE2gQxeODU=",
"owner": "nix-darwin",
"repo": "nix-darwin",
"rev": "06648f4902343228ce2de79f291dd5a58ee12146",
"type": "github"
},
"original": {
"owner": "nix-darwin",
"repo": "nix-darwin",
"type": "github"
}
},
"nix-darwin_2": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1777780666,
"narHash": "sha256-8wURyQMdDkGUarSTKOGdCuFfYiwa3HbzwscUfn3STDE=",
"owner": "nix-darwin",
"repo": "nix-darwin",
"rev": "8c62fba0854ba15c8917aed18894dbccb48a3777",
"type": "github"
},
"original": {
"owner": "nix-darwin",
"ref": "master",
"repo": "nix-darwin",
"type": "github"
}
},
"nix-openclaw": {
"inputs": {
"flake-utils": "flake-utils",
"home-manager": "home-manager_2",
"nix-openclaw-tools": "nix-openclaw-tools",
"nixpkgs": [
"nixpkgs"
],
"qmd": "qmd"
},
"locked": {
"lastModified": 1778353239,
"narHash": "sha256-g0yC+loN19X3Xyn6RuBHeWzevH7Qymt0REW+kyGuCLY=",
"owner": "openclaw",
"repo": "nix-openclaw",
"rev": "e2ea91056fdd0836bef96326a2b687277dbe3e1c",
"type": "github"
},
"original": {
"owner": "openclaw",
"repo": "nix-openclaw",
"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,
"narHash": "sha256-yxcNOha7Cfv2nhVpz9ZXSNKk0R7wt4AiBklJ8D24rVg=",
"rev": "3d1e3860bef36857a01a2ddecba7cdb0a14c35a9",
"type": "tarball",
"url": "https://git.clan.lol/api/v1/repos/clan/nix-select/archive/3d1e3860bef36857a01a2ddecba7cdb0a14c35a9.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://git.clan.lol/clan/nix-select/archive/main.tar.gz"
}
},
"nixos-wsl": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1777732699,
"narHash": "sha256-2uX/XtOWZ/oy2rerRynVhqVA//ZXZ3Fo60PikLHEPQc=",
"owner": "nix-community",
"repo": "NixOS-WSL",
"rev": "5482f113fd31ebac131d1ebeb2ae90bf0d5e41f5",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "main",
"repo": "NixOS-WSL",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1767364772,
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1776169885,
"narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1778274207,
"narHash": "sha256-I4puXmX1iovcCHZlRmztO3vW0mAbbRvq4F8wgIMQ1MM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b3da656039dc7a6240f27b2ef8cc6a3ef3bccae7",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1682134069,
"narHash": "sha256-TnI/ZXSmRxQDt2sjRYK/8j8iha4B4zP2cnQCZZ3vp7k=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fd901ef4bf93499374c5af385b2943f5801c0833",
"type": "github"
},
"original": {
"id": "nixpkgs",
"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",
"clan-core": "clan-core",
"disko": "disko_2",
"flake-parts": "flake-parts_2",
"home-manager": "home-manager",
"import-tree": "import-tree",
"nix-darwin": "nix-darwin_2",
"nix-openclaw": "nix-openclaw",
"nixos-wsl": "nixos-wsl",
"nixpkgs": "nixpkgs_3",
"vscode-server": "vscode-server",
"zen-browser": "zen-browser"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": [
"clan-core",
"nixpkgs"
]
},
"locked": {
"lastModified": 1776119890,
"narHash": "sha256-Zm6bxLNnEOYuS/SzrAGsYuXSwk3cbkRQZY0fJnk8a5M=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "d4971dd58c6627bfee52a1ad4237637c0a2fb0cd",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1774449309,
"narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=",
"owner": "nix-systems",
"repo": "default",
"rev": "c29398b59d2048c4ab79345812849c9bd15e9150",
"type": "github"
},
"original": {
"owner": "nix-systems",
"ref": "future-26.11",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"clan-community",
"nixpkgs"
]
},
"locked": {
"lastModified": 1775125835,
"narHash": "sha256-2qYcPgzFhnQWchHo0SlqLHrXpux5i6ay6UHA+v2iH4U=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "75925962939880974e3ab417879daffcba36c4a3",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
},
"treefmt-nix_2": {
"inputs": {
"nixpkgs": [
"clan-core",
"nixpkgs"
]
},
"locked": {
"lastModified": 1775636079,
"narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
},
"vscode-server": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_4"
},
"locked": {
"lastModified": 1770124655,
"narHash": "sha256-yHmd2B13EtBUPLJ+x0EaBwNkQr9LTne1arLVxT6hSnY=",
"owner": "nix-community",
"repo": "nixos-vscode-server",
"rev": "92ce71c3ba5a94f854e02d57b14af4997ab54ef0",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixos-vscode-server",
"type": "github"
}
},
"zen-browser": {
"inputs": {
"home-manager": "home-manager_3",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1778394798,
"narHash": "sha256-/jR8bModWv0ji305ecMgAB+2eaXLZiYdH+9Z4JIRkuA=",
"owner": "0xc000022070",
"repo": "zen-browser-flake",
"rev": "45bc54456044b96492923739bfae633e1a4352e1",
"type": "github"
},
"original": {
"owner": "0xc000022070",
"repo": "zen-browser-flake",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

44
flake.nix Normal file
View file

@ -0,0 +1,44 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
nixos-wsl.url = "github:nix-community/NixOS-WSL/main";
vscode-server.url = "github:nix-community/nixos-vscode-server";
flake-parts.url = "github:hercules-ci/flake-parts";
flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs";
# Auto-loads every .nix file under ./flake-modules as a flake-parts module.
import-tree.url = "github:vic/import-tree";
nix-darwin.url = "github:nix-darwin/nix-darwin/master";
nix-darwin.inputs.nixpkgs.follows = "nixpkgs";
home-manager.url = "github:nix-community/home-manager";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
zen-browser.url = "github:0xc000022070/zen-browser-flake";
zen-browser.inputs.nixpkgs.follows = "nixpkgs";
disko.url = "github:nix-community/disko";
disko.inputs.nixpkgs.follows = "nixpkgs";
nix-openclaw.url = "github:openclaw/nix-openclaw";
nix-openclaw.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";
# 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";
};
outputs = inputs @ { flake-parts, import-tree, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [ "x86_64-linux" "aarch64-darwin" ];
imports = [ (import-tree ./flake-modules) ];
};
}

35
lib/home-manager-user.nix Normal file
View file

@ -0,0 +1,35 @@
# Shared home-manager wiring for NixOS and nix-darwin hosts.
#
# Usage (from a flake-module):
# modules = [
# inputs.home-manager.nixosModules.home-manager # or .darwinModules
# (import ../lib/home-manager-user.nix {
# lib = inputs.nixpkgs.lib;
# user = "danny";
# homeDirectory = "/home/danny";
# stateVersion = "25.11"; # optional
# userImports = [ ../home/danny/home.nix ]; # optional
# })
# ];
{ lib
, user
, homeDirectory
, stateVersion ? null
, userImports ? [ ]
}:
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
# Automatically back up files before home-manager overwrites them.
home-manager.backupFileExtension = "backup";
home-manager.users.${user} = { ... }: {
imports = userImports;
home = {
username = user;
# Force an absolute path even if another module sets a bad value.
homeDirectory = lib.mkForce homeDirectory;
} // lib.optionalAttrs (stateVersion != null) {
stateVersion = stateVersion;
};
};
}

View file

@ -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 ];
}

View file

@ -0,0 +1,134 @@
# Prometheus + Alertmanager + Grafana on sunken-ship.
#
# Scrape targets are the clan ZeroTier IPv6s — kept in sync with
# vars/per-machine/<host>/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 = [ "[::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 ];
}

View file

@ -0,0 +1,15 @@
# A small set of network/process debugging tools that we'd otherwise
# pick up from `clan.core.enableRecommendedDefaults = true`. The full
# clan defaults also flip systemd-networkd / systemd-resolved on, which
# breaks dnsmasq + navidrome's resolv.conf bind-mount, so we opted out
# fleet-wide and added just the useful packages explicitly here.
{ pkgs, ... }:
{
environment.systemPackages = with pkgs; [
htop # process monitor
tcpdump # packet capture
dnsutils # dig, nslookup, host
jq # JSON parser
curl # HTTP client
];
}

31
nixos/disko-cloud.nix Normal file
View file

@ -0,0 +1,31 @@
# Disko layout for cloud VPS installs (e.g. Hetzner Cloud).
# GPT with a 1MB BIOS boot partition (for GRUB on a BIOS system) + root.
# No LUKS — the provider has physical disk access anyway and there's
# no operator present at boot to enter a passphrase.
{
disko.devices = {
disk.main = {
type = "disk";
device = "/dev/sda";
content = {
type = "gpt";
partitions = {
# GRUB BIOS boot partition — holds stage-1.5 bootloader code.
# Type EF02. No filesystem.
BIOSBOOT = {
size = "1M";
type = "EF02";
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
};
}

View file

@ -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 = "/";
};
};
};
};
};
};
}

View file

@ -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 = "/";
};
};
};
};
};
};
}

View file

@ -11,7 +11,7 @@
weather = "curl wttr.in/?T"; weather = "curl wttr.in/?T";
# TODO: rename and move 25_flakes into dotfiles # TODO: rename and move 25_flakes into dotfiles
nide = "nix develop ~/python-projects/25_flakes/$(basename (pwd)) -c $(which fish)"; nide = "nix develop ~/python-projects/25_flakes/$(basename (pwd)) -c $(which fish)";
nixupdate = "cd ~/dotfiles/nixos && sudo nix flake update && sudo darwin-rebuild switch --flake ~/dotfiles/nixos#Daniel-Macbook-Air"; nixupdate = "cd ~/dotfiles && sudo nix flake update && sudo darwin-rebuild switch --flake ~/dotfiles#Daniel-Macbook-Air";
}; };
interactiveShellInit = '' interactiveShellInit = ''
function fish_user_key_bindings function fish_user_key_bindings
@ -24,6 +24,43 @@
set fish_greeting 🐟: (set_color yellow; date +%T; set_color green; date --iso-8601 2>/dev/null; or date +%F; set_color normal) 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 &
end
# name: Default # name: Default
# author: Lily Ballard # author: Lily Ballard
# edits: DannyDannyDanny # edits: DannyDannyDanny

254
nixos/flake.lock generated
View file

@ -1,254 +0,0 @@
{
"nodes": {
"disko": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1773506317,
"narHash": "sha256-qWKbLUJpavIpvOdX1fhHYm0WGerytFHRoh9lVck6Bh0=",
"owner": "nix-community",
"repo": "disko",
"rev": "878ec37d6a8f52c6c801d0e2a2ad554c75b9353c",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "disko",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1773810247,
"narHash": "sha256-6Vz1Thy/1s7z+Rq5OfkWOBAdV4eD+OrvDs10yH6xJzQ=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "d47357a4c806d18a3e853ad2699eaec3c01622e7",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"home-manager_2": {
"inputs": {
"nixpkgs": [
"zen-browser",
"nixpkgs"
]
},
"locked": {
"lastModified": 1773422513,
"narHash": "sha256-MPjR48roW7CUMU6lu0+qQGqj92Kuh3paIulMWFZy+NQ=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "ef12a9a2b0f77c8fa3dda1e7e494fca668909056",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"nix-darwin": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1773000227,
"narHash": "sha256-zm3ftUQw0MPumYi91HovoGhgyZBlM4o3Zy0LhPNwzXE=",
"owner": "nix-darwin",
"repo": "nix-darwin",
"rev": "da529ac9e46f25ed5616fd634079a5f3c579135f",
"type": "github"
},
"original": {
"owner": "nix-darwin",
"ref": "master",
"repo": "nix-darwin",
"type": "github"
}
},
"nixos-wsl": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1773603777,
"narHash": "sha256-oXSEbMR/IuHYk9nvrbRhaYBxVK5s63DH2UGOZT2ok48=",
"owner": "nix-community",
"repo": "NixOS-WSL",
"rev": "0efe7af73d6e4a8d447a22936c5526d73822b0a7",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "main",
"repo": "NixOS-WSL",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1773282481,
"narHash": "sha256-b/GV2ysM8mKHhinse2wz+uP37epUrSE+sAKXy/xvBY4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fe416aaedd397cacb33a610b33d60ff2b431b127",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1773628058,
"narHash": "sha256-hpXH0z3K9xv0fHaje136KY872VT2T5uwxtezlAskQgY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f8573b9c935cfaa162dd62cc9e75ae2db86f85df",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1682134069,
"narHash": "sha256-TnI/ZXSmRxQDt2sjRYK/8j8iha4B4zP2cnQCZZ3vp7k=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fd901ef4bf93499374c5af385b2943f5801c0833",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"root": {
"inputs": {
"disko": "disko",
"home-manager": "home-manager",
"nix-darwin": "nix-darwin",
"nixos-wsl": "nixos-wsl",
"nixpkgs": "nixpkgs_2",
"vscode-server": "vscode-server",
"zen-browser": "zen-browser"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"vscode-server": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1770124655,
"narHash": "sha256-yHmd2B13EtBUPLJ+x0EaBwNkQr9LTne1arLVxT6hSnY=",
"owner": "nix-community",
"repo": "nixos-vscode-server",
"rev": "92ce71c3ba5a94f854e02d57b14af4997ab54ef0",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixos-vscode-server",
"type": "github"
}
},
"zen-browser": {
"inputs": {
"home-manager": "home-manager_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1773737882,
"narHash": "sha256-P6k0BtT1/idYveVRdcwAZk8By9UjZW8XOMhSoS6wTBY=",
"owner": "0xc000022070",
"repo": "zen-browser-flake",
"rev": "a7f1db35d74faf04e5189b3a32f890186ace5c28",
"type": "github"
},
"original": {
"owner": "0xc000022070",
"repo": "zen-browser-flake",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View file

@ -1,134 +0,0 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
nixos-wsl.url = "github:nix-community/NixOS-WSL/main";
vscode-server.url = "github:nix-community/nixos-vscode-server";
nix-darwin.url = "github:nix-darwin/nix-darwin/master";
nix-darwin.inputs.nixpkgs.follows = "nixpkgs";
home-manager.url = "github:nix-community/home-manager";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
zen-browser.url = "github:0xc000022070/zen-browser-flake";
zen-browser.inputs.nixpkgs.follows = "nixpkgs";
disko.url = "github:nix-community/disko";
disko.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = {
nixpkgs,
nixos-wsl,
vscode-server,
nix-darwin,
self,
home-manager,
zen-browser,
disko,
...
}: {
nixosConfigurations = {
wsl = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
nixos-wsl.nixosModules.default
vscode-server.nixosModules.default
./hosts/wsl.nix
./tmux.nix
# TODO: handle all user-level programs via home-manager
# ./neovim.nix # Now handled via home-manager
./fish.nix
# home-manager.nixosModules.default
];
};
macbookair = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
nixos-wsl.nixosModules.default
vscode-server.nixosModules.default
./hosts/macbookair.nix
./hardware-configuration.nix
./tmux.nix
# TODO: handle all user-level programs via home-manager
# ./neovim.nix # Now handled via home-manager
./fish.nix
# home-manager.nixosModules.default
];
};
sunken-ship = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./hosts/sunken-ship.nix
# Home Manager on NixOS
home-manager.nixosModules.home-manager
({ lib, ... }: {
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.backupFileExtension = "backup";
home-manager.users.danny = { ... }: {
home.username = "danny";
home.homeDirectory = lib.mkForce "/home/danny";
home.stateVersion = "25.11";
};
})
];
};
# For disko-install: LUKS + WiFi; hostname/WiFi via --system-config.
server-install = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
disko.nixosModules.disko
./disko-server.nix
./hosts/server-install.nix
];
};
# Custom minimal installer ISO (build with: nix build .#installer-iso).
# Optional: add ./installer-wifi.nix (gitignored) to modules for live WiFi.
installer-iso = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./installer-iso.nix ];
};
};
packages.x86_64-linux.installer-iso =
self.nixosConfigurations.installer-iso.config.system.build.isoImage;
# macOS (nix-darwin) configuration
darwinConfigurations."Daniel-Macbook-Air" = nix-darwin.lib.darwinSystem {
specialArgs = { inherit zen-browser; };
modules = [
./hosts/macos.nix
./fish.nix
# Home Manager on macOS
home-manager.darwinModules.home-manager
({ lib, zen-browser, ... }: {
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
# Automatically backup files before home-manager overwrites them
home-manager.backupFileExtension = "backup";
# Pass flake inputs to home-manager modules (e.g. home.nix)
home-manager.extraSpecialArgs = { inherit zen-browser; };
home-manager.users.danny = { ... }: {
# Force an absolute path even if another module sets a bad value.
home.username = "danny";
home.homeDirectory = lib.mkForce "/Users/danny";
imports = [
./home/danny/home.nix
];
};
})
];
};
};
}

View file

@ -1,42 +0,0 @@
# 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 = [ "uhci_hcd" "ehci_pci" "ahci" "usbhid" "usb_storage" "sd_mod" ];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" "wl" ];
boot.extraModulePackages = [ config.boot.kernelPackages.broadcom_sta ];
fileSystems."/" =
{ device = "/dev/disk/by-uuid/bf59e35a-f96d-489d-9b14-93f67d5e294d";
fsType = "ext4";
};
boot.initrd.luks.devices."luks-5b4978ab-ee25-4a85-8f56-0bdbe932f154".device = "/dev/disk/by-uuid/5b4978ab-ee25-4a85-8f56-0bdbe932f154";
fileSystems."/boot" =
{ device = "/dev/disk/by-uuid/691B-AF9A";
fsType = "vfat";
options = [ "fmask=0022" "dmask=0022" ];
};
swapDevices =
[ { device = "/dev/disk/by-uuid/08f3fa7a-1e84-4819-b696-1536bc44ef99"; }
];
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
networking.useDHCP = lib.mkDefault true;
# networking.interfaces.wlp2s0b1.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}

View file

@ -1,4 +1,4 @@
{ pkgs, lib, zen-browser ? null, ... }: { pkgs, lib, config, ... }:
{ {
# TODO: remove next two lines from here or from flake.nix # TODO: remove next two lines from here or from flake.nix
# home.username = "danny"; # home.username = "danny";
@ -9,6 +9,24 @@
# Import neovim configuration # Import neovim configuration
imports = [ ../../neovim.nix ]; imports = [ ../../neovim.nix ];
# ZeroTier SSH aliases — managed drop-in under ~/.ssh/config.d/.
# For this to take effect, your ~/.ssh/config must include:
# Include ~/.ssh/config.d/*
# near the top (before any host-specific blocks).
home.file.".ssh/config.d/zerotier".text = ''
Host sunken-ship-zt fdd5:53a2:de33:d269:6499:93d5:53a2:de33
HostName fdd5:53a2:de33:d269:6499:93d5:53a2:de33
User danny
IdentityFile ~/.ssh/id_ed25519_sunken_ship
IdentitiesOnly yes
Host phantom-ship-zt fdd5:53a2:de33:d269:6499:936c:48a:bbdc
HostName fdd5:53a2:de33:d269:6499:936c:48a:bbdc
User danny
IdentityFile ~/.ssh/id_ed25519_phantom_ship
IdentitiesOnly yes
'';
# tmux (user-level; same config on macOS and NixOS if you reuse this file) # tmux (user-level; same config on macOS and NixOS if you reuse this file)
programs.tmux = { programs.tmux = {
enable = true; enable = true;
@ -43,6 +61,12 @@
bind -r C-h select-window -t :- bind -r C-h select-window -t :-
bind -r C-l select-window -t :+ bind -r C-l select-window -t :+
# Resize pane shortcuts
bind -r H resize-pane -L 10
bind -r J resize-pane -D 10
bind -r K resize-pane -U 10
bind -r L resize-pane -R 10
# split with dash and vbar # split with dash and vbar
bind | split-window -h -c "#{pane_current_path}" bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}" bind - split-window -v -c "#{pane_current_path}"
@ -64,6 +88,35 @@
catppuccin catppuccin
tmux-fzf tmux-fzf
extrakto extrakto
# tmux-resurrect: prefix + Ctrl-s saves, prefix + Ctrl-r restores.
# 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
# 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'
'';
}
]; ];
}; };
@ -112,15 +165,31 @@
executable = true; executable = true;
}; };
# Alacritty terminal configuration with conditional theme switching # Palette fragments: synced to system appearance (see scripts/alacritty-sync-system-theme.sh).
xdg.configFile."alacritty/catppuccin-latte-colors.toml".source =
../../../assets/alacritty/catppuccin-latte-colors.toml;
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 = { programs.alacritty = {
enable = true; enable = true;
settings = { settings = {
general = {
live_config_reload = true;
import = [ "${config.xdg.configHome}/alacritty/active-colors.toml" ];
};
window = { window = {
padding = { x = 8; y = 8; }; padding = { x = 8; y = 8; };
dynamic_padding = true; dynamic_padding = true;
decorations = "buttonless"; decorations = "buttonless";
opacity = 0.95; decorations_theme_variant = "None";
opacity = 1.0;
startup_mode = "Maximized"; startup_mode = "Maximized";
option_as_alt = "Both"; option_as_alt = "Both";
}; };
@ -134,51 +203,29 @@
program = "${pkgs.fish}/bin/fish"; program = "${pkgs.fish}/bin/fish";
}; };
}; };
# Conditional colors based on system theme
colors = let
# Set this to true for light theme, false for dark theme
# You can change this and run 'darwin-rebuild switch' to switch themes
isLightTheme = true;
# Catppuccin Latte (Light) colors
lightColors = {
primary = { background = "0xeff1f5"; foreground = "0x4c4f69"; };
cursor = { text = "0xeff1f5"; cursor = "0xdc8a78"; };
normal = {
black = "0x5c5f77"; red = "0xd20f39"; green = "0x40a02b"; yellow = "0xdf8e1d";
blue = "0x1e40af"; magenta = "0xea76cb"; cyan = "0x179299"; white = "0xacb0be";
};
bright = {
black = "0x6c6f85"; red = "0xd20f39"; green = "0x40a02b"; yellow = "0xdf8e1d";
blue = "0x1e40af"; magenta = "0xea76cb"; cyan = "0x179299"; white = "0xbcc0cc";
}; };
}; };
# Catppuccin Mocha (Dark) colors # Writable copy (not a symlink to the store — cp in the sync script must replace a real file).
darkColors = { home.activation.alacrittySystemTheme = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
primary = { background = "0x1e1e2e"; foreground = "0xcdd6f4"; }; MOCHA="${../../../assets/alacritty/catppuccin-mocha-colors.toml}"
cursor = { text = "0x1e1e2e"; cursor = "0xf5e0dc"; }; ACTIVE="${config.xdg.configHome}/alacritty/active-colors.toml"
normal = { $DRY_RUN_CMD mkdir -p "${config.xdg.configHome}/alacritty"
black = "0x45475a"; red = "0xf38ba8"; green = "0xa6e3a1"; yellow = "0xf9e2af"; if [ ! -f "$ACTIVE" ]; then
blue = "0x89b4fa"; magenta = "0xf5c2e7"; cyan = "0x94e2d5"; white = "0xbac2de"; $DRY_RUN_CMD cp "$MOCHA" "$ACTIVE"
}; $DRY_RUN_CMD chmod 0644 "$ACTIVE"
bright = { fi
black = "0x585b70"; red = "0xf38ba8"; green = "0xa6e3a1"; yellow = "0xf9e2af"; $DRY_RUN_CMD ${pkgs.bash}/bin/bash "${../../../scripts/alacritty-sync-system-theme.sh}" || true
blue = "0x89b4fa"; magenta = "0xf5c2e7"; cyan = "0x94e2d5"; white = "0xa6adc8"; '';
};
};
in if isLightTheme then lightColors else darkColors;
};
};
# TODO: Put user-installed binaries here if you want HM to own them (optional) # TODO: Put user-installed binaries here if you want HM to own them (optional)
# Fonts # Fonts
fonts.fontconfig.enable = true; fonts.fontconfig.enable = true;
home.packages = with pkgs; [ home.packages = with pkgs; [
# Zen Browser (Firefox fork; from flake, supports aarch64-darwin) # Zen Browser (Firefox fork; from flake overlay, supports aarch64-darwin)
] ++ (lib.optionals (zen-browser != null) [ ] ++ (lib.optionals (pkgs ? zen-browser) [
zen-browser.packages.${pkgs.stdenv.hostPlatform.system}.default pkgs.zen-browser
]) ++ (with pkgs; [ ]) ++ (with pkgs; [
# Google Fonts (includes Michroma) # Google Fonts (includes Michroma)
google-fonts google-fonts
@ -215,12 +262,15 @@
# alacritty # TODO: configured via programs.alacritty above, so not needed here # alacritty # TODO: configured via programs.alacritty above, so not needed here
# warp-terminal # TODO: Bloat # warp-terminal # TODO: Bloat
# vscodium # TODO: Bloat # vscodium # TODO: Bloat
# zed-editor # TODO: Bloat zed-editor
code-cursor code-cursor
cursor-cli cursor-cli
cinny-desktop # Matrix client (Tauri wrapper around the Cinny web app)
dfu-util # USB DFU firmware flasher (Flipper Zero etc.)
discord discord
mapscii mapscii
mpv mpv
# uhk-agent # UHK keyboard configuration GUI + CLI — removed, nixpkgs marks x86_64-linux only TODO
]); ]);
# First HM version for this user config; bump only if you understand the migration notes. # First HM version for this user config; bump only if you understand the migration notes.

View file

@ -0,0 +1,109 @@
{ config, lib, pkgs, ... }:
let
alacrittySyncSystemTheme = pkgs.writeShellScriptBin "alacritty-sync-system-theme"
(builtins.readFile ../../scripts/alacritty-sync-system-theme.sh);
# nix-darwin's nix.gc / nix.optimise require nix.enable; with Determinate (nix.enable = false)
# we schedule the same commands via launchd using nixpkgs' nix CLI (same defaults as upstream modules).
nixGcInterval = [{ Weekday = 7; Hour = 3; Minute = 15; }];
nixOptimiseInterval = [{ Weekday = 7; Hour = 4; Minute = 15; }];
in {
# Apple Silicon + nix-darwin basics
nixpkgs.hostPlatform = "aarch64-darwin";
nix.enable = false; # Determinate manages Nix
nixpkgs.config.allowUnfree = true;
system.primaryUser = "danny";
# Shells (fish config is in fish.nix, imported via flake.nix)
environment.shells = [ pkgs.fish ];
users.users.danny.shell = pkgs.fish;
# ollama
imports = [../ollama.nix];
services.ollama = {
enable = true;
};
# Networking (macOS-safe)
networking = {
# Set if you want a specific hostname in macOS UI as well:
hostName = "Daniel-Macbook-Air";
knownNetworkServices = [ "Wi-Fi" "Thunderbolt Bridge" ];
};
homebrew = {
enable = true;
casks = [
"google-chrome"
"disk-inventory-x" # Apple Silicon uses Homebrew; nixpkgs package is x86_64-darwin only.
"qflipper" # Flipper Zero firmware updater GUI
"zerotier-one" # Clan homelab overlay — authorize on sunken-ship controller
# "uhk-agent" # Ultimate Hacking Keyboard configuration — removed, nixpkgs marks x86_64-linux only TODO
];
onActivation.cleanup = "zap";
};
# macOS niceties
security.pam.services.sudo_local.touchIdAuth = true;
system.defaults = {
# Keyboard
NSGlobalDomain = {
AppleShowAllExtensions = true;
ApplePressAndHoldEnabled = true;
"com.apple.mouse.tapBehavior" = 1;
"com.apple.sound.beep.volume" = 0.0;
"com.apple.sound.beep.feedback" = 0;
};
# Finder & Dock
finder.AppleShowAllExtensions = true;
dock.autohide = true;
dock.mru-spaces = false;
};
# User-specific packages and environment variables are now in home-manager (home.nix)
# Only system-level packages should remain here if needed
environment.systemPackages = [
alacrittySyncSystemTheme
pkgs.feishin # Subsonic/Navidrome desktop music player
];
# Poll macOS appearance; updates ~/.config/alacritty/active-colors.toml (Alacritty live_config_reload).
launchd.user.agents.alacritty-system-theme = {
serviceConfig = {
RunAtLoad = true;
StartInterval = 30;
ProgramArguments = [ "${alacrittySyncSystemTheme}/bin/alacritty-sync-system-theme" ];
StandardOutPath = "/tmp/alacritty-theme-sync.log";
StandardErrorPath = "/tmp/alacritty-theme-sync-error.log";
};
};
launchd.daemons = {
nix-gc-determ = {
command =
"${lib.getExe' pkgs.nix "nix-collect-garbage"} --delete-older-than 14d";
serviceConfig = {
RunAtLoad = false;
StartCalendarInterval = nixGcInterval;
};
};
nix-store-optimise-determ = {
command = "${lib.getExe' pkgs.nix "nix-store"} --optimise";
serviceConfig = {
RunAtLoad = false;
StartCalendarInterval = nixOptimiseInterval;
};
};
};
# Keep for darwin as well (tracks defaults across upgrades)
# current max per nix-darwin; bump only if a release notes says so
system.stateVersion = 6;
}

View file

@ -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;
}

View file

@ -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 ];
}

View file

@ -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;
}

View file

@ -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 ];
}

View file

@ -1,197 +0,0 @@
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page
# and in the NixOS manual (accessible by running nixos-help).
{ config, pkgs, ... }:
{
# Bootloader.
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
boot.initrd.luks.devices."luks-04715655-635c-46ee-8100-1a5a4f3700a5".device = "/dev/disk/by-uuid/04715655-635c-46ee-8100-1a5a4f3700a5";
networking.hostName = "nixos"; # Define your hostname.
# NOTE: You can not use networking.networkmanager with networking.wireless
# networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
nix.settings.experimental-features = [ "nix-command" "flakes" ]; # for vscode remote server
# Configure network proxy if necessary
# networking.proxy.default = "http://user:password@proxy:port/";
# networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";
# Enable networking
networking.networkmanager.enable = true;
# Set your time zone.
time.timeZone = "Europe/Copenhagen";
# Select internationalisation properties.
i18n.defaultLocale = "en_DK.UTF-8";
i18n.extraLocaleSettings = {
LC_ADDRESS = "da_DK.UTF-8";
LC_IDENTIFICATION = "da_DK.UTF-8";
LC_MEASUREMENT = "da_DK.UTF-8";
LC_MONETARY = "da_DK.UTF-8";
LC_NAME = "da_DK.UTF-8";
LC_NUMERIC = "da_DK.UTF-8";
LC_PAPER = "da_DK.UTF-8";
LC_TELEPHONE = "da_DK.UTF-8";
LC_TIME = "da_DK.UTF-8";
};
# Enable the X11 windowing system.
services.xserver.enable = true;
# Enable the KDE Plasma Desktop Environment.
services.displayManager.sddm.enable = true;
services.desktopManager.plasma6.enable = true;
# Configure keymap in X11
services.xserver = {
xkb.layout = "us";
xkb.variant = "";
};
programs.nix-ld.enable = true;
# TODO: move to home manager (?)
programs = {
direnv = {
enable = true;
enableFishIntegration = true;
nix-direnv.enable = true;
};
};
# Enable CUPS to print documents.
services.printing.enable = true;
# Enable sound with pipewire.
services.pulseaudio.enable = false;
hardware.alsa.enable = false;
security.rtkit.enable = true;
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
# If you want to use JACK applications, uncomment this
#jack.enable = true;
# use the example session manager (no others are packaged yet so this is enabled by default,
# no need to redefine it in your config for now)
#media-session.enable = true;
};
# Enable touchpad support (enabled default in most desktopManager).
# services.xserver.libinput.enable = true;
# Define a user account. Don't forget to set a password with passwd.
users.users.dth = {
isNormalUser = true;
description = "dth";
extraGroups = [ "networkmanager" "wheel" ];
# TODO: use home manager to define user packages
packages = with pkgs; [
vlc # video player
# kate # editor
ripgrep # faster grep
nextcloud-client # private cloud
# digikam # photo / video management
# thunderbird # bloat
];
};
# Install firefox.
programs.firefox.enable = true;
# install kde partition manager
programs.partition-manager.enable = true;
# TODO: install gnome disk manager
# programs.gnome-disks.enable = true;
# Allow unfree packages
nixpkgs.config.allowUnfree = true;
nixpkgs.config.permittedInsecurePackages = [
"broadcom-sta-6.30.223.271-59-6.18.10"
];
boot.kernelModules = [ "wl" ];
# List packages installed in system profile. To search, run:
# $ nix search wget
environment.systemPackages = with pkgs; [
# tmux # activated in tmux.nix
# vim # using neovim in stead
# neovim # activated in neovim.nix
git # version control
gh # github cli tool
claude-code #anthropic's agentic coding cli
ripgrep # faster grep
busybox # useful programs e.g. tree, unzip etc
openssl # cryptography swiss army knife
xdg-utils # terminal desktop intergrations (i.e. allow terminal to open browser)
xclip # terminal clipboard integration (i.e. allow terminal to r/w clipboard)
fastfetch # system info
btop # resource monitor
wget # downloader
tldr # community driven manpage alternative
ntfs3g # mount NTFS drives on linux
gptfdisk # formatting drives - like fdisk but better
# this stuff runs gparted
# gimp # bloat image editing
# blender # bloat 3D modelling
# inkscape # bloat vector graphics / drawing
kdePackages.kdenlive # bloat video editor
# desktop applications
thunderbird # email / calendar
telegram-desktop # instant messager
cowsay
lolcat
];
# firefox smooth scrolling
environment.sessionVariables = {
MOZ_USE_XINPUT2 = "1";
};
# Some programs need SUID wrappers, can be configured further or are
# started in user sessions.
# programs.mtr.enable = true;
# programs.gnupg.agent = {
# enable = true;
# enableSSHSupport = true;
# };
# List services that you want to enable:
# Enable the OpenSSH daemon.
# services.openssh.enable = true;
# Open ports in the firewall.
# networking.firewall.allowedTCPPorts = [ ... ];
# networking.firewall.allowedUDPPorts = [ ... ];
# Or disable the firewall altogether.
# networking.firewall.enable = false;
# This value determines the NixOS release from which the default
# settings for stateful data, like file locations and database versions
# on your system were taken. Its perfectly fine and recommended to leave
# this value at the release version of the first install of this system.
# Before changing this value read the documentation for this option
# (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
system.stateVersion = "23.11"; # Did you read the comment?
}

View file

@ -1,55 +0,0 @@
{ config, lib, pkgs, ... }:
{
# Apple Silicon + nix-darwin basics
nixpkgs.hostPlatform = "aarch64-darwin";
nix.enable = false; # Determinate manages Nix
nixpkgs.config.allowUnfree = true;
system.primaryUser = "danny";
# Shells (fish config is in fish.nix, imported via flake.nix)
environment.shells = [ pkgs.fish ];
users.users.danny.shell = pkgs.fish;
# ollama
imports = [../ollama.nix];
services.ollama = {
enable = true;
};
# Networking (macOS-safe)
networking = {
# Set if you want a specific hostname in macOS UI as well:
hostName = "Daniel-Macbook-Air";
knownNetworkServices = [ "Wi-Fi" "Thunderbolt Bridge" ];
};
# macOS niceties
security.pam.services.sudo_local.touchIdAuth = true;
system.defaults = {
# Keyboard
NSGlobalDomain = {
AppleShowAllExtensions = true;
ApplePressAndHoldEnabled = true;
"com.apple.mouse.tapBehavior" = 1;
"com.apple.sound.beep.volume" = 0.0;
"com.apple.sound.beep.feedback" = 0;
};
# Finder & Dock
finder.AppleShowAllExtensions = true;
dock.autohide = true;
dock.mru-spaces = false;
};
# User-specific packages and environment variables are now in home-manager (home.nix)
# Only system-level packages should remain here if needed
# Keep for darwin as well (tracks defaults across upgrades)
# current max per nix-darwin; bump only if a release notes says so
system.stateVersion = 6;
}

View file

@ -0,0 +1,31 @@
# Generated by nixos-generate-config on phantom-ship (cleaned of chroot bind-mount duplicates)
{ config, lib, pkgs, modulesPath, ... }:
{
imports = [ (modulesPath + "/installer/scan/not-detected.nix") ];
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
boot.initrd.availableKernelModules = [ "xhci_pci" "ahci" "usb_storage" "sd_mod" "sr_mod" "rtsx_pci_sdmmc" ];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ ];
boot.extraModulePackages = [ ];
boot.initrd.luks.devices."crypted".device = "/dev/disk/by-uuid/796c1fcd-af8b-449a-90f2-9ebcb9640462";
fileSystems."/" = {
device = "/dev/mapper/crypted";
fsType = "ext4";
};
fileSystems."/boot" = {
device = "/dev/disk/by-uuid/70E3-E5D8";
fsType = "vfat";
options = [ "fmask=0022" "dmask=0022" ];
};
swapDevices = [ ];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}

View file

@ -0,0 +1,664 @@
# NixOS server: SSH, auto-rebuild, NAT for rusty-anchor, OpenClaw gateway.
{ config, lib, pkgs, ... }:
let
# Telegram user ID(s) - gitignored, not committed to public repo.
# Create openclaw-allow-from.nix with e.g.: [ 12345678 ]
allowFromPath = ./openclaw-allow-from.nix;
openclawAllowFrom = if builtins.pathExists allowFromPath then import allowFromPath else [ ];
haraGmailMcp = pkgs.callPackage ../pkgs/hara-gmail-mcp { };
haraMcpServersJson = builtins.toJSON {
mcpServers = {
gmail = {
command = "${haraGmailMcp}/bin/hara-gmail-mcp";
args = [ ];
env = { };
};
};
};
in
{
imports = [
./phantom-ship-hardware.nix
../pkgs/hara-gmail-mcp/module.nix
];
networking.hostName = "phantom-ship";
networking.useDHCP = lib.mkDefault true;
networking.wireless.enable = true; # credentials in /etc/wpa_supplicant.conf (outside repo)
# NAT: share WiFi internet to rusty-anchor over ethernet
networking.nat = {
enable = true;
externalInterface = "wlp1s0";
internalInterfaces = [ "enp0s31f6" ];
};
networking.interfaces.enp0s31f6.ipv4.addresses = [{
address = "10.0.0.1";
prefixLength = 24;
}];
services.dnsmasq = {
enable = true;
settings = {
interface = "enp0s31f6";
dhcp-range = "10.0.0.10,10.0.0.50,24h";
dhcp-option = [ "3,10.0.0.1" "6,10.0.0.1" ]; # gateway + DNS
};
};
networking.firewall.trustedInterfaces = [ "enp0s31f6" ];
# KomTolk (:8080), Shelfish (:8081), Scuttle (:8082), Bananasimulator
# (:8083), Forgejo (:3000), Escape Hormuz (:8090), bon (:8091),
# 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 8084 8090 8091 8092 8093 ];
hardware.enableRedistributableFirmware = true; # iwlwifi (Intel 8260) + GPU + BT firmware
boot.kernelParams = [ "consoleblank=60" ]; # blank TTY after 60s to reduce burn-in
# Turn off panel backlight after boot so the screen actually dims.
systemd.services.server-backlight-off = {
description = "Turn off panel backlight after console idle (reduce burn-in)";
after = [ "multi-user.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig.Type = "oneshot";
script = ''
${pkgs.coreutils}/bin/sleep 65
for d in /sys/class/backlight/*; do
[ -f "$d/brightness" ] && echo 0 > "$d/brightness" 2>/dev/null || true
done
'';
};
time.timeZone = "Europe/Copenhagen";
nixpkgs.config.permittedInsecurePackages = [ "openclaw-2026.3.12" "openclaw-2026.4.12" ];
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ "claude-code" ];
nix.settings.experimental-features = [ "nix-command" "flakes" ];
programs.nix-ld.enable = true; # run dynamically linked binaries (e.g. Claude Code remote CLI)
system.stateVersion = "24.11";
users.users.danny = {
isNormalUser = true;
extraGroups = [ "wheel" ];
# Password is locked (key-only SSH). Use NixOS installer or recovery to reset if needed.
openssh.authorizedKeys.keys = [
# Mac admin (~/.ssh/id_ed25519_phantom_ship on Daniel-Macbook-Air).
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDNl6PrKcEhmYJVqSXNcFU6cba3neekLBGnQCkD7lWAc danny@phantom-ship"
# Self-loopback (clan ssh-ng:// back to this host).
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPyEX8De/b+sMAxUZIqqiPphcrWCoAsN5p8gRFubzqvB danny@phantom-ship"
];
};
# root needs the mac admin key so `clan machines update` can SSH to
# root@<host> for SOPS upload.
users.users.root.openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDNl6PrKcEhmYJVqSXNcFU6cba3neekLBGnQCkD7lWAc danny@phantom-ship"
];
# Key-only auth; no password or keyboard-interactive.
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
};
};
# Passwordless sudo for wheel.
security.sudo.wheelNeedsPassword = false;
environment.systemPackages = with pkgs; [
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
bun # runtime for claude-code channel plugins
claude-code # Claude Code CLI (channels replaces openclaw)
openai-whisper # voice message transcription
ffmpeg # audio decoding for whisper
];
# OpenClaw AI gateway — DISABLED. Replaced by Claude Code Channels below.
# Config kept for easy rollback during validation; will be fully removed in a
# follow-up commit once Channels is proven stable. Workspace state at
# /var/lib/openclaw/ is preserved and also committed to vimwiki/openclaw/.
# Secrets (not in repo): /etc/openclaw/telegram-bot-token, /etc/openclaw/env (ANTHROPIC_API_KEY)
services.openclaw-gateway = {
enable = false;
environmentFiles = [ "/etc/openclaw/env" ];
servicePath = [ pkgs.git pkgs.nodejs pkgs.openai-whisper ];
config = {
gateway.mode = "local";
channels.telegram = {
tokenFile = "/etc/openclaw/telegram-bot-token";
allowFrom = openclawAllowFrom;
};
};
};
# Claude Code Channels — Telegram bridge for @HarakatBot.
# Uses claude.ai subscription auth (long-lived OAuth token) to bypass
# the API rate limits OpenClaw was hitting.
# Secret (not in repo): /etc/claude-channels/env (CLAUDE_CODE_OAUTH_TOKEN)
# Plugin + pairing state lives at /home/danny/.claude/ (set up interactively).
systemd.services.claude-channels = {
description = "Claude Code Channels (Telegram bridge for @HarakatBot)";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
path = [ pkgs.claude-code pkgs.bun pkgs.git pkgs.util-linux ];
environment = {
HOME = "/home/danny";
};
serviceConfig = {
Type = "simple";
User = "danny";
Group = "users";
WorkingDirectory = "/home/danny";
EnvironmentFile = "/etc/claude-channels/env";
# claude needs a PTY; wrap with script(1). /dev/null discards the typescript.
# Permission bypass lives in ~/.claude/settings.json (permissions.defaultMode)
# — using the CLI flag triggers an interactive warning dialog at startup.
ExecStart = ''${pkgs.util-linux}/bin/script -qfc "${pkgs.claude-code}/bin/claude --channels plugin:telegram@claude-plugins-official --mcp-config /etc/hara/mcp-servers.json" /dev/null'';
Restart = "always";
RestartSec = 5;
};
};
# 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 - -"
"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 - -"
"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
# Gmail+Calendar server in path 2.
services.hara-gmail-mcp = {
enable = true;
package = haraGmailMcp;
accounts = [
{
email = "powerhouseplayer@gmail.com";
password_file = "/etc/openclaw/gmail-powerhouseplayer-app-password";
}
{
email = "wildstylewarrior@gmail.com";
password_file = "/etc/openclaw/gmail-wildstylewarrior-app-password";
}
{
email = "danielth95@gmail.com";
password_file = "/etc/openclaw/gmail-danielth95-app-password";
}
];
};
# MCP server registry consumed by claude-channels via --mcp-config.
environment.etc."hara/mcp-servers.json" = {
text = haraMcpServersJson;
mode = "0644";
};
# Git config for the openclaw user: credential helper reads PAT from file.
# PAT (not in repo): /etc/openclaw/github-token (fine-grained, scoped to specific repos)
environment.etc."openclaw/gitconfig" = {
text = ''
[user]
name = OpenClaw Bot
email = noreply@openclaw.local
[credential "https://github.com"]
helper = "!f() { echo username=x-access-token; echo password=$(cat /etc/openclaw/github-token); }; f"
[safe]
directory = /var/lib/openclaw/repos
'';
mode = "0644";
};
# Harden the openclaw-gateway systemd service (only when enabled).
systemd.services.openclaw-gateway = lib.mkIf config.services.openclaw-gateway.enable {
environment.GIT_CONFIG_GLOBAL = "/etc/openclaw/gitconfig";
serviceConfig = {
ProtectHome = "read-only";
ProtectSystem = "strict";
PrivateTmp = true;
NoNewPrivileges = true;
ReadWritePaths = [ "/var/lib/openclaw" "/etc/openclaw" ];
};
};
# Shipyard — Telegram bot that lists Danny's mini-apps and collects feedback.
# 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, 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
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.
SHIPYARD_OWNER_ID = "66070351"; # @DannyDannyDanny
};
serviceConfig = {
WorkingDirectory = "/home/danny/shipyard";
ExecStart = "${pythonEnv}/bin/python bot.py";
Restart = "on-failure";
RestartSec = 10;
User = "danny";
};
};
# 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
# (the bot that publishes shelfish via shipyard's project list).
# DB lives outside the rsynced code dir so deploys don't clobber state.
# (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
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 :: --port 8081";
Restart = "on-failure";
RestartSec = 10;
User = "danny";
};
};
# 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 (geo: Østerbro)";
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";
SC_TILES_DIR = "/home/danny/.local/share/scuttle/tiles";
};
serviceConfig = {
WorkingDirectory = "/home/danny/scuttle";
ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8082";
Restart = "on-failure";
RestartSec = 10;
User = "danny";
};
};
# 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";
};
};
# 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.
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";
};
};
# Ollama — local LLM runtime, used by bon's structured-data extraction
# step. Listens on 127.0.0.1:11434 only (not exposed over ZT).
# 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 — current default
"qwen2.5:7b-instruct" # ~4.7 GB — A/B testing only
];
};
# 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/<user_id>/
# 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
uvicorn
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
# vyscul or other testers report missed text.
tesseractEng = pkgs.tesseract.override {
enableLanguages = [ "eng" ];
};
in {
description = "bon FastAPI server (receipt scanner)";
after = [ "network-online.target" "ollama.service" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
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";
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";
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
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";
};
};
# 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";
};
};
# 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).
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 pkgs.gnused ];
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 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.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" \
-d "{\"chat_id\": $CHAT_ID, \"text\": $(echo "$MSG" | ${pkgs.jq}/bin/jq -Rs .)}" \
> /dev/null
'';
};
systemd.timers.hara-heartbeat = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "06: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";
};
};
# 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.
}

View file

@ -2,15 +2,11 @@
# #
# One-time on server: clone repo to /etc/dotfiles (root needs git access). # One-time on server: clone repo to /etc/dotfiles (root needs git access).
# If private repo: use SSH (ssh:// or git@) and add root's key to GitHub, or use HTTPS + token. # If private repo: use SSH (ssh:// or git@) and add root's key to GitHub, or use HTTPS + token.
# Then: sudo nixos-rebuild switch --flake /etc/dotfiles/nixos#sunken-ship # Then: sudo nixos-rebuild switch --flake /etc/dotfiles#sunken-ship
# If sudo git is not found: sudo nix run nixpkgs#git -- -C /etc/dotfiles pull origin main # If sudo git is not found: sudo nix run nixpkgs#git -- -C /etc/dotfiles pull origin main
# Timer runs every 15 min: git fetch, pull if origin/main changed, rebuild. # Timer runs every 15 min: git fetch, pull if origin/main changed, rebuild.
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
let
dotfilesDir = "/etc/dotfiles";
flakeRef = "${dotfilesDir}/nixos#sunken-ship";
in
{ {
imports = [ ./sunken-ship-hardware.nix ]; imports = [ ./sunken-ship-hardware.nix ];
@ -22,8 +18,7 @@ in
boot.kernelParams = [ "consoleblank=60" ]; # blank TTY after 60s to reduce burn-in boot.kernelParams = [ "consoleblank=60" ]; # blank TTY after 60s to reduce burn-in
# Turn off panel backlight after boot so the screen actually dims (consoleblank only blanks framebuffer). # Turn off panel backlight after boot so the screen actually dims (consoleblank only blanks framebuffer).
# At the console, run: light -S 100 (or any 0100) to restore brightness. # At the console, run: brightnessctl set 100% (or `brightnessctl max`) to restore brightness.
programs.light.enable = true;
systemd.services.server-backlight-off = { systemd.services.server-backlight-off = {
description = "Turn off panel backlight after console idle (reduce burn-in)"; description = "Turn off panel backlight after console idle (reduce burn-in)";
after = [ "multi-user.target" ]; after = [ "multi-user.target" ];
@ -38,15 +33,28 @@ in
}; };
nix.settings.experimental-features = [ "nix-command" "flakes" ]; nix.settings.experimental-features = [ "nix-command" "flakes" ];
programs.nix-ld.enable = true; # run dynamically linked binaries (e.g. Claude Code remote CLI)
system.stateVersion = "24.11"; system.stateVersion = "24.11";
users.users.danny = { users.users.danny = {
isNormalUser = true; isNormalUser = true;
extraGroups = [ "wheel" "video" ]; # video: backlight control via light(1) extraGroups = [ "wheel" "video" "audio" ]; # video: backlight; audio: sound devices
# SSH keys: push via scp, don't commit. NixOS does not manage authorized_keys so scp'd keys persist. openssh.authorizedKeys.keys = [
# Example: scp ~/.ssh/id_ed25519_sunken_ship.pub danny@server:/tmp/ then on server: mkdir -p ~/.ssh; cat /tmp/*.pub >> ~/.ssh/authorized_keys # Mac admin (~/.ssh/id_ed25519_sunken_ship on Daniel-Macbook-Air).
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKW/akfIiVU5o63YrTAJVZhMj7kXfYHOnXDtlpVFW7pf danny@sunken-ship"
# Self-loopback (used by clan ssh-ng:// during nix-copy-closure
# back to this same host on `clan machines update`). Pubkey of the
# /home/danny/.ssh/id_ed25519 that lives on this host.
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB9t4YAaoHvVouqp+qyFOq8o3SAtXMiAmjF6J0ldyx4g danny@sunken-ship self"
];
}; };
# root needs the mac admin key so `clan machines update` can SSH to
# root@<host> to upload SOPS keys (sops-install-secrets bootstrap).
users.users.root.openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKW/akfIiVU5o63YrTAJVZhMj7kXfYHOnXDtlpVFW7pf danny@sunken-ship"
];
# Key-only auth; no password or keyboard-interactive. # Key-only auth; no password or keyboard-interactive.
services.openssh = { services.openssh = {
enable = true; enable = true;
@ -59,28 +67,437 @@ in
# Passwordless sudo for wheel. # Passwordless sudo for wheel.
security.sudo.wheelNeedsPassword = false; security.sudo.wheelNeedsPassword = false;
environment.systemPackages = [ pkgs.git ]; # for clone/bootstrap and timer
# Pull dotfiles and rebuild if the repo has new commits. # Trust `danny` for Nix remote builds (so the mac can delegate
systemd.services.dotfiles-rebuild = { # x86_64-linux builds here via ssh-ng://danny@sunken-ship-zt).
description = "Pull dotfiles and run nixos-rebuild if repo changed"; nix.settings.trusted-users = [ "root" "danny" ];
path = with pkgs; [ git nix ]; environment.systemPackages = with pkgs; [
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
];
# Avahi (mDNS) — required for AirPlay discovery.
services.avahi = {
enable = true;
nssmdns4 = true;
publish = { enable = true; userServices = true; };
};
# Open firewall for AirPlay (mDNS + UxPlay default ports) + Navidrome.
# bbbot's HTTP backend (port 8080) is intentionally NOT in the global
# allowedTCPPorts — it's only allowed on the ZeroTier interface
# (clan-managed name; matches anything starting with `zt`) so the
# vps-relay Caddy can reach it via the ZT mesh. Same trick could lock
# 4533 down later but Navidrome stays globally accessible for now (LAN
# convenience).
networking.firewall = {
allowedTCPPorts = [ 7000 7001 7100 4533 ];
allowedUDPPorts = [ 5353 6000 6001 7011 ];
# 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).
# Music library: /srv/music (bind-mounted from /home/danny/music).
# Web UI + Substreamer client on port 4533.
services.navidrome = {
enable = true;
settings = {
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).
# 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";
};
};
# 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";
fsType = "none";
options = [ "bind" "ro" ];
};
# Navidrome is now reachable only over the ZeroTier mesh — see the
# sunken-ship-zt SSH alias on the mac, or hit http://[fdd5:53a2:de33:
# d269:6499:93d5:53a2:de33]:4533 directly from any ZT-joined device.
# The Cloudflare Tunnel + its clan vars generator were retired in 4d;
# delete the tunnel itself in the Cloudflare Zero Trust dashboard.
# UxPlay AirPlay receiver — audio-only, outputs directly to Scarlett Solo via ALSA.
# Runs as a system service (no PipeWire needed on a headless server).
systemd.services.uxplay = {
description = "UxPlay AirPlay receiver";
after = [ "network-online.target" "avahi-daemon.service" ];
wants = [ "network-online.target" "avahi-daemon.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = ''${pkgs.uxplay}/bin/uxplay -n sunken-ship -p -vs 0 -as "audioconvert ! audioresample ! alsasink device=plughw:USB,0 buffer-time=200000"'';
Restart = "on-failure";
RestartSec = 5;
User = "danny";
SupplementaryGroups = [ "audio" ];
};
};
# BigBiggerBiggestBot — Mini App backend (no Telegram polling).
# Code: https://github.com/DannyDannyDanny/bigbiggerbiggestbot cloned at /home/danny/tg_fitness_bot
# 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).
# 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.
# 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
aiohttp
]);
in {
description = "BigBiggerBiggestBot Mini App backend";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
path = [ pythonEnv ];
environment.WEBAPP_URL = "https://bbbot.dannydannydanny.me";
# Bind dual-stack so the VPS Caddy can reach us over ZT IPv6.
environment.API_HOST = "::";
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";
};
};
# Pull fitness bot from GitHub and restart the service if the repo has new commits.
# Code lives at /home/danny/tg_fitness_bot (git clone of DannyDannyDanny/bigbiggerbiggestbot).
# workouts.db is gitignored — preserved across pulls.
systemd.services.fitness-bot-pull = {
description = "Pull fitness bot and restart service if repo 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";
script = '' script = ''
set -euo pipefail set -euo pipefail
cd ${dotfilesDir} cd /home/danny/tg_fitness_bot
git fetch origin git fetch origin
if [ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ]; then if [ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ]; then
exit 0 exit 0
fi fi
git pull origin main git pull origin main
exec nixos-rebuild switch --flake ${flakeRef} systemctl restart fitness-bot
''; '';
serviceConfig.Type = "oneshot"; serviceConfig.Type = "oneshot";
}; };
systemd.timers.dotfiles-rebuild = { systemd.timers.fitness-bot-pull = {
wantedBy = [ "timers.target" ]; wantedBy = [ "timers.target" ];
timerConfig.OnCalendar = "*-*-* *:00/15:00"; # every 15 minutes timerConfig.OnCalendar = "*-*-* *:07/15:00"; # every 15 minutes, offset from dotfiles-rebuild
timerConfig.RandomizedDelaySec = "2min"; timerConfig.RandomizedDelaySec = "2min";
}; };
# ── 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).
# Token file: /home/danny/.secrets/shipyard_poc_bot.env
# File contents: BOT_TOKEN=<shipyard_poc_bot token>
# Service won't start until this file exists (ConditionPathExists).
# 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 <branch>:staging → wait ~15 min → tap B3Bot
# beta in shipyard_poc_bot's launcher → test → git push <branch>:main.
systemd.services.fitness-bot-shipyard = let
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
python-dotenv
aiohttp
]);
in {
description = "BigBiggerBiggestBot SHIPYARD STAGING instance";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
path = [ pythonEnv ];
environment.API_HOST = "::";
environment.API_PORT = "8081";
# 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";
EnvironmentFile = "/home/danny/.secrets/shipyard_poc_bot.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
# (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 -"
# 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
pythonEnv = pkgs.python312.withPackages (ps: with ps; [
fastapi
uvicorn
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)";
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";
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";
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";
# 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";
};
};
# 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";
# 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";
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";
};
# 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
# 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
};
};
# Phase 7.5 enrichment one-shot. For tracks where Navidrome's tags
# are empty/Unknown, runs three sources (filename heuristics, yt-dlp
# for SoundCloud `[<id>]` 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 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";
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";
};
};
# 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.
} }

189
nixos/hosts/vps-relay.nix Normal file
View file

@ -0,0 +1,189 @@
# Hetzner Cloud VPS — public reverse proxy into the clan.
#
# Role: terminates public TLS via Caddy + Let's Encrypt, reverse-proxies
# each declared subdomain over ZeroTier to the appropriate homelab host.
# No navidrome/bbbot data ever hits disk here; this box is a relay.
{ config, lib, pkgs, ... }:
{
imports = [ ../disko-cloud.nix ];
nixpkgs.hostPlatform = "x86_64-linux";
# Hetzner Cloud vServers boot in BIOS mode (confirmed via rescue:
# /sys/firmware/efi doesn't exist, product_name=vServer). systemd-boot
# is UEFI-only, so use GRUB/BIOS. disko's EF02 BIOS boot partition
# already tells GRUB where to embed stage-1.5; we just enable grub +
# set the install device list.
boot.loader.systemd-boot.enable = lib.mkForce false;
boot.loader.grub.enable = lib.mkForce true;
boot.loader.grub.efiSupport = lib.mkForce false;
boot.loader.grub.devices = lib.mkForce [ "/dev/sda" ];
# Ensure no default-set .device slips through and duplicates mirroredBoots.
boot.loader.grub.device = lib.mkForce "nodev";
# Hetzner Cloud cx23 uses QEMU virtio-scsi for the disk and virtio-net
# for the NIC. Without these modules in initrd, the kernel can't find
# the root partition and hangs during boot.
boot.initrd.availableKernelModules = [
"virtio_pci"
"virtio_scsi"
"virtio_net"
"virtio_blk"
"ata_piix"
"sd_mod"
"sr_mod"
];
boot.kernelModules = [ "virtio_pci" "virtio_scsi" "virtio_net" ];
# Cloud provisioners add the initial root SSH key via cloud-init or
# equivalent; we don't run cloud-init. All config is baked at install.
networking.hostName = "vps-relay";
networking.useDHCP = lib.mkDefault true;
time.timeZone = "Europe/Copenhagen";
# --- User + SSH ------------------------------------------------------
users.users.danny = {
isNormalUser = true;
extraGroups = [ "wheel" ];
openssh.authorizedKeys.keys = [
# 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 =
config.users.users.danny.openssh.authorizedKeys.keys;
security.sudo.wheelNeedsPassword = false;
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
PermitRootLogin = "prohibit-password";
};
};
# --- Firewall --------------------------------------------------------
# Public: 22 (SSH), 80 + 443 (Caddy).
# ZT interface: trusted (set in the clan ZT module).
networking.firewall.enable = true;
networking.firewall.allowedTCPPorts = [ 22 80 443 ];
# fail2ban — public SSH gets brute-force probed within minutes of any
# cloud VM being created. Ban offending IPs after a few failures.
services.fail2ban = {
enable = true;
bantime = "1h";
bantime-increment = {
enable = true;
multipliers = "1 4 16 64 256"; # 1h, 4h, 16h, ~2.7d, ~10.7d
maxtime = "30d";
};
jails.sshd.settings = {
enabled = true;
maxretry = 5;
findtime = "10m";
};
};
# --- Caddy reverse proxy --------------------------------------------
# Subdomains → clan backends over ZeroTier. IPs are sunken-ship's /
# phantom-ship's ZT IPv6; brackets required in URLs.
services.caddy = {
enable = true;
email = "powerhouseplayer@gmail.com";
# Tell ACME to use Let's Encrypt's production endpoint (Caddy default).
virtualHosts = {
"navidrome.dannydannydanny.me".extraConfig = ''
reverse_proxy http://[fdd5:53a2:de33:d269:6499:93d5:53a2:de33]:4533
'';
"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
'';
# 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
'';
# Bananasimulator — same backend, port 8083.
"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
'';
# 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
'';
# 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
'';
# 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
'';
# 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 = ''
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
'';
# 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
'';
# 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
'';
# 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
'';
};
};
# --- Basic tooling ---------------------------------------------------
environment.systemPackages = with pkgs; [
git
htop
tcpdump
];
nix.settings.experimental-features = [ "nix-command" "flakes" ];
system.stateVersion = "25.11";
}

View file

@ -23,14 +23,7 @@
nix.settings.experimental-features = [ "nix-command" "flakes" ]; nix.settings.experimental-features = [ "nix-command" "flakes" ];
programs.nix-ld.enable = true; programs.nix-ld.enable = true;
# TODO: move to home manager (?) # direnv is now managed by home-manager (home/danny/home.nix)
programs = {
direnv = {
enable = true;
# enableFishIntegration = true;
nix-direnv.enable = true;
};
};
# This value determines the NixOS release from which the default # This value determines the NixOS release from which the default
# settings for stateful data, like file locations and database versions # settings for stateful data, like file locations and database versions
@ -47,42 +40,14 @@
}; };
nixpkgs.config.allowUnfree = true; nixpkgs.config.allowUnfree = true;
environment.variables = {
DBT_USER = "DNTH";
EDITOR = "nvim";
VISUAL = "nvim";
};
# User-level packages (git, ripgrep, fzf, etc.) are managed by
# home-manager via home/danny/home.nix. Only system-wide deps here.
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
# tmux # activated in tmux.nix wget # needed by vscode-server
# vim # using neovim in stead busybox # useful system utilities (tree, unzip, etc.)
# neovim # activated in neovim.nix xdg-utils # terminal desktop integrations
git # version control
gh # github cli tool
ripgrep # faster grep
wget # for vscode-server
busybox # useful programs e.g. tree, unzip etc
openssl # cryptography swiss army knife
xdg-utils # terminal desktop intergrations (i.e. allow terminal to open browser)
# make default.nix in python project folders instead of using a top-level python environment manager
# pyenv
# poetry
fastfetch # system info
btop # resource monitor
tldr # community alternative to man
fzf # fuzzy finder
jq # parse json jq # parse json
# gimp # bloat
# blender # bloat
# inkscape # bloat
cowsay
lolcat
]; ];
services.ollama.enable = true; services.ollama.enable = true;

View file

@ -4,71 +4,110 @@
programs.neovim = { programs.neovim = {
enable = true; enable = true;
defaultEditor = true; defaultEditor = true;
# TODO: refactor (some parts) to extraLuaConfig withRuby = false;
withPython3 = false;
# VimScript settings (options that have no Lua equivalent or are simpler in vim)
extraConfig = '' extraConfig = ''
set title set title
set mouse=a
set nohlsearch set nohlsearch
set number set number
let mapleader="," let mapleader=","
lua << EOF colorscheme catppuccin
local config_file = os.getenv("HOME")..'/.local/share/nvim_color_scheme'
local f = io.open(config_file, "r")
if f ~= nil then
local system_theme = f:read()
io.close(f)
if system_theme == 'dark' then
vim.cmd("set bg=dark")
elseif system_theme == 'light' then
vim.cmd("set bg=light")
else
print('warning: expected value "light" or "dark"')
print(' got:', system_theme)
print(' expected path:', config_file)
end
else
print('warning: nvim color scheme not found')
print(' expected path:', config_file)
end
EOF
colorscheme catppuccin " catppuccin-latte, catppuccin-frappe, catppuccin-macchiato, catppuccin-mocha
" netrw (dir listing) settings " netrw (dir listing) settings
let g:netrw_liststyle = 3 let g:netrw_liststyle = 3
let g:netrw_banner = 0 let g:netrw_banner = 0
let g:netrw_browse_split = 3 let g:netrw_browse_split = 3
let g:netrw_winsize = 25 " % of page let g:netrw_winsize = 25
'';
set listchars=tab:\ ,space:·,nbsp:,trail:,eol:,precedes:«,extends:» initLua = ''
set clipboard+=unnamedplus -- Auto-detect system theme (dark/light) from marker file
local config_file = os.getenv("HOME") .. "/.local/share/nvim_color_scheme"
local f = io.open(config_file, "r")
if f then
local theme = f:read("*l")
f:close()
if theme then
theme = theme:gsub("^%s+", ""):gsub("%s+$", "")
end
if theme == "dark" or theme == "light" then
vim.opt.background = theme
else
vim.notify("nvim_color_scheme: expected 'light' or 'dark', got: " .. tostring(theme), vim.log.levels.WARN)
end
end
" Replace-all is aliased to S. -- General options
nnoremap S :%s//g<Left><Left> vim.opt.cursorline = true
vim.opt.mouse = "a"
vim.opt.listchars = { tab = " ", space = "·", nbsp = "", trail = "", eol = "", precedes = "«", extends = "»" }
vim.opt.clipboard:append("unnamedplus")
vim.opt.spell = true
vim.opt.spelllang = "en_us"
" save file with ,w -- Markdown: fold by heading/section using Treesitter
map <leader>w :w<cr><Space> vim.api.nvim_create_autocmd("FileType", {
pattern = "markdown",
callback = function()
vim.opt_local.foldmethod = "expr"
vim.opt_local.foldexpr = "v:lua.vim.treesitter.foldexpr()"
vim.opt_local.foldenable = true
end,
})
" spellcheck -- Treesitter highlighting: parser-driven syntax highlighting (richer
set spell spelllang=en_us -- than the regex-based default). Leaving `indent` off it's still
setlocal spell! spelllang=en_us -- 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<Left><Left>", { desc = "Replace all" })
vim.keymap.set("n", "<leader>w", ":w<CR>", { desc = "Save file" })
''; '';
plugins = with pkgs.vimPlugins; [ plugins = with pkgs.vimPlugins; [
vim-surround # shortcuts for setting () {} etc. vim-surround # shortcuts for setting () {} etc.
vim-gitgutter # git diff in sign column vim-gitgutter # git diff in sign column
# vim-airline # nice and light status bar # doesn't work nicely with tmux
# coc-nvim coc-git coc-highlight coc-python coc-rls coc-vetur coc-vimtex coc-yaml coc-html coc-json # auto completion
vim-nix # nix highlight vim-nix # nix highlight
# vimtex # latex stuff - disabled due to build check issue
fzf-lua # fuzzy finder through lua fzf-lua # fuzzy finder through lua
nerdtree # file structure inside nvim nerdtree # file structure inside nvim
rainbow # color parenthesis rainbow # color parenthesis
# gruvbox-nvim # theme
catppuccin-nvim # theme catppuccin-nvim # theme
goyo-vim # write prose goyo-vim # write prose
limelight-vim # prose paragraph highlighter 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
]; ];
}; };
} }

View file

@ -0,0 +1,22 @@
# Gmail MCP server for Hara.
#
# Path 1 implementation: IMAP for read/sort, SMTP for reply.
# Slated for replacement by an OAuth2 + Gmail API + Calendar API server later.
{ python3Packages }:
python3Packages.buildPythonApplication {
pname = "hara-gmail-mcp";
version = "0.2.0";
pyproject = true;
src = ./.;
nativeBuildInputs = [ python3Packages.setuptools ];
propagatedBuildInputs = [ python3Packages.mcp ];
# The server is launched via stdio by Claude Code; no tests yet.
doCheck = false;
meta = {
description = "Gmail MCP server for Hara (IMAP+SMTP, throwaway pre-OAuth2)";
mainProgram = "hara-gmail-mcp";
};
}

View file

@ -0,0 +1,71 @@
# NixOS module for the Hara Gmail MCP server.
#
# Generates /etc/hara/gmail-accounts.json from declarative options and
# exposes the server binary through the dotfiles flake's pkgs set. Wiring
# the server into the claude-channels systemd service ExecStart is done
# by the host (phantom-ship.nix) so this module stays composable.
{ config, lib, pkgs, ... }:
let
cfg = config.services.hara-gmail-mcp;
package = pkgs.callPackage ./. { };
accountsJson = builtins.toJSON {
accounts = map (a: {
inherit (a) email password_file;
imap_host = a.imapHost;
imap_port = a.imapPort;
smtp_host = a.smtpHost;
smtp_port = a.smtpPort;
}) cfg.accounts;
};
in
{
options.services.hara-gmail-mcp = {
enable = lib.mkEnableOption "Hara Gmail MCP server (IMAP+SMTP)";
package = lib.mkOption {
type = lib.types.package;
default = package;
description = "The hara-gmail-mcp package to use.";
};
accounts = lib.mkOption {
description = "Gmail accounts the MCP server should expose.";
type = lib.types.listOf (lib.types.submodule {
options = {
email = lib.mkOption {
type = lib.types.str;
example = "user@example.com";
};
password_file = lib.mkOption {
type = lib.types.path;
description = "Path to the file containing the IMAP/SMTP app password.";
};
imapHost = lib.mkOption {
type = lib.types.str;
default = "imap.gmail.com";
};
imapPort = lib.mkOption {
type = lib.types.port;
default = 993;
};
smtpHost = lib.mkOption {
type = lib.types.str;
default = "smtp.gmail.com";
};
smtpPort = lib.mkOption {
type = lib.types.port;
default = 465;
};
};
});
};
};
config = lib.mkIf cfg.enable {
environment.etc."hara/gmail-accounts.json" = {
text = accountsJson;
mode = "0644";
};
};
}

View file

@ -0,0 +1,18 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "hara-gmail-mcp"
version = "0.2.0"
description = "Gmail MCP server for Hara (IMAP+SMTP, throwaway pre-OAuth2)"
requires-python = ">=3.11"
dependencies = [
"mcp>=1.0.0",
]
[project.scripts]
hara-gmail-mcp = "hara_gmail_mcp.__main__:main"
[tool.setuptools.packages.find]
where = ["src"]

View file

@ -0,0 +1,6 @@
"""Entry point for `python -m hara_gmail_mcp` and the `hara-gmail-mcp` script."""
from .server import main
if __name__ == "__main__":
main()

View file

@ -0,0 +1,112 @@
"""Account config loader.
Reads a JSON file (default: /etc/hara/gmail-accounts.json) listing the Gmail
accounts Hara can act on, and the path to each account's IMAP/SMTP app
password. Passwords are loaded once via `sudo -n cat` because the password
files are root:991 0640 and the MCP server process runs as `danny`. The
result is cached in memory for the process lifetime.
Schema:
{
"accounts": [
{
"email": "user@example.com",
"password_file": "/etc/openclaw/gmail-user-app-password",
"imap_host": "imap.gmail.com",
"imap_port": 993,
"smtp_host": "smtp.gmail.com",
"smtp_port": 465
}
]
}
"""
from __future__ import annotations
import json
import os
import shutil
import subprocess
from dataclasses import dataclass
from pathlib import Path
DEFAULT_CONFIG_PATH = "/etc/hara/gmail-accounts.json"
# NixOS keeps the setuid sudo wrapper at /run/wrappers/bin; non-NixOS distros
# put it in /usr/bin or /bin. We try $PATH first, then fall back to these.
_SUDO_FALLBACKS = ["/run/wrappers/bin/sudo", "/usr/bin/sudo", "/bin/sudo"]
def _find_sudo() -> str:
found = shutil.which("sudo")
if found:
return found
for candidate in _SUDO_FALLBACKS:
if Path(candidate).exists():
return candidate
raise RuntimeError(
"sudo not found on PATH or in known locations; "
"cannot read group-restricted password files"
)
@dataclass(frozen=True)
class Account:
email: str
password_file: str
imap_host: str
imap_port: int
smtp_host: str
smtp_port: int
class AccountStore:
"""Holds account metadata and lazily resolves passwords on first use."""
def __init__(self, accounts: list[Account]) -> None:
self._accounts = {a.email: a for a in accounts}
self._password_cache: dict[str, str] = {}
@classmethod
def from_config_file(cls, path: str | os.PathLike[str] | None = None) -> "AccountStore":
config_path = Path(path or os.environ.get("HARA_GMAIL_CONFIG", DEFAULT_CONFIG_PATH))
with config_path.open() as f:
data = json.load(f)
accounts = [
Account(
email=a["email"],
password_file=a["password_file"],
imap_host=a.get("imap_host", "imap.gmail.com"),
imap_port=int(a.get("imap_port", 993)),
smtp_host=a.get("smtp_host", "smtp.gmail.com"),
smtp_port=int(a.get("smtp_port", 465)),
)
for a in data.get("accounts", [])
]
return cls(accounts)
def emails(self) -> list[str]:
return list(self._accounts.keys())
def get(self, email: str) -> Account:
try:
return self._accounts[email]
except KeyError:
raise ValueError(f"Unknown account: {email!r}. Configured: {self.emails()}")
def password_for(self, email: str) -> str:
if email in self._password_cache:
return self._password_cache[email]
account = self.get(email)
# Prefer direct read if the file is reachable (e.g. after path 2
# migration where the daemon owns its own creds), fall back to
# `sudo -n cat` for the current /etc/openclaw/ layout.
try:
value = Path(account.password_file).read_text().strip()
except PermissionError:
value = subprocess.check_output(
[_find_sudo(), "-n", "cat", account.password_file],
text=True,
).strip()
if not value:
raise RuntimeError(f"Empty password file for {email}: {account.password_file}")
self._password_cache[email] = value
return value

View file

@ -0,0 +1,212 @@
"""Minimal IMAP wrapper over stdlib imaplib.
One short-lived IMAP connection per call. Good enough for v1; if latency
hurts when Hara fans out across three accounts in a summary, swap to a
connection pool keyed by account email.
"""
from __future__ import annotations
import email
import email.policy
import imaplib
from contextlib import contextmanager
from dataclasses import dataclass, field
from email.message import EmailMessage
from typing import Iterator
from .accounts import Account, AccountStore
@dataclass
class MessageSummary:
uid: str
subject: str
sender: str
date: str
snippet: str = ""
flags: list[str] = field(default_factory=list)
@dataclass
class FullMessage:
uid: str
subject: str
sender: str
to: str
date: str
body_text: str
body_html: str
flags: list[str] = field(default_factory=list)
@contextmanager
def _open(account: Account, password: str, mailbox: str = "INBOX") -> Iterator[imaplib.IMAP4_SSL]:
conn = imaplib.IMAP4_SSL(account.imap_host, account.imap_port)
try:
conn.login(account.email, password)
# SELECT for read-write, EXAMINE for read-only. Use SELECT so we can
# add/remove flags later (label/archive). Most reads still tolerate
# the implicit \Seen behaviour Gmail applies; we set PEEK below.
typ, _ = conn.select(mailbox)
if typ != "OK":
raise RuntimeError(f"SELECT {mailbox} failed for {account.email}")
yield conn
finally:
try:
conn.logout()
except Exception:
pass
def _decode_header(raw: str | None) -> str:
if not raw:
return ""
parts = email.header.decode_header(raw)
out = []
for chunk, enc in parts:
if isinstance(chunk, bytes):
try:
out.append(chunk.decode(enc or "utf-8", errors="replace"))
except LookupError:
out.append(chunk.decode("utf-8", errors="replace"))
else:
out.append(chunk)
return "".join(out)
def list_inbox(
store: AccountStore,
email_addr: str,
limit: int = 20,
mailbox: str = "INBOX",
) -> list[MessageSummary]:
account = store.get(email_addr)
password = store.password_for(email_addr)
with _open(account, password, mailbox) as conn:
typ, data = conn.uid("search", None, "ALL")
if typ != "OK":
raise RuntimeError(f"SEARCH ALL failed for {email_addr}")
uids = data[0].split()[-limit:][::-1] # most recent first
return [_fetch_summary(conn, uid.decode()) for uid in uids]
def search(
store: AccountStore,
email_addr: str,
query: str,
limit: int = 20,
mailbox: str = "INBOX",
) -> list[MessageSummary]:
"""Run an IMAP SEARCH. `query` is a raw IMAP search expression, e.g.
`FROM alice@example.com`, `UNSEEN`, `SUBJECT "invoice"`, `SINCE 1-Jan-2026`.
"""
account = store.get(email_addr)
password = store.password_for(email_addr)
with _open(account, password, mailbox) as conn:
typ, data = conn.uid("search", None, query)
if typ != "OK":
raise RuntimeError(f"SEARCH {query!r} failed for {email_addr}")
uids = data[0].split()[-limit:][::-1]
return [_fetch_summary(conn, uid.decode()) for uid in uids]
def read_email(
store: AccountStore,
email_addr: str,
uid: str,
mailbox: str = "INBOX",
) -> FullMessage:
account = store.get(email_addr)
password = store.password_for(email_addr)
with _open(account, password, mailbox) as conn:
# BODY.PEEK[] avoids setting \Seen automatically.
typ, data = conn.uid("fetch", uid, "(FLAGS BODY.PEEK[])")
if typ != "OK" or not data or data[0] is None:
raise RuntimeError(f"FETCH uid={uid} failed for {email_addr}")
meta, raw = data[0]
flags = _parse_flags(meta.decode() if isinstance(meta, bytes) else meta)
msg: EmailMessage = email.message_from_bytes(raw, policy=email.policy.default)
body_text = ""
body_html = ""
if msg.is_multipart():
for part in msg.walk():
ctype = part.get_content_type()
if ctype == "text/plain" and not body_text:
body_text = part.get_content()
elif ctype == "text/html" and not body_html:
body_html = part.get_content()
else:
ctype = msg.get_content_type()
if ctype == "text/html":
body_html = msg.get_content()
else:
body_text = msg.get_content()
return FullMessage(
uid=uid,
subject=_decode_header(msg["Subject"]),
sender=_decode_header(msg["From"]),
to=_decode_header(msg["To"]),
date=_decode_header(msg["Date"]),
body_text=body_text,
body_html=body_html,
flags=flags,
)
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",
uid,
"(FLAGS BODY.PEEK[HEADER.FIELDS (SUBJECT FROM DATE)])",
)
if typ != "OK" or not data or data[0] is None:
return MessageSummary(uid=uid, subject="(fetch failed)", sender="", date="")
meta, raw = data[0]
flags = _parse_flags(meta.decode() if isinstance(meta, bytes) else meta)
headers = email.message_from_bytes(raw, policy=email.policy.default)
return MessageSummary(
uid=uid,
subject=_decode_header(headers["Subject"]),
sender=_decode_header(headers["From"]),
date=_decode_header(headers["Date"]),
flags=flags,
)
def _parse_flags(meta: str) -> list[str]:
# meta looks like: b'<uid> (FLAGS (\\Seen \\Answered) BODY[...] {1234}'
start = meta.find("FLAGS (")
if start < 0:
return []
end = meta.find(")", start)
if end < 0:
return []
return meta[start + len("FLAGS (") : end].split()

View file

@ -0,0 +1,133 @@
"""Hara Gmail MCP server.
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
import json
import logging
import os
import sys
from dataclasses import asdict
from mcp.server.fastmcp import FastMCP
from .accounts import AccountStore
from .imap_client import archive, list_inbox, mark_read, read_email, search
logger = logging.getLogger("hara_gmail_mcp")
mcp = FastMCP("hara-gmail-mcp")
_store: AccountStore | None = None
def _get_store() -> AccountStore:
global _store
if _store is None:
_store = AccountStore.from_config_file()
return _store
@mcp.tool()
def list_accounts() -> list[str]:
"""Return the email addresses of all Gmail accounts Hara can access."""
return _get_store().emails()
@mcp.tool()
def gmail_list_inbox(email: str, limit: int = 20) -> str:
"""List the most recent messages in INBOX for the given account.
Args:
email: which configured account to read (use list_accounts to see options)
limit: max number of messages to return, newest first (default 20, cap 100)
Returns:
JSON list of {uid, subject, sender, date, flags}.
"""
limit = max(1, min(int(limit), 100))
msgs = list_inbox(_get_store(), email, limit=limit)
return json.dumps([asdict(m) for m in msgs], ensure_ascii=False)
@mcp.tool()
def gmail_search(email: str, query: str, limit: int = 20) -> str:
"""Run an IMAP SEARCH against the given account's INBOX.
Args:
email: which configured account to search
query: raw IMAP search expression, e.g. 'UNSEEN', 'FROM alice@x.com',
'SUBJECT "invoice"', 'SINCE 1-Jan-2026'. Quote arguments as needed.
limit: max results (default 20, cap 100)
Returns:
JSON list of {uid, subject, sender, date, flags}.
"""
limit = max(1, min(int(limit), 100))
msgs = search(_get_store(), email, query=query, limit=limit)
return json.dumps([asdict(m) for m in msgs], ensure_ascii=False)
@mcp.tool()
def gmail_read_email(email: str, uid: str) -> str:
"""Fetch the full body of one message by IMAP UID.
Args:
email: which configured account
uid: the message UID (returned by gmail_list_inbox or gmail_search)
Returns:
JSON object with subject, sender, to, date, body_text, body_html, flags.
BODY.PEEK is used so reading does not auto-mark the message as seen.
"""
msg = read_email(_get_store(), email, uid=uid)
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"),
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
stream=sys.stderr,
)
logger.info("hara-gmail-mcp starting")
mcp.run()

View file

@ -1,27 +1,39 @@
# NixOS flake # NixOS modules
Rebuild from dotfiles dir: Host-specific NixOS and home-manager modules live under this dir:
- `hosts/<machine>.nix` + `hosts/<machine>-hardware.nix`
- `home/danny/home.nix` (home-manager)
- `fish.nix`, `neovim.nix`, `ollama.nix`, `installer-iso.nix`, `disko-server.nix`
The flake itself (`flake.nix`, `flake.lock`, `flake-modules/`, `lib/`, `modules/`, `sops/`, `vars/`) lives at the **repo root**, not here. See [CLAUDE.md](../CLAUDE.md) at the repo root for rebuild commands, clan.lol operations, and the `dotfiles-rebuild` timer.
## Quick rebuild reference
```bash ```bash
sudo nixos-rebuild switch --flake ~/dotfiles/nixos#macbookair # macOS
# or #wsl cd ~/dotfiles && darwin-rebuild switch --flake .
# macOS: cd ~/dotfiles/nixos && darwin-rebuild switch --flake .
# WSL
sudo nixos-rebuild switch --flake ~/dotfiles#wsl
# Servers (via clan from mac)
nix run git+https://git.clan.lol/clan/clan-core#clan-cli -- \
machines update sunken-ship --flake ~/dotfiles
``` ```
## Server (sunken-ship) ## Server bootstrap (one-time)
One-time bootstrap (no git until first rebuild):
```bash ```bash
nix run --extra-experimental-features "nix-command flakes" nixpkgs#git -- clone https://github.com/DannyDannyDanny/dotfiles.git /tmp/dotfiles nix run --extra-experimental-features "nix-command flakes" nixpkgs#git -- \
clone https://github.com/DannyDannyDanny/dotfiles.git /tmp/dotfiles
sudo mv /tmp/dotfiles /etc/dotfiles sudo mv /tmp/dotfiles /etc/dotfiles
sudo nixos-rebuild switch --flake /etc/dotfiles/nixos#sunken-ship --option accept-flake-config true sudo nixos-rebuild switch --flake /etc/dotfiles#sunken-ship \
--option accept-flake-config true
``` ```
If the daemon doesnt have flakes: copy [server-configuration-with-flakes.nix](server-configuration-with-flakes.nix) to `/etc/nixos/configuration.nix`, run `sudo nixos-rebuild switch`, then build and switch to the flake (see [server-quickstart.md](../server-quickstart.md) for SSH keys). If the daemon doesn't have flakes: copy [server-configuration-with-flakes.nix](server-configuration-with-flakes.nix) to `/etc/nixos/configuration.nix`, `sudo nixos-rebuild switch`, then build the flake.
SSH keys (not in repo): `scp ~/.ssh/*.pub danny@server:/tmp/`, then on server `mkdir -p ~/.ssh; cat /tmp/*.pub >> ~/.ssh/authorized_keys`. See [docs/ssh-and-secrets.md](../docs/ssh-and-secrets.md). SSH keys (not in repo): `scp ~/.ssh/*.pub danny@server:/tmp/`, then on server `mkdir -p ~/.ssh; cat /tmp/*.pub >> ~/.ssh/authorized_keys`. See [docs/ssh-and-secrets.md](../docs/ssh-and-secrets.md).
Timer: every 15 min the server pulls and rebuilds when `main` changes. Config: `hosts/sunken-ship.nix`, `hosts/sunken-ship-hardware.nix`.
No git in PATH: `sudo nix run nixpkgs#git -- -C /etc/dotfiles pull origin main`. No git in PATH: `sudo nix run nixpkgs#git -- -C /etc/dotfiles pull origin main`.

View file

@ -1,57 +0,0 @@
{ config, pkgs, ... }:
{
programs.tmux = {
enable = true;
clock24 = true;
escapeTime = 20;
keyMode = "vi";
historyLimit = 100000;
baseIndex = 1;
extraConfig = ''
# remap prefix from ^+B to alt-f
unbind C-b
set -g prefix M-f
bind M-f send-prefix
# nvim 'checkhealth' advice
set-option -g focus-events on
set-option -sa terminal-overrides ',xterm-256color:RGB'
set-option -g default-terminal "screen-256color"
# enable mouse support for switching panes/windows
set -g mouse on
# pane movement shortcuts
bind h select-pane -L
bind j select-pane -D
bind k select-pane -U
bind l select-pane -R
# window selection
bind -r C-h select-window -t :-
bind -r C-l select-window -t :+
# Resize pane shortcuts
bind -r H resize-pane -L 10
bind -r J resize-pane -D 10
bind -r K resize-pane -U 10
bind -r L resize-pane -R 10
# split with dash and vbar
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
# server-tmux only:
# fix ssh agent when tmux is detached
# setenv -g SSH_AUTH_SOCK $HOME/.ssh/ssh_auth_sock
'';
plugins = [
# pkgs.tmuxPlugins.tmux-powerline # status bar
pkgs.tmuxPlugins.catppuccin
pkgs.tmuxPlugins.tmux-fzf # search tmux commands (prefix + F)
pkgs.tmuxPlugins.extrakto # fuzzyfind text history (prefix + tab)
];
};
}

View file

@ -1,17 +0,0 @@
# article / guide:
# https://taoa.io/posts/Setting-up-ipad-screen-mirroring-on-nixos
# https://gist.github.com/cmrfrd/fe8f61da076f8a4a751bf8fc8cb579a5
# also see: 24_nix_uxplay for script
{ config, pkgs, ... }:
{
services.avahi = {
nssmdns4 = true;
enable = true;
publish = {
enable = true;
userServices = true;
domain = true;
};
};
}

View file

@ -1,4 +1,5 @@
#!/bin/bash #!/usr/bin/env bash
set -euo pipefail
# Required parameters: # Required parameters:
# @raycast.schemaVersion 1 # @raycast.schemaVersion 1
@ -7,11 +8,11 @@
# Optional parameters: # Optional parameters:
# @raycast.icon 🤖 # @raycast.icon 🤖
# @raycast.argument1 { "type": "text", "placeholder": "Placeholder" } # @raycast.argument1 { "type": "text", "placeholder": "Text to count" }
# Documentation: # Documentation:
# @raycast.description counts chars in selected text # @raycast.description counts chars in selected text
# @raycast.author DannyDannyDanny # @raycast.author DannyDannyDanny
# @raycast.authorURL https://raycast.com/DannyDannyDanny # @raycast.authorURL https://raycast.com/DannyDannyDanny
echo -n "$1" | wc -c printf '%s' "${1:-}" | wc -c | awk '{ print $1 }'

1
result
View file

@ -1 +0,0 @@
/nix/store/x8ain9193yl3k10mk0bi667qp5iwk03w-lua-5.2.4

View file

@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Keep Alacritty in sync with macOS light/dark appearance.
# No Nix rebuild: copies a palette into active-colors.toml; Alacritty reloads via live_config_reload.
set -euo pipefail
[[ "$(uname -s)" == "Darwin" ]] || exit 0
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
ALACRITTY_DIR="$XDG_CONFIG_HOME/alacritty"
ACTIVE="$ALACRITTY_DIR/active-colors.toml"
MARKER="$ALACRITTY_DIR/.last-system-theme"
LIGHT="$ALACRITTY_DIR/catppuccin-latte-colors.toml"
DARK="$ALACRITTY_DIR/catppuccin-mocha-colors.toml"
if [[ ! -f "$LIGHT" || ! -f "$DARK" ]]; then
echo "alacritty-sync-system-theme: missing $LIGHT or $DARK (run home-manager switch first)" >&2
exit 1
fi
appearance="$(defaults read -g AppleInterfaceStyle 2>/dev/null || true)"
if [[ "$appearance" == "Dark" ]]; then
want="dark"
else
want="light"
fi
mkdir -p "$ALACRITTY_DIR"
printf '%s' "$want" >"$MARKER"
# Neovim (see nixos/neovim.nix): same file as `theme` on WSL; keep in sync with Appearance.
NVIM_THEME="${XDG_DATA_HOME:-$HOME/.local/share}/nvim_color_scheme"
mkdir -p "$(dirname "$NVIM_THEME")"
printf '%s\n' "$want" >"$NVIM_THEME"
if [[ "$want" == "light" ]]; then
tmp="$(mktemp "$ALACRITTY_DIR/active-colors.toml.XXXXXX")"
cp "$LIGHT" "$tmp"
else
tmp="$(mktemp "$ALACRITTY_DIR/active-colors.toml.XXXXXX")"
cp "$DARK" "$tmp"
fi
chmod 0644 "$tmp"
mv -f "$tmp" "$ACTIVE"

View file

@ -1,13 +1,13 @@
#!/bin/bash #!/bin/bash
# Fetch with curl and run to install NixOS (clone + run nixos-server-install.sh). # Fetch with curl and run to install NixOS (clone + run nixos-server-install.sh).
# On the live system, run only: # On the live system, run only:
# curl -sL https://raw.githubusercontent.com/DannyDannyDanny/dotfiles/server-installer-usb/scripts/bootstrap-install.sh | sudo bash # curl -sL https://raw.githubusercontent.com/DannyDannyDanny/dotfiles/main/scripts/bootstrap-install.sh | sudo bash
# #
# Optional: REPO_URL=... BRANCH=... (default repo and server-installer-usb) # Optional: REPO_URL=... BRANCH=... (default repo and server-installer-usb)
set -euo pipefail set -euo pipefail
REPO_URL="${REPO_URL:-https://github.com/DannyDannyDanny/dotfiles.git}" REPO_URL="${REPO_URL:-https://github.com/DannyDannyDanny/dotfiles.git}"
BRANCH="${BRANCH:-server-installer-usb}" BRANCH="${BRANCH:-main}"
DEST="/tmp/dotfiles" DEST="/tmp/dotfiles"
INSTALL_SCRIPT="$DEST/scripts/nixos-server-install.sh" INSTALL_SCRIPT="$DEST/scripts/nixos-server-install.sh"

View file

@ -5,12 +5,17 @@
# host: SSH host (default: sunken-ship) # host: SSH host (default: sunken-ship)
# output_dir: where to save the ISO on your Mac (default: .) # 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 # 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 set -euo pipefail
HOST="${1:-sunken-ship}" HOST="${1:-sunken-ship}"
OUT="${2:-.}" 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 if [[ -n "${SSH_KEY:-}" ]]; then
SSH_OPTS=(-i "$SSH_KEY") SSH_OPTS=(-i "$SSH_KEY")
elif [[ "$HOST" == "sunken-ship" ]] && [[ -f ~/.ssh/id_ed25519_sunken_ship ]]; then elif [[ "$HOST" == "sunken-ship" ]] && [[ -f ~/.ssh/id_ed25519_sunken_ship ]]; then
@ -19,23 +24,37 @@ else
SSH_OPTS=() SSH_OPTS=()
fi fi
echo "Pushing branch so server can pull..." echo "Pushing main so the server can clone the latest..."
git push origin server-installer-usb 2>/dev/null || true 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 ssh "${SSH_OPTS[@]}" "$HOST" 'set -e
BUILD_DIR=~/dotfiles-iso-build BUILD_DIR=~/dotfiles-iso-build
rm -rf "$BUILD_DIR" rm -rf "$BUILD_DIR"
git clone --branch server-installer-usb https://github.com/DannyDannyDanny/dotfiles.git "$BUILD_DIR" git clone --branch main https://github.com/DannyDannyDanny/dotfiles.git "$BUILD_DIR"
cd "$BUILD_DIR/nixos" '
# 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 nix build .#installer-iso
ls -la result/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") ISO_NAME=$(basename "$ISO_NAME")
echo "Copying $ISO_NAME to $OUT ..." 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 "Done. ISO at $OUT/$ISO_NAME"
echo "Write to USB: diskutil unmountDisk diskN && sudo dd if=$OUT/$ISO_NAME of=/dev/rdiskN bs=4m" echo "Write to USB: diskutil unmountDisk diskN && sudo dd if=$OUT/$ISO_NAME of=/dev/rdiskN bs=4m"

View file

@ -1,13 +0,0 @@
#!/bin/bash
# Detect macOS system theme (light/dark mode)
# Returns "light" or "dark"
# Get the current appearance setting
appearance=$(defaults read -g AppleInterfaceStyle 2>/dev/null)
if [ "$appearance" = "Dark" ]; then
echo "dark"
else
echo "light"
fi

View file

@ -1,26 +1,27 @@
#!/bin/bash #!/bin/bash
# Run on a NixOS minimal live system (or installer ISO) to install NixOS with # Install NixOS with disko (LUKS + root) on a live system.
# disko (LUKS + root). Prompts for hostname and target disk; optionally use # Prompts for hostname and target disk, then provisions the installed system
# INSTALLER_SYSTEM_CONFIG_FILE for WiFi etc. # (clones dotfiles, installs SSH key, generates hardware config).
# #
# Usage (from repo root, e.g. /tmp/dotfiles): # Usage (from repo root, e.g. /tmp/dotfiles):
# sudo ./scripts/nixos-server-install.sh # sudo ./scripts/nixos-server-install.sh
# If you see "command not found", use: sudo bash ./scripts/nixos-server-install.sh
# #
# Optional: FLAKE_REF=github:User/dotfiles or path:/path/to/dotfiles/nixos # Environment variables (all optional):
# # INSTALLER_HOSTNAME — skip hostname prompt
# Optional: INSTALLER_SYSTEM_CONFIG_FILE=/path/to/json with full --system-config # INSTALLER_DISK — skip disk prompt (validated as block device)
# (e.g. hostName + networking.wireless.networks). If unset, only hostname is passed. # SSH_PUBKEY_FILE — path to .pub file; installed to danny's authorized_keys
# FLAKE_REF — override flake reference (default: auto-detect from repo)
# INSTALLER_SYSTEM_CONFIG_FILE — JSON file merged into --system-config
set -euo pipefail set -euo pipefail
FLAKE_REF="${FLAKE_REF:-}" FLAKE_REF="${FLAKE_REF:-}"
if [[ -z "$FLAKE_REF" ]]; then if [[ -z "$FLAKE_REF" ]]; then
if [[ -d "$(dirname "$0")/../nixos" ]] && [[ -f "$(dirname "$0")/../nixos/flake.nix" ]]; then if [[ -f "$(dirname "$0")/../flake.nix" ]]; then
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
FLAKE_REF="path:${REPO_ROOT}/nixos" FLAKE_REF="path:${REPO_ROOT}"
else else
echo "FLAKE_REF not set and not running from dotfiles repo. Example:" echo "FLAKE_REF not set and not running from dotfiles repo. Example:"
echo " export FLAKE_REF=github:USER/REPO # or path:/path/to/dotfiles/nixos" echo " export FLAKE_REF=github:USER/REPO # or path:/path/to/dotfiles"
exit 1 exit 1
fi fi
fi fi
@ -30,21 +31,29 @@ if [[ "$EUID" -ne 0 ]]; then
exit 1 exit 1
fi fi
read -r -p "Hostname (e.g. my-server): " hostname # --- Hostname ---
hostname="${INSTALLER_HOSTNAME:-}"
if [[ -z "$hostname" ]]; then
read -r -p "Hostname (e.g. phantom-ship): " hostname
fi
if [[ -z "$hostname" ]]; then if [[ -z "$hostname" ]]; then
echo "Hostname cannot be empty." echo "Hostname cannot be empty."
exit 1 exit 1
fi fi
# --- Target disk ---
disk="${INSTALLER_DISK:-}"
if [[ -z "$disk" ]]; then
read -r -p "Target disk [default: /dev/sda]: " disk read -r -p "Target disk [default: /dev/sda]: " disk
disk="${disk:-/dev/sda}" disk="${disk:-/dev/sda}"
fi
if [[ ! -b "$disk" ]]; then if [[ ! -b "$disk" ]]; then
echo "Not a block device: $disk" echo "Not a block device: $disk"
exit 1 exit 1
fi fi
# --- System config (hostname + optional extras) ---
if [[ -n "${INSTALLER_SYSTEM_CONFIG_FILE:-}" ]] && [[ -f "$INSTALLER_SYSTEM_CONFIG_FILE" ]]; then if [[ -n "${INSTALLER_SYSTEM_CONFIG_FILE:-}" ]] && [[ -f "$INSTALLER_SYSTEM_CONFIG_FILE" ]]; then
# Use provided JSON; ensure hostname is set
if command -v jq &>/dev/null; then if command -v jq &>/dev/null; then
SYSTEM_CONFIG=$(jq --arg h "$hostname" '.networking.hostName = $h' "$INSTALLER_SYSTEM_CONFIG_FILE") SYSTEM_CONFIG=$(jq --arg h "$hostname" '.networking.hostName = $h' "$INSTALLER_SYSTEM_CONFIG_FILE")
else else
@ -55,8 +64,9 @@ else
SYSTEM_CONFIG='{"networking":{"hostName":"'"$hostname"'"}}' SYSTEM_CONFIG='{"networking":{"hostName":"'"$hostname"'"}}'
fi fi
# Prompt for password for danny so you can log in at console after reboot (no rescue needed) # --- Optional: danny password ---
read -r -p "Set a password for user danny (console/SSH login)? [y/N] " set_pass danny_pass=""
read -r -p "Set a password for user danny? [y/N] " set_pass
if [[ "${set_pass,,}" == "y" || "${set_pass,,}" == "yes" ]]; then if [[ "${set_pass,,}" == "y" || "${set_pass,,}" == "yes" ]]; then
read -s -r -p "Password for danny: " danny_pass read -s -r -p "Password for danny: " danny_pass
echo echo
@ -70,23 +80,27 @@ if [[ "${set_pass,,}" == "y" || "${set_pass,,}" == "yes" ]]; then
echo "Password cannot be empty. Aborted." echo "Password cannot be empty. Aborted."
exit 1 exit 1
fi fi
HASH=$(echo -n "$danny_pass" | openssl passwd -6 -stdin 2>/dev/null) || HASH=$(mkpasswd -6 -m sha-512 "$danny_pass" 2>/dev/null) HASH=$(echo -n "$danny_pass" | openssl passwd -6 -stdin 2>/dev/null) || HASH=$(mkpasswd -6 -m sha-512 "$danny_pass" 2>/dev/null) || true
if [[ -z "$HASH" ]]; then if [[ -n "${HASH:-}" ]]; then
echo "Could not hash password (need openssl or mkpasswd). Skipping password."
else
if command -v jq &>/dev/null; then if command -v jq &>/dev/null; then
SYSTEM_CONFIG=$(echo "$SYSTEM_CONFIG" | jq --arg h "$HASH" '. + {"users":{"users":{"danny":{"hashedPassword":$h}}}}') SYSTEM_CONFIG=$(echo "$SYSTEM_CONFIG" | jq --arg h "$HASH" '. + {"users":{"users":{"danny":{"hashedPassword":$h}}}}')
else else
NEW_CONFIG=$(echo "$SYSTEM_CONFIG" | nix run nixpkgs#jq -- --arg h "$HASH" '. + {"users":{"users":{"danny":{"hashedPassword":$h}}}}' 2>/dev/null) NEW_CONFIG=$(echo "$SYSTEM_CONFIG" | nix run nixpkgs#jq -- --arg h "$HASH" '. + {"users":{"users":{"danny":{"hashedPassword":$h}}}}' 2>/dev/null)
[[ -n "$NEW_CONFIG" ]] && SYSTEM_CONFIG="$NEW_CONFIG" || echo "Could not merge password (jq not found). Set after boot: passwd danny" [[ -n "$NEW_CONFIG" ]] && SYSTEM_CONFIG="$NEW_CONFIG" || echo "Could not merge password. Set after boot: passwd danny"
fi fi
[[ -n "$SYSTEM_CONFIG" ]] && echo "Password will be set for danny." echo "Password will be set for danny."
else
echo "Could not hash password (need openssl or mkpasswd). Set after boot: passwd danny"
fi fi
fi fi
# --- Confirm and install ---
echo ""
echo "=== Install Summary ==="
echo "Flake: ${FLAKE_REF}#server-install" echo "Flake: ${FLAKE_REF}#server-install"
echo "Disk: $disk" echo "Disk: $disk"
echo "Hostname: $hostname" echo "Hostname: $hostname"
echo "SSH pubkey: ${SSH_PUBKEY_FILE:-none}"
echo "System config: $SYSTEM_CONFIG" echo "System config: $SYSTEM_CONFIG"
read -r -p "Proceed? [y/N] " confirm read -r -p "Proceed? [y/N] " confirm
if [[ "${confirm,,}" != "y" && "${confirm,,}" != "yes" ]]; then if [[ "${confirm,,}" != "y" && "${confirm,,}" != "yes" ]]; then
@ -100,33 +114,88 @@ nix run --extra-experimental-features "nix-command flakes" \
--disk main "$disk" \ --disk main "$disk" \
--system-config "$SYSTEM_CONFIG" --system-config "$SYSTEM_CONFIG"
# Set danny password directly on disk (Nix merge can fail); re-open LUKS and chroot echo ""
if [[ -n "${danny_pass:-}" ]]; then echo "=== Post-install provisioning ==="
echo "Setting password for danny on installed system (re-enter LUKS passphrase once)..." echo "Re-opening LUKS to provision the installed system..."
read -s -r -p "LUKS passphrase: " luks_pass read -s -r -p "LUKS passphrase: " luks_pass
echo echo
LUKS_DEV="/dev/disk/by-partlabel/disk-main-luks" LUKS_DEV="/dev/disk/by-partlabel/disk-main-luks"
ESP_DEV="/dev/disk/by-partlabel/disk-main-ESP" ESP_DEV="/dev/disk/by-partlabel/disk-main-ESP"
if [[ ! -b "$LUKS_DEV" ]]; then if [[ ! -b "$LUKS_DEV" ]]; then
LUKS_DEV="${disk}2" LUKS_DEV="${disk}2"
ESP_DEV="${disk}1" ESP_DEV="${disk}1"
fi fi
if [[ -b "$LUKS_DEV" ]]; then
if ! echo -n "$luks_pass" | cryptsetup open "$LUKS_DEV" crypted --key-file -; then if [[ ! -b "$LUKS_DEV" ]]; then
echo "Wrong LUKS passphrase; set danny password after boot: passwd danny" echo "Could not find LUKS partition. Complete these steps manually after boot:"
echo " 1. Clone dotfiles: sudo git clone ... /etc/dotfiles"
echo " 2. Add SSH key: mkdir -p ~/.ssh && cat /tmp/key.pub >> ~/.ssh/authorized_keys"
echo " 3. Generate hardware config: nixos-generate-config --show-hardware-config > /etc/dotfiles/nixos/hosts/${hostname}-hardware.nix"
exit 0
fi
if [[ -e /dev/mapper/crypted ]]; then
echo " [ok] LUKS device already open (left open by disko-install)"
unset luks_pass
elif ! echo -n "$luks_pass" | cryptsetup open "$LUKS_DEV" crypted --key-file -; then
echo "Wrong LUKS passphrase. Complete provisioning manually after boot."
unset luks_pass
exit 0
else else
unset luks_pass
fi
mount /dev/mapper/crypted /mnt mount /dev/mapper/crypted /mnt
[[ -b "$ESP_DEV" ]] && mount "$ESP_DEV" /mnt/boot [[ -b "$ESP_DEV" ]] && mount "$ESP_DEV" /mnt/boot
mount --bind /dev /mnt/dev mount --bind /dev /mnt/dev
mount --bind /proc /mnt/proc mount --bind /proc /mnt/proc
mount --bind /sys /mnt/sys mount --bind /sys /mnt/sys
# 1. Set danny password (belt-and-suspenders; Nix merge can fail)
if [[ -n "$danny_pass" ]]; then
echo "danny:${danny_pass}" | chroot /mnt chpasswd echo "danny:${danny_pass}" | chroot /mnt chpasswd
echo " [ok] danny password set"
fi
unset danny_pass
# 2. Clone dotfiles
if [[ ! -d /mnt/etc/dotfiles ]]; then
chroot /mnt nix run --extra-experimental-features "nix-command flakes" nixpkgs#git -- \
clone https://github.com/DannyDannyDanny/dotfiles.git /etc/dotfiles
echo " [ok] dotfiles cloned to /etc/dotfiles"
else
echo " [skip] /etc/dotfiles already exists"
fi
# 3. Install SSH public key
if [[ -n "${SSH_PUBKEY_FILE:-}" ]] && [[ -f "$SSH_PUBKEY_FILE" ]]; then
mkdir -p /mnt/home/danny/.ssh
cat "$SSH_PUBKEY_FILE" >> /mnt/home/danny/.ssh/authorized_keys
chmod 700 /mnt/home/danny/.ssh
chmod 600 /mnt/home/danny/.ssh/authorized_keys
chroot /mnt chown -R danny:users /home/danny/.ssh
echo " [ok] SSH public key installed"
elif [[ -n "${SSH_PUBKEY_FILE:-}" ]]; then
echo " [warn] SSH_PUBKEY_FILE set but file not found: $SSH_PUBKEY_FILE"
fi
# 4. Generate hardware config
HW_CONFIG="/mnt/etc/dotfiles/nixos/hosts/${hostname}-hardware.nix"
if nixos-generate-config --show-hardware-config --root /mnt > "$HW_CONFIG" 2>/dev/null; then
echo " [ok] hardware config saved to hosts/${hostname}-hardware.nix"
echo " NOTE: Commit this file to the repo after first boot."
else
echo " [warn] nixos-generate-config failed; run manually after boot:"
echo " nixos-generate-config --show-hardware-config > /etc/dotfiles/nixos/hosts/${hostname}-hardware.nix"
fi
umount -R /mnt umount -R /mnt
cryptsetup close crypted cryptsetup close crypted
echo "Password for danny set. Reboot and log in."
fi echo ""
unset luks_pass echo "=== Done! ==="
else echo "Remove the USB and reboot. After unlocking LUKS:"
echo "Could not find LUKS partition; set password after boot: passwd danny" echo " 1. SSH in: ssh danny@${hostname}"
fi echo " 2. First rebuild: cd /etc/dotfiles && sudo nixos-rebuild switch --flake .#${hostname}"
fi echo " 3. Commit ${hostname}-hardware.nix back to the repo"

View file

@ -0,0 +1,61 @@
#!/bin/bash
# Run after disko-install when LUKS is already open.
# Usage: curl -fsSL https://raw.githubusercontent.com/DannyDannyDanny/dotfiles/main/scripts/post-install-provision.sh | sudo bash -s -- phantom-ship
set -euo pipefail
HOSTNAME="${1:-phantom-ship}"
USB_DATA="/tmp/usb-data"
REPO="https://github.com/DannyDannyDanny/dotfiles.git"
echo "=== Post-install provisioning for ${HOSTNAME} ==="
# Mount installed system (LUKS already open from disko-install)
mount /dev/mapper/crypted /mnt
mount /dev/disk/by-partlabel/disk-main-ESP /mnt/boot 2>/dev/null || true
for d in dev proc sys; do mount --bind /$d /mnt/$d; done
# Clone dotfiles — find git or nix, clone directly into /mnt (no chroot)
if [[ ! -d /mnt/etc/dotfiles ]]; then
# Ensure nix is in PATH (live installer may strip it under sudo)
export PATH=$PATH:/run/current-system/sw/bin:/nix/var/nix/profiles/default/bin
if command -v git &>/dev/null; then
git clone "$REPO" /mnt/etc/dotfiles
else
nix run --extra-experimental-features "nix-command flakes" nixpkgs#git -- \
clone "$REPO" /mnt/etc/dotfiles
fi
echo "[ok] dotfiles cloned"
else
echo "[skip] dotfiles already present"
fi
# Install SSH key
if [[ -f "$USB_DATA/authorized_keys" ]]; then
mkdir -p /mnt/home/danny/.ssh
cp "$USB_DATA/authorized_keys" /mnt/home/danny/.ssh/authorized_keys
chmod 700 /mnt/home/danny/.ssh
chmod 600 /mnt/home/danny/.ssh/authorized_keys
chroot /mnt chown -R danny:users /home/danny/.ssh
echo "[ok] SSH key installed"
else
echo "[warn] no authorized_keys on USB — add SSH key manually after boot"
fi
# Generate hardware config
nixos-generate-config --show-hardware-config --root /mnt \
> /mnt/etc/dotfiles/nixos/hosts/${HOSTNAME}-hardware.nix
echo "[ok] hardware config saved to hosts/${HOSTNAME}-hardware.nix"
# Copy hardware config to USB for committing from Mac
mkdir -p "$USB_DATA"
cp /mnt/etc/dotfiles/nixos/hosts/${HOSTNAME}-hardware.nix "$USB_DATA/"
echo "[ok] hardware config also copied to USB ($USB_DATA/)"
umount -R /mnt
cryptsetup close crypted 2>/dev/null || true
echo ""
echo "=== Done! Remove USB and reboot. ==="
echo "After unlocking LUKS, SSH in: ssh danny@${HOSTNAME}"
echo "Then: cd /etc/dotfiles && sudo nixos-rebuild switch --flake .#${HOSTNAME}"
echo "Commit ${HOSTNAME}-hardware.nix from the USB back to the repo."

View file

@ -1,27 +1,9 @@
#!/bin/bash #!/bin/bash
# One-shot sync of Alacritty palette + nvim marker from current macOS appearance.
# Simple setup for Alacritty theme synchronization
# This creates the theme file and rebuilds the Nix configuration
set -e set -e
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "Syncing from system appearance..."
echo "Setting up simple Alacritty theme synchronization..." "$SCRIPT_DIR/alacritty-sync-system-theme.sh"
# Run the theme sync script to create the initial theme file
echo "Detecting current system theme..."
"$SCRIPT_DIR/sync-alacritty-theme.sh"
echo "" echo ""
echo "Setup complete!" echo "Done. Alacritty reloads colors automatically if live_config_reload is enabled."
echo "" echo "A LaunchAgent (nix-darwin: launchd.user.agents.alacritty-system-theme) runs this every 30s."
echo "To apply the theme to Alacritty, run:"
echo " cd nixos && sudo darwin-rebuild switch --flake .#Daniel-Macbook-Air"
echo ""
echo "To sync themes when your system theme changes:"
echo " $SCRIPT_DIR/sync-alacritty-theme.sh && cd nixos && sudo darwin-rebuild switch --flake .#Daniel-Macbook-Air"
echo ""
echo "For automatic theme switching, you can set up a LaunchAgent or"
echo "run the sync script manually when needed."

View file

@ -1,80 +0,0 @@
#!/bin/bash
# Switch Alacritty theme by updating the Nix configuration
# This script changes the isLightTheme variable in home.nix and rebuilds
set -e
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOTFILES_DIR="$(dirname "$SCRIPT_DIR")"
HOME_NIX="$DOTFILES_DIR/nixos/home/danny/home.nix"
# Check if home.nix exists
if [ ! -f "$HOME_NIX" ]; then
echo "Error: home.nix not found at $HOME_NIX"
exit 1
fi
# Function to switch to light theme
switch_to_light() {
echo "Switching to light theme (Catppuccin Latte)..."
sed -i '' 's/isLightTheme = false;/isLightTheme = true;/' "$HOME_NIX"
}
# Function to switch to dark theme
switch_to_dark() {
echo "Switching to dark theme (Catppuccin Mocha)..."
sed -i '' 's/isLightTheme = true;/isLightTheme = false;/' "$HOME_NIX"
}
# Function to show current theme
show_current() {
if grep -q "isLightTheme = true" "$HOME_NIX"; then
echo "Current theme: Light (Catppuccin Latte)"
else
echo "Current theme: Dark (Catppuccin Mocha)"
fi
}
# Function to rebuild the configuration
rebuild() {
echo "Rebuilding configuration..."
cd "$DOTFILES_DIR/nixos"
sudo darwin-rebuild switch --flake .#Daniel-Macbook-Air
}
# Main logic
case "${1:-}" in
"light")
switch_to_light
rebuild
;;
"dark")
switch_to_dark
rebuild
;;
"toggle")
if grep -q "isLightTheme = true" "$HOME_NIX"; then
switch_to_dark
else
switch_to_light
fi
rebuild
;;
"status"|"current")
show_current
;;
*)
echo "Usage: $0 {light|dark|toggle|status}"
echo ""
echo "Commands:"
echo " light - Switch to light theme (Catppuccin Latte)"
echo " dark - Switch to dark theme (Catppuccin Mocha)"
echo " toggle - Toggle between light and dark themes"
echo " status - Show current theme"
echo ""
show_current
exit 1
;;
esac

View file

@ -1,31 +1,5 @@
#!/bin/bash #!/bin/bash
# Back-compat wrapper: sync Alacritty + nvim marker from macOS appearance.
# Sync Alacritty theme with system theme
# This script detects the current system theme and updates the theme file that Nix reads
set -e set -e
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "$SCRIPT_DIR/alacritty-sync-system-theme.sh"
# Paths
THEME_DETECTION_SCRIPT="$SCRIPT_DIR/detect-system-theme.sh"
THEME_FILE="/Users/danny/.local/share/nvim_color_scheme"
# Create the directory if it doesn't exist
mkdir -p "$(dirname "$THEME_FILE")"
# Detect current system theme
if [ ! -f "$THEME_DETECTION_SCRIPT" ]; then
echo "Error: Theme detection script not found at $THEME_DETECTION_SCRIPT"
exit 1
fi
CURRENT_THEME=$("$THEME_DETECTION_SCRIPT")
echo "Current system theme: $CURRENT_THEME"
# Write the theme to the file that Nix reads
echo "$CURRENT_THEME" > "$THEME_FILE"
echo "Theme file updated: $THEME_FILE"
echo "Run 'home-manager switch' to apply the new theme to Alacritty"

View file

@ -17,7 +17,7 @@ show_usage() {
echo "" echo ""
echo "This command switches themes for:" echo "This command switches themes for:"
echo " - Neovim (via nvim_color_scheme file)" echo " - Neovim (via nvim_color_scheme file)"
echo " - Alacritty (via Nix configuration on macOS)" echo " - Alacritty on macOS follows System Settings (LaunchAgent sync)"
echo " - Windows Terminal (via settings.json on WSL)" echo " - Windows Terminal (via settings.json on WSL)"
echo " - Windows system theme (on WSL)" echo " - Windows system theme (on WSL)"
} }
@ -43,19 +43,11 @@ show_status() {
elif [[ "$OSTYPE" == "darwin"* ]]; then elif [[ "$OSTYPE" == "darwin"* ]]; then
echo " Platform: macOS" echo " Platform: macOS"
# Check Alacritty theme from Nix config marker="$HOME/.config/alacritty/.last-system-theme"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [ -f "$marker" ]; then
DOTFILES_DIR="$(dirname "$SCRIPT_DIR")" echo " Alacritty: follows system (active palette: $(tr -d '\n' <"$marker"))"
HOME_NIX="$DOTFILES_DIR/nixos/home/danny/home.nix"
if [ -f "$HOME_NIX" ]; then
if grep -q "isLightTheme = true" "$HOME_NIX"; then
echo " Alacritty: light (Catppuccin Latte)"
else else
echo " Alacritty: dark (Catppuccin Mocha)" echo " Alacritty: follows system (sync after next login or run alacritty-sync-system-theme)"
fi
else
echo " Alacritty: config file not found"
fi fi
else else
echo " Platform: other" echo " Platform: other"
@ -67,17 +59,10 @@ toggle_theme() {
current_theme="" current_theme=""
if [[ "$OSTYPE" == "darwin"* ]]; then if [[ "$OSTYPE" == "darwin"* ]]; then
# On macOS, check the Nix config for current theme if [[ "$(defaults read -g AppleInterfaceStyle 2>/dev/null)" == "Dark" ]]; then
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOTFILES_DIR="$(dirname "$SCRIPT_DIR")"
HOME_NIX="$DOTFILES_DIR/nixos/home/danny/home.nix"
if [ -f "$HOME_NIX" ]; then
if grep -q "isLightTheme = true" "$HOME_NIX"; then
current_theme="light"
else
current_theme="dark" current_theme="dark"
fi else
current_theme="light"
fi fi
fi fi
@ -183,18 +168,8 @@ if [[ -n "$WSL_DISTRO_NAME" ]]; then
powershell.exe -Command "Set-ItemProperty -Path HKCU:\AppEvents\Schemes -Name '(Default)' -Value '.None'" powershell.exe -Command "Set-ItemProperty -Path HKCU:\AppEvents\Schemes -Name '(Default)' -Value '.None'"
elif [[ "$OSTYPE" == "darwin"* ]]; then elif [[ "$OSTYPE" == "darwin"* ]]; then
# macOS platform - handle Alacritty theme
echo "Detected macOS platform" echo "Detected macOS platform"
echo "Alacritty follows System Settings → Appearance (no rebuild). Neovim theme file updated above."
# Use the existing Alacritty theme switching script
alacritty_script="$DOTFILES_DIR/scripts/switch-alacritty-theme.sh"
if [ -f "$alacritty_script" ]; then
echo "Switching Alacritty theme to: $color_scheme"
"$alacritty_script" "$color_scheme"
else
echo "Warning: Alacritty theme script not found at $alacritty_script"
echo "Theme file updated, but Alacritty theme not switched"
fi
else else
# Other platforms - just update the theme file # Other platforms - just update the theme file

View file

@ -44,7 +44,7 @@ Optional: `services.openssh.settings = { PasswordAuthentication = false; PermitR
```bash ```bash
sudo nixos-rebuild switch sudo nixos-rebuild switch
# or: sudo nixos-rebuild switch --flake /path/to/dotfiles/nixos#hostname # or: sudo nixos-rebuild switch --flake /path/to/dotfiles#hostname
``` ```
Then from your main machine: `ssh danny@myserver` Then from your main machine: `ssh danny@myserver`

View file

@ -0,0 +1,6 @@
[
{
"publickey": "age1hjhqyuvcjuh62xh9m5ek3aa2rluaz8c28hgh2pm435jkqtpry9ssdn2l0z",
"type": "age"
}
]

View file

@ -0,0 +1,6 @@
[
{
"publickey": "age1lwl2z6ymqjshknr79277qnr7hvffcc8n7qdqt98sz3t709a5yutq8d7gka",
"type": "age"
}
]

View file

@ -0,0 +1,6 @@
[
{
"publickey": "age18gtjh28qxeltg2r2tzxwl096crkqkqk8tjhersyf7mzdsddady7qs34x0m",
"type": "age"
}
]

View file

@ -0,0 +1,6 @@
[
{
"publickey": "age1zy3q73pujauyajgfqwu0pnyy8732lzwvw87tu7p2xg3xuzaujc2qh6ql77",
"type": "age"
}
]

View file

@ -0,0 +1,6 @@
[
{
"publickey": "age1mlljsdpqf054p4nav9s855rtd5szwyl9av8w2lvg86j59cdtugxqylcn6k",
"type": "age"
}
]

View file

@ -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"
}
}

View file

@ -0,0 +1 @@
../../../users/danny

View file

@ -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"
}
}

View file

@ -0,0 +1 @@
../../../users/danny

View file

@ -0,0 +1,14 @@
{
"data": "ENC[AES256_GCM,data:43IKkW3YpbpEtECD3kXV4zWF6hB39knoWwqy5BGCqvYWSPccKIwwLD3ctCy3SeH806AatvE8Bl2dvHFvP++xtvFtw5PaHdnenn8=,iv:j7ODs5O0rbwD0LWkkv9BEk6O9ySl+uhCiEVa+GkRE3k=,tag:Bk/PkQjOvul8pP7hoh2cwQ==,type:str]",
"sops": {
"age": [
{
"recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVaVlVQ3pvRmpLaVB3WWtl\nYjBIdDBJK0VKeS90eE5YeFhFRnBPak5YckFZCkl5RkVMV3JxL0pSVkM4cjhRaUE3\nK24vSWM0YnFWeXNjc3ZSWDRBb1ZDeWsKLS0tIENabmsxVUl0UGZzN1pncWswTVdM\nWDBVTVMrYzJHUklKSVVjYXBBM2RuajgKCvrGjfjujmqq2lsbNAb8d1xUhv+es2uX\nydcfnqbFRF4pjrku41iRaOolWrZHDvl+PnMslk8bclZG23UKYbSkbA==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2026-04-19T12:31:43Z",
"mac": "ENC[AES256_GCM,data:7/Z1Up1DZUgNMCuuBh2pnfTH3Ih6824yJqD1+w9clqgkSrFtKL6v5oo5EV4TF2FDJcrYQtbbAWQoEgJXfCKXfIYOPBIChfoQEG5N5XxNe57bklkipOMWJBm7448qBhLgy3yJQqAVFkQw6uHTuDrcngRFW5D3xHkCSilHC/xau9U=,iv:WL98Dcuxojg6BQ5tLOuhXYCfFHVXqpIBr680uriPXz0=,tag:FCl6wkBiLJUyMu1RnOqeIw==,type:str]",
"version": "3.12.2"
}
}

View file

@ -0,0 +1 @@
../../../users/danny

View file

@ -0,0 +1,14 @@
{
"data": "ENC[AES256_GCM,data:Mk4Vfs0PvKI4Ynwmz+8myrFtPW1swn9PdtQoeZw0xh9aCT+o6IWstAUypuCfwSgPYkj8PFPi2yq7ysTzglBkhrThV9Zto48U2dA=,iv:jL1WHTpN3mVNQJ/ltHBFd7zMtVtRmh9RIJAnh1SiGZc=,tag:zmRAQvcg6FW1+bEvZd8D6g==,type:str]",
"sops": {
"age": [
{
"recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6NzV5c2FDVndUSWRnZndR\ndXI2bEY2VGRiRndNbjZscHdjL0N0eHUrV1hZCmJMRllSdjNLWS8rcnlYLy94VUcy\ndDlXeUptaGdwb2ZsMW1UZHJoeW5CZzgKLS0tIDBkeUozUDd2YWpIRTFlK3M3K2RH\naW9CMnc1ZXRmM0x4MDYwVHVLZnVpR0UKZSowubfXrUemRSFNYo8hxSaeV6/egOi6\nmtmxPICosAV5VRbf8c5Hn3XGNGfOGVwwox+GmLjzqfpVsM9f2Qm9IQ==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2026-04-19T12:31:44Z",
"mac": "ENC[AES256_GCM,data:SaRWT7Q7joTgG7+LBL2icBQ4k2SJdFfDcPzV3IsBIMgVFC4kQNbkVr0BlTM4mgtfH+IxE8PBQu1v/JFo6kf43njnF3mD/Yzr/EsLxwVmD9U1DTpW+mr1EBUVLfiGqnVrTj2DhMdatKB1g8jRwAlpIcsmrlnsHIKjuSj5HKRIi7Q=,iv:YVV3BMhfh1ThIiYwW4uHUmUKqkHUtCy0i0owiAngKyg=,tag:f4UaL5ZjEp3Gkd6LGiq+uw==,type:str]",
"version": "3.12.2"
}
}

View file

@ -0,0 +1 @@
../../../users/danny

View file

@ -0,0 +1,14 @@
{
"data": "ENC[AES256_GCM,data:+Cd3Hxr5KzX6J/74M2IZ6VOE6KEDsK8NoVyTleSB7UdsDWWGAS+mgdNZTiVBJEIBx+cmMKMcNj2rNu6T4Z2OCvqH/o6GBAhKBmM=,iv:RllA6vH/qWsx08gTEi5Nl4VkvoeI00Bw56IwPp1TOLk=,tag:PdQJpm0oaYZUZvc1y9Cmcw==,type:str]",
"sops": {
"age": [
{
"recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxSmVLKzl3akVZNlE0QWJr\namc0T3NPb0pzM2hvR3VlSEo2TDJ6VDJOQmhBCnJOTkZaOVM2RXpTOEdYUEtWTUht\nS0ZmNDVoVDJzajYxRDVWVFVkTkJLbkUKLS0tICsrUGx0Q2FmZk04NHBVb2wvaU1p\nSktZNVl5bUtKZEJLWm1kYm9wSFl5ZXMKEb+0fq1idxA4mpJAxt3DUWX8kYp8HwYN\nwUQ7SFAlj3k611jfVFwRYdqJZQLYQ0iVbEwy5BfJw4tnqZFeaEBueA==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2026-04-24T11:41:47Z",
"mac": "ENC[AES256_GCM,data:AjAgVpuV7QvCh1E4AvTSP+Oxg/M1at8X08s76C9OxmdCR0Evd67Hb5TaPkujhtX87Qs9IHoOK6yY+aQv2exLXWt6U4uRzapsVIpcofdyA7EUF2q0UaykrqtKLGYW3IY8fXL4XwMMFJ+wmThmwKnJVJO8hUug8AceA83/QVYNccM=,iv:JuxpYvmTROZPv7zawPQ/NpfbWAQqwRfBRp+zhNQnm5I=,tag:v6IMgQTbPP+XEeCSrpVTxg==,type:str]",
"version": "3.12.2"
}
}

View file

@ -0,0 +1 @@
../../../users/danny

6
sops/users/danny/key.json Executable file
View file

@ -0,0 +1,6 @@
[
{
"publickey": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28",
"type": "age"
}
]

View file

@ -0,0 +1 @@
../../../../../../sops/machines/distant-shore

View file

@ -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"
}
}

View file

@ -0,0 +1 @@
../../../../../../sops/users/danny

View file

@ -0,0 +1 @@
../../../../../../sops/machines/distant-shore

View file

@ -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"
}
}

View file

@ -0,0 +1 @@
../../../../../../sops/users/danny

View file

@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAABhcRTNvFEyWkyRBX17KkM5nDuqOvR1xTY5vDqTygvk=
-----END PUBLIC KEY-----

View file

@ -0,0 +1 @@
12D3KooW9pjiKnqmnHSwGRhgyUqKeFydDUE8RvYJDAqHb5PZvzue

View file

@ -0,0 +1 @@
../../../../../../sops/machines/distant-shore

View file

@ -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"
}
}

View file

@ -0,0 +1 @@
../../../../../../sops/users/danny

View file

@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAPVF7m/+s1YroGdvSMxPwKmenJjk4yNrP8tNtZGHEhJI=
-----END PUBLIC KEY-----

Some files were not shown because too many files have changed in this diff Show more