Compare commits

..

8 Commits

Author SHA1 Message Date
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
40 changed files with 1968 additions and 655 deletions

View File

@@ -1,51 +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-color-emoji
liberation_ttf
fira-code
fira-code-symbols
];
# Environment variables for Wayland
environment.sessionVariables = {
NIXOS_OZONE_WL = "1"; # Hint electron apps to use Wayland
};
environment.systemPackages = with pkgs; [
prusa-slicer
];
}

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

View File

@@ -3,7 +3,7 @@
boot.loader.systemd-boot = {
enable = true;
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;
}

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

522
flake.lock generated
View File

@@ -1,42 +1,5 @@
{
"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": {
"flake": false,
"locked": {
@@ -62,11 +25,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1766172613,
"narHash": "sha256-/pEK+Nbt14ALuPZ1DW9muhBvDg742uaeSqKEHnJ+KjY=",
"lastModified": 1766518262,
"narHash": "sha256-ITihWSOexQX1a/i8ZiMt3M609KxX7sLhz63F9rnnguY=",
"owner": "nix-community",
"repo": "browser-previews",
"rev": "8a39d32f542954b0493f663302cdde2bf5c2b909",
"rev": "1e4c4310443ace75d0df9efaeb4c6fc6f8c8678c",
"type": "github"
},
"original": {
@@ -181,22 +144,6 @@
"type": "github"
}
},
"flake-compat_2": {
"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": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
@@ -304,29 +251,6 @@
"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": {
"inputs": {
"nixpkgs": [
@@ -334,11 +258,11 @@
]
},
"locked": {
"lastModified": 1766292113,
"narHash": "sha256-sWTtmkQujRpjWYCnZc8LWdDiCzrRlSBPrGovkZpLkBI=",
"lastModified": 1766553861,
"narHash": "sha256-ZbnG01yA3O8Yr1vUm3+NQ2qk9iRhS5bloAnuXHHy7+c=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "fdec8815a86db36f42fc9c8cb2931cd8485f5aed",
"rev": "0999ed8f965bbbd991437ad9c5ed3434cecbc30e",
"type": "github"
},
"original": {
@@ -348,287 +272,6 @@
"type": "github"
}
},
"hyprcursor": {
"inputs": {
"hyprlang": [
"omarchy-nix",
"hyprland",
"hyprlang"
],
"nixpkgs": [
"omarchy-nix",
"hyprland",
"nixpkgs"
],
"systems": [
"omarchy-nix",
"hyprland",
"systems"
]
},
"locked": {
"lastModified": 1753964049,
"narHash": "sha256-lIqabfBY7z/OANxHoPeIrDJrFyYy9jAM4GQLzZ2feCM=",
"owner": "hyprwm",
"repo": "hyprcursor",
"rev": "44e91d467bdad8dcf8bbd2ac7cf49972540980a5",
"type": "github"
},
"original": {
"owner": "hyprwm",
"repo": "hyprcursor",
"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_5",
"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"
}
},
"impermanence": {
"locked": {
"lastModified": 1737831083,
@@ -685,11 +328,11 @@
},
"nixos-hardware": {
"locked": {
"lastModified": 1764440730,
"narHash": "sha256-ZlJTNLUKQRANlLDomuRWLBCH5792x+6XUJ4YdFRjtO4=",
"lastModified": 1766568855,
"narHash": "sha256-UXVtN77D7pzKmzOotFTStgZBqpOcf8cO95FcupWp4Zo=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "9154f4569b6cdfd3c595851a6ba51bfaa472d9f3",
"rev": "c5db9569ac9cc70929c268ac461f4003e3e5ca80",
"type": "github"
},
"original": {
@@ -701,11 +344,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1766201043,
"narHash": "sha256-eplAP+rorKKd0gNjV3rA6+0WMzb1X1i16F5m5pASnjA=",
"lastModified": 1766622938,
"narHash": "sha256-Eovt/DOCYjFFBZuYbbG9j5jhklzxdNbUGVYYxh3lG3s=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b3aad468604d3e488d627c0b43984eb60e75e782",
"rev": "5900a0a8850cbba98e16d5a7a6ed389402dfcf4f",
"type": "github"
},
"original": {
@@ -763,27 +406,11 @@
},
"nixpkgs-unstable_2": {
"locked": {
"lastModified": 1766070988,
"narHash": "sha256-G/WVghka6c4bAzMhTwT2vjLccg/awmHkdKSd2JrycLc=",
"lastModified": 1766651565,
"narHash": "sha256-QEhk0eXgyIqTpJ/ehZKg9IKS7EtlWxF3N7DXy42zPfU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c6245e83d836d0433170a16eb185cefe0572f8b8",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1754725699,
"narHash": "sha256-iAcj9T/Y+3DBy2J0N+yF9XQQQ8IEb5swLFzs23CdP88=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "85dbfc7aaf52ecb755f87e577ddbe6dbbdbc1054",
"rev": "3e2499d5539c16d0d173ba53552a4ff8547f4539",
"type": "github"
},
"original": {
@@ -802,11 +429,11 @@
"systems": "systems_4"
},
"locked": {
"lastModified": 1766273987,
"narHash": "sha256-Y8hL2zGyt7xn5J1V806GJ9tMEk6NgVlU7xe4dS4fThE=",
"lastModified": 1766721995,
"narHash": "sha256-2qZLSojZFP3AzbC6UNF3ASCIDLahNniR2XP7l/qINm4=",
"owner": "nix-community",
"repo": "nixvim",
"rev": "ff00fe1512dfcb31b01d770738de9299b434449b",
"rev": "66a5dc70e2d8433034bccdbb9c3c7bcecd86f9a6",
"type": "github"
},
"original": {
@@ -815,55 +442,6 @@
"type": "github"
}
},
"omarchy-nix": {
"inputs": {
"home-manager": [
"home-manager"
],
"hyprland": "hyprland",
"nix-colors": "nix-colors",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1762999930,
"narHash": "sha256-uKyxLwiN6sD6EmRSno66y1a8oqISr1XiWxbWHoMJT7I=",
"owner": "henrysipp",
"repo": "omarchy-nix",
"rev": "308e0f85a0deb820c01cfbe1b4faee1daab4da12",
"type": "github"
},
"original": {
"owner": "henrysipp",
"repo": "omarchy-nix",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": "flake-compat_2",
"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": {
"inputs": {
"browser-previews": "browser-previews",
@@ -872,12 +450,12 @@
"ethereum-nix": "ethereum-nix",
"home-manager": "home-manager",
"impermanence": "impermanence",
"nix-colors": "nix-colors",
"nix-index-database": "nix-index-database",
"nixos-hardware": "nixos-hardware",
"nixpkgs": "nixpkgs",
"nixpkgs-unstable": "nixpkgs-unstable_2",
"nixvim": "nixvim",
"omarchy-nix": "omarchy-nix",
"sops-nix": "sops-nix"
}
},
@@ -961,21 +539,6 @@
"type": "github"
}
},
"systems_5": {
"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": {
"inputs": {
"nixpkgs": [
@@ -1014,53 +577,6 @@
"repo": "flake-utils",
"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",

View File

@@ -33,11 +33,7 @@
url = "github:nix-community/browser-previews";
inputs.nixpkgs.follows = "nixpkgs-unstable";
};
omarchy-nix = {
url = "github:henrysipp/omarchy-nix";
inputs.nixpkgs.follows = "nixpkgs";
inputs.home-manager.follows = "home-manager";
};
nix-colors.url = "github:misterio77/nix-colors";
nixos-hardware.url = "github:NixOS/nixos-hardware/master";
};
@@ -53,7 +49,7 @@
impermanence,
sops-nix,
browser-previews,
omarchy-nix,
nix-colors,
nixos-hardware,
...
}@inputs:
@@ -93,37 +89,27 @@
home-manager.nixosModules.home-manager
(
{ lib, ... }:
lib.mkMerge [
{
home-manager = {
useGlobalPkgs = true;
useUserPackages = true;
users.ppetru = {
imports = [
inputs.nix-index-database.homeModules.nix-index
inputs.nixvim.homeModules.nixvim
./home
] ++ lib.optionals (profile == "desktop") [
omarchy-nix.homeManagerModules.default
];
};
extraSpecialArgs = {
inherit profile;
};
{
home-manager = {
useGlobalPkgs = true;
useUserPackages = true;
users.ppetru = {
imports = [
inputs.nix-index-database.homeModules.nix-index
inputs.nixvim.homeModules.nixvim
./home
] ++ lib.optionals (profile == "desktop") [
nix-colors.homeManagerModules.default
];
};
}
(lib.optionalAttrs (profile == "desktop") {
omarchy = {
full_name = "Petru Paler";
email_address = "petru@paler.net";
theme = "tokyo-night";
monitors = [ "DP-1,preferred,auto,1.5" ];
extraSpecialArgs = {
inherit profile nix-colors;
};
})
]
};
}
)
] ++ nixpkgs.lib.optionals (profile == "desktop") [
omarchy-nix.nixosModules.default
./common/desktop
] ++ modules;
specialArgs = {
inherit inputs self;

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 = {
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 }:
let
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; [
browser-previews.google-chrome
foot # Wayland-native terminal emulator
wofi # Application launcher for Wayland
nautilus
blueberry
libnotify
];
in
{
packages = workstationProfile.packages ++ desktopPkgs;
packages = workstationProfile.packages ++ hyprlandPkgs ++ desktopPkgs;
}

View File

@@ -3,6 +3,8 @@ let
serverProfile = import ./server.nix { inherit pkgs; };
cliPkgs = with pkgs; [
ast-grep
yq
unstable.claude-code
unstable.codex
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,23 @@
# 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;
};
};
gestures = lib.mkDefault {
workspace_swipe = 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

@@ -3,7 +3,7 @@
imports = [
../../common/encrypted-btrfs-layout.nix
../../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-tools.nix # Nomad CLI (no service)
./hardware.nix

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/ppetru/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="
];
};
};
});
}

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/ppetru/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
}
}
}
}

87
services/animaltrack.hcl Normal file
View File

@@ -0,0 +1,87 @@
# 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/ppetru/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",
]
check {
type = "http"
path = "/healthz"
interval = "10s"
timeout = "5s"
check_restart {
limit = 3
grace = "60s"
}
}
}
}
}
}

View File

@@ -19,7 +19,9 @@ job "gitea" {
driver = "docker"
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 = [
"http",
"ssh",
@@ -42,6 +44,8 @@ job "gitea" {
GITEA__mailer__FROM = "gitea@paler.net"
GITEA__mailer__PROTOCOL = "smtp"
GITEA__mailer__SMTP_ADDR = "192.168.1.1"
GITEA__actions__ENABLED = "true"
GITEA__actions__DEFAULT_ACTIONS_URL = "https://gitea.com"
}
service {

View File

@@ -9,42 +9,19 @@ job "igsync" {
}
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" {
driver = "exec"
driver = "docker"
config {
command = "/data/services/igsync/run.sh"
}
image = "gitea.v.paler.net/ppetru/igsync:latest"
user = "ppetru"
# Mount the data directory for .env, database, and media files
volumes = [
"/data/services/igsync:/data/services/igsync"
]
volume_mount {
volume = "services"
destination = "/data/services"
}
volume_mount {
volume = "nix-store"
destination = "/nix/store"
}
volume_mount {
volume = "sw"
destination = "/sw"
# Force pull to always get latest image
force_pull = true
}
resources {

View File

@@ -1,6 +1,10 @@
job "tiddlywiki-mcp" {
datacenters = ["alo"]
meta {
uuid = uuidv4()
}
group "servers" {
network {
port "captainslog" {
@@ -40,7 +44,8 @@ job "tiddlywiki-mcp" {
env {
MCP_TRANSPORT = "http"
MCP_PORT = "${NOMAD_PORT_captainslog}"
CONSUL_SERVICE = "captainslog.service.consul"
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"
@@ -94,7 +99,8 @@ job "tiddlywiki-mcp" {
env {
MCP_TRANSPORT = "http"
MCP_PORT = "${NOMAD_PORT_alowiki}"
CONSUL_SERVICE = "alowiki.service.consul"
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"