flake-update-20260505
  1{
  2  pkgs,
  3  inputs,
  4  config,
  5  ...
  6}:
  7{
  8  imports = [
  9    inputs.daneel.nixosModules.daneel
 10    ../common/programs/direnv.nix
 11    ../common/programs/git.nix
 12    ../common/programs/nix-ld.nix
 13    ../common/programs/tmux.nix
 14    ../common/services/networkmanager.nix
 15    ../common/services/containers.nix
 16    ../common/services/docker.nix
 17    ../common/services/binfmt.nix
 18
 19    ../common/services/oomd.nix
 20    ../../modules/laptop-keyboard-inhibit
 21    # Build and cache infrastructure
 22    ../../modules/harmonia
 23    ../../modules/job-notify
 24    ../../modules/flux-generate
 25
 26  ];
 27
 28  # Disable built-in MediaTek MT7922 WiFi (using USB TP-Link AC600 instead)
 29  boot.blacklistedKernelModules = [ "mt7921e" ];
 30
 31  # Xbox controller support (Bluetooth)
 32  hardware.xpadneo.enable = true;
 33
 34  # Gaming setup
 35  programs.steam = {
 36    enable = true;
 37    remotePlay.openFirewall = true; # Steam Link support
 38    dedicatedServer.openFirewall = true; # Game server hosting
 39  };
 40
 41  programs.gamemode = {
 42    enable = true;
 43    settings = {
 44      general = {
 45        renice = 10; # Increase game process priority
 46      };
 47      gpu = {
 48        apply_gpu_optimisations = "accept-responsibility";
 49        gpu_device = 0;
 50        amd_performance_level = "high"; # AMD GPU performance mode
 51      };
 52    };
 53  };
 54
 55  # Greeter wallpaper (accessible to greeter user)
 56  environment.etc."greetd/wallpaper.jpg".source = ../../dots/wallpapers/greeter.jpg;
 57
 58  # QMK/ZMK keyboard flashing support (udev rules + plugdev group)
 59  hardware.keyboard.qmk.enable = true;
 60
 61  # Auto-disable internal keyboard when Eyelash Corne (BT) is connected
 62  services.laptop-keyboard-inhibit = {
 63    enable = false; # disabled: spams D-Bus with bluetooth reconnect attempts
 64    keyboards = [ "Eyelash Corne" ];
 65  };
 66
 67  # Age secrets
 68  age.secrets = {
 69    "opencode-password" = {
 70      file = ../../secrets/okinawa/opencode-password.age;
 71      mode = "400";
 72      owner = "vincent";
 73    };
 74    "groq-api-key" = {
 75      file = ../../secrets/okinawa/groq-api-key.age;
 76      mode = "400";
 77      owner = "vincent";
 78    };
 79    "openrouter-api-key" = {
 80      file = ../../secrets/okinawa/openrouter-api-key.age;
 81      mode = "400";
 82      owner = "vincent";
 83    };
 84    "gemini-api-key" = {
 85      file = ../../secrets/okinawa/gemini-api-key.age;
 86      mode = "400";
 87      owner = "vincent";
 88    };
 89    "xmpp-research-bot-password" = {
 90      file = ../../secrets/okinawa/xmpp-research-bot-password.age;
 91      mode = "400";
 92      owner = "vincent";
 93    };
 94    # Harmonia binary cache signing key
 95    "harmonia-okinawa-signing-key" = {
 96      file = ../../secrets/harmonia/okinawa-signing-key.age;
 97      mode = "440";
 98      owner = "root";
 99      group = "root";
100    };
101    # ntfy notification token (shared secret)
102    "ntfy-token" = {
103      file = ../../secrets/sakhalin/ntfy-token.age;
104      mode = "440";
105      owner = "root";
106      group = "users";
107    };
108  };
109
110  # Daneel XMPP Research Bot
111  services.daneel = {
112    enable = true;
113    xmppJid = "researchbot@xmpp.sbr.pm";
114    ownerJid = "vincent@xmpp.sbr.pm";
115    xmppPasswordFile = config.age.secrets."xmpp-research-bot-password".path;
116    geminiApiKeyFile = config.age.secrets."gemini-api-key".path;
117    inboxPath = "/home/vincent/desktop/org/inbox.org";
118    debug = true;
119    user = "vincent";
120    group = "users";
121  };
122
123  # Binary cache server (x86_64-linux)
124  services.harmonia-cache = {
125    enable = true;
126    signKeyPath = config.age.secrets."harmonia-okinawa-signing-key".path;
127    port = 5000;
128    workers = 4;
129    priority = 30;
130
131    # Nightly cache pre-population
132    builder = {
133      enable = true;
134      systems = [
135        "okinawa" # Self
136        "kyushu" # Work laptop
137        "sakhalin" # Server
138      ];
139      schedule = "02:00"; # 2 AM daily
140      notification = {
141        enable = true;
142        tokenFile = config.age.secrets."ntfy-token".path;
143      };
144    };
145  };
146
147  # Remote build system
148  services.job-notify = {
149    enable = true;
150    ntfyServer = "https://ntfy.sbr.pm";
151    ntfyTokenFile = config.age.secrets."ntfy-token".path;
152    defaultTopic = "builds";
153  };
154
155  # Website flux auto-generate and deploy (hourly)
156  services.flux-generate.enable = true;
157
158  # OpenCode web interface for remote AI coding
159  # Accessible via opencode.sbr.pm through rhea's Traefik reverse proxy
160  systemd.services.opencode-web =
161    let
162      opencode-config = pkgs.writeText "opencode.json" (
163        builtins.toJSON {
164          "$schema" = "https://opencode.ai/config.json";
165          provider = {
166            # Local llama-server (OpenAI-compatible API)
167            llama-server = {
168              npm = "@ai-sdk/openai-compatible";
169              name = "llama-server (local)";
170              options = {
171                baseURL = "http://127.0.0.1:8090/v1";
172                apiKey = "local-test";
173              };
174              models = {
175                "Qwen/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M" = {
176                  name = "Qwen3 Coder 30B MoE";
177                };
178                "Qwen/Qwen2.5-Coder-7B-Instruct-GGUF:Q4_K_M" = {
179                  name = "Qwen 2.5 Coder 7B";
180                };
181                "Qwen/Qwen3-8B-GGUF:Q4_K_M" = {
182                  name = "Qwen3 8B";
183                };
184                "bartowski/DeepSeek-R1-Distill-Qwen-7B-GGUF:Q4_K_M" = {
185                  name = "DeepSeek R1 Distill 7B";
186                };
187                "unsloth/Phi-4-mini-instruct-GGUF:Q4_K_M" = {
188                  name = "Phi-4 mini 3.8B";
189                };
190                "Qwen/Qwen3-Coder-Next-GGUF:Q3_K_M" = {
191                  name = "Qwen3 Coder Next 80B MoE";
192                };
193              };
194            };
195            # Vertex AI (Claude on Google Cloud)
196            google-vertex = {
197              options = {
198                project = "itpc-gcp-pnd-pe-eng-claude";
199                location = "global";
200              };
201            };
202          };
203          server = {
204            hostname = "0.0.0.0";
205            port = 5555;
206          };
207        }
208      );
209    in
210    {
211      description = "OpenCode web interface";
212      after = [
213        "network.target"
214        "llama-cpp.service"
215      ];
216      wantedBy = [ "multi-user.target" ];
217
218      serviceConfig = {
219        Type = "simple";
220        WorkingDirectory = "/home/vincent";
221        ExecStart = pkgs.writeShellScript "opencode-web-start" ''
222          # Load secrets into environment
223          export OPENCODE_SERVER_PASSWORD=$(cat ${config.age.secrets."opencode-password".path})
224          export GROQ_API_KEY=$(cat ${config.age.secrets."groq-api-key".path})
225          export OPENROUTER_API_KEY=$(cat ${config.age.secrets."openrouter-api-key".path})
226          export GOOGLE_GENERATIVE_AI_API_KEY=$(cat ${config.age.secrets."gemini-api-key".path})
227          exec ${pkgs.opencode}/bin/opencode web
228        '';
229        Restart = "on-failure";
230        RestartSec = 10;
231
232        User = "vincent";
233        Group = "users";
234      };
235
236      environment = {
237        HOME = "/home/vincent";
238        OPENCODE_SERVER_USERNAME = "opencode";
239        # Config file location
240        XDG_CONFIG_HOME = "/run/opencode/config";
241        GOOGLE_VERTEX_PROJECT = "itpc-gcp-pnd-pe-eng-claude";
242        GOOGLE_VERTEX_LOCATION = "global";
243      };
244
245      # Copy config file before start (--remove-destination needed because
246      # the nix store source is read-only, so cp can't overwrite in place)
247      preStart = ''
248        mkdir -p /run/opencode/config/opencode
249        cp --remove-destination ${opencode-config} /run/opencode/config/opencode/opencode.json
250      '';
251    };
252
253  # Ensure /run/opencode directory exists
254  systemd.tmpfiles.rules = [
255    "d /run/opencode 0755 vincent users -"
256    "d /run/opencode/config 0755 vincent users -"
257  ];
258
259  # Firewall: OpenCode web + llama-server (VPN access) + monitoring + Harmonia
260  networking.firewall.allowedTCPPorts = [
261    5000 # Harmonia binary cache
262    5555 # OpenCode web
263    8090 # llama-server
264    8880 # readwise-reader triage report
265    9000 # Prometheus node exporter
266  ];
267
268  # llama.cpp server for local LLM inference with Vulkan GPU (RX 6700S)
269  # Router mode: serves multiple models with on-demand loading (--models-max 1)
270  # Only one model loaded in VRAM at a time; auto-swaps on request
271  # Benchmarks on RX 6700S: 7B dense ~40 tok/s, 3B active MoE ~20-40 tok/s (Vulkan, Q4_K_M)
272  systemd.services.llama-cpp =
273    let
274      llama-cpp-vulkan = pkgs.llama-cpp.override { vulkanSupport = true; };
275
276      # Model preset INI for router mode
277      # Section names become model IDs in the API (used in models.json)
278      modelsPreset = pkgs.writeText "llama-models.ini" ''
279        version = 1
280
281        ; Shared settings for all models
282        [*]
283        n-gpu-layers = 99
284        jinja = true
285
286        ; === Coding models ===
287
288        ; Qwen3 Coder Next 80B MoE (3B active) — best coding model, needs ~48GB RAM
289        [Qwen/Qwen3-Coder-Next-GGUF:Q3_K_M]
290
291        ; Qwen3 Coder 30B MoE (3B active) — sweet spot coding, ~19GB RAM
292        [Qwen/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M]
293
294        ; Qwen 2.5 Coder 7B dense — lightweight coding, ~5GB (current default)
295        [Qwen/Qwen2.5-Coder-7B-Instruct-GGUF:Q4_K_M]
296
297        ; === General purpose models ===
298
299        ; Qwen3 8B dense — best all-rounder (reasoning, tool use, multilingual)
300        [Qwen/Qwen3-8B-GGUF:Q4_K_M]
301
302        ; DeepSeek R1 Distill 7B — deep reasoning / chain-of-thought
303        [bartowski/DeepSeek-R1-Distill-Qwen-7B-GGUF:Q4_K_M]
304
305        ; Phi-4 mini 3.8B — ultra-fast utility model
306        [unsloth/Phi-4-mini-instruct-GGUF:Q4_K_M]
307      '';
308
309    in
310    {
311      description = "llama.cpp server (Vulkan, router mode)";
312      after = [ "network.target" ];
313      wantedBy = [ "multi-user.target" ];
314
315      environment = {
316        GGML_VK_VISIBLE_DEVICES = "0"; # Use RX 6700S dGPU only
317        HIP_VISIBLE_DEVICES = ""; # Disable ROCm to avoid conflicts
318      };
319
320      serviceConfig = {
321        Type = "idle";
322        KillSignal = "SIGINT";
323        ExecStart = builtins.concatStringsSep " " [
324          "${llama-cpp-vulkan}/bin/llama-server"
325          "--log-disable"
326          "--host 0.0.0.0"
327          "--port 8090"
328          "--models-preset ${modelsPreset}"
329          "--models-max 1"
330          "-np 1"
331          "--api-key local-test"
332        ];
333        Restart = "on-failure";
334        RestartSec = 30;
335
336        # Run as vincent to access HuggingFace model cache
337        User = "vincent";
338        Group = "users";
339
340        # GPU access requires relaxed sandboxing
341        PrivateDevices = false;
342        ProtectHome = false;
343        MemoryDenyWriteExecute = false;
344      };
345    };
346
347  # Oneshot service to pre-download all LLM models
348  # Run manually: sudo systemctl start llama-download-models
349  systemd.services.llama-download-models =
350    let
351      llama-cpp-vulkan = pkgs.llama-cpp.override { vulkanSupport = true; };
352    in
353    {
354      description = "Download LLM models for llama.cpp";
355      after = [ "network-online.target" ];
356      wants = [ "network-online.target" ];
357
358      serviceConfig = {
359        Type = "oneshot";
360        RemainAfterExit = false;
361        User = "vincent";
362        Group = "users";
363        ExecStart = pkgs.writeShellScript "llama-download-models" ''
364          set -euo pipefail
365          export PATH="${llama-cpp-vulkan}/bin:$PATH"
366
367          models=(
368            "Qwen/Qwen3-Coder-Next-GGUF:Q3_K_M"
369            "Qwen/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M"
370            "Qwen/Qwen2.5-Coder-7B-Instruct-GGUF:Q4_K_M"
371            "Qwen/Qwen3-8B-GGUF:Q4_K_M"
372            "bartowski/DeepSeek-R1-Distill-Qwen-7B-GGUF:Q4_K_M"
373            "unsloth/Phi-4-mini-instruct-GGUF:Q4_K_M"
374          )
375
376          for model in "''${models[@]}"; do
377            echo "Checking/downloading $model..."
378            # Use llama-cli to trigger download, then exit immediately
379            timeout 120 llama-cli -hf "$model" -p "test" -n 1 --no-display-prompt 2>&1 || true
380            echo "Done: $model"
381          done
382        '';
383      };
384    };
385
386  # System packages for LLM, gaming, and tools
387  environment.systemPackages = with pkgs; [
388    # nix-flake-update wrapper with pre-baked config for the home repo
389    # Run manually: nix-flake-update-home [--dry-run] [--no-auto-fix]
390    (pkgs.writeShellScriptBin "nix-flake-update-home" ''
391      export REPO_PATH="/home/vincent/src/home"
392      export FLAKE_PATH="/home/vincent/src/home"
393      export GIT_REMOTE="origin"
394      export MAIN_BRANCH="main"
395      export BRANCH_PREFIX="flake-update-"
396      export NTFY_TOPIC="nix-updates"
397      export NTFY_SERVER="https://ntfy.sbr.pm"
398      export NTFY_TOKEN_FILE="/run/agenix/ntfy-token"
399      export INBOX_ORG="/home/vincent/desktop/org/inbox.org"
400      export BUILD_SYSTEMS="okinawa kyushu sakhalin carthage rhea aion athena demeter aix"
401      export GIT_SSH_COMMAND="ssh -F /dev/null -o IdentitiesOnly=yes -i /home/vincent/.ssh/id_passage -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/home/vincent/.ssh/known_hosts"
402
403      # AI-powered auto-fix
404      export AUTO_FIX="true"
405      export AUTO_FIX_COMMAND="pi"
406      export AUTO_FIX_EXTRA_ARGS="--provider google-vertex-claude --no-session --no-themes --no-skills"
407      export AUTO_FIX_MAX_ATTEMPTS="3"
408      export GOOGLE_CLOUD_PROJECT="itpc-gcp-pnd-pe-eng-claude"
409      export GOOGLE_CLOUD_LOCATION="global"
410
411      # Allow overriding via CLI args
412      for arg in "$@"; do
413        case "$arg" in
414          --dry-run) export DRY_RUN="true" ;;
415          --no-auto-fix) export AUTO_FIX="false" ;;
416        esac
417      done
418
419      exec ${pkgs.nix-flake-update}/bin/nix-flake-update
420    '')
421    # LLM tools (same package as the service, for CLI use)
422    (llama-cpp.override {
423      vulkanSupport = true;
424    })
425
426    # GPU monitoring and management
427    radeontop # GPU usage monitor
428    clinfo # Check OpenCL
429    vulkan-tools # vulkaninfo
430    rocmPackages.rocminfo
431    asusctl # CLI for supergfxctl
432    supergfxctl
433
434    # Gaming tools
435    mangohud # FPS and performance overlay
436    goverlay # MangoHud configuration GUI
437    protonup-qt # Install custom Proton versions (Proton-GE)
438    protontricks # Winetricks for Proton prefixes
439    heroic # GOG/Epic Games launcher
440
441    # Development
442    python3
443    uv
444
445    # Audio utilities for G14
446    alsa-utils # amixer, alsactl for AMP control
447    jamesdsp # DSP/EQ to replace Dolby Atmos (use with asus-jamesdsp presets)
448  ];
449
450  # Prometheus node exporter (configured in common module)
451  # Port and basic collectors already set in ../common/base (prometheus-exporters-node.nix)
452
453  # Keep dGPU (Navi 23 / RX 6700S) awake so HDMI hotplug is detected.
454  # The HDMI port is wired to the dGPU; runtime suspend prevents detection.
455  services.udev.extraRules = ''
456    ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x1002", ATTR{device}=="0x73ef", ATTR{power/control}="on"
457  '';
458
459  # Lid handling: ignore (desktop replacement, mostly on AC)
460  services.logind.settings.Login = {
461    HandleLidSwitch = "ignore";
462    HandleLidSwitchExternalPower = "ignore";
463    HandleLidSwitchDocked = "ignore";
464  };
465
466  # ASUS G14 2022 Audio Fix
467  # Sets ALSA mixer levels at boot and after resume
468  systemd.services.asus-g14-audio-fix = {
469    description = "ASUS G14 Audio Amplifier Initialization";
470    after = [
471      "sound.target"
472      "alsa-restore.service"
473    ];
474    wantedBy = [
475      "multi-user.target"
476      "post-resume.target"
477    ];
478
479    serviceConfig = {
480      Type = "oneshot";
481      RemainAfterExit = true;
482      ExecStart = pkgs.writeShellScript "g14-audio-init" ''
483        # Find the ALC285 audio card (internal speakers)
484        CARD=$(${pkgs.alsa-utils}/bin/aplay -l | grep "ALC285" | head -1 | sed 's/card \([0-9]\).*/\1/')
485
486        if [ -z "$CARD" ]; then
487          echo "ERROR: Could not find ALC285 audio card"
488          exit 1
489        fi
490
491        echo "Initializing G14 audio on card $CARD"
492
493        # Set Master volume to 100% and unmute
494        ${pkgs.alsa-utils}/bin/amixer -c "$CARD" sset Master 100% unmute || true
495
496        # Set Speaker volume (tweeters)
497        ${pkgs.alsa-utils}/bin/amixer -c "$CARD" sset Speaker 100% unmute || true
498
499        # Set Bass Speaker (subwoofers)
500        ${pkgs.alsa-utils}/bin/amixer -c "$CARD" sset 'Bass Speaker' unmute || true
501
502        # Set Headphone volume
503        ${pkgs.alsa-utils}/bin/amixer -c "$CARD" sset Headphone 100% unmute || true
504
505        echo "G14 audio initialization complete"
506      '';
507    };
508  };
509}