Compare commits

..

100 Commits

Author SHA1 Message Date
14d267e12d Fix brain.hcl: add port mapping to 3000 2026-01-25 17:30:58 +00:00
2e8e11ecec Deploy brain.hcl - SilverBullet web UI 2026-01-25 17:29:03 +00:00
f90fa5c23b Install Amp. 2026-01-25 13:38:18 +00:00
caa6d0aafd Update flake. 2026-01-25 13:32:32 +00:00
29043896c8 Add ham radio profile with FLEcli package
Introduces a custom packages overlay (pkgs/) for packages not in nixpkgs.
Adds FLEcli v0.1.7 for processing amateur radio logs (SOTA, POTA, WWFF).
Enables ham-radio profile on beefy.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 10:31:53 +00:00
1af9053cd5 Initial config. 2026-01-25 10:05:46 +00:00
2dcd03cbb0 Add brain fish function for externalized executive function system
Launches Claude in ~/brain directory with welcome message showing
available commands (wrap, inbox).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 19:40:18 +00:00
b5f0cdb429 Docker on beefy. 2026-01-22 17:17:00 +00:00
b63abca296 Add MAILGUN_URL for EU region
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 22:29:08 +00:00
1311aadffb Remove phaseflow-cron batch job
No longer needed - cron scheduling now handled by instrumentation.ts
inside the main phaseflow app.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 22:16:20 +00:00
f903ddeee5 Update phaseflow secrets for Mailgun email provider
Switch from resend_api_key to mailgun_api_key and mailgun_domain.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 19:37:45 +00:00
33f3ddd7e9 Update flake. 2026-01-20 18:17:22 +00:00
1cdedf824c Meta and more RAM. Still not working. 2026-01-20 18:17:11 +00:00
beb856714e Install beads. 2026-01-18 07:34:15 +00:00
fcb2067059 Sync later in the mornings. 2026-01-17 16:28:20 +00:00
cebd236b1f Update flake. 2026-01-16 12:46:13 +00:00
8cc818f6b2 Rename deprecated options. 2026-01-16 10:54:09 +00:00
305a7a5115 Remove unknown option. 2026-01-16 10:41:29 +00:00
526888cd26 Improve phaseflow-cron logging on failure
Show the API response body in logs instead of silently failing
with curl exit code 22.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 07:40:28 +00:00
8d97d09b07 Add phaseflow-cron job and PocketBase admin credentials
- New periodic job for daily Garmin sync at 6 AM
- Added pocketbase_admin_email and pocketbase_admin_password to
  secrets template for cron job authentication

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 07:13:36 +00:00
3f481e0a16 Set the right vars. 2026-01-11 21:22:11 +00:00
15dea7a249 Make PocketBase admin UI accessible. 2026-01-11 17:22:42 +00:00
e1bace9044 Fix phaseflow PocketBase URL to use Nomad address
Docker containers in Nomad don't share network namespace by default.
Use NOMAD_ADDR_pocketbase interpolation instead of localhost.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 17:06:40 +00:00
09f2d2b013 More RAM. 2026-01-11 13:12:09 +00:00
d195efdb0e Add phaseflow service
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 10:20:40 +00:00
3277c810a5 Update flake. 2026-01-10 20:04:51 +00:00
f2baf3daf6 Move to new domain. 2026-01-09 06:17:20 +00:00
931470ee0a Remove farmOS service
Egg harvest data (849 logs, 6704 eggs, Nov 2023 - Jan 2026) exported
to CSV before shutdown. Database and user dropped from PostgreSQL.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 13:31:57 +00:00
41b30788fe Update flake. 2026-01-08 13:03:48 +00:00
01ebff3596 Migrate to alo organization
Update all registry paths from ppetru/* to alo/* and workflow
references from ppetru/alo-cluster to alo/alo-cluster.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 10:49:38 +00:00
ed2c899915 Add reusable CI/CD workflow and documentation
- .gitea/workflows/deploy-nomad.yaml: Shared workflow for build/push/deploy
- docs/CICD_SETUP.md: Guide for adding CI/CD to new services
- nix-runner/README.md: Document the custom Nix runner image

Services can now use a 10-line workflow that calls the shared one:
  uses: ppetru/alo-cluster/.gitea/workflows/deploy-nomad.yaml@master

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 07:47:01 +00:00
c548ead4f7 Add CI/CD infrastructure for animaltrack
New services:
- animaltrack.hcl: Python app with health checks and auto_revert
- act-runner.hcl: Gitea Actions runner on Nomad

New infrastructure:
- nix-runner/: Custom Nix Docker image for CI with modern Nix,
  local cache (c3), and bundled tools (skopeo, jq, etc.)

Modified:
- gitea.hcl: Enable Gitea Actions

The CI workflow (in animaltrack repo) builds Docker images with Nix,
pushes to Gitea registry, and triggers Nomad deployments with
automatic rollback on health check failure.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 07:17:31 +00:00
3b8cd7b742 AI ideas. 2026-01-03 10:38:47 +00:00
d71408b567 Incorporate omarchy-nix. 2026-01-01 16:44:09 +00:00
a8147d9ae5 Fix deprecated pkgs.system usage
Replace pkgs.system with pkgs.stdenv.hostPlatform.system to fix
NixOS evaluation warning about the deprecated attribute.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 15:59:51 +00:00
2b1950d4e3 Install tools for Claude. 2026-01-01 13:34:17 +00:00
322927e2b0 Update env vars. 2026-01-01 13:34:08 +00:00
4cae9fe706 Update flake. 2025-12-26 14:26:40 +00:00
b5b164b543 Switch to docker. 2025-12-24 15:08:10 +00:00
08db384f60 Set up MCP for alo wiki. 2025-12-22 13:03:27 +00:00
3b2cd0c3cf Allow encoded slashes to make Tiddlywikis work again. 2025-12-22 12:37:05 +00:00
13a4467166 Use hostname for alo-cloud-1 so deploys go over tailscale. 2025-12-21 13:36:33 +00:00
4c0b0fb780 Update flake. 2025-12-21 13:33:21 +00:00
a09d1b49c2 Upgrade OIDC plugin to 0.17 2025-12-19 22:37:44 +00:00
8d381ef9f4 Update flake. 2025-12-14 18:20:46 +00:00
79d51c3f58 Upgrade to 25.11. 2025-12-14 18:17:40 +00:00
83fb796a9f Fix netconsole: disable before reconfiguring.
Configfs params can't be modified while the target is enabled.
Disable first if already enabled, then reconfigure and re-enable.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 13:22:27 +00:00
4efc44e964 Fix netconsole: configure via configfs after network up.
The modprobe.conf approach failed because the network interface
doesn't exist when the module loads at boot. Now using a systemd
service to configure netconsole via configfs after network-online.

Also raise console_loglevel to 8 so all kernel messages (not just
KERN_WARNING and above) are sent to netconsole.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 12:08:12 +00:00
3970c60016 More RAM. 2025-12-12 10:26:54 +00:00
a8b63e71c8 Remove unavailable crash analysis packages.
The crash and makedumpfile packages don't exist in nixpkgs.
Kdump will still capture crash dumps to /var/crash.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 07:29:15 +00:00
58c851004d Add stability debugging for beefy lockups.
- Add netconsole receiver on zippy to capture kernel messages
- Configure beefy as netconsole sender to zippy (192.168.1.2)
- Enable kdump with 256M reserved memory for crash analysis
- Add lockup detectors (softlockup_panic, hung_task_panic, nmi_watchdog)
- Add consoleblank=300 for greeter display sleep
- Persist crash dumps and add analysis tools (crash, makedumpfile)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 07:23:33 +00:00
bd889902be Update flake. 2025-12-09 14:48:56 +00:00
7fd79c9911 Enable sysrq for debugging. 2025-12-06 12:25:17 +00:00
41eacfec02 Typo fix. 2025-12-02 20:39:25 +00:00
0a0748b920 Disable byte range locking for smbfs. 2025-12-02 20:38:48 +00:00
d6e0e09e87 Update flake. 2025-11-28 13:00:17 +00:00
61c3020a5e Update flake. 2025-11-25 18:53:43 +00:00
972b973f58 Update flake. 2025-11-25 14:05:10 +00:00
8c5a7b78c6 Update flake. 2025-11-24 13:33:04 +00:00
675204816a Even more RAM for Plex. 2025-11-23 20:10:58 +00:00
3bb82dbc6b Initial config. 2025-11-23 08:55:38 +00:00
0f6233c3ec More RAM. 2025-11-23 07:54:04 +00:00
43fa56bf35 Bind on all addresses and rely on firewall for blocking public ssh.
Otherwise, sshd will try and fail to bind on the tailscale IP before
tailscale is up.
2025-11-23 07:24:09 +00:00
50c930eeaf Add flaresolverr, disable bazarr, tweak resources. 2025-11-22 19:27:37 +00:00
8dde15b8ef Add prowlarr, recyclarr, and jellyseerr. 2025-11-22 17:32:14 +00:00
6100d8dc69 Fix override. 2025-11-21 16:43:39 +00:00
a92f0fcb28 Tighten up security. 2025-11-21 16:39:45 +00:00
bd4604cdcc Auth docs. 2025-11-21 14:12:19 +00:00
31db372b43 Remove now unused authentik config. 2025-11-21 14:00:47 +00:00
360e776745 Set up ollama. 2025-11-17 22:33:44 +00:00
5a819f70bb Static port for claude code accessibility. 2025-11-17 19:05:17 +00:00
b2c055ffb2 MCP server for tiddlywiki. 2025-11-17 17:56:05 +00:00
6e0b34843b Allow claude-code to read&write. 2025-11-16 21:07:58 +00:00
e8485e3bb7 Update flake. 2025-11-12 15:10:11 +00:00
e8cd970960 Make it an exit node. 2025-11-05 16:50:05 +00:00
78b59cec4f Put PHP port back on 9000, where the rest of the stuff expects it. 2025-11-05 15:54:46 +00:00
e6d40a9f7e Set an actual password. 2025-11-04 20:26:50 +00:00
7733a1be46 yet another replication fix. 2025-11-04 19:57:52 +00:00
a5df98bc5a Update docs. 2025-11-04 19:08:27 +00:00
fb9b0dd2f5 Move NFS server to sparky. 2025-11-04 19:00:18 +00:00
0dc214069c Fix curl-induced failures. 2025-11-04 18:59:50 +00:00
a6c4be9530 Use clone source for btrfs send. 2025-11-04 17:51:34 +00:00
6e338e6d65 Stop replicating to c1. 2025-11-04 14:03:49 +00:00
41f16fa0b8 Make sparky a standby again. 2025-11-04 12:58:34 +00:00
1b05728817 Switch to Pocket ID. 2025-11-04 12:58:15 +00:00
520a417316 Pocket ID config. 2025-11-04 11:04:33 +00:00
88ed5360ca Keys for sparky reinstall. 2025-11-04 11:04:20 +00:00
392d40def3 Update flake. 2025-11-04 10:26:18 +00:00
5ef4d832fb Only keep 10 snapshots, and push metrics. 2025-11-04 10:22:11 +00:00
49afc0c084 Remove standby from sparky. 2025-11-04 09:39:45 +00:00
b2c82ceaa8 Don't replicate to sparky for now. 2025-11-04 09:39:23 +00:00
b9286d7243 More CPU. 2025-11-02 06:50:38 +00:00
22931e6747 Add some items. 2025-11-01 17:55:40 +00:00
ac030018c6 Install prusa slicer. 2025-10-31 17:54:27 +00:00
7386d3a5ee Don't try to run consul on the cloud. 2025-10-31 15:55:37 +00:00
2a5a9f2ee9 Actually make sparky a NFS replica. 2025-10-31 15:54:32 +00:00
963a7c10fa Fix include. 2025-10-31 15:45:32 +00:00
283cf9d614 Make sparky a NFS backup instead of desktop. 2025-10-31 15:41:12 +00:00
5b3b4ea2ed Make sure to keep some snapshots around even if they stop coming. 2025-10-31 15:40:19 +00:00
5a9d5de5c4 (try to) show better diffs 2025-10-31 15:40:08 +00:00
92 changed files with 3691 additions and 1176 deletions

View File

@@ -0,0 +1,96 @@
# ABOUTME: Reusable workflow for building Nix Docker images and deploying to Nomad.
# ABOUTME: Called by service repos with: uses: alo/alo-cluster/.gitea/workflows/deploy-nomad.yaml@master
name: Deploy to Nomad
on:
workflow_call:
inputs:
service_name:
required: true
type: string
description: "Nomad job name (must match job ID in services/*.hcl)"
flake_output:
required: false
type: string
default: "dockerImage"
description: "Flake output to build (default: dockerImage)"
registry:
required: false
type: string
default: "gitea.v.paler.net"
description: "Container registry hostname"
secrets:
REGISTRY_USERNAME:
required: true
REGISTRY_PASSWORD:
required: true
NOMAD_ADDR:
required: true
jobs:
build-and-deploy:
runs-on: nix
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: |
echo "Building .#${{ inputs.flake_output }}..."
nix build ".#${{ inputs.flake_output }}" --out-link result
- name: Push to registry
run: |
echo "Pushing to ${{ inputs.registry }}/alo/${{ inputs.service_name }}:latest..."
skopeo copy \
--dest-creds "${{ secrets.REGISTRY_USERNAME }}:${{ secrets.REGISTRY_PASSWORD }}" \
--insecure-policy \
docker-archive:result \
"docker://${{ inputs.registry }}/alo/${{ inputs.service_name }}:latest"
- name: Deploy to Nomad
env:
NOMAD_ADDR: ${{ secrets.NOMAD_ADDR }}
SERVICE: ${{ inputs.service_name }}
run: |
echo "Deploying $SERVICE to Nomad..."
# Fetch current job, update UUID to force deployment
JOB=$(curl -sS "$NOMAD_ADDR/v1/job/$SERVICE")
NEW_UUID=$(cat /proc/sys/kernel/random/uuid)
echo "New deployment UUID: $NEW_UUID"
UPDATED_JOB=$(echo "$JOB" | jq --arg uuid "$NEW_UUID" '.Meta.uuid = $uuid')
# Submit updated job
RESULT=$(echo "{\"Job\": $UPDATED_JOB}" | curl -sS -X POST "$NOMAD_ADDR/v1/jobs" \
-H "Content-Type: application/json" -d @-)
echo "Submit result: $RESULT"
# Monitor deployment
sleep 3
DEPLOY_ID=$(curl -sS "$NOMAD_ADDR/v1/job/$SERVICE/deployments" | jq -r '.[0].ID')
echo "Deployment ID: $DEPLOY_ID"
if [ "$DEPLOY_ID" = "null" ]; then
echo "ERROR: No deployment created. Ensure job has 'update' stanza with 'auto_revert = true'"
exit 1
fi
echo "Monitoring deployment..."
for i in $(seq 1 30); do
STATUS=$(curl -sS "$NOMAD_ADDR/v1/deployment/$DEPLOY_ID" | jq -r '.Status')
echo "[$i/30] Deployment status: $STATUS"
case $STATUS in
successful)
echo "Deployment successful!"
exit 0
;;
failed|cancelled)
echo "Deployment failed or cancelled"
exit 1
;;
esac
sleep 10
done
echo "Timeout waiting for deployment"
exit 1

View File

@@ -2,7 +2,7 @@ keys:
- &admin_ppetru age1df9ukkmg9yn9cjeheq9m6wspa420su8qarmq570rdvf2de3rl38saqauwn - &admin_ppetru age1df9ukkmg9yn9cjeheq9m6wspa420su8qarmq570rdvf2de3rl38saqauwn
- &server_zippy age1gtyw202hd07hddac9886as2cs8pm07e4exlnrgfm72lync75ng9qc5fjac - &server_zippy age1gtyw202hd07hddac9886as2cs8pm07e4exlnrgfm72lync75ng9qc5fjac
- &server_chilly age16yqffw4yl5jqvsr7tyd883vn98zw0attuv9g5snc329juff6dy3qw2w5wp - &server_chilly age16yqffw4yl5jqvsr7tyd883vn98zw0attuv9g5snc329juff6dy3qw2w5wp
- &server_sparky age10zxwwufrf5uu9cv9p9znse2ftfm74q9ce893us6cnvxjc7e3ypcqy709dy - &server_sparky age14aml5s3sxksa8qthnt6apl3pu6egxyn0cz7pdzzvp2yl6wncad0q56udyj
- &server_stinky age1me78u46409q9ez6fj0qanrfffc5e9kuq7n7uuvlljfwwc2mdaezqmyzxhx - &server_stinky age1me78u46409q9ez6fj0qanrfffc5e9kuq7n7uuvlljfwwc2mdaezqmyzxhx
- &server_beefy age1cs8uqj243lspyp042ueu5aes4t3azgyuaxl9au70ggrl2meulq4sgqpc7y - &server_beefy age1cs8uqj243lspyp042ueu5aes4t3azgyuaxl9au70ggrl2meulq4sgqpc7y
- &server_alo_cloud_1 age1w5w4wfvtul3sge9mt205zvrkjaeh3qs9gsxhmq7df2g4dztnvv6qylup8z - &server_alo_cloud_1 age1w5w4wfvtul3sge9mt205zvrkjaeh3qs9gsxhmq7df2g4dztnvv6qylup8z

View File

@@ -8,42 +8,35 @@ NixOS cluster configuration using flakes. Homelab infrastructure with Nomad/Cons
├── common/ ├── common/
│ ├── global/ # Applied to all hosts (backup, sops, users, etc.) │ ├── global/ # Applied to all hosts (backup, sops, users, etc.)
│ ├── minimal-node.nix # Base (ssh, user, boot, impermanence) │ ├── minimal-node.nix # Base (ssh, user, boot, impermanence)
│ ├── cluster-member.nix # Consul + storage clients (NFS/CIFS/GlusterFS) │ ├── cluster-member.nix # Consul agent + storage mounts (NFS/CIFS)
│ ├── nomad-worker.nix # Nomad client (runs jobs) + Docker + NFS deps │ ├── nomad-worker.nix # Nomad client (runs jobs) + Docker + NFS deps
│ ├── nomad-server.nix # Enables Consul + Nomad server mode │ ├── nomad-server.nix # Enables Consul + Nomad server mode
│ ├── cluster-tools.nix # Just CLI tools (nomad, wander, damon) │ ├── cluster-tools.nix # Just CLI tools (nomad, wander, damon)
│ ├── workstation-node.nix # Dev tools (wget, deploy-rs, docker, nix-ld) │ ├── workstation-node.nix # Dev tools (wget, deploy-rs, docker, nix-ld)
│ ├── desktop-node.nix # Hyprland + GUI environment │ ├── desktop-node.nix # Hyprland + GUI environment
│ ├── nfs-services-server.nix # NFS server + btrfs replication (zippy) │ ├── nfs-services-server.nix # NFS server + btrfs replication
│ └── nfs-services-standby.nix # NFS standby + receive replication (c1) │ └── nfs-services-standby.nix # NFS standby + receive replication
├── hosts/ ├── hosts/ # Host configs - check imports for roles
│ ├── c1/, c2/, c3/ # Cattle nodes (quorum + workers)
│ ├── zippy/ # Primary storage + NFS server + worker (not quorum)
│ ├── chilly/ # Home Assistant VM + cluster member (Consul only)
│ ├── sparky/ # Desktop + cluster member (Consul only)
│ ├── fractal/ # (Proxmox, will become NixOS storage node)
│ └── sunny/ # (Standalone ethereum node, not in cluster)
├── docs/ ├── docs/
│ ├── CLUSTER_REVAMP.md # Master plan for architecture changes │ ├── CLUSTER_REVAMP.md # Master plan for architecture changes
│ ├── MIGRATION_TODO.md # Tracking checklist for migration │ ├── MIGRATION_TODO.md # Tracking checklist for migration
── NFS_FAILOVER.md # NFS failover procedures ── NFS_FAILOVER.md # NFS failover procedures
│ └── AUTH_SETUP.md # Authentication (Pocket ID + Traefik OIDC)
└── services/ # Nomad job specs (.hcl files) └── services/ # Nomad job specs (.hcl files)
``` ```
## Current Architecture ## Current Architecture
### Storage Mounts ### Storage Mounts
- `/data/services` - NFS from `data-services.service.consul` (zippy primary, c1 standby) - `/data/services` - NFS from `data-services.service.consul` (check nfs-services-server.nix for primary)
- `/data/media` - CIFS from fractal (existing, unchanged) - `/data/media` - CIFS from fractal
- `/data/shared` - CIFS from fractal (existing, unchanged) - `/data/shared` - CIFS from fractal
### Hosts ### Cluster Roles (check hosts/*/default.nix for each host's imports)
- **c1, c2, c3**: Cattle nodes, run most workloads, Nomad/Consul quorum members - **Quorum**: hosts importing `nomad-server.nix` (3 expected for consensus)
- **zippy**: Primary NFS server, runs workloads (affinity), NOT quorum, replicates to c1 every 5min - **Workers**: hosts importing `nomad-worker.nix` (run Nomad jobs)
- **chilly**: Home Assistant VM, cluster member (Consul agent + CLI tools), no workloads - **NFS server**: host importing `nfs-services-server.nix` (affinity for direct disk access like DBs)
- **sparky**: Desktop/laptop, cluster member (Consul agent + CLI tools), no workloads - **Standby**: hosts importing `nfs-services-standby.nix` (receive replication)
- **fractal**: Storage node (Proxmox/ZFS), will join quorum after GlusterFS removed
- **sunny**: Standalone ethereum staking node (not in cluster)
## Config Architecture ## Config Architecture
@@ -58,19 +51,22 @@ NixOS cluster configuration using flakes. Homelab infrastructure with Nomad/Cons
- `workstation-node.nix` - Dev tools (deploy-rs, docker, nix-ld, emulation) - `workstation-node.nix` - Dev tools (deploy-rs, docker, nix-ld, emulation)
- `desktop-node.nix` - Extends workstation + Hyprland/GUI - `desktop-node.nix` - Extends workstation + Hyprland/GUI
**Host composition examples**: **Composition patterns**:
- c1/c2/c3: `cluster-member + nomad-worker + nomad-server` (quorum + runs jobs) - Quorum member: `cluster-member + nomad-worker + nomad-server`
- zippy: `cluster-member + nomad-worker` (runs jobs, not quorum) - Worker only: `cluster-member + nomad-worker`
- chilly/sparky: `cluster-member + cluster-tools` (Consul + CLI only) - CLI only: `cluster-member + cluster-tools` (Consul agent, no Nomad service)
- NFS primary: `cluster-member + nomad-worker + nfs-services-server`
- Standalone: `minimal-node` only (no cluster membership)
**Key insight**: Profiles (workstation/desktop) no longer imply cluster membership. Hosts explicitly declare roles via imports. **Key insight**: Profiles (workstation/desktop) don't imply cluster roles. Check imports for actual roles.
## Key Patterns ## Key Patterns
**NFS Server/Standby**: **NFS Server/Standby**:
- Primary (zippy): imports `nfs-services-server.nix`, sets `standbys = ["c1"]` - Primary: imports `nfs-services-server.nix`, sets `standbys = [...]`
- Standby (c1): imports `nfs-services-standby.nix`, sets `replicationKeys = [...]` - Standby: imports `nfs-services-standby.nix`, sets `replicationKeys = [...]`
- Replication: btrfs send/receive every 5min, incremental with fallback to full - Replication: btrfs send/receive every 5min, incremental with fallback to full
- Check host configs for current primary/standby assignments
**Backups**: **Backups**:
- Kopia client on all nodes → Kopia server on fractal - Kopia client on all nodes → Kopia server on fractal
@@ -81,6 +77,12 @@ NixOS cluster configuration using flakes. Homelab infrastructure with Nomad/Cons
- SOPS for secrets, files in `secrets/` - SOPS for secrets, files in `secrets/`
- Keys managed per-host - Keys managed per-host
**Authentication**:
- Pocket ID (OIDC provider) at `pocket-id.v.paler.net`
- Traefik uses `traefik-oidc-auth` plugin for SSO
- Services add `middlewares=oidc-auth@file` tag to protect
- See `docs/AUTH_SETUP.md` for details
## Migration Status ## Migration Status
**Phase 3 & 4**: COMPLETE! GlusterFS removed, all services on NFS **Phase 3 & 4**: COMPLETE! GlusterFS removed, all services on NFS
@@ -92,7 +94,7 @@ See `docs/MIGRATION_TODO.md` for detailed checklist.
**Deploy a host**: `deploy -s '.#hostname'` **Deploy a host**: `deploy -s '.#hostname'`
**Deploy all**: `deploy` **Deploy all**: `deploy`
**Check replication**: `ssh zippy journalctl -u replicate-services-to-c1.service -f` **Check replication**: Check NFS primary host, then `ssh <primary> journalctl -u replicate-services-to-*.service -f`
**NFS failover**: See `docs/NFS_FAILOVER.md` **NFS failover**: See `docs/NFS_FAILOVER.md`
**Nomad jobs**: `services/*.hcl` - service data stored at `/data/services/<service-name>` **Nomad jobs**: `services/*.hcl` - service data stored at `/data/services/<service-name>`
@@ -106,8 +108,8 @@ See `docs/MIGRATION_TODO.md` for detailed checklist.
## Important Files ## Important Files
- `common/global/backup.nix` - Kopia backup configuration - `common/global/backup.nix` - Kopia backup configuration
- `hosts/zippy/default.nix` - NFS server config, replication targets - `common/nfs-services-server.nix` - NFS server role (check hosts for which imports this)
- `hosts/c1/default.nix` - NFS standby config, authorized replication keys - `common/nfs-services-standby.nix` - NFS standby role (check hosts for which imports this)
- `flake.nix` - Host definitions, nixpkgs inputs - `flake.nix` - Host definitions, nixpkgs inputs
--- ---

View File

@@ -22,7 +22,6 @@ Each layer extends the previous one, inheriting all configurations. Hosts select
### Special Node Types ### Special Node Types
- **cloud-node**: Minimal + Consul only (cloud VPS deployments)
- **compute-node**: Cluster + Nomad worker (container orchestration) - **compute-node**: Cluster + Nomad worker (container orchestration)
## Directory Structure ## Directory Structure
@@ -40,7 +39,6 @@ Each layer extends the previous one, inheriting all configurations. Hosts select
│ ├── server-node.nix # Server layer: bare metal services (future) │ ├── server-node.nix # Server layer: bare metal services (future)
│ ├── workstation-node.nix # Workstation layer: dev tools │ ├── workstation-node.nix # Workstation layer: dev tools
│ ├── desktop-node.nix # Desktop layer: GUI environment │ ├── desktop-node.nix # Desktop layer: GUI environment
│ ├── cloud-node.nix # Cloud VPS profile
│ ├── compute-node.nix # Nomad worker profile │ ├── compute-node.nix # Nomad worker profile
│ └── [feature modules] # Individual feature configs │ └── [feature modules] # Individual feature configs
├── hosts/ ├── hosts/
@@ -101,7 +99,7 @@ This ensures system and user configurations stay synchronized.
| Host | Profile | Role | Hardware | | Host | Profile | Role | Hardware |
|------|---------|------|----------| |------|---------|------|----------|
| **c1, c2, c3** | compute-node | Nomad workers | Bare metal servers | | **c1, c2, c3** | compute-node | Nomad workers | Bare metal servers |
| **alo-cloud-1** | cloud-node | Reverse proxy | Cloud VPS | | **alo-cloud-1** | minimal | Reverse proxy (Traefik) | Cloud VPS |
| **chilly** | server | Home Assistant in a VM | Bare metal server | | **chilly** | server | Home Assistant in a VM | Bare metal server |
| **zippy** | workstation | Development machine, server | Bare metal server | | **zippy** | workstation | Development machine, server | Bare metal server |
| **sparky** | desktop | Desktop environment | Bare metal desktop | | **sparky** | desktop | Desktop environment | Bare metal desktop |

View File

@@ -1,7 +1,7 @@
{ pkgs, ... }: { pkgs, ... }:
let let
# this line prevents hanging on network split # this line prevents hanging on network split
automount_opts = "x-systemd.automount,noauto,x-systemd.idle-timeout=60,x-systemd.mount-timeout=5s"; automount_opts = "x-systemd.automount,noauto,x-systemd.idle-timeout=60,x-systemd.mount-timeout=5s,nobrl";
in in
{ {
environment.systemPackages = [ pkgs.cifs-utils ]; environment.systemPackages = [ pkgs.cifs-utils ];

View File

@@ -1,8 +0,0 @@
{ pkgs, ... }:
{
# Cloud node: Minimal system with Consul for cloud deployments
imports = [
./minimal-node.nix
./consul.nix
];
}

View File

@@ -1,47 +0,0 @@
{ pkgs, lib, ... }:
{
# Desktop profile: Graphical desktop with Hyprland
# Extends workstation-node with desktop environment
imports = [
./workstation-node.nix
];
# omarchy-nix enables NetworkManager, but we use useDHCP globally
networking.networkmanager.enable = lib.mkForce false;
# Enable Hyprland (Wayland compositor)
programs.hyprland = {
enable = true;
xwayland.enable = true; # For compatibility with X11 apps if needed
};
# Essential desktop services
services.dbus.enable = true;
# polkit for privilege escalation
security.polkit.enable = true;
# Enable sound with pipewire
security.rtkit.enable = true;
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
};
# Fonts
fonts.packages = with pkgs; [
noto-fonts
noto-fonts-cjk-sans
noto-fonts-emoji
liberation_ttf
fira-code
fira-code-symbols
];
# Environment variables for Wayland
environment.sessionVariables = {
NIXOS_OZONE_WL = "1"; # Hint electron apps to use Wayland
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 MiB

View File

@@ -0,0 +1,79 @@
# ABOUTME: NixOS desktop environment module for Hyprland
# ABOUTME: Configures greetd, audio, bluetooth, fonts, and system services
{ config, pkgs, lib, ... }:
{
imports = [
../workstation-node.nix
];
# Force NetworkManager off - we use useDHCP globally
networking.networkmanager.enable = lib.mkForce false;
# Hyprland window manager
programs.hyprland = {
enable = true;
xwayland.enable = true;
};
# greetd display manager with tuigreet
services.greetd = {
enable = true;
settings = {
default_session = {
command = "${pkgs.tuigreet}/bin/tuigreet --time --cmd Hyprland";
user = "greeter";
};
};
};
# Essential desktop services
services.dbus.enable = true;
# polkit for privilege escalation
security.polkit.enable = true;
# DNS resolution
services.resolved.enable = true;
# Bluetooth support
hardware.bluetooth = {
enable = true;
powerOnBoot = true;
};
services.blueman.enable = true;
# Audio with PipeWire
security.rtkit.enable = true;
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
jack.enable = true;
};
# direnv support
programs.direnv.enable = true;
# Fonts
fonts.packages = with pkgs; [
noto-fonts
noto-fonts-cjk-sans
noto-fonts-color-emoji
liberation_ttf
fira-code
fira-code-symbols
nerd-fonts.caskaydia-mono
];
# Environment variables for Wayland
environment.sessionVariables = {
NIXOS_OZONE_WL = "1";
};
# Additional desktop packages
environment.systemPackages = with pkgs; [
prusa-slicer
];
}

8
common/ham-radio.nix Normal file
View File

@@ -0,0 +1,8 @@
# ABOUTME: Ham radio tools profile for amateur radio operators.
# ABOUTME: Provides CLI tools for logging and processing ham radio contacts.
{ pkgs, ... }:
{
environment.systemPackages = [
pkgs.custom.flecli
];
}

View File

@@ -0,0 +1,32 @@
{
config,
lib,
pkgs,
...
}:
{
options.services.netconsoleReceiver = {
enable = lib.mkEnableOption "netconsole UDP receiver";
port = lib.mkOption {
type = lib.types.port;
default = 6666;
description = "UDP port to listen on for netconsole messages";
};
};
config = lib.mkIf config.services.netconsoleReceiver.enable {
systemd.services.netconsole-receiver = {
description = "Netconsole UDP receiver";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
ExecStart = "${pkgs.socat}/bin/socat -u UDP-LISTEN:${toString config.services.netconsoleReceiver.port},fork STDOUT";
StandardOutput = "journal";
StandardError = "journal";
SyslogIdentifier = "netconsole";
Restart = "always";
RestartSec = "5s";
};
};
};
}

View File

@@ -103,11 +103,14 @@ in
] ++ (lib.forEach cfg.standbys (standby: { ] ++ (lib.forEach cfg.standbys (standby: {
"replicate-services-to-${standby}" = { "replicate-services-to-${standby}" = {
description = "Replicate /persist/services to ${standby}"; description = "Replicate /persist/services to ${standby}";
path = [ pkgs.btrfs-progs pkgs.openssh pkgs.coreutils pkgs.findutils pkgs.gnugrep ]; path = [ pkgs.btrfs-progs pkgs.openssh pkgs.coreutils pkgs.findutils pkgs.gnugrep pkgs.curl ];
script = '' script = ''
set -euo pipefail set -euo pipefail
START_TIME=$(date +%s)
REPLICATION_SUCCESS=0
SSH_KEY="/persist/root/.ssh/btrfs-replication" SSH_KEY="/persist/root/.ssh/btrfs-replication"
if [ ! -f "$SSH_KEY" ]; then if [ ! -f "$SSH_KEY" ]; then
echo "ERROR: SSH key not found at $SSH_KEY" echo "ERROR: SSH key not found at $SSH_KEY"
@@ -130,15 +133,19 @@ in
echo "Attempting incremental send from $(basename $PREV_LOCAL) to ${standby}" echo "Attempting incremental send from $(basename $PREV_LOCAL) to ${standby}"
# Try incremental send, if it fails (e.g., parent missing on receiver), fall back to full # Try incremental send, if it fails (e.g., parent missing on receiver), fall back to full
if btrfs send -p "$PREV_LOCAL" "$SNAPSHOT_PATH" | \ # Use -c to help with broken Received UUID chains
if btrfs send -p "$PREV_LOCAL" -c "$PREV_LOCAL" "$SNAPSHOT_PATH" | \
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=accept-new root@${standby} \ ssh -i "$SSH_KEY" -o StrictHostKeyChecking=accept-new root@${standby} \
"btrfs receive /persist/services-standby"; then "btrfs receive /persist/services-standby"; then
echo "Incremental send completed successfully" echo "Incremental send completed successfully"
REPLICATION_SUCCESS=1
else else
echo "Incremental send failed (likely missing parent on receiver), falling back to full send" echo "Incremental send failed (likely missing parent on receiver), falling back to full send"
# Plain full send without clone source (receiver may have no snapshots)
btrfs send "$SNAPSHOT_PATH" | \ btrfs send "$SNAPSHOT_PATH" | \
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=accept-new root@${standby} \ ssh -i "$SSH_KEY" -o StrictHostKeyChecking=accept-new root@${standby} \
"btrfs receive /persist/services-standby" "btrfs receive /persist/services-standby"
REPLICATION_SUCCESS=1
fi fi
else else
# First snapshot, do full send # First snapshot, do full send
@@ -146,10 +153,28 @@ in
btrfs send "$SNAPSHOT_PATH" | \ btrfs send "$SNAPSHOT_PATH" | \
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=accept-new root@${standby} \ ssh -i "$SSH_KEY" -o StrictHostKeyChecking=accept-new root@${standby} \
"btrfs receive /persist/services-standby" "btrfs receive /persist/services-standby"
REPLICATION_SUCCESS=1
fi fi
# Cleanup old snapshots on sender (keep last 24 hours = 288 snapshots at 5min intervals) # Cleanup old snapshots on sender (keep last 10 snapshots, sorted by name/timestamp)
find /persist -maxdepth 1 -name 'services@*' -mmin +1440 -exec btrfs subvolume delete {} \; ls -1d /persist/services@* 2>/dev/null | sort | head -n -10 | xargs -r btrfs subvolume delete
# Calculate metrics
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
SNAPSHOT_COUNT=$(ls -1d /persist/services@* 2>/dev/null | wc -l)
# Push metrics to Prometheus pushgateway
cat <<METRICS | curl -s --data-binary @- http://pushgateway.service.consul:9091/metrics/job/nfs_replication/instance/${standby} || true
# TYPE nfs_replication_last_success_timestamp gauge
nfs_replication_last_success_timestamp $END_TIME
# TYPE nfs_replication_duration_seconds gauge
nfs_replication_duration_seconds $DURATION
# TYPE nfs_replication_snapshot_count gauge
nfs_replication_snapshot_count $SNAPSHOT_COUNT
# TYPE nfs_replication_success gauge
nfs_replication_success $REPLICATION_SUCCESS
METRICS
''; '';
serviceConfig = { serviceConfig = {

View File

@@ -39,17 +39,28 @@ in
noCheck = true; noCheck = true;
}; };
# Cleanup old snapshots on standby (keep last 4 hours for HA failover) # Cleanup old snapshots on standby (keep last 10 snapshots)
systemd.services.cleanup-services-standby-snapshots = { systemd.services.cleanup-services-standby-snapshots = {
description = "Cleanup old btrfs snapshots in services-standby"; description = "Cleanup old btrfs snapshots in services-standby";
path = [ pkgs.btrfs-progs pkgs.findutils ]; path = [ pkgs.btrfs-progs pkgs.findutils pkgs.coreutils pkgs.curl ];
script = '' script = ''
set -euo pipefail set -euo pipefail
# Keep last 4 hours of snapshots (48 snapshots at 5min intervals)
find /persist/services-standby -maxdepth 1 -name 'services@*' -mmin +240 -exec btrfs subvolume delete {} \; || true
'';
# Cleanup old snapshots on standby (keep last 10 snapshots, sorted by name/timestamp)
ls -1d /persist/services-standby/services@* 2>/dev/null | sort | head -n -10 | xargs -r btrfs subvolume delete || true
# Calculate metrics
CLEANUP_TIME=$(date +%s)
SNAPSHOT_COUNT=$(ls -1d /persist/services-standby/services@* 2>/dev/null | wc -l)
# Push metrics to Prometheus pushgateway
cat <<METRICS | curl -s --data-binary @- http://pushgateway.service.consul:9091/metrics/job/nfs_standby_cleanup/instance/$(hostname) || true
# TYPE nfs_standby_snapshot_count gauge
nfs_standby_snapshot_count $SNAPSHOT_COUNT
# TYPE nfs_standby_cleanup_last_run_timestamp gauge
nfs_standby_cleanup_last_run_timestamp $CLEANUP_TIME
METRICS
'';
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
User = "root"; User = "root";

View File

@@ -3,7 +3,7 @@
boot.loader.systemd-boot = { boot.loader.systemd-boot = {
enable = true; enable = true;
configurationLimit = 5; configurationLimit = 5;
memtest86.enable = lib.mkIf (pkgs.system == "x86_64-linux") true; memtest86.enable = lib.mkIf (pkgs.stdenv.hostPlatform.system == "x86_64-linux") true;
}; };
boot.loader.efi.canTouchEfiVariables = true; boot.loader.efi.canTouchEfiVariables = true;
} }

55
docs/AUTH_SETUP.md Normal file
View File

@@ -0,0 +1,55 @@
# Authentication Setup
SSO for homelab services using OIDC.
## Architecture
**Pocket ID** (`pocket-id.v.paler.net`) - Lightweight OIDC provider, data in `/data/services/pocket-id`
**Traefik** - Uses `traefik-oidc-auth` plugin (v0.16.0) to protect services
- Plugin downloaded from GitHub at startup, cached in `/data/services/traefik/plugins-storage`
- Middleware config in `/data/services/traefik/rules/middlewares.yml`
- Protected services add tag: `traefik.http.routers.<name>.middlewares=oidc-auth@file`
## Flow
1. User hits protected service → Traefik intercepts
2. Redirects to Pocket ID for login
3. Pocket ID returns OIDC token
4. Traefik validates and forwards with `X-Oidc-Username` header
## Protected Services
Use `oidc-auth@file` middleware (grep codebase for full list):
- Wikis (TiddlyWiki instances)
- Media stack (Radarr, Sonarr, Plex, etc.)
- Infrastructure (Traefik dashboard, Loki, Jupyter, Unifi)
## Key Files
- `services/pocket-id.hcl` - OIDC provider
- `services/traefik.hcl` - Plugin declaration
- `/data/services/traefik/rules/middlewares.yml` - Middleware definitions (oidc-auth, simple-auth fallback)
## Cold Start Notes
- Traefik needs internet to download plugin on first start
- Pocket ID needs `/data/services` NFS mounted
- Pocket ID down = all protected services inaccessible
## Troubleshooting
**Infinite redirects**: Check `TRUST_PROXY=true` on Pocket ID
**Plugin not loading**: Clear cache in `/data/services/traefik/plugins-storage/`, restart Traefik
**401 after login**: Verify client ID/secret in middlewares.yml matches Pocket ID client config
## Migration History
- Previous: Authentik with forwardAuth (removed Nov 2024)
- Current: Pocket ID + traefik-oidc-auth (simpler, lighter)
---
*Manage users/clients via Pocket ID UI. Basic auth fallback available via `simple-auth` middleware.*

206
docs/CICD_SETUP.md Normal file
View File

@@ -0,0 +1,206 @@
# CI/CD Setup for Nomad Services
Guide for adding automated builds and deployments to a service.
## Prerequisites
### 1. Service Repository
Your service needs a `flake.nix` that exports a Docker image:
```nix
{
outputs = { self, nixpkgs, ... }: {
# The workflow looks for this output by default
dockerImage = pkgs.dockerTools.buildImage {
name = "gitea.v.paler.net/alo/<service>";
tag = "latest";
# ... image config
};
};
}
```
**Important**: Use `extraCommands` instead of `runAsRoot` in your Docker build - the CI runner doesn't have KVM.
### 2. Nomad Job
Your job in `services/<name>.hcl` needs:
```hcl
job "<service>" {
# Required: UUID changes trigger deployments
meta {
uuid = uuidv4()
}
# Required: enables deployment tracking and auto-rollback
update {
max_parallel = 1
health_check = "checks"
min_healthy_time = "30s"
healthy_deadline = "5m"
auto_revert = true
}
# Required: pulls new image on each deployment
task "app" {
config {
force_pull = true
}
# Recommended: health check for deployment validation
service {
check {
type = "http"
path = "/healthz"
interval = "10s"
timeout = "5s"
}
}
}
}
```
## Quick Start
### 1. Create Workflow
Add `.gitea/workflows/deploy.yaml` to your service repo:
```yaml
name: Deploy
on:
push:
branches: [master]
workflow_dispatch:
jobs:
deploy:
uses: alo/alo-cluster/.gitea/workflows/deploy-nomad.yaml@master
with:
service_name: <your-service> # Must match Nomad job ID
secrets: inherit
```
### 2. Add Secrets
In Gitea → Your Repo → Settings → Actions → Secrets, add:
| Secret | Value |
|--------|-------|
| `REGISTRY_USERNAME` | Your Gitea username |
| `REGISTRY_PASSWORD` | Gitea access token with `packages:write` |
| `NOMAD_ADDR` | `http://nomad.service.consul:4646` |
### 3. Push
Push to `master` branch. The workflow will:
1. Build your Docker image with Nix
2. Push to Gitea registry
3. Update the Nomad job to trigger deployment
4. Monitor until deployment succeeds or fails
## Workflow Options
The shared workflow accepts these inputs:
| Input | Default | Description |
|-------|---------|-------------|
| `service_name` | (required) | Nomad job ID |
| `flake_output` | `dockerImage` | Flake output to build |
| `registry` | `gitea.v.paler.net` | Container registry |
Example with custom flake output:
```yaml
jobs:
deploy:
uses: alo/alo-cluster/.gitea/workflows/deploy-nomad.yaml@master
with:
service_name: myservice
flake_output: packages.x86_64-linux.docker
secrets: inherit
```
## How It Works
```
Push to master
Build: nix build .#dockerImage
Push: skopeo → gitea.v.paler.net/alo/<service>:latest
Deploy: Update job meta.uuid → Nomad creates deployment
Monitor: Poll deployment status for up to 5 minutes
Success: Deployment healthy
OR
Failure: Nomad auto-reverts to previous version
```
## Troubleshooting
### Build fails with KVM error
```
Required system: 'x86_64-linux' with features {kvm}
```
Use `extraCommands` instead of `runAsRoot` in your `docker.nix`:
```nix
# Bad - requires KVM
runAsRoot = ''
mkdir -p /tmp
'';
# Good - no KVM needed
extraCommands = ''
mkdir -p tmp
chmod 1777 tmp
'';
```
### No deployment created
Ensure your Nomad job has the `update` stanza with `auto_revert = true`.
### Image not updating
Check that `force_pull = true` is set in the Nomad job's Docker config.
### Deployment fails health checks
- Check your `/healthz` endpoint works
- Increase `healthy_deadline` if startup is slow
- Check `nomad alloc logs <alloc-id>` for errors
### Workflow can't access alo-cluster
If Gitea can't pull the reusable workflow, you may need to make alo-cluster public or use a token. As a fallback, copy the workflow content directly.
## Manual Deployment
If CI fails, you can deploy manually:
```bash
cd <service-repo>
nix build .#dockerImage
skopeo copy --dest-authfile ~/.docker/config.json \
docker-archive:result \
docker://gitea.v.paler.net/alo/<service>:latest
nomad run /path/to/alo-cluster/services/<service>.hcl
```
## Rollback
Nomad auto-reverts on health check failure. For manual rollback:
```bash
nomad job history <service> # List versions
nomad job revert <service> <version> # Revert to specific version
```

354
docs/HOMELAB_AGENT.md Normal file
View File

@@ -0,0 +1,354 @@
# ABOUTME: Vision and design document for an AI agent that manages the homelab cluster.
# ABOUTME: Covers emergent capabilities, technical approach, and implementation strategy.
# Homelab Agent: Vision and Design
## The Core Idea
Not automation. Not "LLM-powered autocomplete for infrastructure." Emergent capabilities.
The same shift Claude Code brought to programming: you describe outcomes, it handles implementation. You become a "product manager" for your infrastructure instead of an "infrastructure engineer."
The cluster stops being infrastructure you manage and becomes an environment that responds to intent.
## What Makes This Different From Automation
**Automation**: "If disk > 90%, delete old logs"
**Emergent**: "Disk is 95% full. What's using space? ...Postgres WAL. Can I safely checkpoint? Last backup was 2h ago, load is low, yes. Running checkpoint... down to 60%. I should note that WAL retention might need tuning."
The difference:
- Novel problem-solving (not pattern matching)
- Contextual safety reasoning
- Adaptation to the specific situation
- Learning for the future
## Examples of Genuinely New Capabilities
### 1. Intent-Driven Infrastructure
> "I want to run Synapse for Matrix"
Agent figures out: Nomad job spec, storage location, Traefik routing, TLS, Consul registration, backup config. Creates it, deploys it, validates it.
You don't need to know Nomad job format or Traefik labels. You describe the outcome.
### 2. Proactive Evolution (The Best One)
The agent doesn't wait for problems or instructions:
- "Synapse 1.98 has a security fix. I've tested it in a local build, no config changes needed. Deploy?"
- "Your NFS server has been primary for 47 days. Want me to test failover to make sure it still works?"
- "I noticed arr services all have the same resource limits but Sonarr consistently uses more. Adjusted."
- "There's a new NixOS module for Traefik that simplifies your current setup. Here's the diff."
Not monitoring. Stewardship.
### 3. The Cluster Has Opinions
> You: "I want to add Plex"
>
> Agent: "You already have Jellyfin, which does the same thing. If you want Plex specifically for its mobile app, I can set it up to share Jellyfin's media library. Or if you want to switch entirely, I can migrate watch history. What's the actual goal?"
Not a command executor. A collaborator that understands your system.
### 4. "Bring This Into the Cluster"
You're running something in Docker on a random VM:
> "Bring this into the cluster"
Agent: connects, inspects, figures out dependencies, writes Nomad job, sets up storage, migrates data, routes traffic, validates, decommissions old instance.
You didn't need to know how.
### 5. Cross-Cutting Changes
> "Add authentication to all public-facing services"
Agent identifies which services are public, understands the auth setup (Pocket ID + traefik-oidc-auth), modifies each service's config, tests that auth works.
Single coherent change across everything, without knowing every service yourself.
### 6. Emergent Debugging
Not runbooks. Actual reasoning:
> "The blog is slow"
Agent checks service health (fine), node resources (fine), network latency (fine), database queries (ah, slow query), traces to missing index, adds index, validates performance improved.
Solved a problem nobody wrote a runbook for.
### 7. Architecture Exploration
> "What if we added a third Nomad server for better quorum?"
Agent reasons about current topology, generates the config, identifies what would change, shows blast radius. Thinking partner for infrastructure decisions.
## Why Nix Makes This Possible
Traditional infrastructure: state is scattered and implicit. Nix: everything is declared.
- **Full system understanding** - agent can read the flake and understand EVERYTHING
- **Safe experimentation** - build without deploying, rollback trivially
- **Reproducibility** - "what was the state 3 days ago?" can be rebuilt exactly
- **Composition** - agent can generate valid configs that compose correctly
- **The ecosystem** - 80k+ packages, thousands of modules the agent can navigate
> "I want a VPN that works with my phone"
Agent knows Nix, finds WireGuard module, configures it, generates QR codes, opens firewall. You didn't learn WireGuard.
## The Validation Pattern
Just like code has linting and tests, infrastructure actions need validation:
| Phase | Code | Infrastructure |
|-------|------|----------------|
| Static | Lint, typecheck | Config parses, secrets exist, no port conflicts |
| Pre-flight | — | Cluster healthy, dependencies up, quorum intact |
| Post-action | Unit tests | Service started, health checks pass, metrics flowing |
| Invariants | CI | NFS mounted, Consul quorum, replication current |
The agent can take actions confidently because it validates outcomes.
## The Reality Check
Some of this works today. Some would fail spectacularly. Some would fail silently and idiotically. Just like Claude Code for coding.
Therefore:
- Tight loop with the human operator
- Assume the human is competent and knowledgeable
- Agent amplifies expertise, doesn't replace it
- Escalate when uncertain
## Technical Approach
### Runtime: Claude Code (Not Agent SDK)
Two options were considered:
| Tool | Pro/Max Subscription | API Billing |
|------|---------------------|-------------|
| Claude Code CLI | Yes | Yes |
| Claude Agent SDK | No | Required |
Claude Code can use existing Max subscription. Agent SDK requires separate API billing.
For v1, use Claude Code as the runtime:
```bash
claude --print "prompt" \
--allowedTools "Bash,Read,Edit" \
--permission-mode acceptEdits
```
Graduate to Agent SDK later if limitations are hit.
### Trigger Architecture
On-demand Claude Code sessions, triggered by:
- **Timer** - periodic health/sanity check
- **Alert** - alertmanager webhook
- **Event** - systemd OnFailure, consul watch
- **Manual** - invoke with a goal
Each trigger provides context and a goal. Claude Code does the rest.
### Structure
```
agent/
├── triggers/
│ ├── scheduled-check # systemd timer
│ ├── on-alert # webhook handler
│ └── on-failure # systemd OnFailure target
├── gather-context.sh # snapshot of cluster state
└── goals/
├── health-check.md # verify health, fix if safe
├── incident.md # investigate alert, fix or escalate
└── proactive.md # look for improvements
```
### Example: Scheduled Health Check
```bash
#!/usr/bin/env bash
CONTEXT=$(./gather-context.sh)
GOAL=$(cat goals/health-check.md)
claude --print "
## Context
$CONTEXT
## Goal
$GOAL
## Constraints
- You can read any file in this repo
- You can run nomad/consul/systemctl commands
- You can edit Nix/HCL files and run deploy
- Before destructive actions, validate with nix build or nomad plan
- If uncertain about safety, output a summary and stop
"
```
### Context Gathering
```bash
#!/usr/bin/env bash
echo "=== Nomad Jobs ==="
nomad job status
echo "=== Consul Members ==="
consul members
echo "=== Failed Systemd Units ==="
systemctl --failed
echo "=== Recent Errors (last hour) ==="
journalctl --since "1 hour ago" -p err --no-pager | tail -100
```
## Edge Cases and the Nix Promise
The NixOS promise mostly works, but sometimes doesn't:
- Mount option changes that require reboot
- Transition states where switch fails even if end state is correct
- Partial application where switch "succeeds" but change didn't take effect
This is where the agent adds value: it can detect when a change needs special handling, apply the appropriate strategy, and verify the change actually took effect.
## Capturing Knowledge
Document edge cases as they're discovered:
```markdown
## CIFS/NFS mount option changes
Switch may fail or succeed without effect. Strategy:
1. Try normal deploy
2. If mount options don't match after, reboot required
3. If deploy fails with mount busy, local switch + reboot
```
The agent reads this, uses it as context, but can also reason about novel situations.
## Path to CI/CD
Eventually: push to main triggers deploy via agent.
```
push to main
|
build all configs (mechanical)
|
agent: "what changed? is this safe to auto-deploy?"
|
├─ clean change -> deploy, validate, done
├─ needs reboot -> deploy, schedule reboot, validate after
├─ risky change -> notify for manual approval
└─ failed -> diagnose, retry with different strategy, or escalate
|
post-deploy verification
|
notification
```
The agent is the intelligence layer on top of mechanical CI/CD.
## Research: What Others Are Doing (January 2026)
### Existing Projects & Approaches
**n8n + Ollama Stack**
The most common pattern is n8n (workflow orchestration) + Ollama (local LLM). Webhooks from
monitoring (Netdata/Prometheus) trigger AI-assisted diagnosis. Philosophy from one practitioner:
"train an employee, not a bot" — build trust, gradually grant autonomy.
Sources:
- [Virtualization Howto: Self-Healing Home Lab](https://www.virtualizationhowto.com/2025/10/how-i-built-a-self-healing-home-lab-that-fixes-itself/)
- [addROM: AI Agent for Homelab with n8n](https://addrom.com/unleashing-the-power-of-an-ai-agent-for-homelab-management-with-n8n/)
**Local Infrastructure Agent (Kelcode)**
Architecture: user question → tool router → query processor → LLM response. Connects to
Kubernetes, Prometheus, Harbor Registry.
Key insight: "The AI's output definition must be perfectly synchronized with the software
it's trying to use." Their K8s tool failed because the prompt generated kubectl commands
while the code expected structured data objects.
Uses phi4-mini via Ollama for routing decisions after testing multiple models.
Source: [Kelcode: Building a Homelab Agentic Ecosystem](https://kelcode.co.uk/building-a-homelab-agentic-ecosystem-part1/)
**nixai**
AI assistant specifically for NixOS. Searches NixOS Wiki, Nixpkgs Manual, nix.dev, Home Manager
docs. Diagnoses issues from piped logs/errors. Privacy-first: defaults to local Ollama.
Limited scope — helper tool, not autonomous agent. But shows NixOS-specific tooling is possible.
Source: [NixOS Discourse: Introducing nixai](https://discourse.nixos.org/t/introducing-nixai-your-ai-powered-nixos-companion/65168)
**AI-Friendly Infrastructure (The Merino Wolf)**
Key insight: make infrastructure "AI-friendly" through structured documentation. CLAUDE.md
provides comprehensive context — "structured knowledge transfer."
Lessons:
- "Context investment pays dividends" — comprehensive documentation is the most valuable asset
- Layered infrastructure design mirrors how both humans and AI think
- Rule-based guidance enforces safety practices automatically
Source: [The Merino Wolf: AI-Powered Homelab](https://themerinowolf.com/posts/ai-powered-homelab/)
**Claude Code Infrastructure Patterns**
Solves "skills don't activate automatically" problem using hooks (UserPromptSubmit, PostToolUse)
+ skill-rules.json for auto-activation.
500-line rule with progressive disclosure: main file for high-level guidance, resource files
for deep dives. Claude loads materials incrementally as needed.
Persistence pattern across context resets using three-file structures (plan, context, tasks).
Born from 6 months managing TypeScript microservices (50k+ lines).
Source: [diet103/claude-code-infrastructure-showcase](https://github.com/diet103/claude-code-infrastructure-showcase)
### Patterns That Work
- Local LLMs (Ollama) + workflow orchestration (n8n) is the popular stack
- Start with read-only/diagnostic agents, gradually add write access
- Pre-approved command lists for safety (e.g., 50 validated bash commands max)
- Structured documentation as foundation — AI is only as good as its context
- Multi-step tool use: agent plans, then executes steps, observing results
### What's Missing in the Space
- Nobody's doing true "emergent capabilities" yet — mostly tool routing
- Most projects are Kubernetes/Docker focused, not NixOS
- Few examples of proactive stewardship (our example #2)
- Limited examples of agents that understand the whole system coherently
### Community Skepticism
From Reddit discussions: doubts exist about using LLM agents in production. Although LLMs can
automate specific tasks, they frequently need human involvement for intricate decision-making.
This validates our approach: tight loop with a competent human, not autonomous operation.
### The Gap We'd Fill
- NixOS-native agent leveraging declarative config as source of truth
- True emergence — not just tool routing, but reasoning about novel situations
- Proactive evolution, not just reactive troubleshooting
- Tight human loop with a competent operator
## Next Steps
1. Build trigger infrastructure (systemd timer, basic webhook handler)
2. Write context gathering scripts
3. Define goal prompts for common scenarios
4. Test with scheduled health checks
5. Iterate based on what works and what doesn't
6. Document edge cases as they're discovered
7. Gradually expand scope as confidence grows

View File

@@ -1,3 +1,7 @@
* remote docker images used, can't come up if internet is down * remote docker images used, can't come up if internet is down
* local docker images pulled from gitea, can't come up if gitea isn't up (yet) * local docker images pulled from gitea, can't come up if gitea isn't up (yet)
* traefik-oidc-auth plugin downloaded from GitHub at startup (cached in /data/services/traefik/plugins-storage)
* renovate system of some kind * renovate system of some kind
* vector (or other log ingestion) everywhere, consider moving it off docker if possible
* monitor backup-persist success/fail
* gitea organization is public -> at least from the internal network, anyone can pull images and probably also clone repos. there should be absolutely zero secrets in the repos (and the ones that are now should be changed before stored somewhere else) and the nomad workers should authenticate to pull images

710
flake.lock generated
View File

@@ -1,42 +1,5 @@
{ {
"nodes": { "nodes": {
"aquamarine": {
"inputs": {
"hyprutils": [
"omarchy-nix",
"hyprland",
"hyprutils"
],
"hyprwayland-scanner": [
"omarchy-nix",
"hyprland",
"hyprwayland-scanner"
],
"nixpkgs": [
"omarchy-nix",
"hyprland",
"nixpkgs"
],
"systems": [
"omarchy-nix",
"hyprland",
"systems"
]
},
"locked": {
"lastModified": 1753216019,
"narHash": "sha256-zik7WISrR1ks2l6T1MZqZHb/OqroHdJnSnAehkE0kCk=",
"owner": "hyprwm",
"repo": "aquamarine",
"rev": "be166e11d86ba4186db93e10c54a141058bdce49",
"type": "github"
},
"original": {
"owner": "hyprwm",
"repo": "aquamarine",
"type": "github"
}
},
"base16-schemes": { "base16-schemes": {
"flake": false, "flake": false,
"locked": { "locked": {
@@ -62,11 +25,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1761420385, "lastModified": 1769196967,
"narHash": "sha256-bfBSmKNHry6L/NGBmdymmEA5P/XLzuLDRw2kqaHDsLc=", "narHash": "sha256-js2jXLzaZbXNFkYTszQntIS8QUJYJumSFK+3bR5nhlo=",
"owner": "nix-community", "owner": "nix-community",
"repo": "browser-previews", "repo": "browser-previews",
"rev": "c5eae237f38310ed4c9bea0f6c19e0fe04ad61ef", "rev": "edc3b1c0455abc74bfe2d6e029abe5fc778b0d62",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -84,11 +47,11 @@
"utils": "utils" "utils": "utils"
}, },
"locked": { "locked": {
"lastModified": 1756719547, "lastModified": 1766051518,
"narHash": "sha256-N9gBKUmjwRKPxAafXEk1EGadfk2qDZPBQp4vXWPHINQ=", "narHash": "sha256-znKOwPXQnt3o7lDb3hdf19oDo0BLP4MfBOYiWkEHoik=",
"owner": "serokell", "owner": "serokell",
"repo": "deploy-rs", "repo": "deploy-rs",
"rev": "125ae9e3ecf62fb2c0fd4f2d894eb971f1ecaed2", "rev": "d5eff7f948535b9c723d60cd8239f8f11ddc90fa",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -105,11 +68,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1741473158, "lastModified": 1768818222,
"narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=", "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=",
"owner": "numtide", "owner": "numtide",
"repo": "devshell", "repo": "devshell",
"rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0", "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -125,11 +88,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1760701190, "lastModified": 1768923567,
"narHash": "sha256-y7UhnWlER8r776JsySqsbTUh2Txf7K30smfHlqdaIQw=", "narHash": "sha256-GVJ0jKsyXLuBzRMXCDY6D5J8wVdwP1DuQmmvYL/Vw/Q=",
"owner": "nix-community", "owner": "nix-community",
"repo": "disko", "repo": "disko",
"rev": "3a9450b26e69dcb6f8de6e2b07b3fc1c288d85f5", "rev": "00395d188e3594a1507f214a2f15d4ce5c07cb28",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -141,7 +104,6 @@
"ethereum-nix": { "ethereum-nix": {
"inputs": { "inputs": {
"devshell": "devshell", "devshell": "devshell",
"flake-compat": "flake-compat_2",
"flake-parts": "flake-parts", "flake-parts": "flake-parts",
"flake-utils": "flake-utils_2", "flake-utils": "flake-utils_2",
"foundry-nix": "foundry-nix", "foundry-nix": "foundry-nix",
@@ -153,11 +115,11 @@
"treefmt-nix": "treefmt-nix" "treefmt-nix": "treefmt-nix"
}, },
"locked": { "locked": {
"lastModified": 1761647152, "lastModified": 1769298686,
"narHash": "sha256-zuciw00Auu3tNquWiznfIVxgVZepLsaIqC7cwC1+GQ4=", "narHash": "sha256-ZwsxXeLyrb5VinFsdjrjt/J7Tp5O2A9yy7lxWaw/h78=",
"owner": "nix-community", "owner": "nix-community",
"repo": "ethereum.nix", "repo": "ethereum.nix",
"rev": "7046ba564c47d7cb298493175ea3c3e9b1186c2e", "rev": "d52663e0592ced611098f80224b45e57d7223453",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -182,47 +144,16 @@
"type": "github" "type": "github"
} }
}, },
"flake-compat_2": {
"locked": {
"lastModified": 1746162366,
"narHash": "sha256-5SSSZ/oQkwfcAz/o/6TlejlVGqeK08wyREBQ5qFFPhM=",
"owner": "nix-community",
"repo": "flake-compat",
"rev": "0f158086a2ecdbb138cd0429410e44994f1b7e4b",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_3": {
"flake": false,
"locked": {
"lastModified": 1747046372,
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": { "flake-parts": {
"inputs": { "inputs": {
"nixpkgs-lib": "nixpkgs-lib" "nixpkgs-lib": "nixpkgs-lib"
}, },
"locked": { "locked": {
"lastModified": 1760948891, "lastModified": 1768135262,
"narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=", "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04", "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -239,11 +170,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1760948891, "lastModified": 1768135262,
"narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=", "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04", "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -294,24 +225,6 @@
"type": "github" "type": "github"
} }
}, },
"flake-utils_3": {
"inputs": {
"systems": "systems_4"
},
"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"
}
},
"foundry-nix": { "foundry-nix": {
"inputs": { "inputs": {
"flake-utils": [ "flake-utils": [
@@ -324,11 +237,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1759569036, "lastModified": 1767517855,
"narHash": "sha256-FuxbXLDArxD1NeRR8zNnsb8Xww5/+qdMwzN1m8Kow/M=", "narHash": "sha256-LnZosb07bahYAyFw07JFzSXslx9j1dCe+npWDZdPFZg=",
"owner": "shazow", "owner": "shazow",
"repo": "foundry.nix", "repo": "foundry.nix",
"rev": "47ba6d3b02bf3faaa857d3572df82ff186d5279a", "rev": "ee376e8a93f537c2865dda9811e748e4567a7aaf",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -338,29 +251,6 @@
"type": "github" "type": "github"
} }
}, },
"gitignore": {
"inputs": {
"nixpkgs": [
"omarchy-nix",
"hyprland",
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"home-manager": { "home-manager": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@@ -368,341 +258,57 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1758463745, "lastModified": 1768949235,
"narHash": "sha256-uhzsV0Q0I9j2y/rfweWeGif5AWe0MGrgZ/3TjpDYdGA=", "narHash": "sha256-TtjKgXyg1lMfh374w5uxutd6Vx2P/hU81aEhTxrO2cg=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "3b955f5f0a942f9f60cdc9cacb7844335d0f21c3", "rev": "75ed713570ca17427119e7e204ab3590cc3bf2a5",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nix-community", "owner": "nix-community",
"ref": "release-25.05", "ref": "release-25.11",
"repo": "home-manager", "repo": "home-manager",
"type": "github" "type": "github"
} }
}, },
"hyprcursor": { "home-manager_2": {
"inputs": { "inputs": {
"hyprlang": [
"omarchy-nix",
"hyprland",
"hyprlang"
],
"nixpkgs": [ "nixpkgs": [
"omarchy-nix", "impermanence",
"hyprland",
"nixpkgs" "nixpkgs"
],
"systems": [
"omarchy-nix",
"hyprland",
"systems"
] ]
}, },
"locked": { "locked": {
"lastModified": 1753964049, "lastModified": 1768598210,
"narHash": "sha256-lIqabfBY7z/OANxHoPeIrDJrFyYy9jAM4GQLzZ2feCM=", "narHash": "sha256-kkgA32s/f4jaa4UG+2f8C225Qvclxnqs76mf8zvTVPg=",
"owner": "hyprwm", "owner": "nix-community",
"repo": "hyprcursor", "repo": "home-manager",
"rev": "44e91d467bdad8dcf8bbd2ac7cf49972540980a5", "rev": "c47b2cc64a629f8e075de52e4742de688f930dc6",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "hyprwm", "owner": "nix-community",
"repo": "hyprcursor", "repo": "home-manager",
"type": "github"
}
},
"hyprgraphics": {
"inputs": {
"hyprutils": [
"omarchy-nix",
"hyprland",
"hyprutils"
],
"nixpkgs": [
"omarchy-nix",
"hyprland",
"nixpkgs"
],
"systems": [
"omarchy-nix",
"hyprland",
"systems"
]
},
"locked": {
"lastModified": 1754305013,
"narHash": "sha256-u+M2f0Xf1lVHzIPQ7DsNCDkM1NYxykOSsRr4t3TbSM4=",
"owner": "hyprwm",
"repo": "hyprgraphics",
"rev": "4c1d63a0f22135db123fc789f174b89544c6ec2d",
"type": "github"
},
"original": {
"owner": "hyprwm",
"repo": "hyprgraphics",
"type": "github"
}
},
"hyprland": {
"inputs": {
"aquamarine": "aquamarine",
"hyprcursor": "hyprcursor",
"hyprgraphics": "hyprgraphics",
"hyprland-protocols": "hyprland-protocols",
"hyprland-qtutils": "hyprland-qtutils",
"hyprlang": "hyprlang",
"hyprutils": "hyprutils",
"hyprwayland-scanner": "hyprwayland-scanner",
"nixpkgs": "nixpkgs_2",
"pre-commit-hooks": "pre-commit-hooks",
"systems": "systems_6",
"xdph": "xdph"
},
"locked": {
"lastModified": 1755184403,
"narHash": "sha256-VI+ZPD/uIFjzYW8IcyvBgvwyDIvUe4/xh/kOHTbITX8=",
"owner": "hyprwm",
"repo": "Hyprland",
"rev": "60d769a89908c29e19100059985db15a7b6bab6a",
"type": "github"
},
"original": {
"owner": "hyprwm",
"repo": "Hyprland",
"type": "github"
}
},
"hyprland-protocols": {
"inputs": {
"nixpkgs": [
"omarchy-nix",
"hyprland",
"nixpkgs"
],
"systems": [
"omarchy-nix",
"hyprland",
"systems"
]
},
"locked": {
"lastModified": 1749046714,
"narHash": "sha256-kymV5FMnddYGI+UjwIw8ceDjdeg7ToDVjbHCvUlhn14=",
"owner": "hyprwm",
"repo": "hyprland-protocols",
"rev": "613878cb6f459c5e323aaafe1e6f388ac8a36330",
"type": "github"
},
"original": {
"owner": "hyprwm",
"repo": "hyprland-protocols",
"type": "github"
}
},
"hyprland-qt-support": {
"inputs": {
"hyprlang": [
"omarchy-nix",
"hyprland",
"hyprland-qtutils",
"hyprlang"
],
"nixpkgs": [
"omarchy-nix",
"hyprland",
"hyprland-qtutils",
"nixpkgs"
],
"systems": [
"omarchy-nix",
"hyprland",
"hyprland-qtutils",
"systems"
]
},
"locked": {
"lastModified": 1749154592,
"narHash": "sha256-DO7z5CeT/ddSGDEnK9mAXm1qlGL47L3VAHLlLXoCjhE=",
"owner": "hyprwm",
"repo": "hyprland-qt-support",
"rev": "4c8053c3c888138a30c3a6c45c2e45f5484f2074",
"type": "github"
},
"original": {
"owner": "hyprwm",
"repo": "hyprland-qt-support",
"type": "github"
}
},
"hyprland-qtutils": {
"inputs": {
"hyprland-qt-support": "hyprland-qt-support",
"hyprlang": [
"omarchy-nix",
"hyprland",
"hyprlang"
],
"hyprutils": [
"omarchy-nix",
"hyprland",
"hyprland-qtutils",
"hyprlang",
"hyprutils"
],
"nixpkgs": [
"omarchy-nix",
"hyprland",
"nixpkgs"
],
"systems": [
"omarchy-nix",
"hyprland",
"systems"
]
},
"locked": {
"lastModified": 1753819801,
"narHash": "sha256-tHe6XeNeVeKapkNM3tcjW4RuD+tB2iwwoogWJOtsqTI=",
"owner": "hyprwm",
"repo": "hyprland-qtutils",
"rev": "b308a818b9dcaa7ab8ccab891c1b84ebde2152bc",
"type": "github"
},
"original": {
"owner": "hyprwm",
"repo": "hyprland-qtutils",
"type": "github"
}
},
"hyprlang": {
"inputs": {
"hyprutils": [
"omarchy-nix",
"hyprland",
"hyprutils"
],
"nixpkgs": [
"omarchy-nix",
"hyprland",
"nixpkgs"
],
"systems": [
"omarchy-nix",
"hyprland",
"systems"
]
},
"locked": {
"lastModified": 1753622892,
"narHash": "sha256-0K+A+gmOI8IklSg5It1nyRNv0kCNL51duwnhUO/B8JA=",
"owner": "hyprwm",
"repo": "hyprlang",
"rev": "23f0debd2003f17bd65f851cd3f930cff8a8c809",
"type": "github"
},
"original": {
"owner": "hyprwm",
"repo": "hyprlang",
"type": "github"
}
},
"hyprutils": {
"inputs": {
"nixpkgs": [
"omarchy-nix",
"hyprland",
"nixpkgs"
],
"systems": [
"omarchy-nix",
"hyprland",
"systems"
]
},
"locked": {
"lastModified": 1754481650,
"narHash": "sha256-6u6HdEFJh5gY6VfyMQbhP7zDdVcqOrCDTkbiHJmAtMI=",
"owner": "hyprwm",
"repo": "hyprutils",
"rev": "df6b8820c4a0835d83d0c7c7be86fbc555f1f7fd",
"type": "github"
},
"original": {
"owner": "hyprwm",
"repo": "hyprutils",
"type": "github"
}
},
"hyprwayland-scanner": {
"inputs": {
"nixpkgs": [
"omarchy-nix",
"hyprland",
"nixpkgs"
],
"systems": [
"omarchy-nix",
"hyprland",
"systems"
]
},
"locked": {
"lastModified": 1751897909,
"narHash": "sha256-FnhBENxihITZldThvbO7883PdXC/2dzW4eiNvtoV5Ao=",
"owner": "hyprwm",
"repo": "hyprwayland-scanner",
"rev": "fcca0c61f988a9d092cbb33e906775014c61579d",
"type": "github"
},
"original": {
"owner": "hyprwm",
"repo": "hyprwayland-scanner",
"type": "github" "type": "github"
} }
}, },
"impermanence": { "impermanence": {
"locked": {
"lastModified": 1737831083,
"narHash": "sha256-LJggUHbpyeDvNagTUrdhe/pRVp4pnS6wVKALS782gRI=",
"owner": "nix-community",
"repo": "impermanence",
"rev": "4b3e914cdf97a5b536a889e939fb2fd2b043a170",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "impermanence",
"type": "github"
}
},
"ixx": {
"inputs": { "inputs": {
"flake-utils": [ "home-manager": "home-manager_2",
"nixvim", "nixpkgs": "nixpkgs"
"nuschtosSearch",
"flake-utils"
],
"nixpkgs": [
"nixvim",
"nuschtosSearch",
"nixpkgs"
]
}, },
"locked": { "locked": {
"lastModified": 1754860581, "lastModified": 1768941735,
"narHash": "sha256-EM0IE63OHxXCOpDHXaTyHIOk2cNvMCGPqLt/IdtVxgk=", "narHash": "sha256-OyxsfXNcOkt06/kM+4bnuC8moDx+t7Qr+RB0BBa83Ig=",
"owner": "NuschtOS", "owner": "nix-community",
"repo": "ixx", "repo": "impermanence",
"rev": "babfe85a876162c4acc9ab6fb4483df88fa1f281", "rev": "69ecf31e8fddc9354a4b418f3a517445d486bb54",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NuschtOS", "owner": "nix-community",
"ref": "v0.1.1", "repo": "impermanence",
"repo": "ixx",
"type": "github" "type": "github"
} }
}, },
@@ -732,11 +338,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1761451000, "lastModified": 1765267181,
"narHash": "sha256-qBJL6xEIjqYq9zOcG2vf2nPTeVBppNJzvO0LuQWMwMo=", "narHash": "sha256-d3NBA9zEtBu2JFMnTBqWj7Tmi7R5OikoU2ycrdhQEws=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nix-index-database", "repo": "nix-index-database",
"rev": "ed6b293161b378a7368cda38659eb8d3d9a0dac4", "rev": "82befcf7dc77c909b0f2a09f5da910ec95c5b78f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -747,11 +353,11 @@
}, },
"nixos-hardware": { "nixos-hardware": {
"locked": { "locked": {
"lastModified": 1760958188, "lastModified": 1769302137,
"narHash": "sha256-2m1S4jl+GEDtlt2QqeHil8Ny456dcGSKJAM7q3j/BFU=", "narHash": "sha256-QEDtctEkOsbx8nlFh4yqPEOtr4tif6KTqWwJ37IM2ds=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixos-hardware", "repo": "nixos-hardware",
"rev": "d6645c340ef7d821602fd2cd199e8d1eed10afbc", "rev": "a351494b0e35fd7c0b7a1aae82f0afddf4907aa8",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -763,27 +369,27 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1761468971, "lastModified": 1768564909,
"narHash": "sha256-vY2OLVg5ZTobdroQKQQSipSIkHlxOTrIF1fsMzPh8w8=", "narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=",
"owner": "NixOS", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "78e34d1667d32d8a0ffc3eba4591ff256e80576e", "rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "nixos",
"ref": "nixos-25.05", "ref": "nixos-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
}, },
"nixpkgs-lib": { "nixpkgs-lib": {
"locked": { "locked": {
"lastModified": 1754788789, "lastModified": 1765674936,
"narHash": "sha256-x2rJ+Ovzq0sCMpgfgGaaqgBSwY+LST+WbZ6TytnT9Rk=", "narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nixpkgs.lib", "repo": "nixpkgs.lib",
"rev": "a73b9c743612e4244d865a2fdee11865283c04e6", "rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -809,11 +415,11 @@
}, },
"nixpkgs-unstable": { "nixpkgs-unstable": {
"locked": { "locked": {
"lastModified": 1761349956, "lastModified": 1769092226,
"narHash": "sha256-tH3wHnOJms+U4k/rK2Nn1RfBrhffX92jLP/2VndSn0w=", "narHash": "sha256-6h5sROT/3CTHvzPy9koKBmoCa2eJKh4fzQK8eYFEgl8=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "02f2cb8e0feb4596d20cc52fda73ccee960e3538", "rev": "b579d443b37c9c5373044201ea77604e37e748c8",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -825,11 +431,11 @@
}, },
"nixpkgs-unstable_2": { "nixpkgs-unstable_2": {
"locked": { "locked": {
"lastModified": 1761373498, "lastModified": 1769170682,
"narHash": "sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c=", "narHash": "sha256-oMmN1lVQU0F0W2k6OI3bgdzp2YOHWYUAw79qzDSjenU=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce", "rev": "c5296fdd05cfa2c187990dd909864da9658df755",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -841,16 +447,16 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1754725699, "lastModified": 1769089682,
"narHash": "sha256-iAcj9T/Y+3DBy2J0N+yF9XQQQ8IEb5swLFzs23CdP88=", "narHash": "sha256-9yA/LIuAVQq0lXelrZPjLuLVuZdm03p8tfmHhnDIkms=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "85dbfc7aaf52ecb755f87e577ddbe6dbbdbc1054", "rev": "078d69f03934859a181e81ba987c2bb033eebfc5",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-unstable", "ref": "nixos-25.11",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
@@ -861,15 +467,14 @@
"nixpkgs": [ "nixpkgs": [
"nixpkgs-unstable" "nixpkgs-unstable"
], ],
"nuschtosSearch": "nuschtosSearch", "systems": "systems_4"
"systems": "systems_5"
}, },
"locked": { "locked": {
"lastModified": 1761657569, "lastModified": 1769247851,
"narHash": "sha256-2D4Tw5Vp52RU5amnBvq0/z+zgZqafwl4bhg8dJBBjXI=", "narHash": "sha256-fbsopU0qWfqq1WRKjWYpYCMxmEYyq+Cmw++VXVke5Ns=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nixvim", "repo": "nixvim",
"rev": "03c0dabb9a63f52bc2ebf571f3755720df1ca81e", "rev": "34a7d94cdcd2b034eb06202992bed1345aa046c9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -878,78 +483,6 @@
"type": "github" "type": "github"
} }
}, },
"nuschtosSearch": {
"inputs": {
"flake-utils": "flake-utils_3",
"ixx": "ixx",
"nixpkgs": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1760652422,
"narHash": "sha256-C88Pgz38QIl9JxQceexqL2G7sw9vodHWx1Uaq+NRJrw=",
"owner": "NuschtOS",
"repo": "search",
"rev": "3ebeebe8b6a49dfb11f771f761e0310f7c48d726",
"type": "github"
},
"original": {
"owner": "NuschtOS",
"repo": "search",
"type": "github"
}
},
"omarchy-nix": {
"inputs": {
"home-manager": [
"home-manager"
],
"hyprland": "hyprland",
"nix-colors": "nix-colors",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1760558991,
"narHash": "sha256-E8MMVwy7QNURBtCLiCjFXfv7uZUEg6QVSZLu4q9YGpk=",
"owner": "henrysipp",
"repo": "omarchy-nix",
"rev": "fba993c589920fbe68d9f7918e52903c476adad2",
"type": "github"
},
"original": {
"owner": "henrysipp",
"repo": "omarchy-nix",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": "flake-compat_3",
"gitignore": "gitignore",
"nixpkgs": [
"omarchy-nix",
"hyprland",
"nixpkgs"
]
},
"locked": {
"lastModified": 1754416808,
"narHash": "sha256-c6yg0EQ9xVESx6HGDOCMcyRSjaTpNJP10ef+6fRcofA=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "9c52372878df6911f9afc1e2a1391f55e4dfc864",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"browser-previews": "browser-previews", "browser-previews": "browser-previews",
@@ -958,12 +491,12 @@
"ethereum-nix": "ethereum-nix", "ethereum-nix": "ethereum-nix",
"home-manager": "home-manager", "home-manager": "home-manager",
"impermanence": "impermanence", "impermanence": "impermanence",
"nix-colors": "nix-colors",
"nix-index-database": "nix-index-database", "nix-index-database": "nix-index-database",
"nixos-hardware": "nixos-hardware", "nixos-hardware": "nixos-hardware",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs_2",
"nixpkgs-unstable": "nixpkgs-unstable_2", "nixpkgs-unstable": "nixpkgs-unstable_2",
"nixvim": "nixvim", "nixvim": "nixvim",
"omarchy-nix": "omarchy-nix",
"sops-nix": "sops-nix" "sops-nix": "sops-nix"
} }
}, },
@@ -974,11 +507,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1760998189, "lastModified": 1769314333,
"narHash": "sha256-ee2e1/AeGL5X8oy/HXsZQvZnae6XfEVdstGopKucYLY=", "narHash": "sha256-+Uvq9h2eGsbhacXpuS7irYO7fFlz514nrhPCSTkASlw=",
"owner": "Mic92", "owner": "Mic92",
"repo": "sops-nix", "repo": "sops-nix",
"rev": "5a7d18b5c55642df5c432aadb757140edfeb70b3", "rev": "2eb9eed7ef48908e0f02985919f7eb9d33fa758f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -1047,36 +580,6 @@
"type": "github" "type": "github"
} }
}, },
"systems_5": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_6": {
"locked": {
"lastModified": 1689347949,
"narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
"owner": "nix-systems",
"repo": "default-linux",
"rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default-linux",
"type": "github"
}
},
"treefmt-nix": { "treefmt-nix": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@@ -1085,11 +588,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1761311587, "lastModified": 1768158989,
"narHash": "sha256-Msq86cR5SjozQGCnC6H8C+0cD4rnx91BPltZ9KK613Y=", "narHash": "sha256-67vyT1+xClLldnumAzCTBvU0jLZ1YBcf4vANRWP3+Ak=",
"owner": "numtide", "owner": "numtide",
"repo": "treefmt-nix", "repo": "treefmt-nix",
"rev": "2eddae033e4e74bf581c2d1dfa101f9033dbd2dc", "rev": "e96d59dff5c0d7fddb9d113ba108f03c3ef99eca",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -1115,53 +618,6 @@
"repo": "flake-utils", "repo": "flake-utils",
"type": "github" "type": "github"
} }
},
"xdph": {
"inputs": {
"hyprland-protocols": [
"omarchy-nix",
"hyprland",
"hyprland-protocols"
],
"hyprlang": [
"omarchy-nix",
"hyprland",
"hyprlang"
],
"hyprutils": [
"omarchy-nix",
"hyprland",
"hyprutils"
],
"hyprwayland-scanner": [
"omarchy-nix",
"hyprland",
"hyprwayland-scanner"
],
"nixpkgs": [
"omarchy-nix",
"hyprland",
"nixpkgs"
],
"systems": [
"omarchy-nix",
"hyprland",
"systems"
]
},
"locked": {
"lastModified": 1753633878,
"narHash": "sha256-js2sLRtsOUA/aT10OCDaTjO80yplqwOIaLUqEe0nMx0=",
"owner": "hyprwm",
"repo": "xdg-desktop-portal-hyprland",
"rev": "371b96bd11ad2006ed4f21229dbd1be69bed3e8a",
"type": "github"
},
"original": {
"owner": "hyprwm",
"repo": "xdg-desktop-portal-hyprland",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View File

@@ -5,7 +5,7 @@
deploy-rs.url = "github:serokell/deploy-rs"; deploy-rs.url = "github:serokell/deploy-rs";
deploy-rs.inputs.nixpkgs.follows = "nixpkgs"; deploy-rs.inputs.nixpkgs.follows = "nixpkgs";
impermanence.url = "github:nix-community/impermanence"; impermanence.url = "github:nix-community/impermanence";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable"; nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable";
disko.url = "github:nix-community/disko"; disko.url = "github:nix-community/disko";
disko.inputs.nixpkgs.follows = "nixpkgs"; disko.inputs.nixpkgs.follows = "nixpkgs";
@@ -14,7 +14,7 @@
inputs.nixpkgs.follows = "nixpkgs-unstable"; inputs.nixpkgs.follows = "nixpkgs-unstable";
}; };
home-manager = { home-manager = {
url = "github:nix-community/home-manager/release-25.05"; url = "github:nix-community/home-manager/release-25.11";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
nix-index-database = { nix-index-database = {
@@ -33,11 +33,7 @@
url = "github:nix-community/browser-previews"; url = "github:nix-community/browser-previews";
inputs.nixpkgs.follows = "nixpkgs-unstable"; inputs.nixpkgs.follows = "nixpkgs-unstable";
}; };
omarchy-nix = { nix-colors.url = "github:misterio77/nix-colors";
url = "github:henrysipp/omarchy-nix";
inputs.nixpkgs.follows = "nixpkgs";
inputs.home-manager.follows = "home-manager";
};
nixos-hardware.url = "github:NixOS/nixos-hardware/master"; nixos-hardware.url = "github:NixOS/nixos-hardware/master";
}; };
@@ -53,7 +49,7 @@
impermanence, impermanence,
sops-nix, sops-nix,
browser-previews, browser-previews,
omarchy-nix, nix-colors,
nixos-hardware, nixos-hardware,
... ...
}@inputs: }@inputs:
@@ -62,15 +58,17 @@
overlay-unstable = final: prev: { overlay-unstable = final: prev: {
unstable = import nixpkgs-unstable { unstable = import nixpkgs-unstable {
inherit (prev) system; system = prev.stdenv.hostPlatform.system;
config.allowUnfree = true; config.allowUnfree = true;
}; };
}; };
overlay-browser-previews = final: prev: { overlay-browser-previews = final: prev: {
browser-previews = browser-previews.packages.${prev.system}; browser-previews = browser-previews.packages.${prev.stdenv.hostPlatform.system};
}; };
overlay-custom = import ./pkgs;
mkHost = mkHost =
system: profile: modules: system: profile: modules:
let let
@@ -83,7 +81,7 @@
( (
{ config, pkgs, ... }: { config, pkgs, ... }:
{ {
nixpkgs.overlays = [ overlay-unstable overlay-browser-previews ]; nixpkgs.overlays = [ overlay-unstable overlay-browser-previews overlay-custom ];
nixpkgs.config.allowUnfree = true; nixpkgs.config.allowUnfree = true;
} }
) )
@@ -93,37 +91,27 @@
home-manager.nixosModules.home-manager home-manager.nixosModules.home-manager
( (
{ lib, ... }: { lib, ... }:
lib.mkMerge [ {
{ home-manager = {
home-manager = { useGlobalPkgs = true;
useGlobalPkgs = true; useUserPackages = true;
useUserPackages = true; users.ppetru = {
users.ppetru = { imports = [
imports = [ inputs.nix-index-database.homeModules.nix-index
inputs.nix-index-database.homeModules.nix-index inputs.nixvim.homeModules.nixvim
inputs.nixvim.homeModules.nixvim ./home
./home ] ++ lib.optionals (profile == "desktop") [
] ++ lib.optionals (profile == "desktop") [ nix-colors.homeManagerModules.default
omarchy-nix.homeManagerModules.default ];
];
};
extraSpecialArgs = {
inherit profile;
};
}; };
} extraSpecialArgs = {
(lib.optionalAttrs (profile == "desktop") { inherit profile nix-colors;
omarchy = {
full_name = "Petru Paler";
email_address = "petru@paler.net";
theme = "tokyo-night";
monitors = [ "DP-1,preferred,auto,1.5" ];
}; };
}) };
] }
) )
] ++ nixpkgs.lib.optionals (profile == "desktop") [ ] ++ nixpkgs.lib.optionals (profile == "desktop") [
omarchy-nix.nixosModules.default ./common/desktop
] ++ modules; ] ++ modules;
specialArgs = { specialArgs = {
inherit inputs self; inherit inputs self;
@@ -159,12 +147,9 @@
c2 = mkHost "x86_64-linux" "minimal" [ ./hosts/c2 ]; c2 = mkHost "x86_64-linux" "minimal" [ ./hosts/c2 ];
c3 = mkHost "x86_64-linux" "minimal" [ ./hosts/c3 ]; c3 = mkHost "x86_64-linux" "minimal" [ ./hosts/c3 ];
alo-cloud-1 = mkHost "aarch64-linux" "cloud" [ ./hosts/alo-cloud-1 ]; alo-cloud-1 = mkHost "aarch64-linux" "cloud" [ ./hosts/alo-cloud-1 ];
zippy = mkHost "x86_64-linux" "minimal" [ zippy = mkHost "x86_64-linux" "minimal" [ ./hosts/zippy ];
ethereum-nix.nixosModules.default
./hosts/zippy
];
chilly = mkHost "x86_64-linux" "workstation" [ ./hosts/chilly ]; chilly = mkHost "x86_64-linux" "workstation" [ ./hosts/chilly ];
sparky = mkHost "x86_64-linux" "desktop" [ ./hosts/sparky ]; sparky = mkHost "x86_64-linux" "minimal" [ ./hosts/sparky ];
beefy = mkHost "x86_64-linux" "desktop" [ ./hosts/beefy ]; beefy = mkHost "x86_64-linux" "desktop" [ ./hosts/beefy ];
stinky = mkHost "aarch64-linux" "minimal" [ stinky = mkHost "aarch64-linux" "minimal" [
nixos-hardware.nixosModules.raspberry-pi-4 nixos-hardware.nixosModules.raspberry-pi-4
@@ -196,7 +181,8 @@
}; };
}; };
alo-cloud-1 = { alo-cloud-1 = {
hostname = "49.13.163.72"; hostname = "alo-cloud-1";
#hostname = "49.13.163.72";
profiles = { profiles = {
system = { system = {
user = "root"; user = "root";

View File

@@ -1,6 +1,14 @@
{ pkgs, profile ? "cli", ... }: { pkgs, lib, profile ? "cli", ... }:
let
# Handle both file and directory imports for profiles
# desktop is a directory, others are files
profilePath =
if builtins.pathExists ./programs/${profile}/default.nix
then ./programs/${profile}
else ./programs/${profile}.nix;
in
{ {
imports = [ ./programs/${profile}.nix ]; imports = [ profilePath ];
home = { home = {
packages = (import ./packages.nix { inherit pkgs profile; }).packages; packages = (import ./packages.nix { inherit pkgs profile; }).packages;

View File

@@ -1,13 +1,31 @@
# ABOUTME: Desktop profile package list
# ABOUTME: Extends workstation with GUI and Wayland tools
{ pkgs }: { pkgs }:
let let
workstationProfile = import ./workstation.nix { inherit pkgs; }; workstationProfile = import ./workstation.nix { inherit pkgs; };
# Hyprland ecosystem packages
hyprlandPkgs = with pkgs; [
hyprshot
hyprpicker
hyprsunset
brightnessctl
pamixer
playerctl
gnome-themes-extra
pavucontrol
wl-clip-persist
clipse
];
# Desktop GUI applications
desktopPkgs = with pkgs; [ desktopPkgs = with pkgs; [
browser-previews.google-chrome browser-previews.google-chrome
foot # Wayland-native terminal emulator nautilus
wofi # Application launcher for Wayland blueberry
libnotify
]; ];
in in
{ {
packages = workstationProfile.packages ++ desktopPkgs; packages = workstationProfile.packages ++ hyprlandPkgs ++ desktopPkgs;
} }

View File

@@ -3,6 +3,10 @@ let
serverProfile = import ./server.nix { inherit pkgs; }; serverProfile = import ./server.nix { inherit pkgs; };
cliPkgs = with pkgs; [ cliPkgs = with pkgs; [
ast-grep
yq
unstable.amp-cli
unstable.beads
unstable.claude-code unstable.claude-code
unstable.codex unstable.codex
unstable.gemini-cli unstable.gemini-cli

View File

@@ -1,27 +0,0 @@
{ pkgs, ... }:
{
imports = [ ./workstation.nix ];
# Override ghostty to use unstable version (1.2.0+) for ssh-terminfo support
programs.ghostty.package = pkgs.unstable.ghostty;
wayland.windowManager.hyprland = {
enable = true;
settings = {
# Remap CapsLock to Super (Mod4)
"$mod" = "SUPER";
input = {
kb_options = "caps:super";
};
"$browser" = "google-chrome-stable --new-window --ozone-platform=wayland";
};
};
# Extend ghostty configuration from omarchy-nix
programs.ghostty.settings = {
# Automatically handle TERM compatibility for SSH (requires ghostty 1.2.0+)
shell-integration-features = "ssh-terminfo";
};
}

View File

@@ -0,0 +1,104 @@
# ABOUTME: Btop system monitor configuration with nix-colors theming
# ABOUTME: Creates a custom theme file and configures btop settings
{ config, pkgs, ... }:
let
cfg = import ./config.nix;
palette = config.colorScheme.palette;
in
{
home.file.".config/btop/themes/${cfg.theme}.theme".text = ''
# Main text color
theme[main_fg]="${palette.base05}"
# Title color for boxes
theme[title]="${palette.base05}"
# Highlight color for keyboard shortcuts
theme[hi_fg]="${palette.base0D}"
# Background color of selected item in processes box
theme[selected_bg]="${palette.base01}"
# Foreground color of selected item in processes box
theme[selected_fg]="${palette.base05}"
# Color of inactive/disabled text
theme[inactive_fg]="${palette.base04}"
# Misc colors for processes box
theme[proc_misc]="${palette.base0D}"
# Box outline colors
theme[cpu_box]="${palette.base0B}"
theme[mem_box]="${palette.base09}"
theme[net_box]="${palette.base0E}"
theme[proc_box]="${palette.base0C}"
# Box divider line
theme[div_line]="${palette.base04}"
# Temperature graph colors
theme[temp_start]="${palette.base0B}"
theme[temp_mid]="${palette.base0A}"
theme[temp_end]="${palette.base08}"
# CPU graph colors
theme[cpu_start]="${palette.base0B}"
theme[cpu_mid]="${palette.base0A}"
theme[cpu_end]="${palette.base08}"
# Mem/Disk meters
theme[free_start]="${palette.base0B}"
theme[cached_start]="${palette.base0A}"
theme[available_start]="${palette.base09}"
theme[used_start]="${palette.base08}"
# Network graph colors
theme[download_start]="${palette.base0E}"
theme[download_mid]="${palette.base0D}"
theme[download_end]="${palette.base0C}"
theme[upload_start]="${palette.base0E}"
theme[upload_mid]="${palette.base0D}"
theme[upload_end]="${palette.base0C}"
'';
programs.btop = {
enable = true;
settings = {
color_theme = cfg.theme;
theme_background = false;
truecolor = true;
force_tty = false;
vim_keys = true;
rounded_corners = true;
graph_symbol = "braille";
shown_boxes = "cpu mem net proc";
update_ms = 2000;
proc_sorting = "cpu lazy";
proc_colors = true;
proc_gradient = false;
proc_per_core = false;
proc_mem_bytes = true;
proc_cpu_graphs = true;
show_uptime = true;
check_temp = true;
show_coretemp = true;
temp_scale = "celsius";
show_cpu_freq = true;
clock_format = "%X";
background_update = true;
mem_graphs = true;
show_swap = true;
swap_disk = true;
show_disks = true;
only_physical = true;
use_fstab = true;
show_io_stat = true;
net_auto = true;
net_sync = true;
show_battery = true;
log_level = "WARNING";
};
};
}

View File

@@ -0,0 +1,21 @@
# ABOUTME: Shared configuration values for desktop environment
# ABOUTME: Centralizes user info, theme, fonts, and display settings
{
user = {
fullName = "Petru Paler";
email = "petru@paler.net";
};
theme = "tokyo-night";
base16Theme = "tokyo-night-dark";
primaryFont = "Liberation Sans 11";
monoFont = "CaskaydiaMono Nerd Font";
scale = 1.5;
monitors = [ "DP-1,preferred,auto,1.5" ];
# Wallpaper for tokyo-night theme
wallpaper = "1-Pawel-Czerwinski-Abstract-Purple-Blue.jpg";
}

View File

@@ -0,0 +1,59 @@
# ABOUTME: Desktop environment home-manager configuration
# ABOUTME: Imports all desktop modules and sets up nix-colors theming
{ config, pkgs, lib, nix-colors, ... }:
let
cfg = import ./config.nix;
in
{
imports = [
../workstation.nix
./ghostty.nix
./hyprland
./waybar.nix
./wofi.nix
./mako.nix
./hyprpaper.nix
./hypridle.nix
./hyprlock.nix
./starship.nix
./vscode.nix
./btop.nix
./git.nix
];
# Set up nix-colors with our theme
colorScheme = nix-colors.colorSchemes.${cfg.base16Theme};
# Override ghostty to use unstable version (1.2.0+) for ssh-terminfo support
programs.ghostty.package = pkgs.unstable.ghostty;
# Extend ghostty configuration
programs.ghostty.settings = {
shell-integration-features = "ssh-terminfo";
};
# GTK theme (dark for tokyo-night)
gtk = {
enable = true;
theme = {
name = "Adwaita-dark";
package = pkgs.gnome-themes-extra;
};
};
# Enable neovim (placeholder for future config)
programs.neovim.enable = true;
# direnv
programs.direnv = {
enable = true;
nix-direnv.enable = true;
};
# zoxide (directory jumping)
programs.zoxide = {
enable = true;
enableBashIntegration = true;
};
}

View File

@@ -0,0 +1,60 @@
# ABOUTME: Ghostty terminal emulator configuration with nix-colors theming
# ABOUTME: Creates a custom color theme from the nix-colors palette
{ config, pkgs, ... }:
let
cfg = import ./config.nix;
palette = config.colorScheme.palette;
in
{
programs.ghostty = {
enable = true;
settings = {
window-padding-x = 14;
window-padding-y = 14;
background-opacity = 0.95;
window-decoration = "none";
font-family = cfg.monoFont;
font-size = 12;
theme = "desktop-theme";
keybind = [
"ctrl+k=reset"
];
};
themes = {
desktop-theme = {
background = "#${palette.base00}";
foreground = "#${palette.base05}";
selection-background = "#${palette.base02}";
selection-foreground = "#${palette.base00}";
palette = [
"0=#${palette.base00}"
"1=#${palette.base08}"
"2=#${palette.base0B}"
"3=#${palette.base0A}"
"4=#${palette.base0D}"
"5=#${palette.base0E}"
"6=#${palette.base0C}"
"7=#${palette.base05}"
"8=#${palette.base03}"
"9=#${palette.base08}"
"10=#${palette.base0B}"
"11=#${palette.base0A}"
"12=#${palette.base0D}"
"13=#${palette.base0E}"
"14=#${palette.base0C}"
"15=#${palette.base07}"
"16=#${palette.base09}"
"17=#${palette.base0F}"
"18=#${palette.base01}"
"19=#${palette.base02}"
"20=#${palette.base04}"
"21=#${palette.base06}"
];
};
};
};
}

View File

@@ -0,0 +1,24 @@
# ABOUTME: Git and GitHub CLI configuration
# ABOUTME: Sets up git with user info and gh CLI integration
{ config, pkgs, ... }:
let
cfg = import ./config.nix;
in
{
programs.git = {
enable = true;
settings = {
user.name = cfg.user.fullName;
user.email = cfg.user.email;
credential.helper = "store";
};
};
programs.gh = {
enable = true;
gitCredentialHelper = {
enable = true;
};
};
}

View File

@@ -0,0 +1,27 @@
# ABOUTME: Hypridle idle daemon configuration
# ABOUTME: Handles screen locking and DPMS after idle timeout
{ config, pkgs, ... }:
{
services.hypridle = {
enable = true;
settings = {
general = {
lock_cmd = "pidof hyprlock || hyprlock";
before_sleep_cmd = "loginctl lock-session";
after_sleep_cmd = "hyprctl dispatch dpms on";
};
listener = [
{
timeout = 300;
on-timeout = "loginctl lock-session";
}
{
timeout = 330;
on-timeout = "hyprctl dispatch dpms off";
on-resume = "hyprctl dispatch dpms on && brightnessctl -r";
}
];
};
};
}

View File

@@ -0,0 +1,17 @@
# ABOUTME: Hyprland autostart configuration
# ABOUTME: Defines programs to run at Hyprland startup
{ config, pkgs, ... }:
{
wayland.windowManager.hyprland.settings = {
exec-once = [
"hyprsunset"
"systemctl --user start hyprpolkitagent"
"wl-clip-persist --clipboard regular & clipse -listen"
];
exec = [
"pkill -SIGUSR2 waybar || waybar"
];
};
}

View File

@@ -0,0 +1,99 @@
# ABOUTME: Hyprland keybindings configuration
# ABOUTME: Defines keyboard and mouse shortcuts for window management
{ config, pkgs, ... }:
{
wayland.windowManager.hyprland.settings = {
bind = [
# Application launchers
"$mod, Space, exec, $menu"
"$mod, Return, exec, $terminal"
"$mod, E, exec, $fileManager"
"$mod, B, exec, $browser"
# Window management
"$mod, W, killactive,"
"$mod, BackSpace, killactive,"
"$mod, V, togglefloating,"
"$mod SHIFT, equal, fullscreen,"
"$mod, J, togglesplit,"
"$mod, P, pseudo,"
# Focus navigation
"$mod, left, movefocus, l"
"$mod, right, movefocus, r"
"$mod, up, movefocus, u"
"$mod, down, movefocus, d"
# Workspace switching
"$mod, 1, workspace, 1"
"$mod, 2, workspace, 2"
"$mod, 3, workspace, 3"
"$mod, 4, workspace, 4"
"$mod, 5, workspace, 5"
"$mod, 6, workspace, 6"
"$mod, 7, workspace, 7"
"$mod, 8, workspace, 8"
"$mod, 9, workspace, 9"
"$mod, 0, workspace, 10"
# Move window to workspace
"$mod SHIFT, 1, movetoworkspace, 1"
"$mod SHIFT, 2, movetoworkspace, 2"
"$mod SHIFT, 3, movetoworkspace, 3"
"$mod SHIFT, 4, movetoworkspace, 4"
"$mod SHIFT, 5, movetoworkspace, 5"
"$mod SHIFT, 6, movetoworkspace, 6"
"$mod SHIFT, 7, movetoworkspace, 7"
"$mod SHIFT, 8, movetoworkspace, 8"
"$mod SHIFT, 9, movetoworkspace, 9"
"$mod SHIFT, 0, movetoworkspace, 10"
# Workspace navigation
"$mod, comma, workspace, m-1"
"$mod, period, workspace, m+1"
# Window resize
"$mod, minus, splitratio, -0.1"
"$mod, equal, splitratio, +0.1"
# Lock screen
"$mod, Escape, exec, loginctl lock-session"
# Screenshots
", Print, exec, hyprshot -m region"
"SHIFT, Print, exec, hyprshot -m window"
"CTRL, Print, exec, hyprshot -m output"
# Color picker
"$mod SHIFT, C, exec, hyprpicker -a"
# Clipboard manager
"$mod SHIFT, V, exec, ghostty --class=clipse -e clipse"
];
bindm = [
# Mouse bindings for window management
"$mod, mouse:272, movewindow"
"$mod, mouse:273, resizewindow"
];
binde = [
# Repeatable bindings for media controls
", XF86AudioRaiseVolume, exec, wpctl set-volume -l 1.5 @DEFAULT_AUDIO_SINK@ 5%+"
", XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"
", XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"
# Brightness controls
", XF86MonBrightnessUp, exec, brightnessctl s +5%"
", XF86MonBrightnessDown, exec, brightnessctl s 5%-"
];
bindl = [
# Media player controls
", XF86AudioNext, exec, playerctl next"
", XF86AudioPrev, exec, playerctl previous"
", XF86AudioPlay, exec, playerctl play-pause"
];
};
}

View File

@@ -0,0 +1,39 @@
# ABOUTME: Hyprland window manager home-manager configuration
# ABOUTME: Imports all hyprland submodules for complete WM setup
{ config, pkgs, lib, ... }:
let
cfg = import ../config.nix;
in
{
imports = [
./bindings.nix
./autostart.nix
./input.nix
./looknfeel.nix
./windows.nix
./envs.nix
];
wayland.windowManager.hyprland = {
enable = true;
systemd.enable = true;
settings = {
# Monitor configuration
monitor = cfg.monitors;
# Default applications
"$terminal" = "ghostty";
"$fileManager" = "nautilus";
"$browser" = "google-chrome-stable --new-window --ozone-platform=wayland";
"$menu" = "wofi --show drun";
# Mod key
"$mod" = "SUPER";
};
};
# Hyprland polkit agent for privilege escalation
services.hyprpolkitagent.enable = true;
}

View File

@@ -0,0 +1,56 @@
# ABOUTME: Hyprland environment variables configuration
# ABOUTME: Sets up Wayland, cursor, and application environment variables
{ config, lib, pkgs, osConfig ? { }, ... }:
let
cfg = import ../config.nix;
hasNvidiaDrivers = builtins.elem "nvidia" (osConfig.services.xserver.videoDrivers or []);
nvidiaEnv = [
"NVD_BACKEND,direct"
"LIBVA_DRIVER_NAME,nvidia"
"__GLX_VENDOR_LIBRARY_NAME,nvidia"
];
in
{
wayland.windowManager.hyprland.settings = {
env = (lib.optionals hasNvidiaDrivers nvidiaEnv) ++ [
"GDK_SCALE,${toString cfg.scale}"
# Cursor size and theme
"XCURSOR_SIZE,24"
"HYPRCURSOR_SIZE,24"
"XCURSOR_THEME,Adwaita"
"HYPRCURSOR_THEME,Adwaita"
# Force Wayland for applications
"GDK_BACKEND,wayland"
"QT_QPA_PLATFORM,wayland"
"QT_STYLE_OVERRIDE,kvantum"
"SDL_VIDEODRIVER,wayland"
"MOZ_ENABLE_WAYLAND,1"
"ELECTRON_OZONE_PLATFORM_HINT,wayland"
"OZONE_PLATFORM,wayland"
# Chromium Wayland support
"CHROMIUM_FLAGS,\"--enable-features=UseOzonePlatform --ozone-platform=wayland --gtk-version=4\""
# Make .desktop files available for wofi
"XDG_DATA_DIRS,$XDG_DATA_DIRS:$HOME/.nix-profile/share:/nix/var/nix/profiles/default/share"
# XCompose support
"XCOMPOSEFILE,~/.XCompose"
"EDITOR,nvim"
# GTK dark theme
"GTK_THEME,Adwaita:dark"
];
xwayland = {
force_zero_scaling = true;
};
ecosystem = {
no_update_news = true;
};
};
}

View File

@@ -0,0 +1,19 @@
# ABOUTME: Hyprland input and gesture configuration
# ABOUTME: Keyboard layout, mouse settings, and touchpad behavior
{ config, lib, pkgs, ... }:
{
wayland.windowManager.hyprland.settings = {
input = lib.mkDefault {
kb_layout = "us";
kb_options = "caps:super,compose:ralt";
follow_mouse = 1;
sensitivity = 0;
touchpad = {
natural_scroll = false;
};
};
};
}

View File

@@ -0,0 +1,89 @@
# ABOUTME: Hyprland visual appearance configuration
# ABOUTME: Window gaps, borders, animations, and decorations with nix-colors theming
{ config, pkgs, ... }:
let
palette = config.colorScheme.palette;
hexToRgba = hex: alpha: "rgba(${hex}${alpha})";
inactiveBorder = hexToRgba palette.base09 "aa";
activeBorder = hexToRgba palette.base0D "aa";
in
{
wayland.windowManager.hyprland.settings = {
general = {
gaps_in = 5;
gaps_out = 10;
border_size = 2;
"col.active_border" = activeBorder;
"col.inactive_border" = inactiveBorder;
resize_on_border = false;
allow_tearing = false;
layout = "dwindle";
};
decoration = {
rounding = 4;
shadow = {
enabled = false;
range = 30;
render_power = 3;
ignore_window = true;
color = "rgba(00000045)";
};
blur = {
enabled = true;
size = 5;
passes = 2;
vibrancy = 0.1696;
};
};
animations = {
enabled = true;
bezier = [
"easeOutQuint,0.23,1,0.32,1"
"easeInOutCubic,0.65,0.05,0.36,1"
"linear,0,0,1,1"
"almostLinear,0.5,0.5,0.75,1.0"
"quick,0.15,0,0.1,1"
];
animation = [
"global, 1, 10, default"
"border, 1, 5.39, easeOutQuint"
"windows, 1, 4.79, easeOutQuint"
"windowsIn, 1, 4.1, easeOutQuint, popin 87%"
"windowsOut, 1, 1.49, linear, popin 87%"
"fadeIn, 1, 1.73, almostLinear"
"fadeOut, 1, 1.46, almostLinear"
"fade, 1, 3.03, quick"
"layers, 1, 3.81, easeOutQuint"
"layersIn, 1, 4, easeOutQuint, fade"
"layersOut, 1, 1.5, linear, fade"
"fadeLayersIn, 1, 1.79, almostLinear"
"fadeLayersOut, 1, 1.39, almostLinear"
"workspaces, 0, 0, ease"
];
};
dwindle = {
pseudotile = true;
preserve_split = true;
force_split = 2;
};
master = {
new_status = "master";
};
misc = {
disable_hyprland_logo = true;
disable_splash_rendering = true;
};
};
}

View File

@@ -0,0 +1,31 @@
# ABOUTME: Hyprland window rules configuration
# ABOUTME: Defines per-application window behavior and layer rules
{ config, pkgs, ... }:
{
wayland.windowManager.hyprland.settings = {
windowrule = [
"suppressevent maximize, class:.*"
"tile, class:^(chromium)$"
"float, class:^(org.pulseaudio.pavucontrol|blueberry.py)$"
"float, class:^(steam)$"
"fullscreen, class:^(com.libretro.RetroArch)$"
"opacity 0.97 0.9, class:.*"
"opacity 1 1, class:^(chromium|google-chrome|google-chrome-unstable)$, title:.*Youtube.*"
"opacity 1 0.97, class:^(chromium|google-chrome|google-chrome-unstable)$"
"opacity 0.97 0.9, initialClass:^(chrome-.*-Default)$"
"opacity 1 1, initialClass:^(chrome-youtube.*-Default)$"
"opacity 1 1, class:^(zoom|vlc|org.kde.kdenlive|com.obsproject.Studio)$"
"opacity 1 1, class:^(com.libretro.RetroArch|steam)$"
"nofocus,class:^$,title:^$,xwayland:1,floating:1,fullscreen:0,pinned:0"
"float, class:(clipse)"
"size 622 652, class:(clipse)"
"stayfocused, class:(clipse)"
];
layerrule = [
"blur,wofi"
"blur,waybar"
];
};
}

View File

@@ -0,0 +1,70 @@
# ABOUTME: Hyprlock screen locker configuration with nix-colors theming
# ABOUTME: Configures lock screen appearance with fingerprint support
{ config, pkgs, nix-colors, ... }:
let
cfg = import ./config.nix;
palette = config.colorScheme.palette;
convert = nix-colors.lib.conversions.hexToRGBString;
wallpaperPath = "~/Pictures/Wallpapers/${cfg.wallpaper}";
backgroundRgb = "rgba(${convert ", " palette.base00}, 0.8)";
surfaceRgb = "rgb(${convert ", " palette.base02})";
foregroundRgb = "rgb(${convert ", " palette.base05})";
foregroundMutedRgb = "rgb(${convert ", " palette.base04})";
in
{
programs.hyprlock = {
enable = true;
settings = {
general = {
disable_loading_bar = true;
no_fade_in = false;
};
auth = {
fingerprint.enabled = true;
};
background = {
monitor = "";
path = wallpaperPath;
};
input-field = {
monitor = "";
size = "600, 100";
position = "0, 0";
halign = "center";
valign = "center";
inner_color = surfaceRgb;
outer_color = foregroundRgb;
outline_thickness = 4;
font_family = cfg.monoFont;
font_size = 32;
font_color = foregroundRgb;
placeholder_color = foregroundMutedRgb;
placeholder_text = " Enter Password 󰈷 ";
check_color = "rgba(131, 192, 146, 1.0)";
fail_text = "Wrong";
rounding = 0;
shadow_passes = 0;
fade_on_empty = false;
};
label = {
monitor = "";
text = "$FPRINTPROMPT";
text_align = "center";
color = "rgb(211, 198, 170)";
font_size = 24;
font_family = cfg.monoFont;
position = "0, -100";
halign = "center";
valign = "center";
};
};
};
}

View File

@@ -0,0 +1,23 @@
# ABOUTME: Hyprpaper wallpaper service configuration
# ABOUTME: Sets up wallpaper based on theme selection
{ config, pkgs, ... }:
let
cfg = import ./config.nix;
wallpaperPath = "~/Pictures/Wallpapers/${cfg.wallpaper}";
in
{
# Copy wallpapers to Pictures directory
home.file."Pictures/Wallpapers" = {
source = ../../../common/desktop/assets/wallpapers;
recursive = true;
};
services.hyprpaper = {
enable = true;
settings = {
preload = [ wallpaperPath ];
wallpaper = [ ",${wallpaperPath}" ];
};
};
}

View File

@@ -0,0 +1,41 @@
# ABOUTME: Mako notification daemon configuration with nix-colors theming
# ABOUTME: Configures notification appearance and behavior
{ config, pkgs, ... }:
let
palette = config.colorScheme.palette;
in
{
services.mako = {
enable = true;
settings = {
background-color = "#${palette.base00}";
text-color = "#${palette.base05}";
border-color = "#${palette.base04}";
progress-color = "#${palette.base0D}";
width = 420;
height = 110;
padding = "10";
margin = "10";
border-size = 2;
border-radius = 0;
anchor = "top-right";
layer = "overlay";
default-timeout = 5000;
ignore-timeout = false;
max-visible = 5;
sort = "-time";
group-by = "app-name";
actions = true;
format = "<b>%s</b>\\n%b";
markup = true;
};
};
}

View File

@@ -0,0 +1,7 @@
# ABOUTME: Starship prompt configuration
# ABOUTME: Enables the cross-shell prompt with default settings
{ config, pkgs, ... }:
{
programs.starship.enable = true;
}

View File

@@ -0,0 +1,32 @@
# ABOUTME: Theme definitions mapping theme names to base16 and VSCode themes
# ABOUTME: Used by vscode and other apps that need theme name mapping
{
"tokyo-night" = {
base16Theme = "tokyo-night-dark";
vscodeTheme = "Tokyo Night";
};
"catppuccin-macchiato" = {
vscodeTheme = "Catppuccin Macchiato";
};
"kanagawa" = {
base16Theme = "kanagawa";
vscodeTheme = "Kanagawa";
};
"everforest" = {
base16Theme = "everforest";
vscodeTheme = "Everforest Dark";
};
"nord" = {
base16Theme = "nord";
vscodeTheme = "Nord";
};
"gruvbox" = {
base16Theme = "gruvbox-dark-hard";
vscodeTheme = "Gruvbox Dark Hard";
};
"gruvbox-light" = {
base16Theme = "gruvbox-light-medium";
vscodeTheme = "Gruvbox Light Medium";
};
}

View File

@@ -0,0 +1,54 @@
# ABOUTME: VSCode configuration with theme extensions
# ABOUTME: Installs vim keybindings and color scheme extensions
{ config, pkgs, ... }:
let
cfg = import ./config.nix;
themes = import ./themes.nix;
theme = themes.${cfg.theme};
in
{
programs.vscode = {
enable = true;
profiles.default = {
extensions =
with pkgs.vscode-extensions;
[
bbenoist.nix
vscodevim.vim
]
++ pkgs.vscode-utils.extensionsFromVscodeMarketplace [
{
name = "everforest";
publisher = "sainnhe";
version = "0.3.0";
sha256 = "sha256-nZirzVvM160ZTpBLTimL2X35sIGy5j2LQOok7a2Yc7U=";
}
{
name = "tokyo-night";
publisher = "enkia";
version = "1.1.2";
sha256 = "sha256-oW0bkLKimpcjzxTb/yjShagjyVTUFEg198oPbY5J2hM=";
}
{
name = "kanagawa";
publisher = "qufiwefefwoyn";
version = "1.5.1";
sha256 = "sha256-AGGioXcK/fjPaFaWk2jqLxovUNR59gwpotcSpGNbj1c=";
}
{
name = "nord-visual-studio-code";
publisher = "arcticicestudio";
version = "0.19.0";
sha256 = "sha256-awbqFv6YuYI0tzM/QbHRTUl4B2vNUdy52F4nPmv+dRU=";
}
{
name = "gruvbox";
publisher = "jdinhlife";
version = "1.28.0";
sha256 = "sha256-XwQzbbZU6MfYcT50/0YgQp8UaOeQskEvEQPZXG72lLk=";
}
];
};
};
}

View File

@@ -0,0 +1,182 @@
# ABOUTME: Waybar status bar configuration with nix-colors theming
# ABOUTME: Configures system tray, workspaces, and status indicators
{ config, pkgs, nix-colors, ... }:
let
palette = config.colorScheme.palette;
convert = nix-colors.lib.conversions.hexToRGBString;
backgroundRgb = "rgb(${convert ", " palette.base00})";
foregroundRgb = "rgb(${convert ", " palette.base05})";
in
{
home.file.".config/waybar/theme.css".text = ''
@define-color background ${backgroundRgb};
* {
color: ${foregroundRgb};
}
window#waybar {
background-color: ${backgroundRgb};
}
'';
home.file.".config/waybar/style.css".text = ''
@import "./theme.css";
* {
border: none;
border-radius: 0;
min-height: 0;
font-family: CaskaydiaMono Nerd Font;
font-size: 14px;
}
#workspaces {
margin-left: 7px;
}
#workspaces button {
all: initial;
padding: 2px 6px;
margin-right: 3px;
}
#custom-dropbox,
#cpu,
#power-profiles-daemon,
#battery,
#network,
#bluetooth,
#wireplumber,
#tray,
#clock {
background-color: transparent;
min-width: 12px;
margin-right: 13px;
}
tooltip {
padding: 2px;
}
tooltip label {
padding: 2px;
}
'';
programs.waybar = {
enable = true;
settings = [
{
layer = "top";
position = "top";
spacing = 0;
height = 26;
modules-left = [ "hyprland/workspaces" ];
modules-center = [ "clock" ];
modules-right = [
"tray"
"bluetooth"
"network"
"wireplumber"
"cpu"
"power-profiles-daemon"
"battery"
];
"hyprland/workspaces" = {
on-click = "activate";
format = "{icon}";
format-icons = {
default = "";
"1" = "1";
"2" = "2";
"3" = "3";
"4" = "4";
"5" = "5";
"6" = "6";
"7" = "7";
"8" = "8";
"9" = "9";
active = "󱓻";
};
persistent-workspaces = {
"1" = [ ];
"2" = [ ];
"3" = [ ];
"4" = [ ];
"5" = [ ];
};
};
cpu = {
interval = 5;
format = "󰍛";
on-click = "ghostty -e btop";
};
clock = {
format = "{:%A %I:%M %p}";
format-alt = "{:%d %B W%V %Y}";
tooltip = false;
};
network = {
format-icons = [ "󰤯" "󰤟" "󰤢" "󰤥" "󰤨" ];
format = "{icon}";
format-wifi = "{icon}";
format-ethernet = "󰀂";
format-disconnected = "󰖪";
tooltip-format-wifi = "{essid} ({frequency} GHz)\n{bandwidthDownBytes} {bandwidthUpBytes}";
tooltip-format-ethernet = "{bandwidthDownBytes} {bandwidthUpBytes}";
tooltip-format-disconnected = "Disconnected";
interval = 3;
nospacing = 1;
on-click = "ghostty -e nmcli";
};
battery = {
interval = 5;
format = "{capacity}% {icon}";
format-discharging = "{icon}";
format-charging = "{icon}";
format-plugged = "";
format-icons = {
charging = [ "󰢜" "󰂆" "󰂇" "󰂈" "󰢝" "󰂉" "󰢞" "󰂊" "󰂋" "󰂅" ];
default = [ "󰁺" "󰁻" "󰁼" "󰁽" "󰁾" "󰁿" "󰂀" "󰂁" "󰂂" "󰁹" ];
};
format-full = "Charged ";
tooltip-format-discharging = "{power:>1.0f}W {capacity}%";
tooltip-format-charging = "{power:>1.0f}W {capacity}%";
states = {
warning = 20;
critical = 10;
};
};
bluetooth = {
format = "󰂯";
format-disabled = "󰂲";
format-connected = "";
tooltip-format = "Devices connected: {num_connections}";
on-click = "blueberry";
};
wireplumber = {
format = "";
format-muted = "󰝟";
scroll-step = 5;
on-click = "pavucontrol";
tooltip-format = "Playing at {volume}%";
on-click-right = "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle";
max-volume = 150;
};
tray = {
spacing = 13;
};
power-profiles-daemon = {
format = "{icon}";
tooltip-format = "Power profile: {profile}";
tooltip = true;
format-icons = {
power-saver = "󰡳";
balanced = "󰊚";
performance = "󰡴";
};
};
}
];
};
}

View File

@@ -0,0 +1,102 @@
# ABOUTME: Wofi application launcher configuration with nix-colors theming
# ABOUTME: Configures the drun launcher appearance and behavior
{ config, pkgs, ... }:
let
cfg = import ./config.nix;
palette = config.colorScheme.palette;
in
{
home.file.".config/wofi/style.css".text = ''
* {
font-family: '${cfg.monoFont}', monospace;
font-size: 18px;
}
window {
margin: 0px;
padding: 20px;
background-color: #${palette.base00};
opacity: 0.95;
}
#inner-box {
margin: 0;
padding: 0;
border: none;
background-color: #${palette.base00};
}
#outer-box {
margin: 0;
padding: 20px;
border: none;
background-color: #${palette.base00};
}
#scroll {
margin: 0;
padding: 0;
border: none;
background-color: #${palette.base00};
}
#input {
margin: 0;
padding: 10px;
border: none;
background-color: #${palette.base00};
color: @text;
}
#input:focus {
outline: none;
box-shadow: none;
border: none;
}
#text {
margin: 5px;
border: none;
color: #${palette.base06};
}
#entry {
background-color: #${palette.base00};
}
#entry:selected {
outline: none;
border: none;
}
#entry:selected #text {
color: #${palette.base02};
}
#entry image {
-gtk-icon-transform: scale(0.7);
}
'';
programs.wofi = {
enable = true;
settings = {
width = 600;
height = 350;
location = "center";
show = "drun";
prompt = "Search...";
filter_rate = 100;
allow_markup = true;
no_actions = true;
halign = "fill";
orientation = "vertical";
content_halign = "fill";
insensitive = true;
allow_images = true;
image_size = 40;
gtk_dark = true;
};
};
}

View File

@@ -339,6 +339,16 @@
set pure_show_prefix_root_prompt true set pure_show_prefix_root_prompt true
set sponge_regex_patterns 'password|passwd' set sponge_regex_patterns 'password|passwd'
''; '';
functions = {
brain = ''
echo "🧠 Brain session starting..."
echo " wrap - end session with notes"
echo " inbox: <thought> - quick capture"
echo ""
cd ~/brain && claude
'';
};
}; };
fzf = { fzf = {
@@ -347,8 +357,12 @@
git = { git = {
enable = true; enable = true;
userEmail = "petru@paler.net"; settings = {
userName = "Petru Paler"; user = {
email = "petru@paler.net";
name = "Petru Paler";
};
};
}; };
home-manager = { home-manager = {

View File

@@ -1,8 +1,8 @@
{ pkgs, inputs, ... }: { pkgs, lib, inputs, ... }:
{ {
imports = [ imports = [
../../common/global ../../common/global
../../common/cloud-node.nix # Minimal system with Consul ../../common/minimal-node.nix
./hardware.nix ./hardware.nix
./reverse-proxy.nix ./reverse-proxy.nix
]; ];
@@ -12,4 +12,27 @@
networking.hostName = "alo-cloud-1"; networking.hostName = "alo-cloud-1";
services.tailscaleAutoconnect.authkey = "tskey-auth-kbdARC7CNTRL-pNQddmWV9q5C2sRV3WGep5ehjJ1qvcfD"; services.tailscaleAutoconnect.authkey = "tskey-auth-kbdARC7CNTRL-pNQddmWV9q5C2sRV3WGep5ehjJ1qvcfD";
services.tailscale = {
enable = true;
useRoutingFeatures = lib.mkForce "server"; # enables IPv4/IPv6 forwarding + loose rp_filter
extraUpFlags = [ "--advertise-exit-node" ];
};
networking.nat = {
enable = true;
externalInterface = "enp1s0";
internalInterfaces = [ "tailscale0" ];
};
networking.firewall = {
enable = lib.mkForce true;
allowedTCPPorts = [ 80 443 ]; # Public web traffic only
allowedUDPPorts = [ 41641 ]; # Tailscale
trustedInterfaces = [ "tailscale0" ]; # Full access via VPN
};
services.openssh = {
settings.PasswordAuthentication = false; # Keys only
};
} }

View File

@@ -1,11 +1,13 @@
{ pkgs, inputs, ... }: { pkgs, inputs, config, ... }:
{ {
imports = [ imports = [
../../common/encrypted-btrfs-layout.nix ../../common/encrypted-btrfs-layout.nix
../../common/global ../../common/global
../../common/desktop-node.nix # Hyprland + GUI environment # Desktop environment is imported via flake.nix for desktop profile
../../common/cluster-member.nix # Consul + storage clients ../../common/cluster-member.nix # Consul + storage clients
../../common/cluster-tools.nix # Nomad CLI (no service) ../../common/cluster-tools.nix # Nomad CLI (no service)
../../common/docker.nix # Docker daemon
../../common/ham-radio.nix # Ham radio tools (FLEcli)
./hardware.nix ./hardware.nix
]; ];
@@ -18,4 +20,58 @@
networking.hostName = "beefy"; networking.hostName = "beefy";
networking.cluster.primaryInterface = "enp1s0"; networking.cluster.primaryInterface = "enp1s0";
services.tailscaleAutoconnect.authkey = "tskey-auth-k79UsDTw2v11CNTRL-oYqji35BE9c7CqM89Dzs9cBF14PmqYsi"; services.tailscaleAutoconnect.authkey = "tskey-auth-k79UsDTw2v11CNTRL-oYqji35BE9c7CqM89Dzs9cBF14PmqYsi";
# Console blanking after 5 minutes (for greeter display sleep)
# NMI watchdog for hardlockup detection
boot.kernelParams = [ "consoleblank=300" "nmi_watchdog=1" ];
# Netconsole - stream kernel messages to zippy (192.168.1.2)
# Must configure via configfs after network is up (interface doesn't exist at module load)
boot.kernelModules = [ "netconsole" ];
boot.kernel.sysctl."kernel.printk" = "8 4 1 7"; # Raise console_loglevel to send all messages
systemd.services.netconsole-sender = {
description = "Configure netconsole to send kernel messages to zippy";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
TARGET=/sys/kernel/config/netconsole/target1
mkdir -p $TARGET
# Disable first if already enabled (can't modify params while enabled)
if [ -f $TARGET/enabled ] && [ "$(cat $TARGET/enabled)" = "1" ]; then
echo 0 > $TARGET/enabled
fi
echo enp1s0 > $TARGET/dev_name
echo 192.168.1.2 > $TARGET/remote_ip
echo 6666 > $TARGET/remote_port
echo c0:3f:d5:62:55:bb > $TARGET/remote_mac
echo 1 > $TARGET/enabled
'';
};
# Kdump for kernel crash analysis
boot.crashDump = {
enable = true;
reservedMemory = "256M";
};
# Lockup detectors - panic on detection so kdump captures state
boot.kernel.sysctl = {
# Enable all SysRq functions for debugging hangs
"kernel.sysrq" = 1;
# Panic on soft lockup (CPU not scheduling for >20s)
"kernel.softlockup_panic" = 1;
# Panic on hung tasks (blocked >120s)
"kernel.hung_task_panic" = 1;
"kernel.hung_task_timeout_secs" = 120;
};
# Persist crash dumps
environment.persistence.${config.custom.impermanence.persistPath}.directories = [
"/var/crash"
];
} }

View File

@@ -23,8 +23,8 @@
networking.hostName = "c1"; networking.hostName = "c1";
services.tailscaleAutoconnect.authkey = "tskey-auth-k2nQ771YHM11CNTRL-YVpoumL2mgR6nLPG51vNhRpEKMDN7gLAi"; services.tailscaleAutoconnect.authkey = "tskey-auth-k2nQ771YHM11CNTRL-YVpoumL2mgR6nLPG51vNhRpEKMDN7gLAi";
# NFS standby configuration: accept replication from zippy
nfsServicesStandby.replicationKeys = [ nfsServicesStandby.replicationKeys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHyTKsMCbwCIlMcC/aopgz5Yfx/Q9QdlWC9jzMLgYFAV root@zippy-replication" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHyTKsMCbwCIlMcC/aopgz5Yfx/Q9QdlWC9jzMLgYFAV root@zippy-replication"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO5s73FSUiysHijWRGYCJY8lCtZkX1DGKAqp2671REDq root@sparky-replication"
]; ];
} }

View File

@@ -3,18 +3,24 @@
imports = [ imports = [
../../common/encrypted-btrfs-layout.nix ../../common/encrypted-btrfs-layout.nix
../../common/global ../../common/global
../../common/desktop-node.nix # Hyprland + GUI environment ../../common/cluster-member.nix
../../common/cluster-member.nix # Consul + storage clients ../../common/nomad-worker.nix
../../common/cluster-tools.nix # Nomad CLI (no service) ../../common/nfs-services-server.nix
# To move NFS server role to another host:
# 1. Follow procedure in docs/NFS_FAILOVER.md
# 2. Replace above line with: ../../common/nfs-services-standby.nix
# 3. Add nfsServicesStandby.replicationKeys with the new server's public key
./hardware.nix ./hardware.nix
]; ];
diskLayout = { diskLayout = {
mainDiskDevice = "/dev/disk/by-id/nvme-Samsung_SSD_970_EVO_Plus_250GB_S4EUNF0MA33640P"; mainDiskDevice = "/dev/disk/by-id/nvme-KIOXIA-EXCERIA_with_Heatsink_SSD_84GF7016FA4S";
#keyDiskDevice = "/dev/disk/by-id/usb-Intenso_Micro_Line_22080777660468-0:0"; #keyDiskDevice = "/dev/disk/by-id/usb-Intenso_Micro_Line_22080777660468-0:0";
keyDiskDevice = "/dev/sda"; keyDiskDevice = "/dev/sda";
}; };
networking.hostName = "sparky"; networking.hostName = "sparky";
services.tailscaleAutoconnect.authkey = "tskey-auth-kBCKN7QNv411CNTRL-n5Td7Jw7h3TAjubEeLmy1THy33JvD9JnM"; services.tailscaleAutoconnect.authkey = "tskey-auth-k6VC79UrzN11CNTRL-rvPmd4viyrQ261ifCrfTrQve7c2FesxrG";
nfsServicesServer.standbys = [ "c1" ];
} }

View File

@@ -5,16 +5,13 @@
../../common/global ../../common/global
../../common/cluster-member.nix # Consul + storage clients ../../common/cluster-member.nix # Consul + storage clients
../../common/nomad-worker.nix # Nomad client (runs jobs) ../../common/nomad-worker.nix # Nomad client (runs jobs)
# NOTE: zippy is NOT a server - no nomad-server.nix import ../../common/netconsole-receiver.nix
# ../../common/ethereum.nix
../../common/nfs-services-server.nix # NFS server for /data/services
# To move NFS server role to another host:
# 1. Follow procedure in docs/NFS_FAILOVER.md
# 2. Replace above line with: ../../common/nfs-services-standby.nix
# 3. Add nfsServicesStandby.replicationKeys with the new server's public key
./hardware.nix ./hardware.nix
]; ];
# Receive kernel messages from beefy via netconsole
services.netconsoleReceiver.enable = true;
diskLayout = { diskLayout = {
mainDiskDevice = "/dev/disk/by-id/ata-KINGSTON_SKC600MS1024G_50026B7785AE0A92"; mainDiskDevice = "/dev/disk/by-id/ata-KINGSTON_SKC600MS1024G_50026B7785AE0A92";
#keyDiskDevice = "/dev/disk/by-id/usb-Intenso_Micro_Line_22080777660702-0:0"; #keyDiskDevice = "/dev/disk/by-id/usb-Intenso_Micro_Line_22080777660702-0:0";
@@ -23,7 +20,4 @@
networking.hostName = "zippy"; networking.hostName = "zippy";
services.tailscaleAutoconnect.authkey = "tskey-auth-ktKyQ59f2p11CNTRL-ut8E71dLWPXsVtb92hevNX9RTjmk4owBf"; services.tailscaleAutoconnect.authkey = "tskey-auth-ktKyQ59f2p11CNTRL-ut8E71dLWPXsVtb92hevNX9RTjmk4owBf";
# NFS server configuration: replicate to c1 as standby
nfsServicesServer.standbys = [ "c1" ];
} }

100
nix-runner/README.md Normal file
View File

@@ -0,0 +1,100 @@
# Nix Runner for Gitea Actions
Custom Docker image for running Nix builds in CI.
## What's Included
- **Nix** with flakes enabled (`experimental-features = nix-command flakes`)
- **Node.js 20** for JavaScript-based GitHub Actions
- **Tools**: git, curl, jq, skopeo, bash, coreutils
- **Binary caches**:
- `c3.mule-stork.ts.net:8501` (local cache proxy)
- `cache.nixos.org` (official)
## Usage
In your workflow:
```yaml
jobs:
build:
runs-on: nix
steps:
- uses: actions/checkout@v4
- run: nix build .#myPackage
```
The `nix` label is configured in `services/act-runner.hcl`.
## Current Version
**Tag**: `v4`
**Image**: `gitea.v.paler.net/alo/nix-runner:v4`
## Updating the Runner
### 1. Edit `flake.nix`
Make your changes, then bump the tag:
```nix
tag = "v5"; # was v4
```
### 2. Build
```bash
cd nix-runner
nix build
```
### 3. Push to Registry
```bash
skopeo copy --dest-authfile ~/.docker/config.json \
docker-archive:result \
docker://gitea.v.paler.net/alo/nix-runner:v5
```
### 4. Update act-runner
Edit `services/act-runner.hcl`:
```hcl
GITEA_RUNNER_LABELS = "ubuntu-latest:docker://node:20-bookworm,nix:docker://gitea.v.paler.net/alo/nix-runner:v5"
```
### 5. Re-register Runner
```bash
sudo rm /data/services/act-runner/.runner
nomad run services/act-runner.hcl
```
The runner will re-register with the new labels.
## Configuration
The image uses `NIX_CONFIG` environment variable for Nix settings:
```
experimental-features = nix-command flakes
sandbox = false
build-users-group =
substituters = http://c3.mule-stork.ts.net:8501 https://cache.nixos.org
trusted-public-keys = cache.nixos.org-1:... c3:...
```
## Troubleshooting
### Build fails with `build-users-group` error
The image runs as root without the nixbld group. This is handled by `build-users-group =` in NIX_CONFIG.
### Can't fetch from cache
Check that the runner container can reach `c3.mule-stork.ts.net:8501` (Tailscale network).
### Missing tool
Add it to `paths` in `flake.nix` and rebuild/push a new version.

61
nix-runner/flake.lock generated Normal file
View File

@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"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"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1767379071,
"narHash": "sha256-EgE0pxsrW9jp9YFMkHL9JMXxcqi/OoumPJYwf+Okucw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fb7944c166a3b630f177938e478f0378e64ce108",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

58
nix-runner/flake.nix Normal file
View File

@@ -0,0 +1,58 @@
# ABOUTME: Flake to build a custom Nix Docker image for Gitea Actions.
# ABOUTME: Includes coreutils (/bin/sleep), modern Nix with flakes, and CI tools.
{
description = "Nix runner image for Gitea Actions";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
in {
packages.default = pkgs.dockerTools.buildImage {
name = "gitea.v.paler.net/alo/nix-runner";
tag = "v4";
copyToRoot = pkgs.buildEnv {
name = "image-root";
paths = with pkgs; [
# Core utilities (provides /bin/sleep that act_runner needs)
coreutils-full
bash
# Nix itself
nix
# For actions that need node
nodejs_20
# Common CI tools
git
curl
jq
skopeo
# CA certificates for HTTPS
cacert
];
pathsToLink = [ "/bin" "/etc" ];
};
# Create temp directories without runAsRoot (which needs KVM)
extraCommands = ''
mkdir -p -m 1777 tmp
mkdir -p -m 1777 var/tmp
'';
config = {
Env = [
"NIX_PAGER=cat"
"USER=root"
"SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
"NIX_SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
"NIX_CONFIG=experimental-features = nix-command flakes\nsandbox = false\nbuild-users-group =\nsubstituters = http://c3.mule-stork.ts.net:8501 https://cache.nixos.org\ntrusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= c3:sI3l1RN80xdehzXLA8u2P6352B0SyRPs2XiYy/YWYro="
];
};
};
});
}

7
pkgs/default.nix Normal file
View File

@@ -0,0 +1,7 @@
# ABOUTME: Custom packages overlay for packages not in nixpkgs.
# ABOUTME: Adds packages under pkgs.custom.* namespace.
final: prev: {
custom = {
flecli = final.callPackage ./flecli.nix { };
};
}

28
pkgs/flecli.nix Normal file
View File

@@ -0,0 +1,28 @@
# ABOUTME: FLEcli - Fast Log Entry CLI for amateur radio logging.
# ABOUTME: Processes FLE-formatted files into ADIF for SOTA, POTA, WWFF, etc.
{
lib,
buildGoModule,
fetchFromGitHub,
}:
buildGoModule rec {
pname = "flecli";
version = "0.1.7";
src = fetchFromGitHub {
owner = "on4kjm";
repo = "FLEcli";
rev = "v${version}";
hash = "sha256-6OFcShgUaK9RwonP6cl8eOD6Cu+F5LHZEUFPjCfWNV0=";
};
vendorHash = "sha256-6m01rcewPyy8pUXnIMwjyW+7I08pyJaTDliwTNp3fOM=";
meta = with lib; {
description = "Fast Log Entry CLI - process amateur radio logs";
homepage = "https://github.com/on4kjm/FLEcli";
license = licenses.mit;
mainProgram = "FLEcli";
};
}

View File

@@ -14,6 +14,102 @@ BLUE='\033[0;34m'
YELLOW='\033[0;33m' YELLOW='\033[0;33m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
# Normalize nix store paths by replacing 32-char hashes with placeholder
normalize_nix_paths() {
sed -E 's|/nix/store/[a-z0-9]{32}-|/nix/store/HASH-|g'
}
# Filter diff output to remove hunks where only nix store hashes differ
# Returns: filtered diff (empty if only hash changes), exit code 0 if real changes found
filter_hash_only_diffs() {
local diff_output="$1"
local current_hunk=""
local output=""
local has_real_changes=false
# Process line by line
while IFS= read -r line || [ -n "$line" ]; do
if [[ "$line" =~ ^@@ ]]; then
# New hunk starts - process previous one if it exists
if [ -n "$current_hunk" ]; then
if hunk_has_real_changes "$current_hunk"; then
output+="$current_hunk"$'\n'
has_real_changes=true
fi
fi
# Start new hunk
current_hunk="$line"$'\n'
else
# Add line to current hunk
current_hunk+="$line"$'\n'
fi
done <<< "$diff_output"
# Process last hunk
if [ -n "$current_hunk" ]; then
if hunk_has_real_changes "$current_hunk"; then
output+="$current_hunk"
has_real_changes=true
fi
fi
# Remove trailing newline
output="${output%$'\n'}"
if [ "$has_real_changes" = true ]; then
echo "$output"
return 0
else
return 1
fi
}
# Check if a diff hunk has real changes (not just hash changes)
hunk_has_real_changes() {
local hunk="$1"
# Use temp file to avoid bash here-string issues
local temp_hunk=$(mktemp)
printf '%s' "$hunk" > "$temp_hunk"
local minus_lines=()
local plus_lines=()
# Extract - and + lines (skip @@ and context lines)
while IFS= read -r line || [ -n "$line" ]; do
if [[ "$line" =~ ^- && ! "$line" =~ ^--- ]]; then
minus_lines+=("${line:1}") # Remove the - prefix
elif [[ "$line" =~ ^\+ && ! "$line" =~ ^\+\+\+ ]]; then
plus_lines+=("${line:1}") # Remove the + prefix
fi
done < "$temp_hunk"
rm -f "$temp_hunk"
# If counts don't match, there are structural changes
if [ ${#minus_lines[@]} -ne ${#plus_lines[@]} ]; then
return 0 # Has real changes
fi
# If no changes at all, skip
if [ ${#minus_lines[@]} -eq 0 ]; then
return 1 # No real changes
fi
# Compare each pair of lines after normalization
for i in "${!minus_lines[@]}"; do
local minus_norm=$(echo "${minus_lines[$i]}" | normalize_nix_paths)
local plus_norm=$(echo "${plus_lines[$i]}" | normalize_nix_paths)
if [ "$minus_norm" != "$plus_norm" ]; then
return 0 # Has real changes
fi
done
# All changes are hash-only
return 1
}
# Check for nvd # Check for nvd
if ! command -v nvd &> /dev/null; then if ! command -v nvd &> /dev/null; then
echo "Error: nvd not found in PATH" echo "Error: nvd not found in PATH"
@@ -205,11 +301,17 @@ for host in $hosts; do
echo echo
echo -e " ${BLUE}$basename${NC}" echo -e " ${BLUE}$basename${NC}"
# If it's a directory, diff key files within it # If it's a directory, diff all files within it
if [ -d "$old_path" ] && [ -d "$new_path" ]; then if [ -d "$old_path" ] && [ -d "$new_path" ]; then
# Focus on important files # Count files to avoid processing huge directories
for pattern in "activate" "etc/*" "*.conf" "*.fish" "*.service" "*.nix"; do file_count=$(find "$new_path" -maxdepth 3 -type f 2>/dev/null | wc -l)
while IFS= read -r file; do
# Skip very large directories (e.g., system-path with 900+ files)
if [ "$file_count" -gt 100 ]; then
echo " (skipping directory with $file_count files - too large)"
else
# Diff all files in the directory
for file in $(find "$new_path" -maxdepth 3 -type f 2>/dev/null); do
[ -z "$file" ] && continue [ -z "$file" ] && continue
relpath="${file#$new_path/}" relpath="${file#$new_path/}"
old_file="$old_path/$relpath" old_file="$old_path/$relpath"
@@ -217,16 +319,38 @@ for host in $hosts; do
if [ -f "$old_file" ] && [ -f "$file" ]; then if [ -f "$old_file" ] && [ -f "$file" ]; then
# Check if file is text # Check if file is text
if file "$file" | grep -q "text"; then if file "$file" | grep -q "text"; then
echo -e " ${YELLOW}$relpath:${NC}" # Get diff output
diff -u "$old_file" "$file" 2>/dev/null | head -50 | tail -n +3 | sed 's/^/ /' || true diff_output=$(diff -u "$old_file" "$file" 2>/dev/null | head -50 | tail -n +3 || true)
# Filter hash-only changes
if [ -n "$diff_output" ]; then
filtered_diff=$(filter_hash_only_diffs "$diff_output" || true)
if [ -n "$filtered_diff" ]; then
echo -e " ${YELLOW}$relpath:${NC}"
echo "$filtered_diff" | sed 's/^/ /'
fi
fi
fi fi
fi fi
done < <(find "$new_path" -type f -name "$pattern" 2>/dev/null | head -20) done
done fi
# If it's a file, diff it directly # If it's a file, diff it directly
elif [ -f "$old_path" ] && [ -f "$new_path" ]; then elif [ -f "$old_path" ] && [ -f "$new_path" ]; then
if file "$new_path" | grep -q "text"; then if file "$new_path" | grep -q "text"; then
diff -u "$old_path" "$new_path" 2>/dev/null | head -50 | tail -n +3 | sed 's/^/ /' || true # Get diff output
diff_output=$(diff -u "$old_path" "$new_path" 2>/dev/null | head -50 | tail -n +3 || true)
# Filter hash-only changes
if [ -n "$diff_output" ]; then
filtered_diff=$(filter_hash_only_diffs "$diff_output" || true)
if [ -n "$filtered_diff" ]; then
echo "$filtered_diff" | sed 's/^/ /'
else
echo " (only hash changes)"
fi
fi
else else
echo " (binary file)" echo " (binary file)"
fi fi

View File

@@ -1,4 +1,4 @@
kopia: ENC[AES256_GCM,data:FrvSs1th,iv:GnoJ9ec26Wx8rH/G5yuN2CwmBp2ITD2C264cYQ2t6io=,tag:zn67Rikn7PXS2jOTf+KQ3Q==,type:str] kopia: ENC[AES256_GCM,data:/6jqArNgeBoGnEdJ1eshrsG8RJs=,iv:2nNdrKczus70QDdvO/MC2wJubGnAf3M8PtzSe1aoBF4=,tag:aOoktsqhQLXr0YkjYZq4OQ==,type:str]
sops: sops:
age: age:
- recipient: age1df9ukkmg9yn9cjeheq9m6wspa420su8qarmq570rdvf2de3rl38saqauwn - recipient: age1df9ukkmg9yn9cjeheq9m6wspa420su8qarmq570rdvf2de3rl38saqauwn
@@ -19,7 +19,7 @@ sops:
R21jYU96SGVHOUxmZjlldS96K2VqbWcKC28wLdT/zx6yHluCLqB/cFRmc0Alq6AH R21jYU96SGVHOUxmZjlldS96K2VqbWcKC28wLdT/zx6yHluCLqB/cFRmc0Alq6AH
DqmAaxRhOg/SI5ljCX1gE5BB9rNIJ1Gq8+li7wCpsdfLMr5Yy/HAsw== DqmAaxRhOg/SI5ljCX1gE5BB9rNIJ1Gq8+li7wCpsdfLMr5Yy/HAsw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2025-10-29T15:41:56Z" lastmodified: "2025-11-04T20:25:17Z"
mac: ENC[AES256_GCM,data:AM5Srw03yvk6gKVf3KF/N5ilYKgF0KKObA98N4KbLGNsxsAagvmQtvzWurgDmOvihbKyBlNyOCjBVCHrKwfzzdCHj0+9lcuCtZ5CC/zOy9a7LMFJvpElj0pQUxpODU+6HcGtdrQQpsfEkzrMBzw1wsJhJ9vC1rp0YdUqK7+wS5g=,iv:M0mTIlXZPdyiTUf/8vYJvmDTMB9bOwH2BKTexPpS/2Q=,tag:CFylsJEP9mePMcRoxrxgwA==,type:str] mac: ENC[AES256_GCM,data:llS+R5Pj51ZUkU8FkJx2KqqE4D42Uno3Btn31FadIl4kFamnrL6uJjbiNEJpFFO+SchXD3l7VCatbBhMSoxsPYd+rdDRT2klq+iIcZU/k413GC87xdmHIwWE+L2pujv36iBjtM+HJTSvXI0xOxjUmzH4FPdEa1r3Z5yGNnCI+Q4=,iv:ld6pSEzvKTSZtBb+QjHyqqj2VT05YegxBrUR2yxhjKY=,tag:7/vYBh8lDOcVXJL3esTIZQ==,type:str]
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.11.0 version: 3.11.0

View File

@@ -4,92 +4,92 @@ sops:
- recipient: age1df9ukkmg9yn9cjeheq9m6wspa420su8qarmq570rdvf2de3rl38saqauwn - recipient: age1df9ukkmg9yn9cjeheq9m6wspa420su8qarmq570rdvf2de3rl38saqauwn
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5cU1JS1VOWGF6aC9VaTZ0 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4WExPaEtTdEljYkF1ZUQw
UzFuUklwTUxEUy9LdUhxaEhRWkZ4VHRNWkNnCituRXNieW5WcUR6MmM2OW1ha3E3 UHhRNDJZb2wydWVUaXFmR213SjJsNDFKU0FjCnJ3Tk1yZDZkU3orcHZ2UDY3elRi
NXNuMWMvcnUvZ2ZZYnhzSnR3dUxIWXcKLS0tIEJXdENxbXVhZ3l0M0oyZGladUdU WW9FMXU0cDNjV3QrOWo3MVB0UzMwakUKLS0tIEhQVldBVWhmR0k0WW9jTE0xc2ZW
MTU3RS81SnlwZW81a2JQQVhYa0FwZkEKPvwH91RMG6t5Uwztp5rTjThYCh8lkIEe RWp4ZjlVN0FWaURlRHNONDhXdmJpS1EKZVXYyFRFD9KdyWuMoQytkQk4VxpBRyAV
LevB8nj5HmlLWYhVdrl/P78DHkbDb2jZrmsbh14cbMx+Z7Z3a1SMQw== lF4FA99wjGMhHFNQExnqYYLYtFkA18/SB6pkneOjdhIvEr0IFLJEqg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1gtyw202hd07hddac9886as2cs8pm07e4exlnrgfm72lync75ng9qc5fjac - recipient: age1gtyw202hd07hddac9886as2cs8pm07e4exlnrgfm72lync75ng9qc5fjac
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiR0FyemxNeFBlbFM2TGgr YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjZ29wdk1aOHZJYWFjaG9v
WjFxemZXQjdrdmxuZjc1V0NBZjcyTTNsTlZJClAyS3ZmdmJIdmJqb3dUYU1TSEdn RGxsek95QmtrZS9xRWdKMFdLSHZ3NmlZRGxzCnBvRXZkYnkxdkhJWkY0Ukg1M0dE
aW9Lc3RnbWNkTGpnRlVuZmhpaXZHeW8KLS0tIDdnNHZ2dlFLSUFUSTBySDBneXBr dWc3QWtCdkV5Ymd4MkxhZWl0ZDNCZXcKLS0tIGMrVWtNNWtscm9STUN1aHVZc2Ny
TmRYdFBQT1ZDZ0NGMnkyQU1LWmtSSVUKJz1v/z97cBXvAqvkDnSM7Jp5lK/BtvY/ Vm1oaFFTbTBpRWxuR3gxbUZ0YkZieVkKdaSSXrDzAUGkj3w8/YcFZaJTiUUEbJdw
sf7V1Gqg5XEE678rAFT/O+vpaniHevr47bt89bKOVCFb3FaheAmgbA== GjuLz7bxX8+HQvhSbu6/KCwG6R4j1eO5Zg1w0wYtyeUOV1HfZEGQog==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age16yqffw4yl5jqvsr7tyd883vn98zw0attuv9g5snc329juff6dy3qw2w5wp - recipient: age16yqffw4yl5jqvsr7tyd883vn98zw0attuv9g5snc329juff6dy3qw2w5wp
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArWktWbk4rMlQwa3ZrNjVv YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBIMGpibmNRUDRFaFVOTDB4
WkRTSWVIVFltbUJVeCt6dnE0WFROYldqSkRFCjJPa2dWN0h3alg4NnVIVFRUUEc2 RVdTc1RrTmRPb0dlZGlpcGxuRlJ1L2w5MVVBCi9HdXNGZmdSaVZsQWRoa2RpVDNV
WjNzb3lObFRWUG12RjNFMTZJVlYrNUkKLS0tIFZLMFV6cXo0bFVNVWFlYnNUVXl6 OXBtS0pwYnhjS2hCUk10UUtwam4zMWcKLS0tIFV0dVpQNGpSOEVoZnE5OGpCZkxa
VUphbmpLL2FDcjB6WVFySkw1QTJIYWsKriG/2kyw63mjnWBkyKMRKqqf82ZYYIMy MFMxSG95dmJncGJzR29mQkVzNjFIQUEKrJ0MDTBmiwiAaLt7CJ1pjlxuFvZJuRkR
TJ6Zuls71RmxTh0WRVFF5/iOoL1YmD2uOeYwwM+Kc8itzBj5vZxGOQ== EuLYOYLdVaxgZ442io5OE7wme0P4LLcxSAreDG84GVs67JHvsFE89g==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age10zxwwufrf5uu9cv9p9znse2ftfm74q9ce893us6cnvxjc7e3ypcqy709dy - recipient: age14aml5s3sxksa8qthnt6apl3pu6egxyn0cz7pdzzvp2yl6wncad0q56udyj
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsbnArTnU3WUU4MmFWOEVx YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBucHdSNGNyRkVITmNDVkpx
M2ZiZkJjUTBIb3NHSFo3N3l3dDhzZTdZTjM0CkJxWi9nVHZDVTBTaUhLME1LeVF3 QVFKK0VucFNSMnNqSGRFRmRoRWpsZ0srUUhrCkwwY2pDSkJ0aGlqc3U3ZXNJUVl0
dlRuWWJTUUgrU3l3aHhiWFJyT1ZQRFUKLS0tIHhRVFV3Y3ZXRWdoc1lkYTJWK0tk bXZMSVg3bDhaK3d1MTBnL1BQVUhkMUkKLS0tIDdxSk1DMVpsbnI1QlFnNEFJYXRD
Ni94RkZQak1nbVl0TmZ2SnVGQTFQVFkKaSe+6pTXj+YxZvl2fUflrobjblr7sFse RTNxYUxlUGxsM1NvekZ4R1hQVE9KMk0KocfE75DTfQMj/RsznOdeF82aO8WwO4HD
kpJziK/UYVvp8FsOoBYdqyfNyi+yCn+J2q+EfDlgHHCipvNm7/Q/lQ== 1xakOM2FHoHi60Q5uOWzfGtz0i+R4ue9hafa5Esn01TOjc3qWSlW3A==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1me78u46409q9ez6fj0qanrfffc5e9kuq7n7uuvlljfwwc2mdaezqmyzxhx - recipient: age1me78u46409q9ez6fj0qanrfffc5e9kuq7n7uuvlljfwwc2mdaezqmyzxhx
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSa0NRY3JLZjQ4L215QTdF YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYTEhiSDZvZTg3ZWxJRXlu
clVtWE5ZMHVFSEE2U1E4dUI3Vk5hNGQrakF3CmQwRFNKWFNVOE1zRWpHQ3g4czdz a0ozOXRVL2lia054SkNLc2tEYmlZUCt1NW1JCkorK0hub1pLQTE0QThEUDRDWXJV
MkV6cFB0UjFBM0xaeFNBNExBc1BsTk0KLS0tIGtCSHdHZ0FoVndBS0xpbHg4S1NL YWtGamNxMTFIYjVDT2RqTXh0Z2hVTjAKLS0tIGxoRTAwc3FKVVNSQndtbTZmc3BR
QVF6KzdmSnVEcUxUa2xReU9OY1JHRzgKShY79DAGVzlN5a3DmKVeb35eaQ6esIcJ QnMrK2lMT25tR1ErV2xvS01JWWswVUEKtrGaLETMfY2D8qmgml/fgGxkvQLoiMTP
KPUyYjezvnnmU/HC10Ft3Dlw1m6foWbL2BqgvuYscTtN58cWWrtH3Q== l3a7Y6kwutuzRnmW1tnWv7yoPbTn+BDwfOwBcnesl2x0aJ5iLUKruA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1cs8uqj243lspyp042ueu5aes4t3azgyuaxl9au70ggrl2meulq4sgqpc7y - recipient: age1cs8uqj243lspyp042ueu5aes4t3azgyuaxl9au70ggrl2meulq4sgqpc7y
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0NTg1eXp5ZGZkbmUzdjhG YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqMFJ1bzQxWjlZTmF6eEl0
RzZaMnJmZ2h0MmEyOVpVTzR4QUFQbVpHOTM0CjJmaFUzcjRnUEVZcitDQ0VGUUQ5 d3VVd0VsbGZGdDBKRG9uNEhaMThmWmpuQ1hFClA1aDhwRU1Pb2pibGh6T0pqdmlq
eDVRcDRZNVh6cUZsWktmMnM1UHBFTjAKLS0tIHE4QWZPZEpuZUQ3OE55OWlVMk9t S3cxM0wyWWlCL3U5TjV4Vkg4blRsUVkKLS0tIENnYk5GbmZWbFo4cElON1Z0ZVlv
eTBUZHFTcElOMk5LQ1lHUUJVQUtGSGcKh+51nVjiyGQ9GbBPBDEy7QgPDJ1V8uK/ ZDdsci9rcG5Wc2V0NlQ3MWx1cFF4dUkKumFT4xtjGDBGK+/SV27Dh/vyGMJAEZNo
rihPxs6KkvwUPHaoPQdgv5tCfIf4VIxEcSM4peST9iuLZdw2pThAHQ== 9gTmVLfR9vXVAXUdOMcqgo7Nl4OJCS4HrDxvVCoER/bVoQVRiPzuXw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1w5w4wfvtul3sge9mt205zvrkjaeh3qs9gsxhmq7df2g4dztnvv6qylup8z - recipient: age1w5w4wfvtul3sge9mt205zvrkjaeh3qs9gsxhmq7df2g4dztnvv6qylup8z
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyOFkrb1JiU1F4cVlOOGhn YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCM2E5a2lsZGJzRnk5N3Rr
alFzemVlMTdqL3dXMzBZSkptUEx6Zi9WV0RzCnM5L0dMcG9uRjd3amhycHJkSmFS bWRwRlI2c0c4NzBNdEVqMGZFWTZtNDlvSzJFCmFPM05XbndsazRGWEw3Zy83dldm
NzUxYm50SDA4K1NnUEFNNjZueUoyYmsKLS0tIG1FQ3NQQm9jTEdkSytxQ2RKNmVO eXhEZUZQZWk5bVNwaEk5SDRka0NOWjAKLS0tIHNvZ016Rjh5bmYwRUxyRWRydFpI
RjdoTUMvSDVIZlFJU0RaQTVLRERzTk0KBofmLU596Ij5FMAo2CZ/H0xl7Oe/0xxj Z0NHYjFzem55bVNORGlVbVNxR2psc2cK6JpNZwznwgl61d/W9g+48/894TQRe4gJ
3baiF/IEJ1JrhrQnd/+UEermMb5T6caj8rbryybmSzb33JV+DBylOw== nl4oDwRPbZwJZgdAKQVfTTujB0QbWpJc24mDGD4I4CydqTKwy6FN3A==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1wwufz86tm3auxn6pn27c47s8rvu7en58rk00nghtaxsdpw0gya6qj6qxdt - recipient: age1wwufz86tm3auxn6pn27c47s8rvu7en58rk00nghtaxsdpw0gya6qj6qxdt
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCeEQybTlXaFoxNmtGUXFs YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlK1A1eVdRQThQUHdqbHdk
aG9zQStLTXIydlJQaWowbnhUQjlza2dPVTFjCnZzdEZGbGF4ckN6SlVLSGVKQmdM b1MyMlBJUFluTm13ZWwwc1RNbThFZUMrNXhzCnRPTVhPSzUzM0VtaUVJbFl5Wllj
UFI3WGJtK2U4aE0zMmpGYnFMVFRUU0kKLS0tIEZsazliSExnTWlKRWRlek1La2oy NUlndzc3Yzhjd1JSb3czajI3UmRDZ1kKLS0tIE03M1hab1MxU0I2VExBWlh2TnJC
dmZ6dEFJNy9wYjBVbzc3dnFJc2ZGYXMK4dVqqtiKABFm4EfTPAVGU4PvpU8S5DqW eGRXRTlsWmlpenJrVkMxakJZVTV0cE0KMQCKscSLnCu3NsurFFiDaUGjJbyIAwd0
PDayS5ta4XPeE0U7rxGrKTnFtd9SGlZH46/JIJj95mjZEwqJD/dGkQ== HTutCiuPYVI4zznQ3RZDBeO5L6a/twXxMRTePUCwOkRNWRWpzR9nxg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1jy7pe4530s8w904wtvrmpxvteztqy5ewdt92a7y3lq87sg9jce5qxxuydt - recipient: age1jy7pe4530s8w904wtvrmpxvteztqy5ewdt92a7y3lq87sg9jce5qxxuydt
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6NXFiUVJnMU4xNXBza0Rp YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1VEJmMWlnemFGNExWYUI4
NG01Tk9GblpqcVNQVkUvc29sNlVnKzd4STM4CjFZMG55c1VZUEgzbWVtZE1jTmYz QWRwRktwODNvSmlEcGJORHNlQXNVeXVpbFNrCms0QUFNdDlrNjMxazU1UTcwc2JF
eVZETm56MXQrbVNKeE94bWFIK09Vc1UKLS0tIHk4T3ZJTXVJUzFULzBiemxCNW8w RC9JUnJsWmgyc01zZU9JQmxuM3V6STQKLS0tIGxQZGFsZ0pNTjQ3QW1sS0E2Y2RM
SVE3UnRJdDFOYXV1SU5hc0pJa2wrNFUK2b0PpYfHbDOi50eq3pSJngdaP4DLKwqR aVVrNW1BNXQ5UDk1UEtVVWJPNHpwUFUKcArFPFknBj8ss1lD38YtMaB06L/ASeu5
sggB0M0ztRARE/uTQkGtQv1hxvjrahZCaoV3MvLAGlEq+YgmMMMPfQ== u4ff0rTDx237snaSFg5RIJ+6uxX16p5ODg3xOYGOMkDeuTLdl2bg3A==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1zjgqu3zks5kvlw6hvy6ytyygq7n25lu0uj2435zlf30smpxuy4hshpmfer - recipient: age1zjgqu3zks5kvlw6hvy6ytyygq7n25lu0uj2435zlf30smpxuy4hshpmfer
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuRzN3UlpvbTlrSG9Na3FM YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArWTNkaFlrQkJHRnd4cTBw
QVBjVGV3cVZhbjVacDBFTEFXK05IM2FUOHhNCklJbWx1bGZmR1NIQ0dkaUlraWl1 N3dnTXk3SlJkQkZDdWpLcEpNQ2Z2RHZoVjBJCjBaK1MzbzdaaXluR1dFaFFNaGEx
UThTbmZSc243UllKOTJPZlJTYldBdFUKLS0tIExnRFBSN2lIWklKcW8wZ3BjWWUv VTNrVU0yeG9KQkhqUkYxU3VBM0E0R1UKLS0tIDJHek9vVldSZGN0M0c0UHcySGhk
U25jb0UxY2U0YTkzNTNiWFd2Vmt6OEEK5LlRKtvtxDMEyCNrKkHH7YxVM6ZPt00F Z2RoZno4bmhidytlL2ZmNWUzNTcwcVEKXvgaO8Uo0R+Kc8lizLtVxmTi0W5XHjYw
z0dwvmcB7DfOTzOnbXMwf6A/NAV/u4kXuZWGXLYFeso/c2kBlaQuUA== 7evdCHQHmFl0vg/bGOJBmcTUhioJv06D0LR3XMl9I6ufXDNaT/NHxw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2025-04-04T09:34:06Z" lastmodified: "2025-04-04T09:34:06Z"
mac: ENC[AES256_GCM,data:YIcRrsPparPfPaI2+MLlKsxu7M19H8nndOsrDLuh/5BXzIZNiuTIWyvxODyhI745rDwlibO+7Q0QctanhTl4+IzGaYtuY4i+rb+3dzBMpcdT2VAbtCHHxcltWeanRGFq2K3WM2tbnQCERst5kejfn0Razjq3UU5vNwfBsdJMwGc=,iv:izDxy0ufVnH8ImkZIngcYhGuj0PGpLqBD/ZDvQyE+5I=,tag:oYBUEQS52pr09h5OvOadNg==,type:str] mac: ENC[AES256_GCM,data:YIcRrsPparPfPaI2+MLlKsxu7M19H8nndOsrDLuh/5BXzIZNiuTIWyvxODyhI745rDwlibO+7Q0QctanhTl4+IzGaYtuY4i+rb+3dzBMpcdT2VAbtCHHxcltWeanRGFq2K3WM2tbnQCERst5kejfn0Razjq3UU5vNwfBsdJMwGc=,iv:izDxy0ufVnH8ImkZIngcYhGuj0PGpLqBD/ZDvQyE+5I=,tag:oYBUEQS52pr09h5OvOadNg==,type:str]

View File

@@ -4,20 +4,20 @@ sops:
- recipient: age1df9ukkmg9yn9cjeheq9m6wspa420su8qarmq570rdvf2de3rl38saqauwn - recipient: age1df9ukkmg9yn9cjeheq9m6wspa420su8qarmq570rdvf2de3rl38saqauwn
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkdHVtQ3hHSjVuRkxRMklO YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtSjhXazlWd3YwNVFKVkw4
NEJjeERFUDBMRkVTTGVGTTJIR1lwM25kZWpRCitlUFIvUTVPUloreEFIWjIvT1dL dDMydVFCN1lLeUJOWkxuSGJ1a0srNm9PaWswCm8yZ3hiOWFHUlAzNVRrck53OElD
S2hBY2NUWHdxOVhRejU0eUpZa1FMY00KLS0tIEZ3bDRIdlQzaVQ5U0kyRjYyeUor b056YmV4S2NtNnEzRkpnRVNEblV5blkKLS0tIG1ramoya3RHV1FJZGlFU2ZSeUtS
c1M2V2J1Q2R3alI3b3NoYk5SK1AzKzgKbOhSxwTpLr7wwbN+nY4aK+6WmpofBxNX KzJlbEsvYWlXaHhEQU5oOS9HaDdYSDAKvlhKgi4Pf8xVB5MnO33GWYg313mRdUGu
CEaEBz98KTTrSQ9Qvm1+/yep95l7i0HPQGdGwCRNKdvUoXzk1KalpQ== kFCs5b1N96x9JOS7zgnM0AKDY8IPBSe33tmDqtYygwPdkOys1PmZkw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age10zxwwufrf5uu9cv9p9znse2ftfm74q9ce893us6cnvxjc7e3ypcqy709dy - recipient: age14aml5s3sxksa8qthnt6apl3pu6egxyn0cz7pdzzvp2yl6wncad0q56udyj
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZb2oyd1ZGR2FXVFlhT2d3 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYTVovQld2RkRxaW90b3lR
NFVlU3VlR1BEWFRyTDBYZFFlQjZxUXVyK240CkdLRmk5M01ZTloyREtwQ3hpRkxZ NGFtbWVLZUNHdnlZVWkrL1RXUHBVeGdvSDJrClJmSmZRZmdjcy8rNnJBVmVUWDZq
RDRKYVlRVFVLYzVSenU3THhFc0ZrdjgKLS0tIG1iMWh1TTZIZDEwazVKY1g3NXg0 M2lPbDBhT0Y0NkJ5a1FNYnU3Zkl0TkEKLS0tIGxqM2h2TDB2akl4ODlYY042R1Z4
NFRKemkwcnBxR0NIbXBGcm8xejdUMjgKOAGxkrvtvf7Y9W5BteL12HuUWA/d5Bah ZVJWN3pZelFJR0Jid3JseEZKVFZtYmsKmKXQRjnghuF/s9z2Xk98sFvxic91fGa2
wVoeBK21Zxz/GodBpVCuDnJ5DwM3c+7O3jnvtTShIW00evDhJIvcvA== V7IGmpqAYQV3jJ1G4cjJxtpidQ6fLCqlnR+sq+y8+dT+LN7i+Zbnnw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2025-10-19T17:33:13Z" lastmodified: "2025-10-19T17:33:13Z"
mac: ENC[AES256_GCM,data:IwEyBr/I7BJa0gWZ494dCT0ogyP2PbnUg5fLOn15vZAHIyYtTB3dI3gV5Lx7oPdqOPlI61MsShIYBnk0uBChpNu6O4oiGUfwvBfegzlDyHHERLx+S7nZpcwmf/3JoNXwq0f2OtOu8nA6Q1V4gVjFFNWUCAh5cq106vG1awsQkn0=,iv:j+JcVtKz2RfyWu55dUeJJTRK6prB9DGLvcjiAAdVySM=,tag:Pg5sKiLzYUFoN9Duu+nF0w==,type:str] mac: ENC[AES256_GCM,data:IwEyBr/I7BJa0gWZ494dCT0ogyP2PbnUg5fLOn15vZAHIyYtTB3dI3gV5Lx7oPdqOPlI61MsShIYBnk0uBChpNu6O4oiGUfwvBfegzlDyHHERLx+S7nZpcwmf/3JoNXwq0f2OtOu8nA6Q1V4gVjFFNWUCAh5cq106vG1awsQkn0=,iv:j+JcVtKz2RfyWu55dUeJJTRK6prB9DGLvcjiAAdVySM=,tag:Pg5sKiLzYUFoN9Duu+nF0w==,type:str]

75
services/act-runner.hcl Normal file
View File

@@ -0,0 +1,75 @@
# ABOUTME: Gitea Actions runner for CI/CD pipelines.
# ABOUTME: Runs containerized actions with Docker-in-Docker support.
# Setup required before running:
# sudo mkdir -p /data/services/act-runner
# nomad var put secrets/act-runner registration_token="<token-from-gitea-ui>"
job "act-runner" {
datacenters = ["alo"]
type = "service"
group "runner" {
network {
mode = "host"
}
task "runner" {
driver = "docker"
config {
image = "gitea/act_runner:latest"
network_mode = "host"
privileged = true
volumes = [
"/var/run/docker.sock:/var/run/docker.sock",
"/data/services/act-runner:/data",
"local/config.yaml:/.runner/config.yaml",
]
}
template {
destination = "local/config.yaml"
data = <<EOH
log:
level: info
runner:
file: /data/.runner
capacity: 2
timeout: 3h
labels:
- "ubuntu-latest:docker://node:20-bookworm"
- "nix:docker://nixos/nix:latest"
cache:
enabled: true
dir: /data/cache
container:
network: "host"
privileged: true
valid_volumes:
- /data/services/**
EOH
}
env {
GITEA_INSTANCE_URL = "https://gitea.v.paler.net"
GITEA_RUNNER_LABELS = "ubuntu-latest:docker://node:20-bookworm,nix:docker://gitea.v.paler.net/alo/nix-runner:v4"
}
# Template needed for nomadVar interpolation (secrets) and Nomad runtime vars
template {
destination = "secrets/env.env"
env = true
data = <<EOH
GITEA_RUNNER_REGISTRATION_TOKEN={{ with nomadVar "secrets/act-runner" }}{{ .registration_token }}{{ end }}
GITEA_RUNNER_NAME={{ env "NOMAD_ALLOC_ID" }}
EOH
}
resources {
cpu = 2000
memory = 2048
}
}
}
}

View File

@@ -27,7 +27,7 @@ job "adminer" {
tags = [ tags = [
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.adminer.entryPoints=websecure", "traefik.http.routers.adminer.entryPoints=websecure",
"traefik.http.routers.adminer.middlewares=authentik@file", "traefik.http.routers.adminer.middlewares=oidc-auth@file",
] ]
} }
} }

88
services/animaltrack.hcl Normal file
View File

@@ -0,0 +1,88 @@
# ABOUTME: Nomad job for AnimalTrack - poultry farm management app.
# ABOUTME: Runs FastHTML Python app with SQLite, behind Traefik with OIDC auth.
# Setup required before running:
# sudo mkdir -p /data/services/animaltrack && sudo chown 1000:1000 /data/services/animaltrack
# nomad var put secrets/animaltrack csrf_secret="$(nix shell nixpkgs#openssl -c openssl rand -base64 32)"
job "animaltrack" {
datacenters = ["alo"]
# Force re-pull of :latest images on each nomad run
meta {
uuid = uuidv4()
}
update {
max_parallel = 1
health_check = "checks"
min_healthy_time = "30s"
healthy_deadline = "5m"
progress_deadline = "10m"
auto_revert = true
}
group "web" {
network {
port "http" {
to = 3366
}
}
task "app" {
driver = "docker"
user = "1000"
config {
image = "gitea.v.paler.net/alo/animaltrack:latest"
ports = ["http"]
force_pull = true
volumes = ["/data/services/animaltrack:/var/lib/animaltrack"]
}
env {
DB_PATH = "/var/lib/animaltrack/animaltrack.db"
AUTH_HEADER_NAME = "X-Oidc-Username"
SEED_ON_START = "true"
TRUSTED_PROXY_IPS = "192.168.1.0/24"
}
# Template needed for nomadVar interpolation (secrets)
template {
destination = "secrets/env.env"
env = true
data = <<EOH
CSRF_SECRET={{ with nomadVar "secrets/animaltrack" }}{{ .csrf_secret }}{{ end }}
EOH
}
resources {
memory = 512
}
service {
name = "animaltrack"
port = "http"
tags = [
"traefik.enable=true",
"traefik.http.routers.animaltrack.entryPoints=websecure",
"traefik.http.routers.animaltrack.middlewares=oidc-auth@file",
"traefik.http.routers.animaltrack.rule=Host(`farm.alo.land`)",
]
check {
type = "http"
path = "/healthz"
interval = "10s"
timeout = "5s"
check_restart {
limit = 3
grace = "60s"
}
}
}
}
}
}

View File

@@ -1,118 +0,0 @@
job "authentik" {
datacenters = ["alo"]
group "auth" {
network {
port "http" {
# traefik forwardAuth hardcodes this port
static = 9000
}
port "https" {
to = 9443
}
port "metrics" {
to = 9300
}
}
task "server" {
driver = "docker"
config {
image = "ghcr.io/goauthentik/server:${var.authentik_version}"
ports = [
"http",
"https",
"metrics"
]
command = "server"
}
env {
AUTHENTIK_REDIS__HOST = "redis.service.consul"
AUTHENTIK_POSTGRESQL__HOST = "postgres.service.consul"
AUTHENTIK_POSTGRESQL__NAME = "${var.pg_db}"
AUTHENTIK_POSTGRESQL__USER = "${var.pg_user}"
AUTHENTIK_POSTGRESQL__PASSWORD = "${var.pg_password}"
AUTHENTIK_SECRET_KEY = "${var.secret_key}"
AUTHENTIK_EMAIL__HOST = "192.168.1.1"
AUTHENTIK_EMAIL__FROM = "authentik@paler.net"
}
resources {
cpu = 2000
memory = 1024
}
service {
name = "authentik"
port = "http"
tags = [
"traefik.enable=true",
# Main UI
"traefik.http.routers.authentik.entryPoints=websecure",
"traefik.http.routers.authentik.rule=Host(`authentik.v.paler.net`) || Host(`authentik.alo.land`)",
# Embedded outpost for forward auth
"traefik.http.routers.authentik-palernet.entryPoints=websecure",
"traefik.http.routers.authentik-palernet.rule=HostRegexp(`{subdomain:[a-z0-9-]+}.v.paler.net`) && PathPrefix(`/outpost.goauthentik.io/`)",
"traefik.http.routers.authentik-aloland.entryPoints=websecure",
"traefik.http.routers.authentik-aloland.rule=HostRegexp(`{subdomain:[a-z0-9-]+}.alo.land`) && PathPrefix(`/outpost.goauthentik.io/`)",
]
}
service {
name = "authentik-metrics"
port = "metrics"
tags = [ "metrics" ]
}
}
task "worker" {
driver = "docker"
config {
image = "ghcr.io/goauthentik/server:${var.authentik_version}"
command = "worker"
}
env {
AUTHENTIK_REDIS__HOST = "redis.service.consul"
AUTHENTIK_POSTGRESQL__HOST = "postgres.service.consul"
AUTHENTIK_POSTGRESQL__NAME = "${var.pg_db}"
AUTHENTIK_POSTGRESQL__USER = "${var.pg_user}"
AUTHENTIK_POSTGRESQL__PASSWORD = "${var.pg_password}"
AUTHENTIK_SECRET_KEY = "${var.secret_key}"
AUTHENTIK_EMAIL__HOST = "192.168.1.1"
AUTHENTIK_EMAIL__FROM = "authentik@paler.net"
}
resources {
memory = 600
}
}
}
}
variable "pg_user" {
type = string
default = "authentik"
}
variable "pg_password" {
type = string
default = "aQueiquuo6aiyah5eoch"
}
variable "pg_db" {
type = string
default = "authentik"
}
variable "secret_key" {
type = string
default = "uUzCYhGV93Z8wKLAScuGFqBskxyzSfG4cz6bnXq6McM67Ho7p9"
}
variable "authentik_version" {
type = string
default = "2025.6"
}

View File

@@ -19,7 +19,7 @@ job "beancount" {
user = "1000" user = "1000"
config { config {
image = "gitea.v.paler.net/ppetru/fava:latest" image = "gitea.v.paler.net/alo/fava:latest"
ports = ["http"] ports = ["http"]
volumes = [ volumes = [
"/data/services/beancount:/beancount", "/data/services/beancount:/beancount",
@@ -37,12 +37,12 @@ job "beancount" {
tags = [ tags = [
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.finances.entryPoints=websecure", "traefik.http.routers.finances.entryPoints=websecure",
"traefik.http.routers.finances.middlewares=authentik@file", "traefik.http.routers.finances.middlewares=oidc-auth@file",
] ]
} }
resources { resources {
memory = 400 memory = 600
} }
} }
} }

50
services/brain.hcl Normal file
View File

@@ -0,0 +1,50 @@
# ABOUTME: Brain - Petre's externalized executive function system
# ABOUTME: SilverBullet for markdown web UI + ttyd for web terminal
job "brain" {
datacenters = ["alo"]
group "web" {
volume "services" {
type = "host"
read_only = false
source = "services"
}
network {
port "silverbullet" {
to = 3000
}
}
task "silverbullet" {
driver = "docker"
user = "1000"
config {
image = "zefhemel/silverbullet:latest"
ports = ["silverbullet"]
volumes = ["/data/services/brain:/space"]
}
resources {
memory = 512
}
service {
name = "brain"
port = "silverbullet"
tags = [
"traefik.enable=true",
"traefik.http.routers.brain.entryPoints=websecure",
"traefik.http.routers.brain.middlewares=oidc-auth@file",
]
}
}
# TODO: terminal task with ttyd for web-based amp/claude access
# Needs custom image with tmux + amp + claude-code installed
# For now, use SSH or local terminal for amp sessions
}
}

View File

@@ -49,7 +49,7 @@ job "evcc" {
tags = [ tags = [
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.evcc.entryPoints=websecure", "traefik.http.routers.evcc.entryPoints=websecure",
"traefik.http.routers.evcc.middlewares=authentik@file", "traefik.http.routers.evcc.middlewares=oidc-auth@file",
] ]
} }
} }

View File

@@ -1,51 +0,0 @@
job "farmos" {
datacenters = ["alo"]
meta {
uuid = uuidv4()
}
group "os" {
network {
port "http" {
to = 80
}
}
task "server" {
driver = "docker"
config {
image = "gitea.v.paler.net/ppetru/farmos:latest"
ports = ["http"]
volumes = [
"/data/services/farmos/sites:/opt/drupal/web/sites",
"/data/services/farmos/keys:/opt/drupal/keys",
]
}
service {
name = "farmos"
port = "http"
check {
type = "http"
port = "http"
path = "/health"
interval = "30s"
timeout = "2s"
}
tags = [
"traefik.enable=true",
"traefik.http.routers.farmos.entryPoints=websecure",
"traefik.http.routers.farmos.rule=Host(`farm.alo.land`)",
]
}
resources {
cpu = 2000
memory = 1024
}
}
}
}

View File

@@ -19,7 +19,9 @@ job "gitea" {
driver = "docker" driver = "docker"
config { config {
image = "gitea/gitea:latest-rootless" # TODO: revert to latest once 1.25.1+ is released
#image = "gitea/gitea:latest-rootless"
image = "gitea/gitea:1.25-nightly-rootless"
ports = [ ports = [
"http", "http",
"ssh", "ssh",
@@ -42,6 +44,8 @@ job "gitea" {
GITEA__mailer__FROM = "gitea@paler.net" GITEA__mailer__FROM = "gitea@paler.net"
GITEA__mailer__PROTOCOL = "smtp" GITEA__mailer__PROTOCOL = "smtp"
GITEA__mailer__SMTP_ADDR = "192.168.1.1" GITEA__mailer__SMTP_ADDR = "192.168.1.1"
GITEA__actions__ENABLED = "true"
GITEA__actions__DEFAULT_ACTIONS_URL = "https://gitea.com"
} }
service { service {
@@ -51,6 +55,7 @@ job "gitea" {
tags = [ tags = [
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.gitea.entryPoints=websecure", "traefik.http.routers.gitea.entryPoints=websecure",
"traefik.http.services.gitea.loadBalancer.serversTransport=gitea-transport@file",
] ]
} }

View File

@@ -25,19 +25,22 @@ job "grafana" {
GF_SERVER_ROOT_URL = "https://grafana.v.paler.net" GF_SERVER_ROOT_URL = "https://grafana.v.paler.net"
GF_AUTH_BASIC_ENABLED = "false" GF_AUTH_BASIC_ENABLED = "false"
GF_AUTH_GENERIC_OAUTH_ENABLED = "true" GF_AUTH_GENERIC_OAUTH_ENABLED = "true"
GF_AUTH_GENERIC_OAUTH_NAME = "authentik" GF_AUTH_GENERIC_OAUTH_NAME = "Pocket ID"
GF_AUTH_GENERIC_OAUTH_CLIENT_ID = "E78NG1AZeW6FaAox0mUhaTSrHeqFgNkWG12My2zx" GF_AUTH_GENERIC_OAUTH_CLIENT_ID = "99e44cf2-ecc6-4e82-8882-129c017f8a4a"
GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET = "N7u2RfFZ5KVLdEkhlpUTzymGxeK5rLo9SYZLSGGBXJDr46p5g5uv1qZ4Jm2d1rP4aJX4PSzauZlxHhkG2byiBFMbdo6K742KXcEimZsOBFiNKeWOHxofYerBnPuoECQW" GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET = "NjJ9Uro4MK7siqLGSmkiQmjFuESulqQN"
GF_AUTH_GENERIC_OAUTH_SCOPES = "openid profile email offline_access" GF_AUTH_GENERIC_OAUTH_SCOPES = "openid profile email groups"
GF_AUTH_GENERIC_OAUTH_AUTH_URL = "https://authentik.v.paler.net/application/o/authorize/" GF_AUTH_GENERIC_OAUTH_AUTH_URL = "https://pocket-id.v.paler.net/authorize"
GF_AUTH_GENERIC_OAUTH_TOKEN_URL = "https://authentik.v.paler.net/application/o/token/" GF_AUTH_GENERIC_OAUTH_TOKEN_URL = "https://pocket-id.v.paler.net/api/oidc/token"
GF_AUTH_GENERIC_OAUTH_API_URL = "https://authentik.v.paler.net/application/o/userinfo/" GF_AUTH_GENERIC_OAUTH_API_URL = "https://pocket-id.v.paler.net/api/oidc/userinfo"
GF_AUTH_SIGNOUT_REDIRECT_URL = "https://authentik.v.paler.net/application/o/grafana/end-session/" GF_AUTH_SIGNOUT_REDIRECT_URL = "https://pocket-id.v.paler.net/logout"
# Optionally enable auto-login (bypasses Grafana login screen) # Optionally enable auto-login (bypasses Grafana login screen)
GF_AUTH_OAUTH_AUTO_LOGIN = "true" GF_AUTH_OAUTH_AUTO_LOGIN = "true"
# Optionally map user groups to Grafana roles # Optionally map user groups to Grafana roles
GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH = "contains(groups[*], 'Grafana Admins') && 'Admin' || contains(groups[*], 'Grafana Editors') && 'Editor' || 'Viewer'" GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH = "contains(groups[*], 'admins') && 'Admin' || contains(groups[*], 'residents') && 'Editor' || 'Viewer'"
GF_AUTH_GENERIC_OAUTH_USE_REFRESH_TOKEN = "true" GF_AUTH_GENERIC_OAUTH_USE_REFRESH_TOKEN = "true"
GF_AUTH_GENERIC_OAUTH_EMAIL_ATTRIBUTE_PATH = "email"
GF_AUTH_GENERIC_OAUTH_LOGIN_ATTRIBUTE_PATH = "preferred_username"
GF_AUTH_GENERIC_OAUTH_NAME_ATTRIBUTE_PATH = "name"
#GF_LOG_LEVEL = "debug" #GF_LOG_LEVEL = "debug"
} }

50
services/homepage.hcl Normal file
View File

@@ -0,0 +1,50 @@
job "homepage" {
datacenters = ["alo"]
group "app" {
network {
port "http" { to = 3000 }
}
task "homepage" {
driver = "docker"
config {
image = "ghcr.io/gethomepage/homepage:latest"
ports = [ "http" ]
volumes = [
"/data/services/homepage:/app/config",
]
}
env {
PUID = 1000
PGID = 1000
HOMEPAGE_ALLOWED_HOSTS = "homepage.v.paler.net"
}
resources {
cpu = 200
memory = 256
}
service {
name = "homepage"
port = "http"
tags = [
"traefik.enable=true",
"traefik.http.routers.homepage.entryPoints=websecure",
"traefik.http.routers.homepage.middlewares=oidc-auth@file",
]
check {
type = "http"
path = "/"
interval = "10s"
timeout = "5s"
}
}
}
}
}

View File

@@ -9,42 +9,19 @@ job "igsync" {
} }
group "cron" { group "cron" {
volume "nix-store" {
type = "host"
read_only = true
source = "nix-store"
}
volume "sw" {
type = "host"
read_only = true
source = "sw"
}
volume "services" {
type = "host"
read_only = false
source = "services"
}
task "sync" { task "sync" {
driver = "exec" driver = "docker"
config { config {
command = "/data/services/igsync/run.sh" image = "gitea.v.paler.net/alo/igsync:latest"
}
user = "ppetru" # Mount the data directory for .env, database, and media files
volumes = [
"/data/services/igsync:/data/services/igsync"
]
volume_mount { # Force pull to always get latest image
volume = "services" force_pull = true
destination = "/data/services"
}
volume_mount {
volume = "nix-store"
destination = "/nix/store"
}
volume_mount {
volume = "sw"
destination = "/sw"
} }
resources { resources {

View File

@@ -38,7 +38,7 @@ job "jupyter" {
tags = [ tags = [
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.jupyter.entryPoints=websecure", "traefik.http.routers.jupyter.entryPoints=websecure",
"traefik.http.routers.jupyter.middlewares=authentik@file", "traefik.http.routers.jupyter.middlewares=oidc-auth@file",
] ]
} }
} }

View File

@@ -126,7 +126,7 @@ EOH
tags = [ tags = [
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.loki.entryPoints=websecure", "traefik.http.routers.loki.entryPoints=websecure",
"traefik.http.routers.loki.middlewares=authentik@file", "traefik.http.routers.loki.middlewares=oidc-auth@file",
"metrics", "metrics",
] ]
} }

View File

@@ -65,7 +65,7 @@ job "maps" {
to = 80 to = 80
} }
port "php" { port "php" {
static = 9001 static = 9000
} }
} }

View File

@@ -7,9 +7,12 @@ job "media" {
group "servers" { group "servers" {
network { network {
port "radarr" { to = 7878 } port "radarr" { static = 7878 }
port "sonarr" { to = 8989 } port "sonarr" { static = 8989 }
port "bazarr" { to = 6767 } port "bazarr" { to = 6767 }
port "prowlarr" { static = 9696 }
port "jellyseerr" { static = 5055 }
port "flaresolverr" { static = 8191 }
port "pms" { static = 32400 } port "pms" { static = 32400 }
port "qbt_ui" { static = 8080 } port "qbt_ui" { static = 8080 }
port "qbt_torrent" { static = 51413 } port "qbt_torrent" { static = 51413 }
@@ -34,7 +37,7 @@ job "media" {
} }
resources { resources {
cpu = 200 cpu = 1000
} }
service { service {
@@ -44,7 +47,7 @@ job "media" {
tags = [ tags = [
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.radarr.entryPoints=websecure", "traefik.http.routers.radarr.entryPoints=websecure",
"traefik.http.routers.radarr.middlewares=authentik@file", "traefik.http.routers.radarr.middlewares=oidc-auth@file",
] ]
} }
} }
@@ -68,7 +71,8 @@ job "media" {
} }
resources { resources {
cpu = 200 cpu = 1000
memory = 500
} }
service { service {
@@ -78,20 +82,54 @@ job "media" {
tags = [ tags = [
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.sonarr.entryPoints=websecure", "traefik.http.routers.sonarr.entryPoints=websecure",
"traefik.http.routers.sonarr.middlewares=authentik@file", "traefik.http.routers.sonarr.middlewares=oidc-auth@file",
] ]
} }
} }
task "bazarr" { # task "bazarr" {
# driver = "docker"
#
# config {
# image = "ghcr.io/hotio/bazarr:latest"
# ports = [ "bazarr" ]
# volumes = [
# "/data/services/media/bazarr:/config",
# "/data/media/media:/data/media",
# ]
# }
#
# env {
# PUID = 1000
# PGID = 1000
# TZ = "Europe/Lisbon"
# }
#
# resources {
# cpu = 200
# memory = 500
# }
#
# service {
# name = "bazarr"
# port = "bazarr"
#
# tags = [
# "traefik.enable=true",
# "traefik.http.routers.bazarr.entryPoints=websecure",
# "traefik.http.routers.bazarr.middlewares=oidc-auth@file",
# ]
# }
# }
task "prowlarr" {
driver = "docker" driver = "docker"
config { config {
image = "ghcr.io/hotio/bazarr:latest" image = "ghcr.io/hotio/prowlarr:latest"
ports = [ "bazarr" ] ports = [ "prowlarr" ]
volumes = [ volumes = [
"/data/services/media/bazarr:/config", "/data/services/media/prowlarr:/config",
"/data/media/media:/data/media",
] ]
} }
@@ -106,17 +144,93 @@ job "media" {
} }
service { service {
name = "bazarr" name = "prowlarr"
port = "bazarr" port = "prowlarr"
tags = [ tags = [
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.bazarr.entryPoints=websecure", "traefik.http.routers.prowlarr.entryPoints=websecure",
"traefik.http.routers.bazarr.middlewares=authentik@file", "traefik.http.routers.prowlarr.middlewares=oidc-auth@file",
] ]
} }
} }
task "jellyseerr" {
driver = "docker"
config {
image = "fallenbagel/jellyseerr:latest"
ports = [ "jellyseerr" ]
volumes = [
"/data/services/media/jellyseerr:/app/config",
]
}
env {
TZ = "Europe/Lisbon"
}
resources {
cpu = 200
}
service {
name = "jellyseerr"
port = "jellyseerr"
tags = [
"traefik.enable=true",
"traefik.http.routers.jellyseerr.entryPoints=websecure",
"traefik.http.routers.jellyseerr.middlewares=oidc-auth@file",
]
}
}
task "flaresolverr" {
driver = "docker"
config {
image = "ghcr.io/flaresolverr/flaresolverr:latest"
ports = [ "flaresolverr" ]
}
env {
LOG_LEVEL = "info"
TZ = "Europe/Lisbon"
}
resources {
cpu = 500
memory = 1024
}
service {
name = "flaresolverr"
port = "flaresolverr"
}
}
task "recyclarr" {
driver = "docker"
config {
image = "ghcr.io/recyclarr/recyclarr:latest"
volumes = [
"/data/services/media/recyclarr:/config",
]
}
env {
TZ = "Europe/Lisbon"
CRON_SCHEDULE = "0 0 * * *" # Daily at midnight
}
resources {
cpu = 100
memory = 256
}
}
task "plex" { task "plex" {
driver = "docker" driver = "docker"
@@ -138,7 +252,7 @@ job "media" {
resources { resources {
cpu = 2000 cpu = 2000
memory = 1000 memory = 2000
} }
service { service {
@@ -148,7 +262,7 @@ job "media" {
tags = [ tags = [
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.plex.entryPoints=websecure", "traefik.http.routers.plex.entryPoints=websecure",
"traefik.http.routers.plex.middlewares=authentik@file", "traefik.http.routers.plex.middlewares=oidc-auth@file",
] ]
} }
} }
@@ -177,7 +291,7 @@ job "media" {
resources { resources {
cpu = 2000 cpu = 2000
memory = 1000 memory = 1500
} }
service { service {
@@ -187,7 +301,7 @@ job "media" {
tags = [ tags = [
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.torrent.entryPoints=websecure", "traefik.http.routers.torrent.entryPoints=websecure",
"traefik.http.routers.torrent.middlewares=authentik@file", "traefik.http.routers.torrent.middlewares=oidc-auth@file",
] ]
} }
} }

View File

@@ -39,10 +39,10 @@ job "netbox" {
REMOTE_AUTH_ENABLED = "true" REMOTE_AUTH_ENABLED = "true"
REMOTE_AUTH_BACKEND = "social_core.backends.open_id_connect.OpenIdConnectAuth" REMOTE_AUTH_BACKEND = "social_core.backends.open_id_connect.OpenIdConnectAuth"
SOCIAL_AUTH_OIDC_ENDPOINT = "https://authentik.v.paler.net/application/o/netbox/" SOCIAL_AUTH_OIDC_ENDPOINT = "https://pocket-id.v.paler.net/"
SOCIAL_AUTH_OIDC_KEY = "XiPhZmWy2mp8hQyHLXCwk7njRNPSLTp2vSHhvWYI" SOCIAL_AUTH_OIDC_KEY = "6ce1f1bb-d5e8-4ba5-b136-2643dc8bcbcf"
SOCIAL_AUTH_OIDC_SECRET = "Kkop2dStx0gN52V1LfPnoxcaemuur6zMsvRnqpWSDe2qSngJVcqWfvFXaNeTbdURRB6TPwjlaNJ5BXR2ChcSmokWGTGargu84Ox1D6M2zXTsfLFj9B149Mhblos4mJL1" SOCIAL_AUTH_OIDC_SECRET = "Af7sJvCn9BuijoJXrB5aWv6fTmEqLCAf"
LOGOUT_REDIRECT_URL = "https://authentik.v.paler.net/application/o/netbox/end-session/" LOGOUT_REDIRECT_URL = "https://pocket-id.v.paler.net/logout"
} }
resources { resources {

39
services/ollama.hcl Normal file
View File

@@ -0,0 +1,39 @@
job "ollama" {
datacenters = ["alo"]
type = "service"
group "ollama" {
network {
port "http" {
static = 11434
}
}
task "server" {
driver = "docker"
config {
image = "ollama/ollama:latest"
ports = ["http"]
volumes = ["/data/services/ollama:/root/.ollama"]
}
service {
name = "ollama"
port = "http"
check {
type = "http"
path = "/"
interval = "30s"
timeout = "5s"
}
}
resources {
cpu = 8000
memory = 2048
}
}
}
}

138
services/phaseflow.hcl Normal file
View File

@@ -0,0 +1,138 @@
# ABOUTME: Nomad job for PhaseFlow - menstrual cycle tracking and training app.
# ABOUTME: Runs Next.js app with PocketBase sidecar for data persistence.
# Setup required before running:
# sudo mkdir -p /data/services/phaseflow/pb_data && sudo chown 1000:1000 /data/services/phaseflow /data/services/phaseflow/pb_data
# nomad var put secrets/phaseflow \
# mailgun_api_key="key-xxxx" \
# mailgun_domain="paler.net" \
# encryption_key="$(openssl rand -hex 16)" \
# cron_secret="$(openssl rand -hex 32)" \
# pocketbase_admin_email="admin@example.com" \
# pocketbase_admin_password="your-admin-password"
job "phaseflow" {
datacenters = ["alo"]
# Force re-pull of :latest images on each nomad run
meta {
uuid = uuidv4()
}
update {
max_parallel = 1
health_check = "checks"
min_healthy_time = "30s"
healthy_deadline = "5m"
progress_deadline = "10m"
auto_revert = true
}
group "app" {
network {
port "http" {
to = 3000
}
port "pocketbase" {
to = 8090
}
}
# PocketBase sidecar - starts first, provides database
task "pocketbase" {
driver = "docker"
user = "1000"
lifecycle {
hook = "prestart"
sidecar = true
}
config {
image = "ghcr.io/muchobien/pocketbase:latest"
ports = ["pocketbase"]
volumes = ["/data/services/phaseflow/pb_data:/pb_data"]
}
env {
PB_DATA_DIR = "/pb_data"
}
resources {
memory = 256
}
service {
name = "pocketbase-phaseflow"
port = "pocketbase"
tags = [
"traefik.enable=true",
"traefik.http.routers.pocketbase-phaseflow.entryPoints=websecure",
]
}
}
# Main Next.js application
task "app" {
driver = "docker"
user = "1000"
config {
image = "gitea.v.paler.net/alo/phaseflow:latest"
ports = ["http"]
force_pull = true
}
env {
NODE_ENV = "production"
POCKETBASE_URL = "http://${NOMAD_ADDR_pocketbase}"
NEXT_PUBLIC_POCKETBASE_URL = "https://pocketbase-phaseflow.v.paler.net"
APP_URL = "https://phaseflow.v.paler.net"
EMAIL_FROM = "phaseflow@paler.net"
MAILGUN_URL = "https://api.eu.mailgun.net"
}
# Secrets from Nomad variables
template {
destination = "secrets/env.env"
env = true
data = <<EOH
MAILGUN_API_KEY={{ with nomadVar "secrets/phaseflow" }}{{ .mailgun_api_key }}{{ end }}
MAILGUN_DOMAIN={{ with nomadVar "secrets/phaseflow" }}{{ .mailgun_domain }}{{ end }}
ENCRYPTION_KEY={{ with nomadVar "secrets/phaseflow" }}{{ .encryption_key }}{{ end }}
CRON_SECRET={{ with nomadVar "secrets/phaseflow" }}{{ .cron_secret }}{{ end }}
POCKETBASE_ADMIN_EMAIL={{ with nomadVar "secrets/phaseflow" }}{{ .pocketbase_admin_email }}{{ end }}
POCKETBASE_ADMIN_PASSWORD={{ with nomadVar "secrets/phaseflow" }}{{ .pocketbase_admin_password }}{{ end }}
EOH
}
resources {
memory = 512
}
service {
name = "phaseflow"
port = "http"
tags = [
"traefik.enable=true",
"traefik.http.routers.phaseflow.entryPoints=websecure",
"metrics",
]
check {
type = "http"
path = "/api/health"
interval = "10s"
timeout = "5s"
check_restart {
limit = 3
grace = "60s"
}
}
}
}
}
}

51
services/pocket-id.hcl Normal file
View File

@@ -0,0 +1,51 @@
job "pocket-id" {
datacenters = ["alo"]
group "app" {
network {
port "http" {
to = 1411
}
}
task "server" {
driver = "docker"
config {
image = "ghcr.io/pocket-id/pocket-id:v1"
ports = ["http"]
volumes = [
"/data/services/pocket-id:/app/data",
]
}
env {
APP_URL = "https://pocket-id.v.paler.net"
TRUST_PROXY = "true"
MAXMIND_LICENSE_KEY = "${var.maxmind_license_key}"
PUID = "1000"
PGID = "1000"
}
resources {
cpu = 500
memory = 512
}
service {
name = "pocket-id"
port = "http"
tags = [
"traefik.enable=true",
"traefik.http.routers.pocket-id.entryPoints=websecure",
]
}
}
}
}
variable "maxmind_license_key" {
type = string
default = "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"
}

View File

@@ -91,15 +91,15 @@ job "postgres" {
PGADMIN_CONFIG_OAUTH2_AUTO_CREATE_USER = "True" PGADMIN_CONFIG_OAUTH2_AUTO_CREATE_USER = "True"
PGADMIN_CONFIG_OAUTH2_CONFIG = <<EOH PGADMIN_CONFIG_OAUTH2_CONFIG = <<EOH
[{ [{
'OAUTH2_NAME' : 'authentik', 'OAUTH2_NAME' : 'pocket-id',
'OAUTH2_DISPLAY_NAME' : 'SSO', 'OAUTH2_DISPLAY_NAME' : 'SSO',
'OAUTH2_CLIENT_ID' : 'o4p3B03ayTQ2kpwmM7GswbcfO78JHCTdoZqKJEut', 'OAUTH2_CLIENT_ID' : '180133da-1bd7-4cde-9c18-2f277e962dab',
'OAUTH2_CLIENT_SECRET' : '7UYHONOCVdjpRMK9Ojwds0qPPpxCiztbIRhK7FJ2IFBpUgN6tnmpEjlkPYimiGKfaHLhy4XE7kQm7Et1Jm0hgyia0iB1VIlp623ckppbwkM6IfpTE1LfEmTMtPrxSngx', 'OAUTH2_CLIENT_SECRET' : 'ELYNAfiWSGYJQUXUDOdpm7tTtyLbrs4E',
'OAUTH2_TOKEN_URL' : 'https://authentik.v.paler.net/application/o/token/', 'OAUTH2_TOKEN_URL' : 'https://pocket-id.v.paler.net/api/oidc/token',
'OAUTH2_AUTHORIZATION_URL' : 'https://authentik.v.paler.net/application/o/authorize/', 'OAUTH2_AUTHORIZATION_URL' : 'https://pocket-id.v.paler.net/authorize',
'OAUTH2_API_BASE_URL' : 'https://authentik.v.paler.net/', 'OAUTH2_API_BASE_URL' : 'https://pocket-id.v.paler.net/',
'OAUTH2_USERINFO_ENDPOINT' : 'https://authentik.v.paler.net/application/o/userinfo/', 'OAUTH2_USERINFO_ENDPOINT' : 'https://pocket-id.v.paler.net/api/oidc/userinfo',
'OAUTH2_SERVER_METADATA_URL' : 'https://authentik.v.paler.net/application/o/pgadmin/.well-known/openid-configuration', 'OAUTH2_SERVER_METADATA_URL' : 'https://pocket-id.v.paler.net/.well-known/openid-configuration',
'OAUTH2_SCOPE' : 'openid email profile', 'OAUTH2_SCOPE' : 'openid email profile',
'OAUTH2_ICON' : 'fa-database', 'OAUTH2_ICON' : 'fa-database',
'OAUTH2_BUTTON_COLOR' : '#00ff00' 'OAUTH2_BUTTON_COLOR' : '#00ff00'

View File

@@ -54,7 +54,7 @@ job "prometheus" {
tags = [ tags = [
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.prometheus.entryPoints=websecure", "traefik.http.routers.prometheus.entryPoints=websecure",
"traefik.http.routers.prometheus.middlewares=authentik@file", "traefik.http.routers.prometheus.middlewares=oidc-auth@file",
] ]
check { check {

146
services/tiddlywiki-mcp.hcl Normal file
View File

@@ -0,0 +1,146 @@
job "tiddlywiki-mcp" {
datacenters = ["alo"]
meta {
uuid = uuidv4()
}
group "servers" {
network {
port "captainslog" {
static = 3500
}
port "alowiki" {
static = 3501
}
}
volume "services" {
type = "host"
source = "services"
read_only = false
}
volume "nix-store" {
type = "host"
source = "nix-store"
read_only = true
}
volume "sw" {
type = "host"
source = "sw"
read_only = true
}
task "captainslog" {
driver = "exec"
config {
command = "/sw/bin/node"
args = ["/data/services/tiddlywiki-mcp/dist/index.js"]
}
env {
MCP_TRANSPORT = "http"
MCP_PORT = "${NOMAD_PORT_captainslog}"
TIDDLYWIKI_URL = "captainslog.service.consul"
OLLAMA_URL = "ollama.service.consul"
AUTH_HEADER = "X-Oidc-Username"
AUTH_USER = "claude-code"
EMBEDDINGS_DB_PATH = "/data/services/tiddlywiki-mcp/embeddings-captainslog.db"
}
volume_mount {
volume = "services"
destination = "/data/services"
read_only = false
}
volume_mount {
volume = "nix-store"
destination = "/nix/store"
read_only = true
}
volume_mount {
volume = "sw"
destination = "/sw"
read_only = true
}
service {
name = "tiddlywiki-mcp-captainslog"
port = "captainslog"
check {
type = "http"
path = "/health"
interval = "10s"
timeout = "2s"
}
}
resources {
memory = 256
}
user = "ppetru"
}
task "alowiki" {
driver = "exec"
config {
command = "/sw/bin/node"
args = ["/data/services/tiddlywiki-mcp/dist/index.js"]
}
env {
MCP_TRANSPORT = "http"
MCP_PORT = "${NOMAD_PORT_alowiki}"
TIDDLYWIKI_URL = "alowiki.service.consul"
OLLAMA_URL = "ollama.service.consul"
AUTH_HEADER = "X-Oidc-Username"
AUTH_USER = "claude-code"
EMBEDDINGS_DB_PATH = "/data/services/tiddlywiki-mcp/embeddings-alowiki.db"
}
volume_mount {
volume = "services"
destination = "/data/services"
read_only = false
}
volume_mount {
volume = "nix-store"
destination = "/nix/store"
read_only = true
}
volume_mount {
volume = "sw"
destination = "/sw"
read_only = true
}
service {
name = "tiddlywiki-mcp-alowiki"
port = "alowiki"
check {
type = "http"
path = "/health"
interval = "10s"
timeout = "2s"
}
}
resources {
memory = 256
}
user = "ppetru"
}
}
}

View File

@@ -34,7 +34,7 @@ job "traefik" {
tags = [ tags = [
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.api.entryPoints=websecure", "traefik.http.routers.api.entryPoints=websecure",
"traefik.http.routers.api.middlewares=authentik@file", "traefik.http.routers.api.middlewares=oidc-auth@file",
"traefik.http.routers.api.rule=Host(`traefik.v.paler.net`)", "traefik.http.routers.api.rule=Host(`traefik.v.paler.net`)",
"traefik.http.routers.api.service=api@internal", "traefik.http.routers.api.service=api@internal",
] ]
@@ -63,6 +63,7 @@ job "traefik" {
volumes = [ volumes = [
"local/traefik.yml:/etc/traefik/traefik.yml", "local/traefik.yml:/etc/traefik/traefik.yml",
"/data/services/traefik:/config", "/data/services/traefik:/config",
"/data/services/traefik/plugins-storage:/plugins-storage",
] ]
} }
@@ -75,6 +76,12 @@ global:
#log: #log:
# level: debug # level: debug
experimental:
plugins:
traefik-oidc-auth:
moduleName: "github.com/sevensolutions/traefik-oidc-auth"
version: "v0.17.0"
api: api:
dashboard: true dashboard: true
@@ -126,6 +133,8 @@ entryPoints:
websecure: websecure:
address: ":{{{ env "NOMAD_PORT_https" }}}" address: ":{{{ env "NOMAD_PORT_https" }}}"
http: http:
encodedCharacters:
allowEncodedSlash: true
tls: tls:
certResolver: letsencrypt certResolver: letsencrypt
@@ -145,7 +154,7 @@ EOH
} }
resources { resources {
cpu = 100 cpu = 200
memory = 512 memory = 512
} }
} }

View File

@@ -69,7 +69,7 @@ job "unifi" {
tags = [ tags = [
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.unifi.entryPoints=websecure", "traefik.http.routers.unifi.entryPoints=websecure",
"traefik.http.routers.unifi.middlewares=authentik@file", "traefik.http.routers.unifi.middlewares=oidc-auth@file",
"traefik.http.services.unifi.loadbalancer.server.scheme=https", "traefik.http.services.unifi.loadbalancer.server.scheme=https",
] ]
} }

View File

@@ -4,6 +4,10 @@
job "urbit" { job "urbit" {
datacenters = ["alo"] datacenters = ["alo"]
meta {
uuid = uuidv4()
}
group "os" { group "os" {
network { network {
port "http" { port "http" {
@@ -25,7 +29,7 @@ job "urbit" {
# You can also set a variable loom size (Urbit memory size) using # You can also set a variable loom size (Urbit memory size) using
# --loom=$LOOM_SIZE. Passing /bin/start-urbit --loom=32 for example, would set up # --loom=$LOOM_SIZE. Passing /bin/start-urbit --loom=32 for example, would set up
# a 4GiB loom (2^32 bytes = 4GiB). The default loom size is 31 (2GiB). # a 4GiB loom (2^32 bytes = 4GiB). The default loom size is 31 (2GiB).
"--loom=31", "--loom=32",
] ]
volumes = [ volumes = [
"/data/services/urbit:/urbit", "/data/services/urbit:/urbit",
@@ -39,13 +43,13 @@ job "urbit" {
tags = [ tags = [
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.urbit.entryPoints=websecure", "traefik.http.routers.urbit.entryPoints=websecure",
"traefik.http.routers.urbit.middlewares=authentik@file", "traefik.http.routers.urbit.middlewares=oidc-auth@file",
] ]
} }
resources { resources {
# dependent on --loom setting + some buffer # dependent on --loom setting + some buffer
memory = 2100 memory = 4200
} }
} }
} }

48
services/wavelog.hcl Normal file
View File

@@ -0,0 +1,48 @@
# ABOUTME: Nomad job spec for Wavelog amateur radio logging application
# ABOUTME: Uses MySQL database and OIDC auth via Traefik middleware
job "wavelog" {
datacenters = ["alo"]
group "wavelog" {
network {
port "http" {
to = 80
}
}
task "wavelog" {
driver = "docker"
config {
image = "ghcr.io/wavelog/wavelog:latest"
ports = ["http"]
volumes = [
"/data/services/wavelog/config:/var/www/html/application/config/docker",
"/data/services/wavelog/uploads:/var/www/html/uploads",
"/data/services/wavelog/userdata:/var/www/html/userdata",
]
}
env {
CI_ENV = "docker"
}
resources {
cpu = 300
memory = 512
}
service {
name = "wavelog"
port = "http"
tags = [
"traefik.enable=true",
"traefik.http.routers.wavelog.entryPoints=websecure",
"traefik.http.routers.wavelog.middlewares=oidc-auth@file",
]
}
}
}
}

View File

@@ -73,7 +73,7 @@ EOH
tags = [ tags = [
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.webodm.entryPoints=websecure", "traefik.http.routers.webodm.entryPoints=websecure",
"traefik.http.routers.webodm.middlewares=authentik@file", "traefik.http.routers.webodm.middlewares=oidc-auth@file",
] ]
} }
} }
@@ -97,7 +97,7 @@ EOH
tags = [ tags = [
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.clusterodm.entryPoints=websecure", "traefik.http.routers.clusterodm.entryPoints=websecure",
"traefik.http.routers.clusterodm.middlewares=authentik@file", "traefik.http.routers.clusterodm.middlewares=oidc-auth@file",
] ]
} }

View File

@@ -19,7 +19,7 @@ job "weewx" {
driver = "docker" driver = "docker"
config { config {
image = "gitea.v.paler.net/ppetru/weewx:latest" image = "gitea.v.paler.net/alo/weewx:latest"
# to be able to receive UDP broadcast packets from the weatherlink # to be able to receive UDP broadcast packets from the weatherlink
network_mode = "host" network_mode = "host"
volumes = [ volumes = [
@@ -29,7 +29,7 @@ job "weewx" {
} }
resources { resources {
memory = 1024 memory = 1500
} }
} }
@@ -54,7 +54,7 @@ job "weewx" {
driver = "docker" driver = "docker"
config { config {
image = "gitea.v.paler.net/ppetru/opensprinkler-weather:latest" image = "gitea.v.paler.net/alo/opensprinkler-weather:latest"
ports = [ "osweather" ] ports = [ "osweather" ]
} }

View File

@@ -22,7 +22,7 @@ job "whoami" {
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.whoami.rule=Host(`test.alo.land`)", "traefik.http.routers.whoami.rule=Host(`test.alo.land`)",
"traefik.http.routers.whoami.entryPoints=websecure", "traefik.http.routers.whoami.entryPoints=websecure",
"traefik.http.routers.whoami.middlewares=authentik@file", "traefik.http.routers.whoami.middlewares=oidc-auth@file",
] ]
} }
} }

View File

@@ -36,9 +36,9 @@ job "wiki" {
"--listen", "--listen",
"host=0.0.0.0", "host=0.0.0.0",
"port=${NOMAD_PORT_captainslog}", "port=${NOMAD_PORT_captainslog}",
"authenticated-user-header=X-authentik-username", "authenticated-user-header=X-Oidc-Username",
"readers=ppetru", "readers=ppetru,claude-code",
"writers=ppetru", "writers=ppetru,claude-code",
"admin=ppetru", "admin=ppetru",
] ]
} }
@@ -64,7 +64,7 @@ job "wiki" {
tags = [ tags = [
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.captainslog.entryPoints=websecure", "traefik.http.routers.captainslog.entryPoints=websecure",
"traefik.http.routers.captainslog.middlewares=authentik@file", "traefik.http.routers.captainslog.middlewares=oidc-auth@file",
] ]
} }
@@ -85,9 +85,9 @@ job "wiki" {
"--listen", "--listen",
"host=0.0.0.0", "host=0.0.0.0",
"port=${NOMAD_PORT_alo}", "port=${NOMAD_PORT_alo}",
"authenticated-user-header=X-authentik-username", "authenticated-user-header=X-Oidc-Username",
"readers=ppetru,ines", "readers=ppetru,ines,claude-code",
"writers=ppetru,ines", "writers=ppetru,ines,claude-code",
"admin=ppetru", "admin=ppetru",
] ]
} }
@@ -112,7 +112,7 @@ job "wiki" {
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.alowiki.rule=Host(`wiki.alo.land`)", "traefik.http.routers.alowiki.rule=Host(`wiki.alo.land`)",
"traefik.http.routers.alowiki.entryPoints=websecure", "traefik.http.routers.alowiki.entryPoints=websecure",
"traefik.http.routers.alowiki.middlewares=authentik@file", "traefik.http.routers.alowiki.middlewares=oidc-auth@file",
] ]
} }
@@ -133,7 +133,7 @@ job "wiki" {
"--listen", "--listen",
"host=0.0.0.0", "host=0.0.0.0",
"port=${NOMAD_PORT_pispace}", "port=${NOMAD_PORT_pispace}",
"authenticated-user-header=X-authentik-username", "authenticated-user-header=X-Oidc-Username",
"readers=ppetru,ines", "readers=ppetru,ines",
"writers=ppetru,ines", "writers=ppetru,ines",
"admin=ppetru", "admin=ppetru",
@@ -160,7 +160,7 @@ job "wiki" {
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.pispace.rule=Host(`pi.paler.net`)", "traefik.http.routers.pispace.rule=Host(`pi.paler.net`)",
"traefik.http.routers.pispace.entryPoints=websecure", "traefik.http.routers.pispace.entryPoints=websecure",
"traefik.http.routers.pispace.middlewares=authentik@file", "traefik.http.routers.pispace.middlewares=oidc-auth@file",
] ]
} }
@@ -181,7 +181,7 @@ job "wiki" {
"--listen", "--listen",
"host=0.0.0.0", "host=0.0.0.0",
"port=${NOMAD_PORT_grok}", "port=${NOMAD_PORT_grok}",
"authenticated-user-header=X-authentik-username", "authenticated-user-header=X-Oidc-Username",
"readers=ppetru", "readers=ppetru",
"writers=ppetru", "writers=ppetru",
"admin=ppetru", "admin=ppetru",
@@ -207,7 +207,7 @@ job "wiki" {
tags = [ tags = [
"traefik.enable=true", "traefik.enable=true",
"traefik.http.routers.groktw.entryPoints=websecure", "traefik.http.routers.groktw.entryPoints=websecure",
"traefik.http.routers.groktw.middlewares=authentik@file", "traefik.http.routers.groktw.middlewares=oidc-auth@file",
] ]
} }

View File

@@ -17,7 +17,7 @@ job "wordpress" {
user = "237" user = "237"
config { config {
image = "gitea.v.paler.net/ppetru/wordpress" image = "gitea.v.paler.net/alo/wordpress"
ports = ["http"] ports = ["http"]
volumes = [ volumes = [
"/data/services/wordpress:/var/www/html", "/data/services/wordpress:/var/www/html",