Compare commits

...

36 Commits

Author SHA1 Message Date
14d659bef0 feat(kodi): use qt-pinned nixpkgs for jellyfin-media-player
Decouple jellyfin-media-player from main nixpkgs update cycle by
sourcing it from pkgs.qt-pinned namespace. This allows the Qt-heavy
package to update on its own Renovate schedule, avoiding massive
qt5webengine rebuilds when updating other packages.

x-xiiep

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 15:16:49 -08:00
1b585847ab Merge branch 'polecat/chrome/x-ymkgu@mlebby8e': update renovate schedules to Saturday afternoon
All checks were successful
CI / check (push) Successful in 16m9s
2026-02-08 14:58:17 -08:00
e7906331dc feat(renovate): update schedules to Saturday afternoon
- lockFileMaintenance: Saturday 2-4pm (was Monday 5am)
- nix-stable-ecosystem: Saturday 2-4pm
- nix-unstable-ecosystem: Saturday 2-4pm
- Add nixpkgs-qt rule: Saturday 4-6pm (staggered)

This allows CI builds to run overnight Saturday→Sunday, with human
review Saturday evening and builds complete by Sunday morning.

Closes: x-ymkgu
2026-02-08 14:58:05 -08:00
dc722843a9 Merge branch 'polecat/rust/x-lnr8g@mlebamik': add nixpkgs-qt input for qt5webengine
Some checks failed
CI / check (push) Has been cancelled
2026-02-08 14:57:19 -08:00
03f169284d feat(flake): add nixpkgs-qt input for qt5webengine packages
Add separate nixpkgs input for qt5webengine-dependent packages like
jellyfin-media-player. This input updates on a separate Renovate
schedule from main nixpkgs to avoid massive qt5webengine rebuilds
when updating other packages.

- Add nixpkgs-qt input pinned to nixos-25.11
- Create pkgs.qt-pinned overlay namespace

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 14:35:35 -08:00
8908500073 feat(home-kodi): enable kdeconnect for kodi user on boxy
All checks were successful
CI / check (push) Successful in 3m26s
Allows KDE Connect discovery and pairing to work when logged in as
the kodi user on the media center.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 13:43:17 -08:00
87f6d5c759 feat(deps): update beads to 0.49.1 with dolt server mode, claude-code to 2.1.30
All checks were successful
CI / check (push) Successful in 5m17s
beads:
- Pin to commit 93965b4a (last before Go 1.25.6 requirement)
- Build locally with corrected vendorHash (upstream default.nix is stale)
- Enables dolt server mode support (gt-1mf.3)

claude-code: 2.1.19 → 2.1.30

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 14:56:24 -08:00
a851c2551c fix(deps): update gastown patch and pin beads to Go 1.24 compatible version
All checks were successful
CI / check (push) Successful in 5m25s
- Update gastown-fix-agent-bead-address-title.patch line numbers (326→315)
  for current upstream gastown source
- Remove obsolete gastown patches (rig-prefix, copydir-symlinks) that are
  now handled upstream
- Pin beads to 55e733c (v0.47.2) which uses Go 1.24.0 - newer versions
  require Go 1.25.6 which isn't in nixpkgs-unstable yet
- Remove beads-search-query-optimization.patch as it targets newer code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 13:18:46 -08:00
mayor
6cf63e86c1 Merge branch 'polecat/rust/x-0cf@ml2ye219': fix doom-intermediates.drv CI failure
All checks were successful
CI / check (push) Successful in 7m37s
Updated nix-doom-emacs-unstraightened flake input to fix stale IFD derivation.

Closes: x-0cf, x-qwd7, hq-cv-mnzq4
2026-01-31 15:50:43 -08:00
c3ed6c0a26 fix(deps): update nix-doom-emacs-unstraightened to fix live-usb flake check
Updates nix-doom-emacs-unstraightened from Jan 25 to Jan 31 release,
which fixes the stale doom-intermediates.drv reference that was causing
nixosConfigurations.live-usb to fail flake check.

Closes: x-0cf

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 15:48:34 -08:00
mayor
53fa89b2e9 Merge branch 'polecat/rust/hq-0h1p9m@ml2ugjq1': fix gastown statusline patch
All checks were successful
CI / check (push) Successful in 5m19s
Regenerated patch with correct hunk headers against locked rev 177094a2.
Root cause was malformed patch format, not a flake.lock issue.

Closes: hq-0h1p9m, x-bwld
2026-01-31 14:01:06 -08:00
3acf9d2796 fix(gastown): regenerate statusline optimization patch with correct line numbers
The patch file had malformed hunk headers with incorrect line numbers
and counts, causing it to fail to apply against the locked gastown rev
(177094a2). This was NOT a flake.lock issue - gastown source was properly
locked.

Changes:
- Regenerated patch from scratch against locked gastown revision
- Re-enabled the patch in default.nix (was commented out with TODO)
- Updated comment to accurately describe the optimization

The optimization skips expensive beads queries for detached tmux sessions
and caches status line output with a 10-second TTL, reducing Dolt CPU
usage from ~70% to ~20%.

Closes: hq-0h1p9m

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 13:59:11 -08:00
123e7d3b3a fix(gastown): repair malformed patch files for nixos-rebuild
Some checks failed
CI / check (push) Failing after 17m48s
- Remove 'index 0000000..1111111' lines that made patches appear as new files
- Fix hunk line counts in several patches
- Add missing leading spaces to blank context lines
- Temporarily disable statusline optimization patch (needs regenerating)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 13:20:12 -08:00
nixos_configs/crew/harry
56097aefa4 refactor(development): move gastown patches to separate files
All checks were successful
CI / check (push) Successful in 3m37s
Replace inline postPatch substituteInPlace calls with proper unified
diff patch files, following the pattern established by beads.

This improves maintainability:
- Each patch is in its own file with clear naming
- Patches use proper unified diff format
- Easier to review, update, and track individual fixes
- Default.nix is cleaner (237 lines of substituteInPlace -> 15 lines)

Patches included:
- gastown-fix-validate-recipient.patch
- gastown-fix-agent-bead-address-title.patch
- gastown-fix-agent-bead-rig-prefix.patch
- gastown-fix-role-home-paths.patch
- gastown-fix-town-root-detection.patch
- gastown-fix-copydir-symlinks.patch
- gastown-statusline-optimization.patch

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 09:06:05 -08:00
21a8b5c5d9 Fix bd SearchIssues inefficient WHERE IN query pattern for Dolt
All checks were successful
CI / check (push) Successful in 3m25s
The Dolt backend's SearchIssues was using a two-phase query:
1. SELECT id FROM issues WHERE ... -> collect all IDs
2. SELECT * FROM issues WHERE id IN (id1, id2, ... id8000+)

With 8000+ issues, this second query with 8000+ placeholders hammers
Dolt CPU at 100%+. The fix changes SearchIssues to select all columns
directly in the first query and scan results inline.

See: hq-ihwsj

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 18:29:46 -08:00
8f8582b0f3 feat(gastown): add statusline cache writes for CPU optimization
All checks were successful
CI / check (push) Successful in 3m24s
Complete the statusline optimization by adding cache writes to all
output functions. The existing patch added cache functions and cache
reads, but never wrote to the cache.

Changes:
- Add early-return for detached sessions (return static "○ |")
- Add cache read check for attached sessions
- Add setStatusLineCache() calls in all 5 output functions:
  - runWorkerStatusLine
  - runMayorStatusLine
  - runDeaconStatusLine
  - runWitnessStatusLine
  - runRefineryStatusLine

This should reduce Dolt CPU from ~70% to ~20% when agents are idle,
as tmux status lines will use cached results instead of spawning
beads queries every 5 seconds.

Testing: Run `nix switch` then monitor Dolt CPU with `top`

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 12:20:39 -08:00
94fb5a3e64 Fix gastown mail routing for rig-specific agent beads
All checks were successful
CI / check (push) Successful in 3m18s
- Add title-based lookup for hq- prefixed beads (uses title as address if contains "/")
- Add rig-specific prefix handling to parse IDs like j-java-crew-americano → java/crew/americano
- Handles crew, polecat, witness, refinery role patterns

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 17:20:03 -08:00
7df68ba8c8 Add gastown postPatch bug fixes from jt flake
All checks were successful
CI / check (push) Successful in 5m17s
- Fix mail router normalization in validateRecipient
- Fix agentBeadToAddress to use title field for hq- prefixed beads
- Fix crew/polecat home paths (remove incorrect /rig suffix)
- Fix town root detection (RoleUnknown instead of RoleMayor)
- Fix copyDir symlink handling
- Pin to gastown commit 177094a matching jt flake

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 16:14:52 -08:00
2799632308 Add gastown postPatch bug fixes from jt flake
- Fix mail router normalization in validateRecipient
- Fix agentBeadToAddress to use title field for hq- prefixed beads
- Fix crew/polecat home paths (remove incorrect /rig suffix)
- Fix town root detection (RoleUnknown instead of RoleMayor)
- Fix copyDir symlink handling
- Pin to gastown commit 177094a matching jt flake

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 16:14:06 -08:00
346c031278 Match darwin configuration name to actual hostname
All checks were successful
CI / check (push) Successful in 3m33s
Use uppercase BLKFV4YF49KT7 so darwin-rebuild --flake ./ works without
explicitly specifying the configuration name.
2026-01-27 11:32:53 -08:00
188d2befb0 Fix sketchybar disk usage showing incorrect percentage
Monitor /System/Volumes/Data instead of / since root is a read-only
APFS snapshot with minimal usage. Also fix inverted formula that was
calculating 100-used instead of just using the capacity value directly.
2026-01-27 11:32:48 -08:00
8e8b5f4304 chore(machines): remove tart-agent-sandbox config
All checks were successful
CI / check (push) Successful in 5m44s
Pivoted to Docker container approach for agent sandboxing instead of
Tart VMs due to networking issues with Cloudflare WARP.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 10:54:33 -08:00
4098ee3987 revert tart agent sandbox sway idea
All checks were successful
CI / check (push) Successful in 3m34s
2026-01-27 09:58:50 -08:00
e1e37da7c2 feat(tart-agent-sandbox): add sway desktop with auto-login
All checks were successful
CI / check (push) Successful in 3m41s
- Enable desktop role with wayland/sway
- Use greetd for passwordless auto-login to sway
- Add video/input groups to agent user

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 09:48:03 -08:00
a46d11a770 feat(machines): add tart-agent-sandbox VM config
All checks were successful
CI / check (push) Successful in 4m26s
NixOS configuration for running LLM agents in isolated Tart VMs on
Apple Silicon. Includes:
- Headless server setup with SSH access
- Agent user with passwordless sudo
- Docker support
- Dev tools for cloning large repos
- Git config optimized for large repositories

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 09:24:47 -08:00
harry
8553b9826e feat(roles): add rclone-mount role for WebDAV mounts
Some checks failed
CI / check (push) Failing after 12m14s
Add a new system-level role for mounting WebDAV filesystems via rclone.
Includes rclone-torbox-setup helper script that uses rbw to bootstrap
credentials from Bitwarden.

Key features:
- Configurable WebDAV URL, username, mount point
- VFS cache mode and buffer size tuning for media streaming
- RequiresMountsFor option for ZFS pool dependencies
- Obscured password storage via environment file

Enable on john-endesktop for TorBox WebDAV access by rdt-client and
Jellyfin. Mount waits for /media ZFS pool before starting.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 19:45:00 -08:00
a0c081e12e fix(aerospace): disable ctrl shortcuts
All checks were successful
CI / check (push) Successful in 5m43s
2026-01-26 17:22:33 -08:00
d92e4b3ddf feat(development): add perles TUI for beads 2026-01-26 17:22:28 -08:00
70b40966be chore(claude-code): update to 2.1.19 2026-01-26 17:10:45 -08:00
475a633ab7 feat(base): add watch to base role packages 2026-01-26 17:10:42 -08:00
a39416c9db chore: switch beads and gastown to upstream GitHub repos 2026-01-26 17:10:37 -08:00
63c3f4e84d fix(sketchybar): show disk used% instead of free%
All checks were successful
CI / check (push) Successful in 3m27s
Inverts the df output to show percentage used, matching the other
resource monitors (CPU, memory).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 16:20:31 -08:00
baf64f7f4a fix(emacs): make rbw password helper graceful when rbw unavailable
Add optional no-error parameter to my/get-rbw-password that returns nil
instead of signaling an error when rbw isn't installed or the entry is
missing. Use this for gptel API key so config loads without errors in
environments without rbw configured.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 14:20:33 -08:00
mayor
f0b6ede7ed add dolt to development role
All checks were successful
CI / check (push) Successful in 5m50s
Required for beads dolt backend migration in Gas Town.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 11:55:33 -08:00
d0cb16391f update gt and/or beads 2026-01-26 11:51:06 -08:00
d872293f19 fix(gastown): add ldflags for BuiltProperly check
All checks were successful
CI / check (push) Successful in 3m57s
The gastown build now requires BuiltProperly=1 to be set via ldflags,
otherwise gt errors with "This binary was built with 'go build' directly".

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 08:20:20 -08:00
24 changed files with 771 additions and 53 deletions

83
flake.lock generated
View File

@@ -8,17 +8,18 @@
]
},
"locked": {
"lastModified": 1769367425,
"narHash": "sha256-r7VNku6B8VNYr88jQfuMInu6tKCPtoIXFYozTTAJpMY=",
"ref": "refs/heads/main",
"rev": "b0a6a456bad1c46a3351d999455968a715f52744",
"revCount": 5499,
"type": "git",
"url": "https://git.johnogle.info/johno/beads.git"
"lastModified": 1769840331,
"narHash": "sha256-Yp0K4JoXX8EcHp1juH4OZ7dcCmkopDu4VvAgZEOxgL8=",
"owner": "steveyegge",
"repo": "beads",
"rev": "93965b4abeed920a4701e03571d1b6bb75810722",
"type": "github"
},
"original": {
"type": "git",
"url": "https://git.johnogle.info/johno/beads.git"
"owner": "steveyegge",
"repo": "beads",
"rev": "93965b4abeed920a4701e03571d1b6bb75810722",
"type": "github"
}
},
"doomemacs": {
@@ -47,11 +48,11 @@
]
},
"locked": {
"lastModified": 1769329593,
"narHash": "sha256-u5PSA+8TUYF/13ziBcnoE67nkDwpjAdecKh3srcJJm0=",
"lastModified": 1769848312,
"narHash": "sha256-ggBocPd1L4l5MFNV0Fw9aSGZZO4aGzCfgh4e6hQ77RE=",
"owner": "nix-community",
"repo": "emacs-overlay",
"rev": "776dc33d735af583a14cc56b406ea658398964a7",
"rev": "be0b4f4f28f69be61e9174807250e3235ee11d50",
"type": "github"
},
"original": {
@@ -81,17 +82,17 @@
"gastown": {
"flake": false,
"locked": {
"lastModified": 1769375540,
"narHash": "sha256-8PkI0LGaYmQf9HjErc7kPGmyVGhj7SWH6zgE9F88igQ=",
"ref": "refs/heads/main",
"rev": "647a573e7ccb6bb48b780b1637fa9ba394ca4ba2",
"revCount": 3065,
"type": "git",
"url": "https://git.johnogle.info/johno/gastown.git"
"lastModified": 1770098007,
"narHash": "sha256-CFlN57BXlR5FobTChdE2GgdIGx4xJcFFCk1E5Q98cSQ=",
"owner": "steveyegge",
"repo": "gastown",
"rev": "13461161063bf7b2365fe5fd4df88e32c3ba2a28",
"type": "github"
},
"original": {
"type": "git",
"url": "https://git.johnogle.info/johno/gastown.git"
"owner": "steveyegge",
"repo": "gastown",
"type": "github"
}
},
"google-cookie-retrieval": {
@@ -206,11 +207,11 @@
"systems": "systems_2"
},
"locked": {
"lastModified": 1769330679,
"narHash": "sha256-X7rw5ouiAYKmbbKLtkEc/Kqcg6DxKgOtgaftzuchy/M=",
"lastModified": 1769849328,
"narHash": "sha256-BjH1Ge6O8ObN6Z97un2U87pl4POO99Q8RSsgIuTZq8Q=",
"owner": "marienz",
"repo": "nix-doom-emacs-unstraightened",
"rev": "0c2d527055f448c8856129c6d063535e06aeff4d",
"rev": "fc1d7190c49558cdc6af20d7657075943a500a93",
"type": "github"
},
"original": {
@@ -257,6 +258,22 @@
"type": "github"
}
},
"nixpkgs-qt": {
"locked": {
"lastModified": 1770464364,
"narHash": "sha256-z5NJPSBwsLf/OfD8WTmh79tlSU8XgIbwmk6qB1/TFzY=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "23d72dabcb3b12469f57b37170fcbc1789bd7457",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1769170682,
@@ -273,6 +290,22 @@
"type": "github"
}
},
"perles": {
"flake": false,
"locked": {
"lastModified": 1769460725,
"narHash": "sha256-zM2jw+emxe8+mNyR1ebMWkQiEx8uSmhoqqI0IxXLDgs=",
"owner": "zjrosen",
"repo": "perles",
"rev": "57b20413eea461452b59e13f5a4a367953b1f768",
"type": "github"
},
"original": {
"owner": "zjrosen",
"repo": "perles",
"type": "github"
}
},
"plasma-manager": {
"inputs": {
"home-manager": [
@@ -330,7 +363,9 @@
"nix-darwin": "nix-darwin",
"nix-doom-emacs-unstraightened": "nix-doom-emacs-unstraightened",
"nixpkgs": "nixpkgs",
"nixpkgs-qt": "nixpkgs-qt",
"nixpkgs-unstable": "nixpkgs-unstable",
"perles": "perles",
"plasma-manager": "plasma-manager",
"plasma-manager-unstable": "plasma-manager-unstable"
}

View File

@@ -4,6 +4,9 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable";
# Separate nixpkgs for qt5webengine-dependent packages (jellyfin-media-player, etc.)
# Updates on separate Renovate schedule to avoid massive qt rebuilds
nixpkgs-qt.url = "github:nixos/nixpkgs/nixos-25.11";
nix-darwin = {
url = "github:nix-darwin/nix-darwin/nix-darwin-25.11";
@@ -43,12 +46,19 @@
};
beads = {
url = "git+https://git.johnogle.info/johno/beads.git";
# v0.49.1 has dolt server mode support (gt-1mf.3)
# Pinned to 259ddd92 - uses Go 1.24 compatible with nixpkgs
url = "github:steveyegge/beads/93965b4abeed920a4701e03571d1b6bb75810722";
inputs.nixpkgs.follows = "nixpkgs-unstable";
};
gastown = {
url = "git+https://git.johnogle.info/johno/gastown.git";
url = "github:steveyegge/gastown";
flake = false; # No flake.nix upstream yet
};
perles = {
url = "github:zjrosen/perles";
flake = false; # No flake.nix upstream yet
};
@@ -69,6 +79,11 @@
config.allowUnfree = true;
overlays = unstableOverlays;
};
# Separate nixpkgs for qt5webengine-heavy packages to avoid rebuild churn
qt-pinned = import inputs.nixpkgs-qt {
system = prev.stdenv.hostPlatform.system;
config.allowUnfree = true;
};
custom = prev.callPackage ./packages {};
# Compatibility: bitwarden renamed to bitwarden-desktop in unstable
bitwarden-desktop = prev.bitwarden-desktop or prev.bitwarden;
@@ -210,7 +225,7 @@
};
# Darwin/macOS configurations
darwinConfigurations."blkfv4yf49kt7" = inputs.nix-darwin.lib.darwinSystem rec {
darwinConfigurations."BLKFV4YF49KT7" = inputs.nix-darwin.lib.darwinSystem rec {
system = "aarch64-darwin";
modules = darwinModules ++ [
./machines/johno-macbookpro/configuration.nix

View File

@@ -107,7 +107,7 @@
aerospace = {
enable = true;
leader = "cmd";
ctrlShortcuts.enable = true;
ctrlShortcuts.enable = false;
sketchybar.enable = true;
# Optional: Add per-machine userSettings overrides
# userSettings = {

View File

@@ -12,6 +12,7 @@
home.roles = {
base.enable = true;
plasma-manager-kodi.enable = true;
kdeconnect.enable = true;
};
home.packages = with pkgs; [

View File

@@ -632,7 +632,9 @@ in
text = ''
#!/bin/bash
DISK_USAGE=$(df -H / | grep -v Filesystem | awk '{print $5}')
# Monitor /System/Volumes/Data which contains user data on APFS
# The root / is a read-only snapshot with minimal usage
DISK_USAGE=$(df -H /System/Volumes/Data | grep -v Filesystem | awk '{print $5}')
${pkgs.sketchybar}/bin/sketchybar --set $NAME label="$DISK_USAGE"
'';

View File

@@ -22,6 +22,7 @@ in
shellcheck
tmux
tree
watch
];
# Automatic garbage collection for user profile (home-manager generations).

View File

@@ -0,0 +1,44 @@
diff --git a/internal/storage/dolt/queries.go b/internal/storage/dolt/queries.go
index 7d8214ee..8acdaae2 100644
--- a/internal/storage/dolt/queries.go
+++ b/internal/storage/dolt/queries.go
@@ -212,8 +212,21 @@ func (s *DoltStore) SearchIssues(ctx context.Context, query string, filter types
}
// nolint:gosec // G201: whereSQL contains column comparisons with ?, limitSQL is a safe integer
+ // Performance fix: SELECT all columns directly instead of id-only + WHERE IN (all_ids)
+ // See: hq-ihwsj - bd list uses inefficient WHERE IN (all_ids) query pattern
querySQL := fmt.Sprintf(`
- SELECT id FROM issues
+ SELECT id, content_hash, title, description, design, acceptance_criteria, notes,
+ status, priority, issue_type, assignee, estimated_minutes,
+ created_at, created_by, owner, updated_at, closed_at, external_ref,
+ compaction_level, compacted_at, compacted_at_commit, original_size, source_repo, close_reason,
+ deleted_at, deleted_by, delete_reason, original_type,
+ sender, ephemeral, pinned, is_template, crystallizes,
+ await_type, await_id, timeout_ns, waiters,
+ hook_bead, role_bead, agent_state, last_activity, role_type, rig, mol_type,
+ event_kind, actor, target, payload,
+ due_at, defer_until,
+ quality_score, work_type, source_system
+ FROM issues
%s
ORDER BY priority ASC, created_at DESC
%s
@@ -225,7 +238,15 @@ func (s *DoltStore) SearchIssues(ctx context.Context, query string, filter types
}
defer rows.Close()
- return s.scanIssueIDs(ctx, rows)
+ var issues []*types.Issue
+ for rows.Next() {
+ issue, err := scanIssueRow(rows)
+ if err != nil {
+ return nil, err
+ }
+ issues = append(issues, issue)
+ }
+ return issues, rows.Err()
}
// GetReadyWork returns issues that are ready to work on (not blocked)

View File

@@ -5,21 +5,62 @@ with lib;
let
cfg = config.home.roles.development;
# FIXME: Temporary override for upstream beads vendorHash mismatch
# Remove after upstream fix: https://github.com/steveyegge/beads/issues/XXX
beadsPackage = globalInputs.beads.packages.${system}.default.overrideAttrs (old: {
vendorHash = "sha256-YU+bRLVlWtHzJ1QPzcKJ70f+ynp8lMoIeFlm+29BNPE=";
});
# Build beads from flake input with corrected vendorHash
# The upstream default.nix has stale vendorHash for commits with server mode
beadsRev = builtins.substring 0 8 (globalInputs.beads.rev or "unknown");
beadsPackage = pkgs.buildGoModule {
pname = "beads";
version = "0.49.1-${beadsRev}";
src = globalInputs.beads;
subPackages = [ "cmd/bd" ];
doCheck = false;
# Regenerated vendorHash for commit 93965b4a (has dolt server mode, Go 1.24)
vendorHash = "sha256-gwxGv8y4+1+k0741CnOYcyJPTJ5vTrynqPoO8YS9fbQ=";
nativeBuildInputs = [ pkgs.git ];
meta = with lib; {
description = "beads (bd) - An issue tracker designed for AI-supervised coding workflows";
homepage = "https://github.com/steveyegge/beads";
license = licenses.mit;
mainProgram = "bd";
};
};
# Gastown - multi-agent workspace manager (no upstream flake.nix yet)
# Source is tracked via flake input for renovate updates
gastownRev = builtins.substring 0 8 (globalInputs.gastown.rev or "unknown");
gastownPackage = pkgs.buildGoModule {
pname = "gastown";
version = "unstable-${builtins.substring 0 8 globalInputs.gastown.rev or "unknown"}";
version = "unstable-${gastownRev}";
src = globalInputs.gastown;
vendorHash = "sha256-ripY9vrYgVW8bngAyMLh0LkU/Xx1UUaLgmAA7/EmWQU=";
subPackages = [ "cmd/gt" ];
doCheck = false;
# Must match ldflags from gastown Makefile - BuiltProperly=1 is required
# or gt will error with "This binary was built with 'go build' directly"
ldflags = [
"-X github.com/steveyegge/gastown/internal/cmd.Version=${gastownRev}"
"-X github.com/steveyegge/gastown/internal/cmd.Commit=${gastownRev}"
"-X github.com/steveyegge/gastown/internal/cmd.BuildTime=nix-build"
"-X github.com/steveyegge/gastown/internal/cmd.BuiltProperly=1"
];
# Bug fixes not yet merged upstream
# Each patch is stored in a separate file for clarity and maintainability
patches = [
# Fix validateRecipient bug: normalize addresses before comparison
./gastown-fix-validate-recipient.patch
# Fix agentBeadToAddress to use title field for hq- prefixed beads
./gastown-fix-agent-bead-address-title.patch
# Fix crew/polecat home paths: remove incorrect /rig suffix
./gastown-fix-role-home-paths.patch
# Fix town root detection: don't map to Mayor (causes spurious mismatch warnings)
./gastown-fix-town-root-detection.patch
# Statusline optimization: skip expensive beads queries for detached sessions
# Reduces Dolt CPU from ~70% to ~20% by caching and early-exit
./gastown-statusline-optimization.patch
];
meta = with lib; {
description = "Gas Town - multi-agent workspace manager by Steve Yegge";
homepage = "https://github.com/steveyegge/gastown";
@@ -28,6 +69,28 @@ let
};
};
# Perles - TUI for beads issue tracking (no upstream flake.nix yet)
# Source is tracked via flake input for renovate updates
perlesRev = builtins.substring 0 8 (globalInputs.perles.rev or "unknown");
perlesPackage = pkgs.buildGoModule {
pname = "perles";
version = "unstable-${perlesRev}";
src = globalInputs.perles;
vendorHash = "sha256-JHERJDzbiqgjWXwRhXVjgDEiDQ3AUXRIONotfPF21B0=";
doCheck = false;
ldflags = [
"-X main.version=${perlesRev}"
];
meta = with lib; {
description = "Perles - Terminal UI for beads issue tracking";
homepage = "https://github.com/zjrosen/perles";
license = licenses.mit;
mainProgram = "perles";
};
};
# Fetch the claude-plugins repository (for humanlayer commands/agents)
# Update the rev to get newer versions of the commands
claudePluginsRepo = builtins.fetchGit {
@@ -62,9 +125,11 @@ in
home.packages = [
beadsPackage
gastownPackage
perlesPackage
pkgs.unstable.claude-code
pkgs.unstable.claude-code-router
pkgs.unstable.codex
pkgs.dolt
pkgs.sqlite
# Custom packages

View File

@@ -0,0 +1,15 @@
diff --git a/internal/mail/router.go b/internal/mail/router.go
--- a/internal/mail/router.go
+++ b/internal/mail/router.go
@@ -315,7 +315,10 @@ func agentBeadToAddress(bead *agentBead) string {
}
// For other hq- agents, fall back to description parsing
- return parseAgentAddressFromDescription(bead.Description)
+ if bead.Title != "" && strings.Contains(bead.Title, "/") {
+ return bead.Title
+ }
+ return parseAgentAddressFromDescription(bead.Description)
}
// Handle gt- prefixed IDs (legacy format)

View File

@@ -0,0 +1,35 @@
diff --git a/internal/mail/router.go b/internal/mail/router.go
--- a/internal/mail/router.go
+++ b/internal/mail/router.go
@@ -330,8 +330,29 @@ func agentBeadToAddress(bead *agentBead) string {
}
// Handle gt- prefixed IDs (legacy format)
- if !strings.HasPrefix(id, "gt-") {
- return "" // Not a valid agent bead ID
+ // Handle rig-specific prefixes: <prefix>-<rig>-<role>-<name>
+ // Examples: j-java-crew-americano -> java/crew/americano
+ idParts := strings.Split(id, "-")
+ if len(idParts) >= 3 {
+ for i, part := range idParts {
+ if part == "crew" || part == "polecat" || part == "polecats" {
+ if i >= 1 && i < len(idParts)-1 {
+ rig := idParts[i-1]
+ name := strings.Join(idParts[i+1:], "-")
+ return rig + "/" + part + "/" + name
+ }
+ }
+ if part == "witness" || part == "refinery" {
+ if i >= 1 {
+ return idParts[i-1] + "/" + part
+ }
+ }
+ }
+ }
+
+ // Handle gt- prefixed IDs (legacy format)
+ if !strings.HasPrefix(id, "gt-") {
+ return "" // Not a valid agent bead ID
}
// Strip prefix

View File

@@ -0,0 +1,25 @@
diff --git a/internal/git/git.go b/internal/git/git.go
--- a/internal/git/git.go
+++ b/internal/git/git.go
@@ -73,7 +73,19 @@ func copyDir(src, dest string) error {
srcPath := filepath.Join(src, entry.Name())
destPath := filepath.Join(dest, entry.Name())
- if entry.IsDir() {
+ // Handle symlinks (recreate them, do not follow)
+ if entry.Type()&os.ModeSymlink != 0 {
+ linkTarget, err := os.Readlink(srcPath)
+ if err != nil {
+ return err
+ }
+ if err := os.Symlink(linkTarget, destPath); err != nil {
+ return err
+ }
+ continue
+ }
+
+ if entry.IsDir() {
if err := copyDir(srcPath, destPath); err != nil {
return err
}

View File

@@ -0,0 +1,18 @@
diff --git a/internal/cmd/role.go b/internal/cmd/role.go
--- a/internal/cmd/role.go
+++ b/internal/cmd/role.go
@@ -326,11 +326,11 @@ func getRoleHome(role Role, rig, polecat, townRoot string) string {
if rig == "" || polecat == "" {
return ""
}
- return filepath.Join(townRoot, rig, "polecats", polecat, "rig")
+ return filepath.Join(townRoot, rig, "polecats", polecat)
case RoleCrew:
if rig == "" || polecat == "" {
return ""
}
- return filepath.Join(townRoot, rig, "crew", polecat, "rig")
+ return filepath.Join(townRoot, rig, "crew", polecat)
default:
return ""
}

View File

@@ -0,0 +1,18 @@
diff --git a/internal/cmd/prime.go b/internal/cmd/prime.go
--- a/internal/cmd/prime.go
+++ b/internal/cmd/prime.go
@@ -276,11 +276,12 @@ func detectRole(cwd, townRoot string) RoleInfo {
// Check for mayor role
// At town root, or in mayor/ or mayor/rig/
if relPath == "." || relPath == "" {
- ctx.Role = RoleMayor
- return ctx
+ return ctx // RoleUnknown - town root is shared space
}
+
+ // Check for mayor role: mayor/ or mayor/rig/
if len(parts) >= 1 && parts[0] == "mayor" {
ctx.Role = RoleMayor
return ctx
}

View File

@@ -0,0 +1,13 @@
diff --git a/internal/mail/router.go b/internal/mail/router.go
index b864c069..4b6a045b 100644
--- a/internal/mail/router.go
+++ b/internal/mail/router.go
@@ -646,7 +646,7 @@ func (r *Router) validateRecipient(identity string) error {
}
for _, agent := range agents {
- if agentBeadToAddress(agent) == identity {
+ if AddressToIdentity(agentBeadToAddress(agent)) == AddressToIdentity(identity) {
return nil // Found matching agent
}
}

View File

@@ -0,0 +1,135 @@
diff --git a/internal/cmd/statusline.go b/internal/cmd/statusline.go
index 2edf1be8..00253eea 100644
--- a/internal/cmd/statusline.go
+++ b/internal/cmd/statusline.go
@@ -6,6 +6,7 @@ import (
"path/filepath"
"sort"
"strings"
+ "time"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/beads"
@@ -14,6 +15,37 @@ import (
"github.com/steveyegge/gastown/internal/tmux"
"github.com/steveyegge/gastown/internal/workspace"
)
+// statusLineCacheTTL is how long cached status output remains valid.
+const statusLineCacheTTL = 10 * time.Second
+
+// statusLineCachePath returns the cache file path for a session.
+func statusLineCachePath(session string) string {
+ return filepath.Join(os.TempDir(), fmt.Sprintf("gt-status-%s", session))
+}
+
+// getStatusLineCache returns cached status if fresh, empty string otherwise.
+func getStatusLineCache(session string) string {
+ path := statusLineCachePath(session)
+ info, err := os.Stat(path)
+ if err != nil {
+ return ""
+ }
+ if time.Since(info.ModTime()) > statusLineCacheTTL {
+ return ""
+ }
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return ""
+ }
+ return string(data)
+}
+
+// setStatusLineCache writes status to cache file.
+func setStatusLineCache(session, status string) {
+ path := statusLineCachePath(session)
+ _ = os.WriteFile(path, []byte(status), 0644)
+}
+
var (
statusLineSession string
@@ -34,6 +66,19 @@ func init() {
func runStatusLine(cmd *cobra.Command, args []string) error {
t := tmux.NewTmux()
+ // Optimization: skip expensive beads queries for detached sessions
+ if statusLineSession != "" {
+ if !t.IsSessionAttached(statusLineSession) {
+ fmt.Print("○ |")
+ return nil
+ }
+ // Check cache for attached sessions too
+ if cached := getStatusLineCache(statusLineSession); cached != "" {
+ fmt.Print(cached)
+ return nil
+ }
+ }
+
// Get session environment
var rigName, polecat, crew, issue, role string
@@ -150,7 +195,11 @@ func runWorkerStatusLine(t *tmux.Tmux, session, rigName, polecat, crew, issue st
// Output
if len(parts) > 0 {
- fmt.Print(strings.Join(parts, " | ") + " |")
+ output := strings.Join(parts, " | ") + " |"
+ if statusLineSession != "" {
+ setStatusLineCache(statusLineSession, output)
+ }
+ fmt.Print(output)
}
return nil
@@ -389,7 +438,11 @@ func runMayorStatusLine(t *tmux.Tmux) error {
}
}
- fmt.Print(strings.Join(parts, " | ") + " |")
+ output := strings.Join(parts, " | ") + " |"
+ if statusLineSession != "" {
+ setStatusLineCache(statusLineSession, output)
+ }
+ fmt.Print(output)
return nil
}
@@ -458,7 +511,11 @@ func runDeaconStatusLine(t *tmux.Tmux) error {
}
}
- fmt.Print(strings.Join(parts, " | ") + " |")
+ output := strings.Join(parts, " | ") + " |"
+ if statusLineSession != "" {
+ setStatusLineCache(statusLineSession, output)
+ }
+ fmt.Print(output)
return nil
}
@@ -526,7 +583,11 @@ func runWitnessStatusLine(t *tmux.Tmux, rigName string) error {
}
}
- fmt.Print(strings.Join(parts, " | ") + " |")
+ output := strings.Join(parts, " | ") + " |"
+ if statusLineSession != "" {
+ setStatusLineCache(statusLineSession, output)
+ }
+ fmt.Print(output)
return nil
}
@@ -617,7 +678,11 @@ func runRefineryStatusLine(t *tmux.Tmux, rigName string) error {
}
}
- fmt.Print(strings.Join(parts, " | ") + " |")
+ output := strings.Join(parts, " | ") + " |"
+ if statusLineSession != "" {
+ setStatusLineCache(statusLineSession, output)
+ }
+ fmt.Print(output)
return nil
}

View File

@@ -210,18 +210,25 @@
:around #'my/org-caldav-add-until-property)
)
(defun my/get-rbw-password (alias)
(defun my/get-rbw-password (alias &optional no-error)
"Return the password for ALIAS via rbw, unlocking the vault only if needed.
Returns nil and signals an error if the entry is not found."
(let* ((cmd (format "rbw get %s 2>/dev/null" (shell-quote-argument alias)))
(output (string-trim (shell-command-to-string cmd))))
(if (string-empty-p output)
(user-error "rbw: no entry found for '%s' - run: rbw add %s" alias alias)
output)))
If NO-ERROR is non-nil, return nil instead of signaling an error when
rbw is unavailable or the entry is not found."
(if (not (executable-find "rbw"))
(if no-error
nil
(user-error "rbw: not installed or not in PATH"))
(let* ((cmd (format "rbw get %s 2>/dev/null" (shell-quote-argument alias)))
(output (string-trim (shell-command-to-string cmd))))
(if (string-empty-p output)
(if no-error
nil
(user-error "rbw: no entry found for '%s' - run: rbw add %s" alias alias))
output))))
(after! gptel
:config
(setq! gptel-api-key (my/get-rbw-password "openai-api-key-chatgpt-el")
(setq! gptel-api-key (my/get-rbw-password "openai-api-key-chatgpt-el" t)
gptel-default-mode 'org-mode
gptel-use-tools t
gptel-confirm-tool-calls 'always

View File

@@ -90,6 +90,8 @@ with lib;
htop
tmux
zfs
rclone
custom.rclone-torbox-setup # Helper script to set up TorBox credentials via rbw
];
# Enable SSH
@@ -126,6 +128,26 @@ with lib;
roles.virtualisation.enable = true;
# TorBox WebDAV mount for rdt-client and Jellyfin
roles.rclone-mount = {
enable = true;
mounts.torbox = {
webdavUrl = "https://webdav.torbox.app";
username = "john@ogle.fyi"; # TorBox account email
mountPoint = "/media/media/torbox-rclone";
environmentFile = "/etc/rclone/torbox.env";
vfsCacheMode = "full"; # Best for streaming media
dirCacheTime = "5m";
extraArgs = [
"--buffer-size=64M"
"--vfs-read-chunk-size=32M"
"--vfs-read-chunk-size-limit=off"
];
# Wait for ZFS media pool to be mounted before starting
requiresMountsFor = [ "/media" ];
};
};
# Time zone
time.timeZone = "America/Los_Angeles"; # Adjust as needed

View File

@@ -6,24 +6,24 @@
}:
let
version = "2.1.12";
version = "2.1.30";
srcs = {
aarch64-darwin = {
url = "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/${version}/darwin-arm64/claude";
sha256 = "40be59519a84bd35eb1111aa46f72aa6b3443866d3f6336252a198fdcaefbbe5";
sha256 = "3ccc14f322b1e8da0cd58afc254fd5100eee066fa14729f30745e67a3f7979f7";
};
x86_64-darwin = {
url = "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/${version}/darwin-x64/claude";
sha256 = "0eee4b46c91749480bf856f88e49b15a3e944faa9d346679c5f0c0d7fa6f2f54";
sha256 = "8a083696006483b8382ec0e47cd8f2e3223f3d2cab1a21c524fa08c082b5600e";
};
x86_64-linux = {
url = "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/${version}/linux-x64/claude";
sha256 = "3fe979215489dc1b31463fadf95ed2d2d5473a9969447bb7a46431f4578847d4";
sha256 = "ada8f1cf9272965d38b10f1adb6cea885e621c83f7e7bb233008c721f43fad54";
};
aarch64-linux = {
url = "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/${version}/linux-arm64/claude";
sha256 = "e214b1d3b5afd4cd2de9177359001d41a3eb98cb1e3665fe97edc592f5aa132f";
sha256 = "45fbf35a1011b06f86170b20beb64c599db0658aac70e2de2410c45d15775596";
};
};

View File

@@ -4,4 +4,5 @@
app-launcher-server = pkgs.callPackage ./app-launcher-server {};
claude-code = pkgs.callPackage ./claude-code {};
mcrcon-rbw = pkgs.callPackage ./mcrcon-rbw {};
rclone-torbox-setup = pkgs.callPackage ./rclone-torbox-setup {};
}

View File

@@ -0,0 +1,98 @@
{ pkgs, ... }:
pkgs.writeShellScriptBin "rclone-torbox-setup" ''
set -euo pipefail
# Default values
RBW_ENTRY="''${1:-torbox}"
ENV_FILE="''${2:-/etc/rclone/torbox.env}"
usage() {
echo "Usage: rclone-torbox-setup [rbw-entry] [env-file]"
echo ""
echo "Sets up rclone credentials for TorBox WebDAV mount."
echo "Retrieves password from rbw (Bitwarden), obscures it for rclone,"
echo "and writes it to the environment file for the systemd service."
echo ""
echo "Arguments:"
echo " rbw-entry Name of the Bitwarden entry containing the password (default: torbox)"
echo " env-file Path to write the environment file (default: /etc/rclone/torbox.env)"
echo ""
echo "The Bitwarden entry should contain your TorBox password as the password field."
echo ""
echo "Example:"
echo " rclone-torbox-setup torbox-password /etc/rclone/torbox.env"
exit 1
}
if [[ "''${1:-}" == "-h" ]] || [[ "''${1:-}" == "--help" ]]; then
usage
fi
echo "rclone TorBox credential setup"
echo "=============================="
echo ""
# Check if rbw is available
if ! command -v rbw &> /dev/null; then
echo "Error: rbw is not available. Please ensure rbw is installed and configured."
exit 1
fi
# Check if rclone is available
if ! command -v rclone &> /dev/null; then
echo "Error: rclone is not available. Please ensure rclone is installed."
exit 1
fi
echo "Retrieving password from rbw entry: $RBW_ENTRY"
# Retrieve password from Bitwarden
if ! TORBOX_PASS=$(rbw get "$RBW_ENTRY" 2>/dev/null); then
echo ""
echo "Error: Failed to retrieve password from rbw entry '$RBW_ENTRY'"
echo ""
echo "Please ensure:"
echo " 1. The entry '$RBW_ENTRY' exists in Bitwarden"
echo " 2. rbw is unlocked: rbw unlock"
echo " 3. rbw is synced: rbw sync"
echo ""
echo "To create the entry in Bitwarden:"
echo " - Name: $RBW_ENTRY"
echo " - Password: Your TorBox password"
exit 1
fi
echo "Password retrieved successfully"
# Obscure the password for rclone
echo "Obscuring password for rclone..."
if ! OBSCURED_PASS=$(echo -n "$TORBOX_PASS" | rclone obscure -); then
echo "Error: Failed to obscure password with rclone"
exit 1
fi
# Create the directory if needed (requires sudo)
ENV_DIR=$(dirname "$ENV_FILE")
if [[ ! -d "$ENV_DIR" ]]; then
echo "Creating directory $ENV_DIR (requires sudo)..."
sudo mkdir -p "$ENV_DIR"
fi
# Write the environment file
echo "Writing environment file to $ENV_FILE (requires sudo)..."
echo "RCLONE_WEBDAV_PASS=$OBSCURED_PASS" | sudo tee "$ENV_FILE" > /dev/null
sudo chmod 600 "$ENV_FILE"
echo ""
echo "Setup complete!"
echo ""
echo "The environment file has been created at: $ENV_FILE"
echo "The rclone-mount-torbox systemd service will use this file."
echo ""
echo "To activate the mount after NixOS rebuild:"
echo " sudo systemctl start rclone-mount-torbox"
echo ""
echo "To check status:"
echo " sudo systemctl status rclone-mount-torbox"
''

View File

@@ -13,7 +13,7 @@
"lockFileMaintenance": {
"enabled": true,
"schedule": [
"before 5am on monday"
"after 2pm and before 4pm on Saturday"
]
},
"dependencyDashboard": true,
@@ -37,6 +37,9 @@
"/^nixpkgs$/",
"/^home-manager$/",
"/^nix-darwin$/"
],
"schedule": [
"after 2pm and before 4pm on Saturday"
]
},
{
@@ -48,6 +51,21 @@
"matchPackageNames": [
"/nixpkgs-unstable/",
"/home-manager-unstable/"
],
"schedule": [
"after 2pm and before 4pm on Saturday"
]
},
{
"description": "nixpkgs-qt updates on Saturday (staggered from main ecosystem)",
"matchManagers": [
"nix"
],
"matchPackageNames": [
"/nixpkgs-qt/"
],
"schedule": [
"after 4pm and before 6pm on Saturday"
]
},
{

View File

@@ -14,6 +14,7 @@ with lib;
./nfs-mounts
./nvidia
./printing
./rclone-mount
./remote-build
./spotifyd
./users

View File

@@ -47,23 +47,23 @@ in
if cfg.jellyfinScaleFactor != null
then pkgs.symlinkJoin {
name = "jellyfin-media-player-scaled";
paths = [ pkgs.jellyfin-media-player ];
paths = [ pkgs.qt-pinned.jellyfin-media-player ];
nativeBuildInputs = [ pkgs.makeWrapper ];
postBuild = ''
mkdir -p $out/bin
rm -f $out/bin/jellyfin-desktop
makeWrapper ${pkgs.jellyfin-media-player}/bin/jellyfin-desktop $out/bin/jellyfin-desktop \
makeWrapper ${pkgs.qt-pinned.jellyfin-media-player}/bin/jellyfin-desktop $out/bin/jellyfin-desktop \
--add-flags "--tv --scale-factor ${toString cfg.jellyfinScaleFactor}"
# Update .desktop file to include scale factor and TV mode arguments
mkdir -p $out/share/applications
rm -f $out/share/applications/org.jellyfin.JellyfinDesktop.desktop
substitute ${pkgs.jellyfin-media-player}/share/applications/org.jellyfin.JellyfinDesktop.desktop \
substitute ${pkgs.qt-pinned.jellyfin-media-player}/share/applications/org.jellyfin.JellyfinDesktop.desktop \
$out/share/applications/org.jellyfin.JellyfinDesktop.desktop \
--replace-fail "Exec=jellyfin-desktop" "Exec=jellyfin-desktop --tv --scale-factor ${toString cfg.jellyfinScaleFactor}"
'';
}
else pkgs.jellyfin-media-player;
else pkgs.qt-pinned.jellyfin-media-player;
in mkIf cfg.enable
{
users.extraUsers.kodi = {

View File

@@ -0,0 +1,149 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.roles.rclone-mount;
# Generate systemd service for a single mount
mkMountService = name: mountCfg: {
description = "rclone mount for ${name}";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
# Wait for parent mount points (e.g., ZFS pools) to be available
unitConfig = mkIf (mountCfg.requiresMountsFor != []) {
RequiresMountsFor = mountCfg.requiresMountsFor;
};
serviceConfig = {
Type = "notify";
ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p ${mountCfg.mountPoint}";
ExecStart = concatStringsSep " " ([
"${pkgs.rclone}/bin/rclone mount"
":webdav:${mountCfg.remotePath}"
"${mountCfg.mountPoint}"
"--webdav-url=${mountCfg.webdavUrl}"
"--webdav-vendor=${mountCfg.webdavVendor}"
"--webdav-user=${mountCfg.username}"
"--allow-other"
"--vfs-cache-mode=${mountCfg.vfsCacheMode}"
"--dir-cache-time=${mountCfg.dirCacheTime}"
"--poll-interval=${mountCfg.pollInterval}"
"--log-level=${mountCfg.logLevel}"
] ++ mountCfg.extraArgs);
ExecStop = "${pkgs.fuse}/bin/fusermount -uz ${mountCfg.mountPoint}";
Restart = "on-failure";
RestartSec = "10s";
EnvironmentFile = mountCfg.environmentFile;
};
};
in
{
options.roles.rclone-mount = {
enable = mkEnableOption "Enable rclone WebDAV mounts";
mounts = mkOption {
type = types.attrsOf (types.submodule {
options = {
webdavUrl = mkOption {
type = types.str;
description = "WebDAV server URL (e.g., https://webdav.torbox.app)";
};
webdavVendor = mkOption {
type = types.enum [ "other" "nextcloud" "owncloud" "sharepoint" "sharepoint-ntlm" "fastmail" ];
default = "other";
description = "WebDAV server vendor for optimizations";
};
username = mkOption {
type = types.str;
description = "WebDAV username (often email address)";
};
environmentFile = mkOption {
type = types.path;
description = ''
Path to environment file containing RCLONE_WEBDAV_PASS.
The password should be obscured using: rclone obscure <password>
File format: RCLONE_WEBDAV_PASS=<obscured_password>
'';
};
mountPoint = mkOption {
type = types.str;
description = "Local mount point path";
};
remotePath = mkOption {
type = types.str;
default = "/";
description = "Remote path on WebDAV server to mount";
};
vfsCacheMode = mkOption {
type = types.enum [ "off" "minimal" "writes" "full" ];
default = "full";
description = ''
VFS cache mode. For streaming media, 'full' is recommended.
- off: No caching (direct reads/writes)
- minimal: Cache open files only
- writes: Cache writes and open files
- full: Full caching of all files
'';
};
dirCacheTime = mkOption {
type = types.str;
default = "5m";
description = "Time to cache directory entries";
};
pollInterval = mkOption {
type = types.str;
default = "1m";
description = "Poll interval for remote changes";
};
logLevel = mkOption {
type = types.enum [ "DEBUG" "INFO" "NOTICE" "ERROR" ];
default = "INFO";
description = "rclone log level";
};
extraArgs = mkOption {
type = types.listOf types.str;
default = [];
description = "Extra arguments to pass to rclone mount";
};
requiresMountsFor = mkOption {
type = types.listOf types.str;
default = [];
description = ''
List of mount points that must be available before this service starts.
Use this when the mount point's parent is on a ZFS pool or other filesystem
that may not be mounted at boot time.
Example: [ "/media" ] to wait for the media ZFS pool to mount.
'';
};
};
});
default = {};
description = "Attribute set of rclone WebDAV mounts to configure";
};
};
config = mkIf cfg.enable {
# Ensure FUSE is available
environment.systemPackages = [ pkgs.rclone pkgs.fuse ];
programs.fuse.userAllowOther = true;
# Create systemd services for each mount
systemd.services = mapAttrs' (name: mountCfg:
nameValuePair "rclone-mount-${name}" (mkMountService name mountCfg)
) cfg.mounts;
};
}