diff --git a/README.md b/README.md index 9b70d8a..b515db3 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,10 @@ Personal Nix flake for four machines: - `modules/` - flake-parts modules, auto-imported via `import-tree` - `modules/hosts/` - per-host composition modules -- `modules/hosts/_parts/` - host-private leaf modules like hardware, disks, and services +- `modules/hosts/_parts/` - host-private leaf modules like hardware, disks, and literal networking - `modules/profiles/` - shared host and user profile bundles - `modules/_lib/` - local helper functions +- `modules/_notability/`, `modules/_paperless/` - feature-owned scripts and templates - `apps/` - Nushell apps exposed through the flake - `secrets/` - SOPS-encrypted secrets - `flake.nix` - generated flake entrypoint @@ -27,7 +28,8 @@ This repo uses `den` and organizes configuration around aspects instead of putti - the machine inventory lives in `modules/inventory.nix` - shared bundles live in `modules/profiles/{host,user}/` - host composition happens in `modules/hosts/.nix` -- host-private imports live in `modules/hosts/_parts//` +- host-private imports live in `modules/hosts/_parts//` and stay limited to true machine leaf files +- feature-owned services live in top-level modules like `modules/gitea.nix`, `modules/notability.nix`, and `modules/paperless.nix` - user-level config mostly lives in Home Manager aspects Common examples: @@ -35,6 +37,8 @@ Common examples: - `modules/core.nix` - shared Nix and shell foundation - `modules/dev-tools.nix` - VCS, language, and developer tooling - `modules/network.nix` - SSH, fail2ban, and tailscale aspects +- `modules/gitea.nix` - Gitea, Litestream, and backup stack for `michael` +- `modules/notability.nix` - Notability ingest services and user tooling for `tahani` - `modules/profiles/user/workstation.nix` - shared developer workstation user bundle - `modules/hosts/michael.nix` - server composition for `michael` - `modules/hosts/tahani.nix` - server/workstation composition for `tahani` diff --git a/modules/_darwin/dock.nix b/modules/_darwin/dock.nix index 3928abb..3dd04b4 100644 --- a/modules/_darwin/dock.nix +++ b/modules/_darwin/dock.nix @@ -6,7 +6,9 @@ }: with lib; let cfg = config.local.dock; - inherit (pkgs) stdenv dockutil; + inherit (pkgs) dockutil stdenv; + local = import ../_lib/local.nix; + userHome = local.mkHome local.hosts.chidi.system; in { options = { local.dock = { @@ -45,7 +47,7 @@ in { {path = "/System/Applications/Music.app/";} {path = "/System/Applications/System Settings.app/";} { - path = "/Users/cschmatzler/Downloads"; + path = "${userHome}/Downloads"; section = "others"; options = "--sort name --view grid --display stack"; } @@ -56,7 +58,7 @@ in { mkOption { description = "Username to apply the dock settings to"; type = types.str; - default = "cschmatzler"; + default = local.user.name; }; }; }; diff --git a/modules/_lib/caddy.nix b/modules/_lib/caddy.nix new file mode 100644 index 0000000..506768a --- /dev/null +++ b/modules/_lib/caddy.nix @@ -0,0 +1,19 @@ +let + local = import ./local.nix; +in { + inherit (local) tailscaleHost; + + mkTailscaleVHost = { + name, + configText, + }: { + "${local.tailscaleHost name}" = { + extraConfig = '' + tls { + get_certificate tailscale + } + ${configText} + ''; + }; + }; +} diff --git a/modules/_lib/hosts.nix b/modules/_lib/hosts.nix new file mode 100644 index 0000000..d55257e --- /dev/null +++ b/modules/_lib/hosts.nix @@ -0,0 +1,34 @@ +{ + den, + lib, +}: { + mkUserHost = { + system, + host, + user, + userAspect ? "${host}-${user}", + includes ? [], + homeManager ? null, + }: + (lib.setAttrByPath ["den" "hosts" system host "users" user "aspect"] userAspect) + // (lib.setAttrByPath ["den" "aspects" userAspect] ({inherit includes;} + // lib.optionalAttrs (homeManager != null) { + inherit homeManager; + })); + + mkPerHostAspect = { + host, + includes ? [], + darwin ? null, + nixos ? null, + }: + lib.setAttrByPath ["den" "aspects" host "includes"] [ + (den.lib.perHost ({inherit includes;} + // lib.optionalAttrs (darwin != null) { + inherit darwin; + } + // lib.optionalAttrs (nixos != null) { + inherit nixos; + })) + ]; +} diff --git a/modules/_lib/local.nix b/modules/_lib/local.nix new file mode 100644 index 0000000..b431493 --- /dev/null +++ b/modules/_lib/local.nix @@ -0,0 +1,33 @@ +rec { + user = { + name = "cschmatzler"; + fullName = "Christoph Schmatzler"; + emails = { + personal = "christoph@schmatzler.com"; + work = "christoph@tuist.dev"; + icloud = "christoph.schmatzler@icloud.com"; + }; + }; + + secretPath = name: "/run/secrets/${name}"; + + mkHome = system: + if builtins.match ".*-darwin" system != null + then "/Users/${user.name}" + else "/home/${user.name}"; + + mkHost = system: { + inherit system; + home = mkHome system; + }; + + hosts = { + chidi = mkHost "aarch64-darwin"; + janet = mkHost "aarch64-darwin"; + michael = mkHost "x86_64-linux"; + tahani = mkHost "x86_64-linux"; + }; + + tailscaleDomain = "manticore-hippocampus.ts.net"; + tailscaleHost = name: "${name}.${tailscaleDomain}"; +} diff --git a/modules/_lib/secrets.nix b/modules/_lib/secrets.nix new file mode 100644 index 0000000..02fd282 --- /dev/null +++ b/modules/_lib/secrets.nix @@ -0,0 +1,44 @@ +{lib}: let + local = import ./local.nix; +in rec { + mkBinarySecret = { + name, + sopsFile, + owner ? null, + group ? null, + path ? local.secretPath name, + }: + { + inherit path sopsFile; + format = "binary"; + } + // lib.optionalAttrs (owner != null) { + inherit owner; + } + // lib.optionalAttrs (group != null) { + inherit group; + }; + + mkUserBinarySecret = { + name, + sopsFile, + owner ? local.user.name, + path ? local.secretPath name, + }: + mkBinarySecret { + inherit name owner path sopsFile; + }; + + mkServiceBinarySecret = { + name, + sopsFile, + serviceUser, + serviceGroup ? serviceUser, + path ? local.secretPath name, + }: + mkBinarySecret { + inherit name path sopsFile; + group = serviceGroup; + owner = serviceUser; + }; +} diff --git a/modules/hosts/_parts/tahani/notability/jobs.nu b/modules/_notability/jobs.nu similarity index 100% rename from modules/hosts/_parts/tahani/notability/jobs.nu rename to modules/_notability/jobs.nu diff --git a/modules/hosts/_parts/tahani/notability/lib.nu b/modules/_notability/lib.nu similarity index 100% rename from modules/hosts/_parts/tahani/notability/lib.nu rename to modules/_notability/lib.nu diff --git a/modules/hosts/_parts/tahani/notability/reconcile.nu b/modules/_notability/reconcile.nu similarity index 100% rename from modules/hosts/_parts/tahani/notability/reconcile.nu rename to modules/_notability/reconcile.nu diff --git a/modules/hosts/_parts/tahani/notability/reingest.nu b/modules/_notability/reingest.nu similarity index 100% rename from modules/hosts/_parts/tahani/notability/reingest.nu rename to modules/_notability/reingest.nu diff --git a/modules/hosts/_parts/tahani/notability/status.nu b/modules/_notability/status.nu similarity index 100% rename from modules/hosts/_parts/tahani/notability/status.nu rename to modules/_notability/status.nu diff --git a/modules/hosts/_parts/tahani/notability/watch.nu b/modules/_notability/watch.nu similarity index 100% rename from modules/hosts/_parts/tahani/notability/watch.nu rename to modules/_notability/watch.nu diff --git a/modules/hosts/_parts/tahani/notability/webdav.nu b/modules/_notability/webdav.nu similarity index 100% rename from modules/hosts/_parts/tahani/notability/webdav.nu rename to modules/_notability/webdav.nu diff --git a/modules/hosts/_parts/tahani/notability/worker.nu b/modules/_notability/worker.nu similarity index 100% rename from modules/hosts/_parts/tahani/notability/worker.nu rename to modules/_notability/worker.nu diff --git a/modules/hosts/_parts/tahani/paperless-gpt-prompts/tag_prompt.tmpl b/modules/_paperless/tag_prompt.tmpl similarity index 100% rename from modules/hosts/_parts/tahani/paperless-gpt-prompts/tag_prompt.tmpl rename to modules/_paperless/tag_prompt.tmpl diff --git a/modules/hosts/_parts/tahani/paperless-gpt-prompts/title_prompt.tmpl b/modules/_paperless/title_prompt.tmpl similarity index 100% rename from modules/hosts/_parts/tahani/paperless-gpt-prompts/title_prompt.tmpl rename to modules/_paperless/title_prompt.tmpl diff --git a/modules/adguardhome.nix b/modules/adguardhome.nix new file mode 100644 index 0000000..d97c915 --- /dev/null +++ b/modules/adguardhome.nix @@ -0,0 +1,62 @@ +{...}: let + caddyLib = import ./_lib/caddy.nix; +in { + den.aspects.adguardhome.nixos = {config, ...}: { + services.adguardhome = { + enable = true; + host = "127.0.0.1"; + port = 10000; + settings = { + dhcp.enabled = false; + dns.upstream_dns = [ + "1.1.1.1" + "1.0.0.1" + ]; + filtering = { + protection_enabled = true; + filtering_enabled = true; + safe_search.enabled = false; + safebrowsing_enabled = true; + blocked_response_ttl = 10; + filters_update_interval = 24; + blocked_services.ids = [ + "reddit" + "twitter" + ]; + }; + filters = [ + { + enabled = true; + url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/pro.txt"; + name = "HaGeZi Multi PRO"; + id = 1; + } + { + enabled = true; + url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/tif.txt"; + name = "HaGeZi Threat Intelligence Feeds"; + id = 2; + } + { + enabled = true; + url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/gambling.txt"; + name = "HaGeZi Gambling"; + id = 3; + } + { + enabled = true; + url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/nsfw.txt"; + name = "HaGeZi NSFW"; + id = 4; + } + ]; + }; + }; + + services.caddy.virtualHosts = + caddyLib.mkTailscaleVHost { + name = "adguard"; + configText = "reverse_proxy localhost:${toString config.services.adguardhome.port}"; + }; + }; +} diff --git a/modules/ai-tools.nix b/modules/ai-tools.nix index e3b6ff8..24efe1a 100644 --- a/modules/ai-tools.nix +++ b/modules/ai-tools.nix @@ -1,12 +1,14 @@ -{inputs, ...}: { +{inputs, ...}: let + local = import ./_lib/local.nix; + inherit (local) secretPath; + opencodeSecretPath = secretPath "opencode-api-key"; +in { den.aspects.ai-tools.homeManager = { lib, pkgs, inputs', ... - }: let - opencodeSecretPath = "/run/secrets/opencode-api-key"; - in { + }: { home.packages = [ inputs'.llm-agents.packages.pi pkgs.cog-cli @@ -35,7 +37,7 @@ ".pi/agent/extensions/review.ts".source = ./_ai-tools/extensions/review.ts; ".pi/agent/extensions/session-name.ts".source = ./_ai-tools/extensions/session-name.ts; ".pi/agent/notability" = { - source = ./hosts/_parts/tahani/notability; + source = ./_notability; recursive = true; }; ".pi/agent/skills/elixir-dev" = { diff --git a/modules/cache.nix b/modules/cache.nix new file mode 100644 index 0000000..4db0e5c --- /dev/null +++ b/modules/cache.nix @@ -0,0 +1,11 @@ +{...}: let + caddyLib = import ./_lib/caddy.nix; +in { + den.aspects.cache.nixos = { + services.caddy.virtualHosts = + caddyLib.mkTailscaleVHost { + name = "cache"; + configText = "reverse_proxy localhost:32843"; + }; + }; +} diff --git a/modules/darwin.nix b/modules/darwin.nix index 23ac6ea..226e8f4 100644 --- a/modules/darwin.nix +++ b/modules/darwin.nix @@ -1,4 +1,7 @@ -{inputs, ...}: { +{inputs, ...}: let + local = import ./_lib/local.nix; + userHome = local.mkHome local.hosts.chidi.system; +in { den.aspects.darwin-system.darwin = {pkgs, ...}: { imports = [ inputs.nix-homebrew.darwinModules.nix-homebrew @@ -6,7 +9,7 @@ ./_darwin/dock.nix ]; - system.primaryUser = "cschmatzler"; + system.primaryUser = local.user.name; # Darwin system utilities environment.systemPackages = with pkgs; [ @@ -111,7 +114,7 @@ }; nix = { - settings.trusted-users = ["cschmatzler"]; + settings.trusted-users = [local.user.name]; gc.interval = { Weekday = 0; Hour = 2; @@ -119,18 +122,16 @@ }; }; - users.users.cschmatzler = { - name = "cschmatzler"; - home = "/Users/cschmatzler"; + users.users.${local.user.name} = { + name = local.user.name; + home = userHome; isHidden = false; shell = pkgs.nushell; }; - home-manager.useGlobalPkgs = true; - nix-homebrew = { enable = true; - user = "cschmatzler"; + user = local.user.name; mutableTaps = true; taps = { "homebrew/homebrew-core" = inputs.homebrew-core; diff --git a/modules/defaults.nix b/modules/defaults.nix index 1800df2..c17f387 100644 --- a/modules/defaults.nix +++ b/modules/defaults.nix @@ -25,8 +25,19 @@ flake.flakeModules = { # Shared system foundations core = ./core.nix; + darwin = ./darwin.nix; network = ./network.nix; nixos-system = ./nixos-system.nix; + overlays = ./overlays.nix; + secrets = ./secrets.nix; + + # Shared host features + adguardhome = ./adguardhome.nix; + cache = ./cache.nix; + gitea = ./gitea.nix; + notability = ./notability.nix; + opencode = ./opencode.nix; + paperless = ./paperless.nix; # User environment ai-tools = ./ai-tools.nix; @@ -34,7 +45,6 @@ desktop = ./desktop.nix; dev-tools = ./dev-tools.nix; email = ./email.nix; - finance = ./finance.nix; neovim = ./neovim.nix; shell = ./shell.nix; ssh-client = ./ssh-client.nix; @@ -44,7 +54,12 @@ }; den.default.nixos.system.stateVersion = "25.11"; den.default.darwin.system.stateVersion = 6; - den.default.homeManager.home.stateVersion = "25.11"; + den.default.homeManager = { + home.stateVersion = "25.11"; + programs.home-manager.enable = true; + }; + den.default.nixos.home-manager.useGlobalPkgs = true; + den.default.darwin.home-manager.useGlobalPkgs = true; den.default.includes = [ den.provides.define-user diff --git a/modules/deploy.nix b/modules/deploy.nix index de41d0d..1fe8993 100644 --- a/modules/deploy.nix +++ b/modules/deploy.nix @@ -2,11 +2,13 @@ inputs, config, ... -}: { +}: let + local = import ./_lib/local.nix; +in { flake.deploy.nodes = { michael = { hostname = "michael"; - sshUser = "cschmatzler"; + sshUser = local.user.name; profiles.system = { user = "root"; path = inputs.deploy-rs.lib.x86_64-linux.activate.nixos config.flake.nixosConfigurations.michael; @@ -14,7 +16,7 @@ }; tahani = { hostname = "tahani"; - sshUser = "cschmatzler"; + sshUser = local.user.name; profiles.system = { user = "root"; path = inputs.deploy-rs.lib.x86_64-linux.activate.nixos config.flake.nixosConfigurations.tahani; diff --git a/modules/dev-tools.nix b/modules/dev-tools.nix index 38e0a6d..c8f0813 100644 --- a/modules/dev-tools.nix +++ b/modules/dev-tools.nix @@ -1,10 +1,12 @@ -{...}: { +{...}: let + local = import ./_lib/local.nix; +in { den.aspects.dev-tools.homeManager = { pkgs, lib, ... }: let - name = "Christoph Schmatzler"; + name = local.user.fullName; in { home.packages = with pkgs; [ @@ -85,7 +87,7 @@ settings = { user = { name = name; - email = "christoph@schmatzler.com"; + email = local.user.emails.personal; }; git = { sign-on-push = true; @@ -117,7 +119,7 @@ revset-aliases = { "closest_bookmark(to)" = "heads(::to & bookmarks())"; "closest_pushable(to)" = "heads(::to & mutable() & ~description(exact:\"\") & (~empty() | merges()))"; - "mine()" = "author(\"christoph@schmatzler.com\")"; + "mine()" = "author(\"${local.user.emails.personal}\")"; "wip()" = "mine() ~ immutable()"; "open()" = "mine() ~ ::trunk()"; "current()" = "@:: & mutable()"; diff --git a/modules/email.nix b/modules/email.nix index cc926a4..ccd73fb 100644 --- a/modules/email.nix +++ b/modules/email.nix @@ -1,4 +1,6 @@ -{...}: { +{...}: let + local = import ./_lib/local.nix; +in { den.aspects.email.homeManager = {pkgs, ...}: { programs.himalaya = { enable = true; @@ -19,13 +21,13 @@ }; accounts.email = { - accounts."christoph@schmatzler.com" = { + accounts.${local.user.emails.personal} = { primary = true; - maildir.path = "christoph@schmatzler.com"; - address = "christoph@schmatzler.com"; - userName = "christoph.schmatzler@icloud.com"; - realName = "Christoph Schmatzler"; - passwordCommand = ["${pkgs.coreutils}/bin/cat" "/run/secrets/tahani-email-password"]; + maildir.path = local.user.emails.personal; + address = local.user.emails.personal; + userName = local.user.emails.icloud; + realName = local.user.fullName; + passwordCommand = ["${pkgs.coreutils}/bin/cat" (local.secretPath "tahani-email-password")]; folders = { inbox = "INBOX"; drafts = "Drafts"; diff --git a/modules/finance.nix b/modules/finance.nix deleted file mode 100644 index fe90b58..0000000 --- a/modules/finance.nix +++ /dev/null @@ -1,5 +0,0 @@ -{...}: { - den.aspects.finance.homeManager = {pkgs, ...}: { - home.packages = [pkgs.hledger]; - }; -} diff --git a/modules/gitea.nix b/modules/gitea.nix new file mode 100644 index 0000000..ba63ba9 --- /dev/null +++ b/modules/gitea.nix @@ -0,0 +1,166 @@ +{lib, ...}: let + secretLib = import ./_lib/secrets.nix {inherit lib;}; +in { + den.aspects.gitea.nixos = { + config, + lib, + pkgs, + ... + }: { + sops.secrets = { + michael-gitea-litestream = + secretLib.mkServiceBinarySecret { + name = "michael-gitea-litestream"; + serviceUser = "gitea"; + sopsFile = ../secrets/michael-gitea-litestream; + }; + michael-gitea-restic-password = + secretLib.mkServiceBinarySecret { + name = "michael-gitea-restic-password"; + serviceUser = "gitea"; + sopsFile = ../secrets/michael-gitea-restic-password; + }; + michael-gitea-restic-env = + secretLib.mkServiceBinarySecret { + name = "michael-gitea-restic-env"; + serviceUser = "gitea"; + sopsFile = ../secrets/michael-gitea-restic-env; + }; + }; + + networking.firewall.allowedTCPPorts = [80 443]; + + services.redis.servers.gitea = { + enable = true; + port = 6380; + bind = "127.0.0.1"; + settings = { + maxmemory = "64mb"; + maxmemory-policy = "allkeys-lru"; + }; + }; + + services.gitea = { + enable = true; + database = { + type = "sqlite3"; + path = "/var/lib/gitea/data/gitea.db"; + }; + settings = { + server = { + ROOT_URL = "https://git.schmatzler.com/"; + DOMAIN = "git.schmatzler.com"; + HTTP_ADDR = "127.0.0.1"; + HTTP_PORT = 3000; + LANDING_PAGE = "explore"; + }; + service.DISABLE_REGISTRATION = true; + security.INSTALL_LOCK = true; + cache = { + ADAPTER = "redis"; + HOST = "redis://127.0.0.1:6380/0?pool_size=100&idle_timeout=180s"; + ITEM_TTL = "16h"; + }; + "cache.last_commit" = { + ITEM_TTL = "8760h"; + COMMITS_COUNT = 100; + }; + session = { + PROVIDER = "redis"; + PROVIDER_CONFIG = "redis://127.0.0.1:6380/1?pool_size=100&idle_timeout=180s"; + COOKIE_SECURE = true; + SAME_SITE = "strict"; + }; + api.ENABLE_SWAGGER = false; + }; + }; + + services.litestream = { + enable = true; + environmentFile = config.sops.secrets.michael-gitea-litestream.path; + settings.dbs = [ + { + path = "/var/lib/gitea/data/gitea.db"; + replicas = [ + { + type = "s3"; + bucket = "michael-gitea-litestream"; + path = "gitea"; + endpoint = "s3.eu-central-003.backblazeb2.com"; + } + ]; + } + ]; + }; + + systemd.services.litestream.serviceConfig = { + User = lib.mkForce "gitea"; + Group = lib.mkForce "gitea"; + }; + + services.caddy = { + enable = true; + virtualHosts."git.schmatzler.com".extraConfig = '' + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains" + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + Referrer-Policy "strict-origin-when-cross-origin" + } + reverse_proxy localhost:3000 + ''; + }; + + services.restic.backups.gitea = { + repository = "s3:s3.eu-central-003.backblazeb2.com/michael-gitea-repositories"; + paths = ["/var/lib/gitea"]; + exclude = [ + "/var/lib/gitea/log" + "/var/lib/gitea/data/gitea.db" + "/var/lib/gitea/data/gitea.db-shm" + "/var/lib/gitea/data/gitea.db-wal" + ]; + passwordFile = config.sops.secrets.michael-gitea-restic-password.path; + environmentFile = config.sops.secrets.michael-gitea-restic-env.path; + pruneOpts = [ + "--keep-daily 7" + "--keep-weekly 4" + "--keep-monthly 6" + ]; + timerConfig = { + OnCalendar = "daily"; + Persistent = true; + RandomizedDelaySec = "1h"; + }; + }; + + systemd.services.restic-backups-gitea = { + wants = ["restic-init-gitea.service"]; + after = ["restic-init-gitea.service"]; + serviceConfig = { + User = lib.mkForce "gitea"; + Group = lib.mkForce "gitea"; + }; + }; + + systemd.services.restic-init-gitea = { + description = "Initialize Restic repository for Gitea backups"; + wantedBy = ["multi-user.target"]; + after = ["network-online.target"]; + wants = ["network-online.target"]; + path = [pkgs.restic]; + serviceConfig = { + Type = "oneshot"; + User = "gitea"; + Group = "gitea"; + RemainAfterExit = true; + EnvironmentFile = config.sops.secrets.michael-gitea-restic-env.path; + }; + script = '' + export RESTIC_PASSWORD=$(cat ${config.sops.secrets.michael-gitea-restic-password.path}) + restic -r s3:s3.eu-central-003.backblazeb2.com/michael-gitea-repositories snapshots &>/dev/null || \ + restic -r s3:s3.eu-central-003.backblazeb2.com/michael-gitea-repositories init + ''; + }; + }; +} diff --git a/modules/hosts/_parts/michael/backups.nix b/modules/hosts/_parts/michael/backups.nix deleted file mode 100644 index 38444ad..0000000 --- a/modules/hosts/_parts/michael/backups.nix +++ /dev/null @@ -1,58 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: { - services.restic.backups.gitea = { - repository = "s3:s3.eu-central-003.backblazeb2.com/michael-gitea-repositories"; - paths = ["/var/lib/gitea"]; - exclude = [ - "/var/lib/gitea/log" - "/var/lib/gitea/data/gitea.db" - "/var/lib/gitea/data/gitea.db-shm" - "/var/lib/gitea/data/gitea.db-wal" - ]; - passwordFile = config.sops.secrets.michael-gitea-restic-password.path; - environmentFile = config.sops.secrets.michael-gitea-restic-env.path; - pruneOpts = [ - "--keep-daily 7" - "--keep-weekly 4" - "--keep-monthly 6" - ]; - timerConfig = { - OnCalendar = "daily"; - Persistent = true; - RandomizedDelaySec = "1h"; - }; - }; - - systemd.services.restic-backups-gitea = { - wants = ["restic-init-gitea.service"]; - after = ["restic-init-gitea.service"]; - serviceConfig = { - User = lib.mkForce "gitea"; - Group = lib.mkForce "gitea"; - }; - }; - - systemd.services.restic-init-gitea = { - description = "Initialize Restic repository for Gitea backups"; - wantedBy = ["multi-user.target"]; - after = ["network-online.target"]; - wants = ["network-online.target"]; - path = [pkgs.restic]; - serviceConfig = { - Type = "oneshot"; - User = "gitea"; - Group = "gitea"; - RemainAfterExit = true; - EnvironmentFile = config.sops.secrets.michael-gitea-restic-env.path; - }; - script = '' - export RESTIC_PASSWORD=$(cat ${config.sops.secrets.michael-gitea-restic-password.path}) - restic -r s3:s3.eu-central-003.backblazeb2.com/michael-gitea-repositories snapshots &>/dev/null || \ - restic -r s3:s3.eu-central-003.backblazeb2.com/michael-gitea-repositories init - ''; - }; -} diff --git a/modules/hosts/_parts/michael/gitea.nix b/modules/hosts/_parts/michael/gitea.nix deleted file mode 100644 index 1801993..0000000 --- a/modules/hosts/_parts/michael/gitea.nix +++ /dev/null @@ -1,114 +0,0 @@ -{ - config, - lib, - ... -}: { - sops.secrets = { - michael-gitea-litestream = { - sopsFile = ../../../../secrets/michael-gitea-litestream; - format = "binary"; - owner = "gitea"; - group = "gitea"; - path = "/run/secrets/michael-gitea-litestream"; - }; - michael-gitea-restic-password = { - sopsFile = ../../../../secrets/michael-gitea-restic-password; - format = "binary"; - owner = "gitea"; - group = "gitea"; - path = "/run/secrets/michael-gitea-restic-password"; - }; - michael-gitea-restic-env = { - sopsFile = ../../../../secrets/michael-gitea-restic-env; - format = "binary"; - owner = "gitea"; - group = "gitea"; - path = "/run/secrets/michael-gitea-restic-env"; - }; - }; - - networking.firewall.allowedTCPPorts = [80 443]; - - services.redis.servers.gitea = { - enable = true; - port = 6380; - bind = "127.0.0.1"; - settings = { - maxmemory = "64mb"; - maxmemory-policy = "allkeys-lru"; - }; - }; - - services.gitea = { - enable = true; - database = { - type = "sqlite3"; - path = "/var/lib/gitea/data/gitea.db"; - }; - settings = { - server = { - ROOT_URL = "https://git.schmatzler.com/"; - DOMAIN = "git.schmatzler.com"; - HTTP_ADDR = "127.0.0.1"; - HTTP_PORT = 3000; - LANDING_PAGE = "explore"; - }; - service.DISABLE_REGISTRATION = true; - security.INSTALL_LOCK = true; - cache = { - ADAPTER = "redis"; - HOST = "redis://127.0.0.1:6380/0?pool_size=100&idle_timeout=180s"; - ITEM_TTL = "16h"; - }; - "cache.last_commit" = { - ITEM_TTL = "8760h"; - COMMITS_COUNT = 100; - }; - session = { - PROVIDER = "redis"; - PROVIDER_CONFIG = "redis://127.0.0.1:6380/1?pool_size=100&idle_timeout=180s"; - COOKIE_SECURE = true; - SAME_SITE = "strict"; - }; - api.ENABLE_SWAGGER = false; - }; - }; - - services.litestream = { - enable = true; - environmentFile = config.sops.secrets.michael-gitea-litestream.path; - settings = { - dbs = [ - { - path = "/var/lib/gitea/data/gitea.db"; - replicas = [ - { - type = "s3"; - bucket = "michael-gitea-litestream"; - path = "gitea"; - endpoint = "s3.eu-central-003.backblazeb2.com"; - } - ]; - } - ]; - }; - }; - - systemd.services.litestream.serviceConfig = { - User = lib.mkForce "gitea"; - Group = lib.mkForce "gitea"; - }; - - services.caddy = { - enable = true; - virtualHosts."git.schmatzler.com".extraConfig = '' - header { - Strict-Transport-Security "max-age=31536000; includeSubDomains" - X-Content-Type-Options "nosniff" - X-Frame-Options "DENY" - Referrer-Policy "strict-origin-when-cross-origin" - } - reverse_proxy localhost:3000 - ''; - }; -} diff --git a/modules/hosts/_parts/tahani/adguardhome.nix b/modules/hosts/_parts/tahani/adguardhome.nix deleted file mode 100644 index 89f167f..0000000 --- a/modules/hosts/_parts/tahani/adguardhome.nix +++ /dev/null @@ -1,69 +0,0 @@ -{config, ...}: { - services.adguardhome = { - enable = true; - host = "127.0.0.1"; - port = 10000; - settings = { - dhcp = { - enabled = false; - }; - dns = { - upstream_dns = [ - "1.1.1.1" - "1.0.0.1" - ]; - }; - filtering = { - protection_enabled = true; - filtering_enabled = true; - safe_search = { - enabled = false; - }; - safebrowsing_enabled = true; - blocked_response_ttl = 10; - filters_update_interval = 24; - blocked_services = { - ids = [ - "reddit" - "twitter" - ]; - }; - }; - filters = [ - { - enabled = true; - url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/pro.txt"; - name = "HaGeZi Multi PRO"; - id = 1; - } - { - enabled = true; - url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/tif.txt"; - name = "HaGeZi Threat Intelligence Feeds"; - id = 2; - } - { - enabled = true; - url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/gambling.txt"; - name = "HaGeZi Gambling"; - id = 3; - } - { - enabled = true; - url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/nsfw.txt"; - name = "HaGeZi NSFW"; - id = 4; - } - ]; - }; - }; - - services.caddy.virtualHosts."adguard.manticore-hippocampus.ts.net" = { - extraConfig = '' - tls { - get_certificate tailscale - } - reverse_proxy localhost:${toString config.services.adguardhome.port} - ''; - }; -} diff --git a/modules/hosts/_parts/tahani/cache.nix b/modules/hosts/_parts/tahani/cache.nix deleted file mode 100644 index bd8df07..0000000 --- a/modules/hosts/_parts/tahani/cache.nix +++ /dev/null @@ -1,10 +0,0 @@ -{...}: { - services.caddy.virtualHosts."cache.manticore-hippocampus.ts.net" = { - extraConfig = '' - tls { - get_certificate tailscale - } - reverse_proxy localhost:32843 - ''; - }; -} diff --git a/modules/hosts/_parts/tahani/notability.nix b/modules/hosts/_parts/tahani/notability.nix deleted file mode 100644 index eba77b8..0000000 --- a/modules/hosts/_parts/tahani/notability.nix +++ /dev/null @@ -1,128 +0,0 @@ -{ - config, - inputs', - pkgs, - ... -}: let - homeDir = "/home/cschmatzler"; - notabilityScripts = ./notability; - dataRoot = "${homeDir}/.local/share/notability-ingest"; - stateRoot = "${homeDir}/.local/state/notability-ingest"; - notesRoot = "${homeDir}/Notes"; - webdavRoot = "${dataRoot}/webdav-root"; - userPackages = with pkgs; [ - qmd - poppler-utils - rclone - sqlite - zk - ]; - commonPath = with pkgs; - [ - inputs'.llm-agents.packages.pi - coreutils - inotify-tools - nushell - util-linux - ] - ++ userPackages; - commonEnvironment = { - HOME = homeDir; - NOTABILITY_ARCHIVE_ROOT = "${dataRoot}/archive"; - NOTABILITY_DATA_ROOT = dataRoot; - NOTABILITY_DB_PATH = "${stateRoot}/db.sqlite"; - NOTABILITY_NOTES_DIR = notesRoot; - NOTABILITY_RENDER_ROOT = "${dataRoot}/rendered-pages"; - NOTABILITY_SESSIONS_ROOT = "${stateRoot}/sessions"; - NOTABILITY_STATE_ROOT = stateRoot; - NOTABILITY_TRANSCRIPT_ROOT = "${stateRoot}/transcripts"; - NOTABILITY_WEBDAV_ROOT = webdavRoot; - XDG_CONFIG_HOME = "${homeDir}/.config"; - }; - mkTmpDirRule = path: "d ${path} 0755 cschmatzler users -"; - mkNotabilityService = { - description, - script, - after ? [], - requires ? [], - environment ? {}, - }: { - inherit after description requires; - wantedBy = ["multi-user.target"]; - path = commonPath; - environment = commonEnvironment // environment; - serviceConfig = { - ExecStart = "${pkgs.nushell}/bin/nu ${notabilityScripts}/${script}"; - Group = "users"; - Restart = "always"; - RestartSec = 5; - User = "cschmatzler"; - WorkingDirectory = homeDir; - }; - }; -in { - sops.secrets.tahani-notability-webdav-password = { - sopsFile = ../../../../secrets/tahani-notability-webdav-password; - format = "binary"; - owner = "cschmatzler"; - path = "/run/secrets/tahani-notability-webdav-password"; - }; - - home-manager.users.cschmatzler = { - home.packages = userPackages; - home.file.".config/qmd/index.yml".text = '' - collections: - notes: - path: ${notesRoot} - pattern: "**/*.md" - ''; - }; - - systemd.tmpfiles.rules = - builtins.map mkTmpDirRule [ - notesRoot - dataRoot - webdavRoot - "${dataRoot}/archive" - "${dataRoot}/rendered-pages" - stateRoot - "${stateRoot}/jobs" - "${stateRoot}/jobs/queued" - "${stateRoot}/jobs/running" - "${stateRoot}/jobs/failed" - "${stateRoot}/jobs/done" - "${stateRoot}/jobs/results" - "${stateRoot}/sessions" - "${stateRoot}/transcripts" - ]; - - services.caddy.virtualHosts."tahani.manticore-hippocampus.ts.net".extraConfig = '' - tls { - get_certificate tailscale - } - handle /notability* { - reverse_proxy 127.0.0.1:9980 - } - ''; - - systemd.services.notability-webdav = - mkNotabilityService { - description = "Notability WebDAV landing zone"; - script = "webdav.nu"; - after = ["network.target"]; - environment = { - NOTABILITY_WEBDAV_ADDR = "127.0.0.1:9980"; - NOTABILITY_WEBDAV_BASEURL = "/notability"; - NOTABILITY_WEBDAV_PASSWORD_FILE = config.sops.secrets.tahani-notability-webdav-password.path; - NOTABILITY_WEBDAV_USER = "notability"; - }; - }; - - systemd.services.notability-watch = - mkNotabilityService { - description = "Watch and ingest Notability WebDAV uploads"; - script = "watch.nu"; - after = ["notability-webdav.service"]; - requires = ["notability-webdav.service"]; - }; -} diff --git a/modules/hosts/_parts/tahani/paperless.nix b/modules/hosts/_parts/tahani/paperless.nix deleted file mode 100644 index 193d666..0000000 --- a/modules/hosts/_parts/tahani/paperless.nix +++ /dev/null @@ -1,87 +0,0 @@ -{config, ...}: { - services.caddy = { - enable = true; - enableReload = false; - globalConfig = '' - admin off - ''; - virtualHosts."docs.manticore-hippocampus.ts.net" = { - extraConfig = '' - tls { - get_certificate tailscale - } - reverse_proxy localhost:${toString config.services.paperless.port} - ''; - }; - virtualHosts."docs-ai.manticore-hippocampus.ts.net" = { - extraConfig = '' - tls { - get_certificate tailscale - } - reverse_proxy localhost:8081 - ''; - }; - }; - - virtualisation.oci-containers = { - backend = "docker"; - containers.paperless-gpt = { - image = "icereed/paperless-gpt:latest"; - autoStart = true; - ports = [ - "127.0.0.1:8081:8080" - ]; - volumes = [ - "paperless-gpt-data:/app/data" - "paperless-gpt-prompts:/app/prompts" - "${./paperless-gpt-prompts/tag_prompt.tmpl}:/app/prompts/tag_prompt.tmpl:ro" - "${./paperless-gpt-prompts/title_prompt.tmpl}:/app/prompts/title_prompt.tmpl:ro" - ]; - environment = { - PAPERLESS_BASE_URL = "http://host.docker.internal:${toString config.services.paperless.port}"; - LLM_PROVIDER = "openai"; - LLM_MODEL = "gpt-5.4"; - LLM_LANGUAGE = "German"; - VISION_LLM_PROVIDER = "openai"; - VISION_LLM_MODEL = "gpt-5.4"; - LOG_LEVEL = "info"; - }; - environmentFiles = [ - config.sops.secrets.tahani-paperless-gpt-env.path - ]; - extraOptions = [ - "--add-host=host.docker.internal:host-gateway" - ]; - }; - }; - - services.redis.servers.paperless = { - enable = true; - port = 6379; - bind = "127.0.0.1"; - settings = { - maxmemory = "256mb"; - maxmemory-policy = "allkeys-lru"; - }; - }; - - services.paperless = { - enable = true; - address = "0.0.0.0"; - consumptionDir = "/var/lib/paperless/consume"; - passwordFile = config.sops.secrets.tahani-paperless-password.path; - settings = { - PAPERLESS_DBENGINE = "sqlite"; - PAPERLESS_REDIS = "redis://127.0.0.1:6379"; - PAPERLESS_CONSUMER_IGNORE_PATTERN = [ - ".DS_STORE/*" - "desktop.ini" - ]; - PAPERLESS_CONSUMER_POLLING = 30; - PAPERLESS_CONSUMER_RECURSIVE = true; - PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS = true; - PAPERLESS_OCR_LANGUAGE = "deu+eng"; - PAPERLESS_CSRF_TRUSTED_ORIGINS = "https://docs.manticore-hippocampus.ts.net"; - }; - }; -} diff --git a/modules/hosts/chidi.nix b/modules/hosts/chidi.nix index b1676ae..d956909 100644 --- a/modules/hosts/chidi.nix +++ b/modules/hosts/chidi.nix @@ -1,33 +1,34 @@ -{den, ...}: { - den.hosts.aarch64-darwin.chidi.users.cschmatzler.aspect = "chidi-cschmatzler"; - - den.aspects.chidi-cschmatzler = { +{ + den, + lib, + ... +}: let + hostLib = import ../_lib/hosts.nix {inherit den lib;}; + local = import ../_lib/local.nix; + host = "chidi"; + hostMeta = local.hosts.chidi; +in + hostLib.mkUserHost { + system = hostMeta.system; + inherit host; + user = local.user.name; includes = [den.aspects.user-darwin-laptop]; - homeManager = {...}: { - programs.git.settings.user.email = "christoph@tuist.dev"; + programs.git.settings.user.email = local.user.emails.work; }; - }; + } + // hostLib.mkPerHostAspect { + inherit host; + includes = [ + den.aspects.host-darwin-base + den.aspects.opencode-api-key + ]; + darwin = {...}: { + networking.hostName = host; + networking.computerName = host; - den.aspects.chidi.includes = [ - (den.lib.perHost { - includes = [den.aspects.host-darwin-base]; - - darwin = {...}: { - networking.hostName = "chidi"; - networking.computerName = "chidi"; - - sops.secrets.opencode-api-key = { - sopsFile = ../../secrets/opencode-api-key; - format = "binary"; - owner = "cschmatzler"; - path = "/run/secrets/opencode-api-key"; - }; - - homebrew.casks = [ - "slack" - ]; - }; - }) - ]; -} + homebrew.casks = [ + "slack" + ]; + }; + } diff --git a/modules/hosts/janet.nix b/modules/hosts/janet.nix index 21774c9..1695b84 100644 --- a/modules/hosts/janet.nix +++ b/modules/hosts/janet.nix @@ -1,28 +1,30 @@ -{den, ...}: { - den.hosts.aarch64-darwin.janet.users.cschmatzler.aspect = "janet-cschmatzler"; - - den.aspects.janet-cschmatzler = { +{ + den, + lib, + ... +}: let + hostLib = import ../_lib/hosts.nix {inherit den lib;}; + local = import ../_lib/local.nix; + host = "janet"; + hostMeta = local.hosts.janet; +in + hostLib.mkUserHost { + system = hostMeta.system; + inherit host; + user = local.user.name; includes = [ den.aspects.user-darwin-laptop den.aspects.user-personal ]; - }; - - den.aspects.janet.includes = [ - (den.lib.perHost { - includes = [den.aspects.host-darwin-base]; - - darwin = {...}: { - networking.hostName = "janet"; - networking.computerName = "janet"; - - sops.secrets.opencode-api-key = { - sopsFile = ../../secrets/opencode-api-key; - format = "binary"; - owner = "cschmatzler"; - path = "/run/secrets/opencode-api-key"; - }; - }; - }) - ]; -} + } + // hostLib.mkPerHostAspect { + inherit host; + includes = [ + den.aspects.host-darwin-base + den.aspects.opencode-api-key + ]; + darwin = {...}: { + networking.hostName = host; + networking.computerName = host; + }; + } diff --git a/modules/hosts/michael.nix b/modules/hosts/michael.nix index 62da41a..d752bf9 100644 --- a/modules/hosts/michael.nix +++ b/modules/hosts/michael.nix @@ -1,30 +1,34 @@ { den, inputs, + lib, ... -}: { - den.hosts.x86_64-linux.michael.users.cschmatzler.aspect = "michael-cschmatzler"; - - den.aspects.michael-cschmatzler = { +}: let + hostLib = import ../_lib/hosts.nix {inherit den lib;}; + local = import ../_lib/local.nix; + host = "michael"; + hostMeta = local.hosts.michael; +in + hostLib.mkUserHost { + system = hostMeta.system; + inherit host; + user = local.user.name; includes = [den.aspects.user-minimal]; - }; + } + // hostLib.mkPerHostAspect { + inherit host; + includes = [ + den.aspects.host-public-server + den.aspects.gitea + ]; + nixos = {modulesPath, ...}: { + imports = [ + (modulesPath + "/installer/scan/not-detected.nix") + ./_parts/michael/disk-config.nix + ./_parts/michael/hardware-configuration.nix + inputs.disko.nixosModules.default + ]; - den.aspects.michael.includes = [ - (den.lib.perHost { - includes = [den.aspects.host-public-server]; - - nixos = {modulesPath, ...}: { - imports = [ - (modulesPath + "/installer/scan/not-detected.nix") - ./_parts/michael/backups.nix - ./_parts/michael/disk-config.nix - ./_parts/michael/gitea.nix - ./_parts/michael/hardware-configuration.nix - inputs.disko.nixosModules.default - ]; - - networking.hostName = "michael"; - }; - }) - ]; -} + networking.hostName = host; + }; + } diff --git a/modules/hosts/tahani.nix b/modules/hosts/tahani.nix index 029d273..3b094a2 100644 --- a/modules/hosts/tahani.nix +++ b/modules/hosts/tahani.nix @@ -1,13 +1,23 @@ -{den, ...}: { - den.hosts.x86_64-linux.tahani.users.cschmatzler.aspect = "tahani-cschmatzler"; - - den.aspects.tahani-cschmatzler = { +{ + den, + lib, + ... +}: let + hostLib = import ../_lib/hosts.nix {inherit den lib;}; + local = import ../_lib/local.nix; + secretLib = import ../_lib/secrets.nix {inherit lib;}; + host = "tahani"; + hostMeta = local.hosts.tahani; +in + hostLib.mkUserHost { + system = hostMeta.system; + inherit host; + user = local.user.name; includes = [ den.aspects.user-workstation den.aspects.user-personal den.aspects.email ]; - homeManager = { programs.nushell.extraConfig = '' if $nu.is-interactive and ('SSH_CONNECTION' in ($env | columns)) and ('ZELLIJ' not-in ($env | columns)) { @@ -20,61 +30,45 @@ } ''; }; - }; + } + // hostLib.mkPerHostAspect { + inherit host; + includes = [ + den.aspects.host-nixos-base + den.aspects.opencode-api-key + den.aspects.adguardhome + den.aspects.cache + den.aspects.notability + den.aspects.paperless + ]; + nixos = {...}: { + imports = [ + ./_parts/tahani/networking.nix + ]; - den.aspects.tahani.includes = [ - (den.lib.perHost { - includes = [den.aspects.host-nixos-base]; + networking.hostName = host; - nixos = {...}: { - imports = [ - ./_parts/tahani/adguardhome.nix - ./_parts/tahani/cache.nix - ./_parts/tahani/networking.nix - ./_parts/tahani/notability.nix - ./_parts/tahani/paperless.nix - ]; - - networking.hostName = "tahani"; - - sops.secrets = { - opencode-api-key = { - sopsFile = ../../secrets/opencode-api-key; - format = "binary"; - owner = "cschmatzler"; - path = "/run/secrets/opencode-api-key"; - }; - tahani-paperless-password = { - sopsFile = ../../secrets/tahani-paperless-password; - format = "binary"; - path = "/run/secrets/tahani-paperless-password"; - }; - tahani-paperless-gpt-env = { - sopsFile = ../../secrets/tahani-paperless-gpt-env; - format = "binary"; - path = "/run/secrets/tahani-paperless-gpt-env"; - }; - tahani-email-password = { - sopsFile = ../../secrets/tahani-email-password; - format = "binary"; - owner = "cschmatzler"; - path = "/run/secrets/tahani-email-password"; - }; - }; - virtualisation.docker.enable = true; - users.users.cschmatzler.extraGroups = ["docker" "paperless"]; - - systemd.tmpfiles.rules = [ - "d /var/lib/paperless/consume 2775 paperless paperless -" - "d /var/lib/paperless/consume/inbox-triage 2775 paperless paperless -" - ]; - swapDevices = [ - { - device = "/swapfile"; - size = 16 * 1024; - } - ]; + sops.secrets.tahani-email-password = + secretLib.mkUserBinarySecret { + name = "tahani-email-password"; + sopsFile = ../../secrets/tahani-email-password; }; - }) - ]; -} + + virtualisation.docker.enable = true; + users.users.${local.user.name}.extraGroups = [ + "docker" + "paperless" + ]; + + systemd.tmpfiles.rules = [ + "d /var/lib/paperless/consume 2775 paperless paperless -" + "d /var/lib/paperless/consume/inbox-triage 2775 paperless paperless -" + ]; + swapDevices = [ + { + device = "/swapfile"; + size = 16 * 1024; + } + ]; + }; + } diff --git a/modules/inventory.nix b/modules/inventory.nix index f9dc580..05afe8b 100644 --- a/modules/inventory.nix +++ b/modules/inventory.nix @@ -1,6 +1,10 @@ -{...}: { - den.hosts.aarch64-darwin.chidi.users.cschmatzler = {}; - den.hosts.aarch64-darwin.janet.users.cschmatzler = {}; - den.hosts.x86_64-linux.michael.users.cschmatzler = {}; - den.hosts.x86_64-linux.tahani.users.cschmatzler = {}; -} +{lib, ...}: let + local = import ./_lib/local.nix; +in + lib.foldl' lib.recursiveUpdate {} ( + lib.mapAttrsToList ( + host: hostMeta: + lib.setAttrByPath ["den" "hosts" hostMeta.system host "users" local.user.name] {} + ) + local.hosts + ) diff --git a/modules/network.nix b/modules/network.nix index 9fd73fb..cef5225 100644 --- a/modules/network.nix +++ b/modules/network.nix @@ -21,24 +21,20 @@ overalljails = true; }; jails = { - sshd = { - settings = { - enabled = true; - port = "ssh"; - filter = "sshd"; - maxretry = 3; - }; + sshd.settings = { + enabled = true; + port = "ssh"; + filter = "sshd"; + maxretry = 3; }; - gitea = { - settings = { - enabled = true; - filter = "gitea"; - logpath = "/var/lib/gitea/log/gitea.log"; - maxretry = 10; - findtime = 3600; - bantime = 900; - action = "iptables-allports"; - }; + gitea.settings = { + enabled = true; + filter = "gitea"; + logpath = "/var/lib/gitea/log/gitea.log"; + maxretry = 10; + findtime = 3600; + bantime = 900; + action = "iptables-allports"; }; }; }; @@ -60,23 +56,6 @@ }; den.aspects.tailscale.darwin = { - services.tailscale = { - enable = true; - }; - }; - - # Network tools - den.aspects.network.homeManager = { - pkgs, - lib, - ... - }: { - home.packages = with pkgs; - [ - dig - ] - ++ lib.optionals stdenv.isDarwin [ - tailscale - ]; + services.tailscale.enable = true; }; } diff --git a/modules/nixos-system.nix b/modules/nixos-system.nix index 2c81bf0..5646fc8 100644 --- a/modules/nixos-system.nix +++ b/modules/nixos-system.nix @@ -1,11 +1,14 @@ -{inputs, ...}: { +{inputs, ...}: let + local = import ./_lib/local.nix; + userHome = local.mkHome local.hosts.michael.system; +in { den.aspects.nixos-system.nixos = {pkgs, ...}: { imports = [inputs.home-manager.nixosModules.home-manager]; security.sudo.enable = true; security.sudo.extraRules = [ { - users = ["cschmatzler"]; + users = [local.user.name]; commands = [ { command = "/run/current-system/sw/bin/nix-env"; @@ -46,9 +49,9 @@ time.timeZone = "UTC"; nix = { - settings.trusted-users = ["cschmatzler"]; + settings.trusted-users = [local.user.name]; gc.dates = "weekly"; - nixPath = ["nixos-config=/home/cschmatzler/.local/share/src/nixos-config:/etc/nixos"]; + nixPath = ["nixos-config=${userHome}/.local/share/src/nixos-config:/etc/nixos"]; }; boot = { @@ -71,9 +74,9 @@ }; users.users = { - cschmatzler = { + ${local.user.name} = { isNormalUser = true; - home = "/home/cschmatzler"; + home = userHome; extraGroups = [ "wheel" "sudo" @@ -93,7 +96,5 @@ ]; }; }; - - home-manager.useGlobalPkgs = true; }; } diff --git a/modules/notability.nix b/modules/notability.nix new file mode 100644 index 0000000..da84c70 --- /dev/null +++ b/modules/notability.nix @@ -0,0 +1,136 @@ +{lib, ...}: let + caddyLib = import ./_lib/caddy.nix; + local = import ./_lib/local.nix; + secretLib = import ./_lib/secrets.nix {inherit lib;}; + inherit (local) user; + notabilityScripts = ./_notability; + tahani = local.hosts.tahani; +in { + den.aspects.notability.nixos = { + config, + inputs', + pkgs, + ... + }: let + homeDir = tahani.home; + dataRoot = "${homeDir}/.local/share/notability-ingest"; + stateRoot = "${homeDir}/.local/state/notability-ingest"; + notesRoot = "${homeDir}/Notes"; + webdavRoot = "${dataRoot}/webdav-root"; + userPackages = with pkgs; [ + qmd + poppler-utils + rclone + sqlite + zk + ]; + commonPath = with pkgs; + [ + inputs'.llm-agents.packages.pi + coreutils + inotify-tools + nushell + util-linux + ] + ++ userPackages; + commonEnvironment = { + HOME = homeDir; + NOTABILITY_ARCHIVE_ROOT = "${dataRoot}/archive"; + NOTABILITY_DATA_ROOT = dataRoot; + NOTABILITY_DB_PATH = "${stateRoot}/db.sqlite"; + NOTABILITY_NOTES_DIR = notesRoot; + NOTABILITY_RENDER_ROOT = "${dataRoot}/rendered-pages"; + NOTABILITY_SESSIONS_ROOT = "${stateRoot}/sessions"; + NOTABILITY_STATE_ROOT = stateRoot; + NOTABILITY_TRANSCRIPT_ROOT = "${stateRoot}/transcripts"; + NOTABILITY_WEBDAV_ROOT = webdavRoot; + XDG_CONFIG_HOME = "${homeDir}/.config"; + }; + mkTmpDirRule = path: "d ${path} 0755 ${user.name} users -"; + mkNotabilityService = { + description, + script, + after ? [], + requires ? [], + environment ? {}, + }: { + inherit after description requires; + wantedBy = ["multi-user.target"]; + path = commonPath; + environment = commonEnvironment // environment; + serviceConfig = { + ExecStart = "${pkgs.nushell}/bin/nu ${notabilityScripts}/${script}"; + Group = "users"; + Restart = "always"; + RestartSec = 5; + User = user.name; + WorkingDirectory = homeDir; + }; + }; + in { + sops.secrets.tahani-notability-webdav-password = + secretLib.mkUserBinarySecret { + name = "tahani-notability-webdav-password"; + sopsFile = ../secrets/tahani-notability-webdav-password; + }; + + home-manager.users.${user.name} = { + home.packages = userPackages; + home.file.".config/qmd/index.yml".text = '' + collections: + notes: + path: ${notesRoot} + pattern: "**/*.md" + ''; + }; + + systemd.tmpfiles.rules = + builtins.map mkTmpDirRule [ + notesRoot + dataRoot + webdavRoot + "${dataRoot}/archive" + "${dataRoot}/rendered-pages" + stateRoot + "${stateRoot}/jobs" + "${stateRoot}/jobs/queued" + "${stateRoot}/jobs/running" + "${stateRoot}/jobs/failed" + "${stateRoot}/jobs/done" + "${stateRoot}/jobs/results" + "${stateRoot}/sessions" + "${stateRoot}/transcripts" + ]; + + services.caddy.virtualHosts = + caddyLib.mkTailscaleVHost { + name = "tahani"; + configText = '' + handle /notability* { + reverse_proxy 127.0.0.1:9980 + } + ''; + }; + + systemd.services.notability-webdav = + mkNotabilityService { + description = "Notability WebDAV landing zone"; + script = "webdav.nu"; + after = ["network.target"]; + environment = { + NOTABILITY_WEBDAV_ADDR = "127.0.0.1:9980"; + NOTABILITY_WEBDAV_BASEURL = "/notability"; + NOTABILITY_WEBDAV_PASSWORD_FILE = config.sops.secrets.tahani-notability-webdav-password.path; + NOTABILITY_WEBDAV_USER = "notability"; + }; + }; + + systemd.services.notability-watch = + mkNotabilityService { + description = "Watch and ingest Notability WebDAV uploads"; + script = "watch.nu"; + after = ["notability-webdav.service"]; + requires = ["notability-webdav.service"]; + }; + }; +} diff --git a/modules/opencode.nix b/modules/opencode.nix new file mode 100644 index 0000000..db0fa6d --- /dev/null +++ b/modules/opencode.nix @@ -0,0 +1,11 @@ +{lib, ...}: let + secretLib = import ./_lib/secrets.nix {inherit lib;}; +in { + den.aspects.opencode-api-key.os = { + sops.secrets.opencode-api-key = + secretLib.mkUserBinarySecret { + name = "opencode-api-key"; + sopsFile = ../secrets/opencode-api-key; + }; + }; +} diff --git a/modules/paperless.nix b/modules/paperless.nix new file mode 100644 index 0000000..ee2a8a3 --- /dev/null +++ b/modules/paperless.nix @@ -0,0 +1,100 @@ +{lib, ...}: let + caddyLib = import ./_lib/caddy.nix; + local = import ./_lib/local.nix; + secretLib = import ./_lib/secrets.nix {inherit lib;}; + paperlessPrompts = ./_paperless; +in { + den.aspects.paperless.nixos = {config, ...}: { + sops.secrets = { + tahani-paperless-password = + secretLib.mkBinarySecret { + name = "tahani-paperless-password"; + sopsFile = ../secrets/tahani-paperless-password; + }; + tahani-paperless-gpt-env = + secretLib.mkBinarySecret { + name = "tahani-paperless-gpt-env"; + sopsFile = ../secrets/tahani-paperless-gpt-env; + }; + }; + + services.caddy = { + enable = true; + enableReload = false; + globalConfig = '' + admin off + ''; + virtualHosts = + caddyLib.mkTailscaleVHost { + name = "docs"; + configText = "reverse_proxy localhost:${toString config.services.paperless.port}"; + } + // caddyLib.mkTailscaleVHost { + name = "docs-ai"; + configText = "reverse_proxy localhost:8081"; + }; + }; + + virtualisation.oci-containers = { + backend = "docker"; + containers.paperless-gpt = { + image = "icereed/paperless-gpt:latest"; + autoStart = true; + ports = [ + "127.0.0.1:8081:8080" + ]; + volumes = [ + "paperless-gpt-data:/app/data" + "paperless-gpt-prompts:/app/prompts" + "${paperlessPrompts}/tag_prompt.tmpl:/app/prompts/tag_prompt.tmpl:ro" + "${paperlessPrompts}/title_prompt.tmpl:/app/prompts/title_prompt.tmpl:ro" + ]; + environment = { + PAPERLESS_BASE_URL = "http://host.docker.internal:${toString config.services.paperless.port}"; + LLM_PROVIDER = "openai"; + LLM_MODEL = "gpt-5.4"; + LLM_LANGUAGE = "German"; + VISION_LLM_PROVIDER = "openai"; + VISION_LLM_MODEL = "gpt-5.4"; + LOG_LEVEL = "info"; + }; + environmentFiles = [ + config.sops.secrets.tahani-paperless-gpt-env.path + ]; + extraOptions = [ + "--add-host=host.docker.internal:host-gateway" + ]; + }; + }; + + services.redis.servers.paperless = { + enable = true; + port = 6379; + bind = "127.0.0.1"; + settings = { + maxmemory = "256mb"; + maxmemory-policy = "allkeys-lru"; + }; + }; + + services.paperless = { + enable = true; + address = "0.0.0.0"; + consumptionDir = "/var/lib/paperless/consume"; + passwordFile = config.sops.secrets.tahani-paperless-password.path; + settings = { + PAPERLESS_DBENGINE = "sqlite"; + PAPERLESS_REDIS = "redis://127.0.0.1:6379"; + PAPERLESS_CONSUMER_IGNORE_PATTERN = [ + ".DS_STORE/*" + "desktop.ini" + ]; + PAPERLESS_CONSUMER_POLLING = 30; + PAPERLESS_CONSUMER_RECURSIVE = true; + PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS = true; + PAPERLESS_OCR_LANGUAGE = "deu+eng"; + PAPERLESS_CSRF_TRUSTED_ORIGINS = "https://${local.tailscaleHost "docs"}"; + }; + }; + }; +} diff --git a/modules/profiles/user/base.nix b/modules/profiles/user/base.nix index fc43717..0e5c033 100644 --- a/modules/profiles/user/base.nix +++ b/modules/profiles/user/base.nix @@ -1,17 +1,11 @@ {den, ...}: { - den.aspects.user-base = { - includes = [ - den.aspects.shell - den.aspects.ssh-client - den.aspects.terminal - den.aspects.atuin - den.aspects.secrets - den.aspects.zellij - den.aspects.zk - ]; - - homeManager = { - programs.home-manager.enable = true; - }; - }; + den.aspects.user-base.includes = [ + den.aspects.shell + den.aspects.ssh-client + den.aspects.terminal + den.aspects.atuin + den.aspects.secrets + den.aspects.zellij + den.aspects.zk + ]; } diff --git a/modules/profiles/user/minimal.nix b/modules/profiles/user/minimal.nix index 7d94833..4cd5d37 100644 --- a/modules/profiles/user/minimal.nix +++ b/modules/profiles/user/minimal.nix @@ -1,11 +1,5 @@ {den, ...}: { - den.aspects.user-minimal = { - includes = [ - den.aspects.shell - ]; - - homeManager = { - programs.home-manager.enable = true; - }; - }; + den.aspects.user-minimal.includes = [ + den.aspects.shell + ]; } diff --git a/modules/profiles/user/personal.nix b/modules/profiles/user/personal.nix index 116976c..7872960 100644 --- a/modules/profiles/user/personal.nix +++ b/modules/profiles/user/personal.nix @@ -1,5 +1,7 @@ -{...}: { +{...}: let + local = import ../../_lib/local.nix; +in { den.aspects.user-personal.homeManager = { - programs.git.settings.user.email = "christoph@schmatzler.com"; + programs.git.settings.user.email = local.user.emails.personal; }; } diff --git a/modules/secrets.nix b/modules/secrets.nix index f695300..3f92ed8 100644 --- a/modules/secrets.nix +++ b/modules/secrets.nix @@ -1,4 +1,6 @@ -{inputs, ...}: { +{inputs, ...}: let + local = import ./_lib/local.nix; +in { # Import sops-nix modules into den.default per-class den.default.nixos.imports = [inputs.sops-nix.nixosModules.sops]; den.default.darwin.imports = [inputs.sops-nix.darwinModules.sops]; @@ -8,7 +10,7 @@ # Configure Darwin SOPS defaults den.default.darwin = { - sops.age.keyFile = "/Users/cschmatzler/.config/sops/age/keys.txt"; + sops.age.keyFile = "${local.mkHome local.hosts.chidi.system}/.config/sops/age/keys.txt"; sops.age.sshKeyPaths = []; sops.gnupg.sshKeyPaths = []; }; diff --git a/modules/shell.nix b/modules/shell.nix index 869ae36..7939347 100644 --- a/modules/shell.nix +++ b/modules/shell.nix @@ -268,9 +268,11 @@ git_state = { disabled = true; }; - custom.scm = { + custom.scm = let + local = import ./_lib/local.nix; + in { when = "jj-starship detect"; - shell = ["jj-starship" "--strip-bookmark-prefix" "cschmatzler/" "--truncate-name" "20" "--bookmarks-display-limit" "1"]; + shell = ["jj-starship" "--strip-bookmark-prefix" "${local.user.name}/" "--truncate-name" "20" "--bookmarks-display-limit" "1"]; format = "$output "; }; lua = {