1 Commits

Author SHA1 Message Date
finn.markwitz 69913494fb Initial commit 2026-06-27 15:14:02 +02:00
69 changed files with 2 additions and 4319 deletions
-7
View File
@@ -1,7 +0,0 @@
export DIRENV_WARN_TIMEOUT=5m
if ! has nix_direnv_version || ! nix_direnv_version 2.2.0; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.0/direnvrc" "sha256-5EwyKnkJNQeXrRkYbwwRBcXbibosCJqyIUuz9Xq+LRc="
fi
use flake
-1
View File
@@ -1 +0,0 @@
AGENTS.md
-13
View File
@@ -1,13 +0,0 @@
stages:
- build
jupiter-build:
image: nixos/nix
stage: build
script:
- ls -la
- nix-build --version
- nix flake show --extra-experimental-features 'nix-command flakes'
- nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel' --extra-experimental-features 'nix-command flakes'
-8
View File
@@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
-5
View File
@@ -1,5 +0,0 @@
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/nixos.iml" filepath="$PROJECT_DIR$/.idea/nixos.iml" />
</modules>
</component>
</project>
-9
View File
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
-92
View File
@@ -1,92 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
```bash
# Apply configuration (switch/boot/test)
sudo nixos-rebuild switch --flake '.#jupiter'
sudo nixos-rebuild switch --flake '.#mibook'
# Build without switching (CI-style check)
nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel'
# Update all flake inputs
nix flake update
# Format Nix files
nixfmt-rfc-style <file> # or: find . -name '*.nix' | xargs nixfmt-rfc-style
```
## Architecture
This is a flake-parts NixOS configuration for two machines:
- **jupiter** — home server running media/automation services
- **mibook** — laptop running KDE desktop + development tools
### Module Loading Chain
```
flake.nix
└── machines/configuration.nix # flake-parts module; defines nixosConfigurations
├── machines/core/ # base modules applied to every machine
│ ├── core.nix # system packages, locale, timezone
│ ├── network.nix
│ ├── nix.nix
│ └── users.nix
├── modules/ # custom NixOS option modules (my.profiles.*, my.hardware.*, my.services.*)
│ ├── environments/ # per-service/app profiles
│ ├── hardware/ # hardware profiles (nvidia, bluetooth, sound, wifi)
│ └── services/ # infrastructure services (vpn, webserver)
└── machines/<name>/
├── configuration.nix # machine-specific NixOS settings
├── environments.nix # enables profiles via my.profiles.* / my.hardware.* options
├── disks.nix
└── hardware-configuration.nix
```
### Profile / Module Pattern
Every module under `modules/` follows the same structure:
```nix
{ config, lib, pkgs, ... }:
let cfg = config.my.profiles.<name>; in
{
options.my.profiles.<name>.enable = lib.mkEnableOption "...";
config = lib.mkIf cfg.enable { ... };
}
```
Namespaces in use:
- `my.profiles.*` — application/service profiles
- `my.hardware.*` — hardware profiles
- `my.services.*` — infrastructure services
Profiles are enabled per-machine in `machines/<name>/environments.nix`.
### Unstable Packages
`pkgs.unstable` is available everywhere via an overlay defined in `machines/configuration.nix`. Use it when a package isn't in the pinned stable channel (`nixpkgs/nixos-25.11`).
### Homepage Dashboard Integration
Modules that expose a web UI can self-register with the homepage dashboard by adding to `my.homepage.services`:
```nix
my.homepage.services = [{
group = "Services";
name = "My Service";
description = "...";
href = "http://${hostName}:PORT";
icon = "si-iconname"; # optional
}];
```
### Adding a New Service Module
1. Create `modules/environments/<name>/default.nix` following the profile pattern above.
2. Add `./environments/<name>` to `modules/environments/default.nix` (or the relevant `default.nix`).
3. Enable it in the target machine's `machines/<name>/environments.nix`.
+2 -30
View File
@@ -1,31 +1,3 @@
# Private NixOS Configuration
# nixos
[Felix Nixos Config](https://github.com/Stunkymonkey/nixos)
## Structure
```
.
├── images # custom image generations
├── machines # machine definitions
├── modules # own nix-options, to modularize services/hardware/...
├── overlays # overlays
├── pkgs # own packages, which are not available in nixpkgs
└── environments # summarize module collections into single options
```
## Commands
```bash
# Rebuild (switch/boot/test)
sudo nixos-rebuild switch --flake '.#jupiter'
# Update Flake
nix flake update
# Channel list
sudo nix-channel --list
# Channel update
sudo nix-channel --update
```
Private NixOS configuration
@@ -1,607 +0,0 @@
# ZBT-2 Thread + OTBR Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Run the Home Assistant Connect ZBT-2 as an OpenThread Border Router on `jupiter`, fully integrated with the existing native `services.home-assistant` + `services.matter-server` stack so Matter-over-Thread devices commission through the dongle.
**Architecture:** Single NixOS module file (`modules/environments/home-assistant/default.nix`) is edited to import the `services.openthread-border-router` module from `nixos-unstable` (not yet in 25.11 stable), enable it against the ZBT-2's `/dev/serial/by-id/...` path, and add HA's `otbr` + `thread` extra components. The previous ZHA-direction commit on this branch is reverted first. The dongle is one-time-flashed from Zigbee NCP firmware to OpenThread RCP firmware via `universal-silabs-flasher` outside the NixOS lifecycle (per design decision: option B, CLI-only).
**Tech Stack:** Nix flakes (flake-parts), NixOS 25.11 stable + nixos-unstable, `services.openthread-border-router`, `services.home-assistant`, `services.matter-server`, `python313Packages.universal-silabs-flasher`.
**Spec:** [`docs/superpowers/specs/2026-05-10-zbt2-thread-otbr-design.md`](../specs/2026-05-10-zbt2-thread-otbr-design.md) — read this before starting.
**User feedback rules in force:**
- Never commit to `master`; this branch is `feature/ha-zbt-2-thread`. Final merge happens at the end via PR or operator-driven merge.
- Do not SSH to `jupiter`. All commands targeting jupiter are operator handoffs — present the command, the user runs it and pastes output back.
---
## File Map
| Action | File | Responsibility |
|--------|------|----------------|
| Modify | `modules/environments/home-assistant/default.nix` | Import unstable OTBR module; enable OTBR for the ZBT-2; add `otbr` + `thread` HA components |
| Modify | `machines/configuration.nix` | Pass `self` via `specialArgs` so it's available during NixOS module **imports** evaluation (not just config) |
| Create (auto) | _(no new files)_ | All work fits in the two modules |
The `git revert` of `e8d09f4` automatically un-modifies the home-assistant module (drops `"zha"` and the `dialout` line). No host-level (`machines/jupiter/`) changes.
**Why the flake-level edit is needed:** the existing `_module.args.self = self;` in `machines/configuration.nix:21` makes `self` available in module bodies (option definitions, `config` blocks). It does **not** make `self` available during `imports` evaluation — `_module.args` is resolved from `config`, but `imports` are collected **before** `config` is evaluated, so `self` in `imports` causes an infinite recursion error. Promoting `self` to `specialArgs` short-circuits that and is the conventional fix.
---
## Validation Approach (instead of unit tests)
This is a NixOS configuration change; there's no test framework. We use `nix eval` against `nixosConfigurations.jupiter.config.*` as the equivalent of unit tests — assert option resolution **before** the change (red), then **after** the change (green). Functional / smoke tests happen post-`nixos-rebuild` on jupiter via systemctl, mDNS, and the HA UI.
All `nix eval` commands run on the dev Mac. All `systemctl` / `journalctl` / `nixos-rebuild` commands run on jupiter (operator handoff).
---
### Task 1: Revert the prior ZHA commit
**Files:**
- Modify: `modules/environments/home-assistant/default.nix` (via `git revert`)
- [ ] **Step 1: Verify pre-state**
On dev Mac, in the repo root:
```bash
git log --oneline -3
```
Expected: `dbeda27` (design spec) on top of `e8d09f4` (the ZHA commit) on top of `098e632`.
Also confirm current `extraComponents` includes `"zha"`:
```bash
nix eval --json .#nixosConfigurations.jupiter.config.services.home-assistant.extraComponents
```
Expected: `["matter","mobile_app","zha"]`
- [ ] **Step 2: Revert**
```bash
git revert --no-edit e8d09f4
```
Expected: revert commit created cleanly (no merge conflicts), single file changed.
- [ ] **Step 3: Verify post-state**
```bash
nix eval --json .#nixosConfigurations.jupiter.config.services.home-assistant.extraComponents
```
Expected: `["matter","mobile_app"]``zha` is gone.
```bash
nix eval --json .#nixosConfigurations.jupiter.config.users.users.hass.extraGroups
```
Expected: `[]``dialout` is gone.
```bash
git log --oneline -4
```
Expected: revert commit on top of `dbeda27` on top of `e8d09f4`.
(No explicit `git commit` step — `git revert` produced its own commit.)
---
### Task 2: Wire the unstable OTBR module import (still disabled)
This task gets the module into scope so options become available, but leaves `services.openthread-border-router.enable = false` (the default). The point is to confirm the import path works before adding device-specific config.
**Files:**
- Modify: `machines/configuration.nix` (add `specialArgs = { inherit self; };` to each `nixosSystem` call)
- Modify: `modules/environments/home-assistant/default.nix`
- [ ] **Step 1: Write the failing eval check**
On dev Mac:
```bash
nix eval --json .#nixosConfigurations.jupiter.options.services.openthread-border-router.enable.description 2>&1 | head -3
```
Expected: error containing `attribute 'openthread-border-router' missing` or similar — the option doesn't exist yet because the module isn't imported.
- [ ] **Step 1a: Promote `self` to `specialArgs` in `machines/configuration.nix`**
`self` must be reachable during `imports` evaluation (not just `config` evaluation). The existing `_module.args.self = self;` only covers `config`-time access. Edit each `nixosSystem` call (`jupiter` and `mibook`) to add `specialArgs`.
Current shape (lines 5056 and 5763):
```nix
jupiter = nixosSystem {
system = "x86_64-linux";
modules = defaultModules ++ [
# nixos-hardware.nixosModules.bmax-b7-power
./jupiter/configuration.nix
];
};
mibook = nixosSystem {
system = "x86_64-linux";
modules = defaultModules ++ [
# nixos-hardware.nixosModules.mibook
./mibook/configuration.nix
];
};
```
Add `specialArgs = { inherit self; };` to each:
```nix
jupiter = nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit self; };
modules = defaultModules ++ [
# nixos-hardware.nixosModules.bmax-b7-power
./jupiter/configuration.nix
];
};
mibook = nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit self; };
modules = defaultModules ++ [
# nixos-hardware.nixosModules.mibook
./mibook/configuration.nix
];
};
```
- [ ] **Step 2: Add `self` to the module's argument list and add the `imports` block**
Current header (`modules/environments/home-assistant/default.nix` lines 111):
```nix
# manages home automations
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.home-assistant;
hostName = config.networking.hostName;
in
```
Replace lines 111 with:
```nix
# manages home automations
{
config,
lib,
pkgs,
self,
...
}:
let
cfg = config.my.profiles.home-assistant;
hostName = config.networking.hostName;
in
```
Then, immediately after the opening brace on line 12 of the modified file (i.e. at the top of the attribute set body, before `options.my.profiles.home-assistant`), add:
```nix
imports = [
# services.openthread-border-router isn't in nixos-25.11; pull from
# nixpkgs-unstable. Package comes from the existing unstable overlay.
"${self.inputs.nixpkgs-unstable}/nixos/modules/services/home-automation/openthread-border-router.nix"
];
```
- [ ] **Step 3: Re-run the eval check**
```bash
nix eval --json .#nixosConfigurations.jupiter.options.services.openthread-border-router.enable.description 2>&1 | head -3
```
Expected: a JSON string describing the option (e.g. `"Whether to enable the OpenThread Border Router."`).
- [ ] **Step 4: Verify the service is currently disabled**
```bash
nix eval --json .#nixosConfigurations.jupiter.config.services.openthread-border-router.enable
```
Expected: `false`.
- [ ] **Step 5: Verify whole config still evaluates**
```bash
nix eval .#nixosConfigurations.jupiter.config.system.build.toplevel.drvPath
```
Expected: a `/nix/store/...drv` path. Pre-existing trace warnings (the `*.service ordered after network-online.target` ones) are fine; no errors.
- [ ] **Step 6: Commit**
```bash
git add machines/configuration.nix modules/environments/home-assistant/default.nix
git commit -m "$(cat <<'EOF'
feat(home-assistant): import openthread-border-router module from unstable
Pulls the services.openthread-border-router NixOS module directly from
nixpkgs-unstable since it isn't in 25.11 yet. Service stays disabled
in this commit; configuration follows.
Also promotes `self` from `_module.args` to `specialArgs` in
machines/configuration.nix, since `imports` are evaluated before
`config` and so can't reach `_module.args.self`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"
```
---
### Task 3: Operator handoff — get the ZBT-2 device path from jupiter
This task has no code. It collects the runtime parameter (USB serial number) that Task 4 needs.
**Files:** _(none)_
- [ ] **Step 1: Hand off**
Tell the operator:
> "Plug the ZBT-2 into a USB-2 port on jupiter (it's still on stock Zigbee firmware — that's fine for this step). Then run `ls -l /dev/serial/by-id/` on jupiter and paste the full output back. We're after the line that contains `Nabu_Casa_Home_Assistant_Connect_ZBT-2`."
- [ ] **Step 2: Wait for the operator's pasted output**
Expected shape: a line like
`lrwxrwxrwx 1 root root 13 May 10 14:30 usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_<serial-string>-if00 -> ../../ttyACM0`
- [ ] **Step 3: Record the by-id path**
Capture the value `/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_<serial-string>-if00` for use in Task 4. Use the **by-id** path (not `/dev/ttyACM0`) so USB renumbering can't break OTBR.
---
### Task 4: Enable OTBR + add HA otbr/thread components
**Files:**
- Modify: `modules/environments/home-assistant/default.nix`
- [ ] **Step 1: Write the failing eval checks**
On dev Mac:
```bash
nix eval --json .#nixosConfigurations.jupiter.config.services.openthread-border-router.enable
```
Expected: `false` (still disabled from Task 2).
```bash
nix eval --json .#nixosConfigurations.jupiter.config.services.home-assistant.extraComponents
```
Expected: `["matter","mobile_app"]` — no `otbr`, no `thread` yet.
- [ ] **Step 2: Add `"otbr"` and `"thread"` to `extraComponents`**
In `modules/environments/home-assistant/default.nix`, locate the `extraComponents` list (currently `[ "matter" "mobile_app" ]`) and replace it with:
```nix
extraComponents = [
"matter"
"mobile_app"
"otbr"
"thread"
];
```
- [ ] **Step 3: Add the `services.openthread-border-router` block**
In the same file, **after** the `services.home-assistant.config = { ... };` block and **before** `my.homepage.services`, add:
```nix
services.openthread-border-router = {
enable = true;
package = pkgs.unstable.openthread-border-router;
openFirewall = true;
backboneInterfaces = [ "enp3s0" ];
radio.device = "<PASTE-BY-ID-PATH-FROM-TASK-3>";
};
```
Replace `<PASTE-BY-ID-PATH-FROM-TASK-3>` with the literal string captured in Task 3 step 3 (e.g. `"/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_AB12CD34-if00"`).
- [ ] **Step 4: Run the green eval checks**
```bash
nix eval --json .#nixosConfigurations.jupiter.config.services.home-assistant.extraComponents
```
Expected: `["matter","mobile_app","otbr","thread"]`.
```bash
nix eval --json .#nixosConfigurations.jupiter.config.services.openthread-border-router.enable
```
Expected: `true`.
```bash
nix eval --raw .#nixosConfigurations.jupiter.config.services.openthread-border-router.radio.url
```
Expected: a string like `spinel+hdlc+uart:///dev/serial/by-id/usb-Nabu_Casa_..._ZBT-2_<serial>-if00?uart-baudrate=115200` (the module composes this from `radio.device` automatically).
- [ ] **Step 5: Full eval — system derivation must build**
```bash
nix eval .#nixosConfigurations.jupiter.config.system.build.toplevel.drvPath
```
Expected: a `/nix/store/...drv` path with no eval errors.
- [ ] **Step 6: `nix flake check` for good measure**
```bash
nix flake check
```
Expected: no errors. (Same pre-existing trace warnings as before are acceptable.)
- [ ] **Step 7: Commit**
```bash
git add modules/environments/home-assistant/default.nix
git commit -m "$(cat <<'EOF'
feat(home-assistant): enable OTBR for ZBT-2 + add HA otbr/thread components
Brings up otbr-agent against the ZBT-2 over Spinel/UART, opens the
REST API on :8081, and wires HA's otbr + thread integrations so
Matter-over-Thread devices can commission through the existing
matter-server.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"
```
---
### Task 5: Operator handoff — flash OpenThread RCP firmware on the dongle
The dongle is currently running Zigbee NCP firmware and won't speak Spinel until reflashed. This must happen **before** Task 6's rebuild (otherwise `otbr-agent` will try to talk to a Zigbee-firmware dongle and fail).
**Files:** _(none on dev Mac)_
- [ ] **Step 1: Hand off — fetch firmware**
Tell the operator:
> "On any machine with a browser: download the latest **ZBT-2 OpenThread RCP** `.gbl` from <https://github.com/NabuCasa/silabs-firmware-builder/releases>. The asset name will look like `ot-rcp-zbt-2-<version>.gbl`. Get it onto jupiter — `scp` it over, or just `curl` from jupiter's shell. Confirm by running `ls ~/ot-rcp-zbt-2-*.gbl` on jupiter and pasting the result."
- [ ] **Step 2: Wait for confirmation**
Expected: a single matching path, e.g. `/home/finn/ot-rcp-zbt-2-2025.10.0.gbl`.
- [ ] **Step 3: Hand off — flash**
Tell the operator:
> "On jupiter, run (substituting the actual by-id path from Task 3 and the actual `.gbl` filename):
>
> ```bash
> nix shell nixpkgs#python313Packages.universal-silabs-flasher -c \
> universal-silabs-flasher \
> --device /dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_<serial>-if00 \
> flash --firmware ~/ot-rcp-zbt-2-<version>.gbl
> ```
>
> Paste the full output. Expected duration: ~30 seconds. The tool detects the running firmware, drops the dongle into bootloader mode, writes the `.gbl`, and reboots back to RCP."
- [ ] **Step 4: Verify the flash succeeded**
Expected output ends with something like `Firmware update complete` (or equivalent success message). If the tool reports CRC failure / partial write — re-run; the bootloader stays addressable.
If the operator reports `--help` shows different subcommand syntax (universal-silabs-flasher's CLI has changed across versions), have them check `universal-silabs-flasher --help` and adapt — but the `flash --firmware <path>` form has been stable since 1.0.x.
---
### Task 6: Operator handoff — `nixos-rebuild switch` on jupiter
**Files:** _(none on dev Mac)_
- [ ] **Step 1: Push the branch so jupiter can fetch it**
On dev Mac:
```bash
git push -u origin feature/ha-zbt-2-thread
```
(If the operator pulls via a different mechanism — local checkout, fileshare — adapt accordingly. The standard pattern in this repo is `git pull` on jupiter.)
- [ ] **Step 2: Hand off — pull + rebuild**
Tell the operator:
> "On jupiter:
>
> ```bash
> cd ~/development/nixos # or wherever the flake lives on jupiter
> git fetch origin
> git checkout feature/ha-zbt-2-thread
> sudo nixos-rebuild switch --flake .#jupiter
> ```
>
> Paste the tail of the output (everything from the first `building ...` line onward). Expected: build completes, switch to the new generation, no errors."
- [ ] **Step 3: Verify the switch succeeded**
If the operator's pasted output includes `error:` or the switch failed mid-activation, **stop here**. Common failure: option name mismatch with whatever version of nixos-unstable is locked in the flake. Fix on dev Mac, push, ask operator to pull + rebuild again.
If the rebuild succeeded, proceed to Task 7.
---
### Task 7: Operator handoff — service-level verification on jupiter
**Files:** _(none)_
- [ ] **Step 1: Hand off — service health**
Tell the operator:
> "On jupiter, run each command and paste output:
>
> ```bash
> systemctl status otbr-agent.service --no-pager
> journalctl -u otbr-agent.service -n 50 --no-pager
> ip link show wpan0
> ```"
- [ ] **Step 2: Verify**
Expected:
- `systemctl status` reports `active (running)`.
- `journalctl` shows OTBR startup messages, no repeated restart loops.
- `ip link show wpan0` shows the interface exists; state DOWN is correct (HA hasn't formed a network yet).
If `otbr-agent` is in restart loop with `Failed to open device`: device path mismatch. Re-check Task 3's path.
- [ ] **Step 3: Hand off — mDNS publication**
Tell the operator:
> "On jupiter:
>
> ```bash
> avahi-browse -r -t _meshcop._udp
> ```"
Expected: one entry whose hostname matches jupiter, advertising port 8081.
If empty: `backboneInterfaces` is wrong. On jupiter, run `ip link show` and tell operator to paste; pick the actual primary LAN interface, update `backboneInterfaces`, re-rebuild.
- [ ] **Step 4: Hand off — REST API reachability**
Tell the operator:
> "On jupiter:
>
> ```bash
> curl -s http://127.0.0.1:8081/node/state
> ```"
Expected: a JSON state string, most likely `"disabled"` (HA hasn't formed a network yet).
If connection refused: OTBR isn't actually listening — re-check `journalctl`.
---
### Task 8: Operator handoff — HA UI smoke test
**Files:** _(none)_
- [ ] **Step 1: Hand off — confirm discovery**
Tell the operator:
> "Open `http://jupiter:8123` in a browser. Go to **Settings → Devices & Services**. Within ~30s of the rebuild, you should see **'Open Thread Border Router'** under 'Discovered'. Click **Configure**. Let HA form a new Thread network (or import existing dataset if you have one). Tell me when that's done — and paste any errors if it doesn't work."
- [ ] **Step 2: Wait for confirmation**
Expected: HA reports the Thread network is formed; the OTBR integration appears under 'Configured'.
If discovery doesn't happen: cross-check with Task 7 step 3 (`avahi-browse`). HA reads from the system's avahi cache.
- [ ] **Step 3: Hand off — Matter-over-Thread pairing**
Tell the operator:
> "Pick one Matter-over-Thread device. Use the HA Companion app, scan its Matter QR code, and follow the prompts. Tell me when it's paired — or paste any errors. Pairing should complete in 3090s."
- [ ] **Step 4: Wait for confirmation**
Expected: device appears under both Matter and Thread integrations in HA, and is controllable from the dashboard.
If pairing times out: see "Failure modes" table in the spec — most likely Thread mesh prefix isn't routed back to LAN. Operator runs `nft list ruleset` and `ip -6 route` on jupiter; debug from there.
---
### Task 9: Merge to master
**Files:** _(none)_
- [ ] **Step 1: Final branch state**
On dev Mac:
```bash
git log --oneline master..feature/ha-zbt-2-thread
```
Expected (in chronological order from oldest to newest):
1. `e8d09f4` — original ZHA commit
2. `dbeda27` — design spec
3. `<revert hash>` — Revert "feat(home-assistant): enable ZHA for ZBT-2 Zigbee dongle"
4. `<task-2 hash>` — feat(home-assistant): import openthread-border-router module from unstable
5. `<task-4 hash>` — feat(home-assistant): enable OTBR for ZBT-2 + add HA otbr/thread components
That's a fine history to merge as-is (the ZHA→revert pair is honest about the pivot).
- [ ] **Step 2: Hand off — merge**
The user runs the merge themselves (per repo policy: never commit to master without explicit consent). Tell the operator:
> "If the smoke tests in Task 8 worked, merge with:
>
> ```bash
> git switch master
> git merge --no-ff feature/ha-zbt-2-thread
> git push origin master
> ```
>
> Or open a merge request / PR if you prefer review first."
- [ ] **Step 3: Optional cleanup**
After merge:
```bash
git branch -d feature/ha-zbt-2-thread
git push origin --delete feature/ha-zbt-2-thread
```
---
## Self-Review
**Spec coverage:**
- Goals (4 bullets) → Tasks 2 (OTBR module wiring), 4 (OTBR enable + HA components), 5 (firmware flash), 8 (Matter-over-Thread smoke test) ✓
- Non-goals → respected; no multipan, no auto-flash, no fallback paths ✓
- Architecture diagram → Task 4 produces the wiring shown; Tasks 68 verify it ✓
- File changes (one module) → Tasks 1, 2, 4 ✓
- Reverts of prior ZHA commit → Task 1 ✓
- Operator workflow steps 07 → Tasks 1, 2, 3, 4, 5, 6, 7, 8 ✓
- Verification (eval-only / service-level / functional) → Tasks 2/4/6/7/8 ✓
- Failure-mode table → referenced in Tasks 6, 7, 8 for triage ✓
**Placeholder scan:**
- `<PASTE-BY-ID-PATH-FROM-TASK-3>` in Task 4 step 3 is intentional — it's a runtime parameter the operator fills in, captured in Task 3.
- `<serial>`, `<version>` in shell commands are intentional placeholders for operator substitution.
- No "TBD", "TODO", "implement later", or vague "handle errors" steps.
**Type / name consistency:**
- `services.openthread-border-router` used consistently (matches the unstable module's option path).
- `pkgs.unstable.openthread-border-router` matches the overlay (`machines/configuration.nix:11`).
- `extraComponents` strings (`"otbr"`, `"thread"`) match HA Core integration names.
- `radio.device``radio.url` relationship documented (module composes `url` from `device`).
@@ -1,232 +0,0 @@
# MiBook Claude Code Module Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a `my.profiles.claude-code` NixOS module that installs the Claude Code CLI and companion tooling on the MiBook.
**Architecture:** A single new environment module (`modules/environments/claude-code/default.nix`) declares the `my.profiles.claude-code.enable` option and installs packages for user `finn`. It is registered in `modules/environments/default.nix` and toggled on in `machines/mibook/environments.nix`.
**Tech Stack:** Nix flakes, NixOS module system, `pkgs.unstable` overlay (already present in repo)
---
## File Map
| Action | Path | Responsibility |
|---|---|---|
| Create | `modules/environments/claude-code/default.nix` | Declares option + installs packages |
| Modify | `modules/environments/default.nix` | Registers the new module so NixOS loads it |
| Modify | `machines/mibook/environments.nix` | Enables the profile for the MiBook host |
---
### Task 1: Verify `claude-code` exists in `nixpkgs-unstable`
**Files:**
- Read-only check — no file changes
- [ ] **Step 1: Check if the package is available**
Run:
```bash
nix eval --extra-experimental-features 'nix-command flakes' 'github:NixOS/nixpkgs/nixos-unstable#claude-code.version' 2>&1
```
Expected (success): prints a version string like `"0.2.x"`
Expected (failure): `error: attribute 'claude-code' missing`
- [ ] **Step 2: If the package exists — note the attribute path and continue to Task 2**
The module will use `pkgs.unstable.claude-code`.
- [ ] **Step 3: If the package does NOT exist — add a custom derivation first**
Create `pkgs/claude-code/default.nix`:
```nix
{ lib, buildNpmPackage, fetchFromGitHub }:
buildNpmPackage rec {
pname = "claude-code";
version = "0.2.116"; # update to latest release tag
src = fetchFromGitHub {
owner = "anthropics";
repo = "claude-code";
rev = "v${version}";
hash = lib.fakeHash; # run nix build to get real hash
};
npmDepsHash = lib.fakeHash; # run nix build to get real hash
meta = {
description = "Claude Code CLI by Anthropic";
homepage = "https://github.com/anthropics/claude-code";
license = lib.licenses.unfree;
mainProgram = "claude";
};
}
```
Then register it in `pkgs/default.nix`:
```nix
final: prev: {
claude-code = final.callPackage ./claude-code { };
}
```
And use `pkgs.claude-code` (not `pkgs.unstable.claude-code`) in the module.
---
### Task 2: Create the `claude-code` module
**Files:**
- Create: `modules/environments/claude-code/default.nix`
- [ ] **Step 1: Create the module file**
Create `modules/environments/claude-code/default.nix` with this exact content:
```nix
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.claude-code;
in
{
options.my.profiles.claude-code = with lib; {
enable = mkEnableOption "Claude Code CLI";
};
config = lib.mkIf cfg.enable {
users.users.finn.packages = with pkgs; [
unstable.claude-code
ripgrep
fd
gh
jq
];
# Future: headless Claude Code service
# A natural next step is exposing Claude Code as a persistent background service —
# e.g. a systemd user service that accepts work via an HTTP API or Unix socket,
# triggerable over SSH or a local network endpoint. This would turn the MiBook
# into a true remote execution node without requiring an interactive session.
# See: my.profiles.claude-code.service.enable (not yet implemented)
};
}
```
> Note: if Task 1 Step 3 was taken (custom derivation), replace `unstable.claude-code` with `claude-code`.
---
### Task 3: Register the module
**Files:**
- Modify: `modules/environments/default.nix`
- [ ] **Step 1: Add the import**
In `modules/environments/default.nix`, add `./claude-code` to the imports list (alphabetical order puts it between `./audiobookshelf` and `./development`):
```nix
{ ... }:
{
imports = [
./actual
./apps
./audiobookshelf
./claude-code
./development
./home-assistant
./hyprland
./zsh
./paperless
./prowlarr
./radarr
./docker
./homepage
./kde-desktop
./readarr
./sonarr
./jellyfin
./jellyseerr
];
}
```
- [ ] **Step 2: Verify the option is now defined (without enabling it)**
Run:
```bash
nix eval --extra-experimental-features 'nix-command flakes' '.#nixosConfigurations.mibook.options.my.profiles.claude-code.enable.description'
```
Expected: `"Whether to enable Claude Code CLI."`
If this errors, the module isn't loading — re-check the import path.
---
### Task 4: Enable on MiBook and verify the build
**Files:**
- Modify: `machines/mibook/environments.nix`
- [ ] **Step 1: Enable the profile**
In `machines/mibook/environments.nix`, add `claude-code.enable = true` inside the `my.profiles` block:
```nix
my.profiles = {
kde-desktop.enable = true;
zsh.enable = true;
apps = {
desktop_apps = true;
dev_apps = true;
};
development.enable = true;
docker.enable = true;
claude-code.enable = true;
};
```
- [ ] **Step 2: Verify the package appears in finn's user packages**
Run:
```bash
nix eval --extra-experimental-features 'nix-command flakes' '.#nixosConfigurations.mibook.config.users.users.finn.packages' --apply 'builtins.map (p: p.name)' 2>&1 | grep -i claude
```
Expected: a line containing `claude-code-<version>`
- [ ] **Step 3: Dry-run build to confirm the full config evaluates**
Run:
```bash
nix build '.#nixosConfigurations.mibook.config.system.build.toplevel' --extra-experimental-features 'nix-command flakes' --dry-run 2>&1 | tail -5
```
Expected: exits 0, output lists derivations to build (or "nothing to do" if already cached). No evaluation errors.
---
### Task 5: Commit
**Files:**
- All changed files from Tasks 14
- [ ] **Step 1: Stage and commit**
```bash
git add modules/environments/claude-code/default.nix \
modules/environments/default.nix \
machines/mibook/environments.nix
git commit -m "feat(mibook): add claude-code profile module"
```
If Task 1 Step 3 was taken, also stage `pkgs/claude-code/default.nix` and `pkgs/default.nix`.
@@ -1,154 +0,0 @@
# Jellyfin Hardware Transcoding (jupiter) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Give jupiter's Jellyfin service access to the Intel iGPU's VAAPI render node so Quick Sync hardware transcoding can be enabled, instead of every transcode falling back to CPU.
**Architecture:** One NixOS module change (`modules/environments/jellyfin/default.nix`) grants the `jellyfin` systemd service supplementary access to the `video`/`render` groups and installs `libva-utils` for verification. This is declarative and build-verifiable from the Mac. Enabling Quick Sync inside Jellyfin's own dashboard, and the on-machine verification, is a manual step run by the user on jupiter after deploy — the NixOS module has no option for it and this environment's convention is that the assistant never SSHes into jupiter directly (see `docs/superpowers/specs/2026-07-26-jellyfin-hw-transcoding.md`).
**Tech Stack:** NixOS (flake-parts), nixpkgs `services.jellyfin` module, VAAPI/`intel-media-driver`, `libva-utils`.
## Global Constraints
- No SSH from the assistant into jupiter — all on-machine commands are given to the user to run and paste back.
- Follow the existing profile pattern in `modules/environments/jellyfin/default.nix` (`config = lib.mkIf cfg.enable { ... }`); don't introduce a new toggle option — hardcode the hardware-acceleration wiring on, per the approved spec.
- Verify locally via `nix eval` / `nix build` before asking the user to deploy.
---
### Task 1: Grant Jellyfin access to the iGPU and verify the build
**Files:**
- Modify: `modules/environments/jellyfin/default.nix`
**Interfaces:**
- Produces: `systemd.services.jellyfin.serviceConfig.SupplementaryGroups = [ "video" "render" ];` — verified via `nix eval` in Step 2.
- [ ] **Step 1: Add the device-access config and `libva-utils` package**
Read the current file first (`modules/environments/jellyfin/default.nix`), then edit the `config = lib.mkIf cfg.enable { ... }` block so it reads:
```nix
config = lib.mkIf cfg.enable {
services.jellyfin = {
enable = true;
openFirewall = true;
};
environment.systemPackages = [ pkgs.libva-utils ];
my.homepage.services = [
{
group = "Media";
name = "Jellyfin";
description = "Media server";
href = "http://${hostName}:${toString port}";
icon = "jellyfin.png";
}
];
systemd.services.jellyfin = {
after = [ "network-online.target" ];
serviceConfig.SupplementaryGroups = [
"video"
"render"
];
};
};
```
Note the two existing `systemd.services.jellyfin` keys (`after`) and the new `serviceConfig.SupplementaryGroups` now live in the same attrset — don't create a second `systemd.services.jellyfin = { ... }` block, it would overwrite the first.
- [ ] **Step 2: Verify the rendered config with `nix eval`**
Run (from the repo root on the Mac):
```bash
nix eval '.#nixosConfigurations.jupiter.config.systemd.services.jellyfin.serviceConfig.SupplementaryGroups' \
--extra-experimental-features 'nix-command flakes'
```
Expected output: `[ "video" "render" ]`
- [ ] **Step 3: Verify the machine still builds**
Run:
```bash
nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel' \
--extra-experimental-features 'nix-command flakes' --no-link
```
Expected: build succeeds with no errors (may take a while; watch for any evaluation error mentioning `jellyfin` or `libva-utils`).
- [ ] **Step 4: Format and commit**
```bash
nixfmt-rfc-style modules/environments/jellyfin/default.nix
git add modules/environments/jellyfin/default.nix
git commit -m "feat(jellyfin): grant iGPU access for Quick Sync hardware transcoding"
```
---
### Task 2: Deploy on jupiter and enable Quick Sync (user-executed)
**Files:** none (on-machine deploy + Jellyfin dashboard UI)
**Interfaces:**
- Consumes: the `SupplementaryGroups` change from Task 1, already merged into the flake.
These steps run **on jupiter**, by the user — paste the output back so we can confirm each one before moving to the next.
- [ ] **Step 1: Deploy**
```bash
sudo nixos-rebuild switch --flake '.#jupiter'
```
Expected: switch succeeds, no errors mentioning `jellyfin`.
- [ ] **Step 2: Confirm the service picked up the new groups**
```bash
systemctl show jellyfin -p SupplementaryGroups
systemctl status jellyfin --no-pager
```
Expected: `SupplementaryGroups=video render` (order may vary) and the service is `active (running)`.
- [ ] **Step 3: Confirm VAAPI driver loads**
```bash
vainfo
```
Expected: output starts with something like `vainfo: VA-API version: 1.x` and `Driver version: Intel iHD driver`, followed by a list of supported VAProfiles/VAEntrypoints (e.g. `VAProfileH264Main : VAEntrypointVLD`, `VAEntrypointEncSlice`).
If this instead prints a permissions or "no VA display" error, paste it back — that means the group grant isn't reaching the process and Task 1 needs a follow-up fix (e.g. the jellyfin service may be more sandboxed than expected, requiring an explicit `DeviceAllow=char-drm rw` in `serviceConfig` as well).
- [ ] **Step 4: Enable Quick Sync in the Jellyfin dashboard**
In the Jellyfin web UI:
1. **Dashboard → Playback**.
2. Hardware acceleration: **Intel QuickSync (QSV)**.
3. VA-API device: `/dev/dri/renderD128`.
4. Enable hardware decoding for the codecs your library uses (H264 at minimum).
5. If the library has HDR content, enable tone-mapping.
6. Save.
- [ ] **Step 5: Functional check**
Play a file that requires transcoding (or force a lower quality in the client's playback settings to trigger one), then:
```bash
journalctl -u jellyfin -n 50 --no-pager
```
Look for a line referencing `qsv` or `vaapi` in the transcode command. Separately, watch CPU usage (`htop`) during playback — it should stay low on the core doing the transcode, rather than pegging at 100%, since the iGPU is now doing the encode/decode work.
## Self-Review Notes
- Spec coverage: NixOS change (Task 1) ✓, manual dashboard step (Task 2 Step 4) ✓, verification via `vainfo`/build (Task 1 Step 2-3, Task 2 Step 3) ✓, functional check (Task 2 Step 5) ✓. Toggle option explicitly excluded per approved spec — not present, correctly.
- No placeholders — every step has literal commands/code.
- `SupplementaryGroups` key/value matches exactly between Task 1 (produced) and Task 2 (consumed/checked).
@@ -1,241 +0,0 @@
# ZBT-2 as a Thread Border Router for Home Assistant on `jupiter`
**Date:** 2026-05-10
**Branch:** `feature/ha-zbt-2-thread`
**Status:** Design — pending implementation plan
## Context
Home Assistant on `jupiter` already runs natively (`services.home-assistant`) with the Matter integration and `services.matter-server` enabled, but has no Zigbee or Thread radio. The user has acquired a **Home Assistant Connect ZBT-2** (Nabu Casa's Silicon Labs EFR32MG24based USB Zigbee/Thread radio).
The user wants the dongle running as an **OpenThread Border Router (OTBR)** — Thread only, not Zigbee — so Matter-over-Thread devices can be onboarded through the existing HA Matter integration.
A previous iteration of this work shipped `zha` enablement on the same branch (commit `e8d09f4`). That commit will be reverted as part of implementation; this design supersedes it.
## Goals
- Bring up `otbr-agent` on jupiter against the ZBT-2.
- Have Home Assistant auto-discover the OTBR via mDNS and use its REST API to manage the Thread network.
- Have `services.matter-server` (already enabled) consume Thread credentials from HA so Matter-over-Thread devices commission through the ZBT-2.
- One-time, manual firmware flash from Zigbee NCP to OpenThread RCP via `universal-silabs-flasher` (option B from brainstorming — no HA-driven update flow).
## Non-goals
- **Multipan / multiprotocol** (Zigbee + Thread on one radio). Out of scope; the dongle will be Thread-only.
- **Falling back to ZHA** if Thread misbehaves. Thread-only by choice; if it fails the response is to debug, not to dual-stack.
- **HA-UI-driven firmware updates.** The HAOS "Silicon Labs Multiprotocol" add-on workflow doesn't translate to native NixOS without faking a supervisor; the user explicitly accepted CLI-only flashing.
- **Thread network credential backups.** HA owns the dataset; standard HA backup hygiene (separate concern) covers it.
## Architecture
```
┌────────────────────────── jupiter (NixOS) ──────────────────────────┐
│ │
ZBT-2 USB ──►│ /dev/serial/by-id/usb-Nabu_Casa_..._ZBT-2_<serial>-... │
│ │ │
│ │ spinel+hdlc+uart, 115200 baud │
│ ▼ │
│ ┌───────────────┐ REST :8081 (loopback) ┌──────────────────┐ │
│ │ otbr-agent │ ◄─────────────────────►│ home-assistant │ │
│ │ (systemd) │ │ + matter-server │ │
│ │ wpan0 ───────┼── advertises via ─┐ │ extraComponents:│ │
│ └───────────────┘ avahi (_meshcop) │ │ matter, │ │
│ ▼ │ mobile_app, │ │
│ enp3s0 (LAN — backbone) │ otbr, thread │ │
│ └──────────────────┘ │
└────────────────────────────────────┬──────────────────────────────────┘
home LAN ◄─┘
(Matter-over-Thread devices join here)
```
### Components
1. **The radio.** ZBT-2, USB-attached, running OpenThread RCP firmware after a one-time flash.
2. **`otbr-agent`** (systemd). Managed by the unstable `services.openthread-border-router` NixOS module imported via `inputs.nixpkgs-unstable`. Owns `wpan0`, talks Spinel to the dongle, exposes the OTBR REST API on `127.0.0.1:8081`, advertises `_meshcop._udp` over `enp3s0` via avahi.
3. **Home Assistant** (already running). Gains the `otbr` and `thread` extra components. Discovers OTBR via mDNS, drives the REST API, supplies Thread operational datasets to `matter-server` during Matter commissioning.
### Data flows
- **OTBR ↔ ZBT-2:** Spinel-over-HDLC over UART. Built automatically by the module from `radio.device` as `spinel+hdlc+uart://<device>?uart-baudrate=115200`.
- **HA ↔ OTBR:** mDNS discovery (`_meshcop._udp`) → REST calls to `127.0.0.1:8081` for network management.
- **Matter commissioning:** HA scans QR → `matter-server` does BLE commissioning → asks HA for Thread dataset → HA fetches from OTBR → ships to device → device joins Thread mesh through the ZBT-2.
HA never opens the serial port directly; `matter-server` never talks to OTBR directly. HA brokers between them — that's why all four extra components are needed.
## NixOS-side changes
All changes live in **`modules/environments/home-assistant/default.nix`**. No host-level changes in `machines/jupiter/` (the existing profile activation handles that), no flake-level changes (the existing `_module.args.self = self;` wiring is sufficient).
### Edited module sketch
```nix
{ config, lib, pkgs, self, ... }:
let
cfg = config.my.profiles.home-assistant;
hostName = config.networking.hostName;
in
{
imports = [
# OTBR module isn't in 25.11 yet; use unstable's directly. Package
# comes from the existing `unstable` overlay.
"${self.inputs.nixpkgs-unstable}/nixos/modules/services/home-automation/openthread-border-router.nix"
];
options.my.profiles.home-assistant.enable = lib.mkEnableOption "Home Automation";
config = lib.mkIf cfg.enable {
services.matter-server.enable = true;
services.home-assistant = {
enable = true;
openFirewall = true;
extraComponents = [
"matter"
"mobile_app"
"otbr"
"thread"
];
};
services.home-assistant.config = {
name = "Home - Rechberg";
unit_system = "metric";
mobile_app = { };
};
services.openthread-border-router = {
enable = true;
package = pkgs.unstable.openthread-border-router;
openFirewall = true;
backboneInterfaces = [ "enp3s0" ]; # verify with `ip link` post-deploy
radio.device = "/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_<serial>-...";
# web.enable left default (off) — HA UI is the management surface
};
my.homepage.services = [
{
group = "Services";
name = "Home Assistant";
description = "Home automation";
href = "http://${hostName}:8123";
icon = "si-homeassistant";
}
];
};
}
```
### Reverts of the prior ZHA commit
Drop both lines from commit `e8d09f4`:
- `"zha"` from `extraComponents` (replaced by `"otbr"` + `"thread"`).
- `users.users.hass.extraGroups = [ "dialout" ];``otbr-agent` runs as root and owns the device directly; HA never opens the serial port itself.
Done by `git revert e8d09f4` at the start of implementation, before applying the new diff.
### Decisions captured
- **No `universal-silabs-flasher` in `environment.systemPackages`.** Flashing is a once-or-twice-a-year operation; `nix shell nixpkgs#python313Packages.universal-silabs-flasher` is sufficient when needed and avoids a perma-dep on a tool that's idle most of the time.
- **No firmware pinning in the flake.** Consistent with option B (CLI-only manual flashing). The user fetches the `.gbl` from <https://github.com/NabuCasa/silabs-firmware-builder/releases> at update time.
- **`backboneInterfaces = [ "enp3s0" ]`** as a starting value (per `machines/jupiter/hardware-configuration.nix:64`). To be verified against `ip link` after first deploy; correctable in a follow-up commit if the actual primary interface differs.
## Operator workflow
All commands the user runs themselves; nothing is SSH'd from the dev session.
### Step 0 — branch hygiene (dev Mac)
```
git switch feature/ha-zbt-2-thread # already renamed
git revert --no-edit e8d09f4 # drops ZHA + dialout commit
```
### Step 1 — apply the module changes (dev Mac)
Edit `modules/environments/home-assistant/default.nix` per the sketch above. Leave `<serial>` as a placeholder; fill after Step 3.
### Step 2 — eval-only sanity check (dev Mac)
```
nix flake check
```
or, equivalently,
```
nixos-rebuild dry-build --flake .#jupiter
```
Catches: bad import path, option typos, version skew between unstable and stable.
### Step 3 — plug ZBT-2 into jupiter (still on stock Zigbee firmware)
On jupiter:
```
ls -l /dev/serial/by-id/
```
Then on dev Mac: copy the full `usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_<serial>-...` path into `radio.device`, commit on the feature branch.
### Step 4 — flash OpenThread RCP firmware (one-time, on jupiter)
```
nix shell nixpkgs#python313Packages.universal-silabs-flasher -c \
universal-silabs-flasher \
--device /dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_<serial>-... \
flash --firmware ~/ot-rcp-zbt-2-<version>.gbl
```
Firmware download: latest ZBT-2 OpenThread RCP `.gbl` from <https://github.com/NabuCasa/silabs-firmware-builder/releases>.
OTBR isn't running yet at this point, so there's no contention on the device.
### Step 5 — rebuild (on jupiter)
```
sudo nixos-rebuild switch --flake .#jupiter
```
Brings up `otbr-agent.service`, opens TCP/8081, loads `otbr` + `thread` integrations in HA.
### Step 6 — confirm HA discovered it
- `http://jupiter:8123` → Settings → Devices & Services → "Open Thread Border Router" appears as auto-discovered within ~30 s.
- Click "Configure", form a new Thread network (or import an existing dataset).
- "Matter" integration page now shows Thread credentials available.
### Step 7 — Matter-over-Thread smoke test
Pair one Matter-over-Thread device end-to-end via the HA Companion app. Pairing should complete in 3090 s. If it does, merge `feature/ha-zbt-2-thread` into `master`.
### Future updates
Identical to Step 4: stop `otbr-agent.service`, run the flasher with a new `.gbl`, start the service.
## Failure modes
| Symptom | Likely cause | Mitigation |
|---|---|---|
| `otbr-agent.service` fails: "Failed to open device" | Dongle unplugged or `radio.device` path stale (e.g. after replacement) | Module sets `Restart = "on-failure"`; check `systemctl status otbr-agent`, re-check `/dev/serial/by-id/`, update path. |
| OTBR up but HA never discovers it | mDNS not propagating on `enp3s0` (most often: `backboneInterfaces` wrong) | `avahi-browse -r _meshcop._udp` should show one entry. If not: `ip link`, fix `backboneInterfaces`, rebuild. |
| HA shows OTBR but Matter pairing times out | Thread mesh prefix not routed to LAN, or matter-server can't reach the device's IPv6 ULA | `nft list ruleset` should show OTBR's forwarding rules; `ip -6 route` should include the Thread mesh prefix. |
| Dongle stuck after a half-completed flash | Flasher interrupted mid-write | Re-run the flash; bootloader stays addressable even if RCP firmware is corrupt. The tool detects bootloader-mode automatically. |
| `nixos-rebuild` fails: "option `services.openthread-border-router` does not exist" | Unstable module import path wrong / not in scope | Caught by Step 2 (eval-only). Fix before deploy. |
## Verification
### Eval-only (dev Mac, before deploy)
```
nix flake check
nix eval --json .#nixosConfigurations.jupiter.config.services.openthread-border-router.radio.url
nix eval --json .#nixosConfigurations.jupiter.config.services.home-assistant.extraComponents
```
Expected: flake check passes; `radio.url` is a `spinel+hdlc+uart://...` string built from the by-id path; `extraComponents` includes `"otbr"` and `"thread"`.
### Service-level (jupiter, after rebuild)
```
systemctl status otbr-agent.service
journalctl -u otbr-agent.service -n 50 --no-pager
ip link show wpan0
avahi-browse -r -t _meshcop._udp
curl -s http://127.0.0.1:8081/node/state
```
Expected: service active; `wpan0` exists (DOWN until HA forms a network — correct); one `_meshcop._udp` entry; REST returns a JSON state string.
### Functional (HA UI)
- "Open Thread Border Router" appears under auto-discovered integrations.
- Forming a Thread network from the integration UI succeeds.
- Pairing one Matter-over-Thread device end-to-end succeeds.
## Open questions / risks
- **Unstable module ABI.** The `services.openthread-border-router` module is in `nixos-unstable` and may change shape before landing in 26.05. If options rename, the eval-only step catches it before deploy. Acceptable risk; we can pin the unstable input revision if churn becomes annoying.
- **Backbone interface name.** `enp3s0` is a best guess from `hardware-configuration.nix:64`'s commented-out line. Definitive answer comes from `ip link` on the actual host. Trivial to correct if wrong.
- **First-flash chicken-and-egg.** Deferred to `nix shell` rather than baked into the system, because the dongle must be flashed *before* `otbr-agent` claims it. This is documented in Step 4.
@@ -1,111 +0,0 @@
# Smartphone push notifications from Home Assistant
## Goal
Send push notifications from jupiter's Home Assistant to the user's
smartphones over the home Tailscale (Headscale) tailnet `solar.internal`.
## Architecture
- HA's `mobile_app` integration is enabled (already in `extraComponents`
and present as `mobile_app = {}` in config).
- Each smartphone runs the **HA Companion app**, signs in to HA, and
auto-registers as a `notify.mobile_app_<device_slug>` service.
- Reach: phones connect to HA via Tailscale, so HA's `external_url` is
set to the Headscale FQDN `http://jupiter.solar.internal:8123`. The
`internal_url` is `http://jupiter:8123` for LAN-attached devices.
- No public exposure, no reverse proxy, no TLS termination in scope.
## Phase A — done in this branch
NixOS module change in `modules/environments/home-assistant/default.nix`:
```nix
homeassistant = {
name = "Home - Rechberg";
unit_system = "metric";
internal_url = "http://${hostName}:8123";
external_url = "http://jupiter.solar.internal:8123";
};
```
After deploy, perform the user-side registration:
1. Install the Companion app:
- iOS: search "Home Assistant" in the App Store.
- Android: search "Home Assistant" in Google Play.
2. Ensure Tailscale is running and connected on the phone.
3. Open the Companion app. When asked to connect, enter
`http://jupiter.solar.internal:8123` and sign in with the HA account.
4. Approve the registration prompt in HA.
5. In HA, go to **Settings → Devices & Services → Mobile App** and
confirm the phone appears as a device.
6. In HA, go to **Developer Tools → Services**, type `notify.mobile_app_`
and note the exact service slug for each phone (e.g.
`notify.mobile_app_iphone_finn`). These slugs are needed for Phase B.
### Verifying Phase A end-to-end
Build-time:
```
nix eval '.#nixosConfigurations.jupiter.config.services.home-assistant.config.homeassistant' \
--extra-experimental-features 'nix-command flakes'
```
Expect the rendered attrset to contain both `external_url` and
`internal_url`.
Deploy on jupiter:
```
sudo nixos-rebuild switch --flake '.#jupiter'
systemctl status home-assistant
journalctl -u home-assistant -n 50 --no-pager
```
Functional check after Companion sign-in:
- HA UI → **Developer Tools → Services** → choose
`notify.mobile_app_<your_device>` → service data
`{ "message": "Phase A test" }`**Call Service** → push arrives on
the phone.
## Phase B — follow-up commit (after registration)
Once device slugs are known, a separate commit adds:
1. A `notify` group fanning out to every registered phone:
```nix
notify = [
{
name = "all_phones";
platform = "group";
services = [
{ service = "mobile_app_<slug_1>"; }
{ service = "mobile_app_<slug_2>"; }
];
}
];
```
2. A smoke-test mechanism. Approach to be decided in Phase B based on
whether future Nix-managed automations are expected:
- Pragmatic: document a one-time UI call to `notify.all_phones` from
Developer Tools (no automation in YAML).
- Compromise: switch `automation = "!include automations.yaml"` to
`!include_dir_merge_list automations/` so a Nix-managed
`00-smoke-test.yaml` can coexist with UI-editable automations.
### Verifying Phase B end-to-end
- Restart HA, watch `journalctl -u home-assistant` for YAML schema errors.
- Call `notify.all_phones` from Developer Tools — every registered phone
receives the push.
## Open items
- After Companion registration, collect the `mobile_app_<slug>` service
names from HA and update this spec + open Phase B PR.
@@ -1,61 +0,0 @@
# MiBook Claude Code Execution Machine
**Date:** 2026-05-25
**Branch:** feature/ha-zbt-2-thread
**Status:** Approved
## Goal
Configure the MiBook as an interactive Claude Code execution machine by installing the Claude Code CLI and its companion tooling via a new, independently-toggleable NixOS profile module.
## Scope
Phase 1 (this spec): interactive CLI session — user logs in or SSHs in and runs `claude` directly.
Phase 2 (future): headless systemd user service exposing Claude Code over an HTTP API or Unix socket, turning the MiBook into a remotely-triggerable execution node without an interactive session.
## Module Structure
```
modules/environments/claude-code/default.nix ← new module
modules/environments/default.nix ← add import
machines/mibook/environments.nix ← my.profiles.claude-code.enable = true
```
Follows the exact same pattern as every other environment module in this repo.
## Option Declaration
```
my.profiles.claude-code.enable (mkEnableOption)
```
No sub-options for Phase 1. Phase 2 would add `my.profiles.claude-code.service.enable`.
## Packages (users.users.finn.packages)
| Package | Source | Purpose |
|---|---|---|
| `claude-code` | `pkgs.unstable` | Claude Code CLI |
| `ripgrep` | `pkgs` | Primary file search tool used by Claude Code |
| `fd` | `pkgs` | Fast `find` replacement used by Claude Code |
| `gh` | `pkgs` | GitHub CLI for PR/issue operations |
| `jq` | `pkgs` | JSON processing in shell pipelines |
`git`, `nodejs`, and `docker` are already provided by existing modules and are not duplicated.
**Contingency:** If `pkgs.unstable.claude-code` does not exist at eval time, the fallback is a custom derivation in `pkgs/claude-code/default.nix` added to the local overlay — the same mechanism used for other absent packages.
## No Secrets Management
The Anthropic API key is handled manually by the user (browser login / `claude` interactive setup). No sops-nix or shell-profile injection needed.
## Future: Headless Service
A commented block in the module documents the upgrade path: a systemd user service that accepts work via an HTTP API or Unix socket, triggerable over SSH or a local network endpoint, without requiring an interactive session.
## Files Changed
1. `modules/environments/claude-code/default.nix` — new profile module
2. `modules/environments/default.nix` — add `./claude-code` import
3. `machines/mibook/environments.nix` — set `my.profiles.claude-code.enable = true`
@@ -1,145 +0,0 @@
# Intel Quick Sync hardware transcoding for Jellyfin on jupiter
## Problem
Jellyfin streams stutter on jupiter whenever a client needs a transcode
(unsupported codec/container, bitrate cap, or a client that can't
direct-play). Transcoding currently runs entirely on CPU.
jupiter's Intel iGPU is already usable at the OS level:
- `hardware.graphics.enable = true` with `intel-media-driver` (the `iHD`
VAAPI driver) is configured in
`machines/jupiter/hardware-configuration.nix:20-27`.
- The commented-out `i915.force_probe = "9a49"` kernel param there
corresponds to a Quick-Sync-capable Intel UHD iGPU, confirming the
hardware supports it.
But `modules/environments/jellyfin/default.nix` never grants the
`jellyfin` systemd service access to `/dev/dri`, so Jellyfin has no path
to the GPU and silently falls back to software transcoding.
## Goal
Give the Jellyfin service access to the iGPU's VAAPI render node, so
Quick Sync can be enabled in Jellyfin's own dashboard and transcodes are
offloaded from the CPU.
## Non-goals
- Remote/external access or reverse-proxy tuning.
- General CPU/RAM headroom review of jupiter.
- A toggle option (`my.profiles.jellyfin.hardwareAcceleration.enable`) —
jupiter only has the one iGPU, so this is hardcoded on rather than
made configurable.
## Design
### NixOS change (declarative)
In `modules/environments/jellyfin/default.nix`, inside the existing
`config = lib.mkIf cfg.enable { ... }` block, grant the systemd service
supplementary access to the `video` and `render` groups (the groups that
own `/dev/dri/card*` and `/dev/dri/renderD*`):
```nix
systemd.services.jellyfin.serviceConfig.SupplementaryGroups = [
"video"
"render"
];
```
This is additive to the existing `systemd.services.jellyfin.after = [
"network-online.target" ];` block already in the file — both apply to
the same service.
Also add `libva-utils` to `environment.systemPackages` (or scoped to
this module) so `vainfo` is available on jupiter to verify the driver
loads correctly.
### Manual step (not declarative)
Jellyfin stores its transcoding/hardware-acceleration choice in its own
internal `encoding.xml`, which the NixOS module does not expose as an
option. After deploying the Nix change, one-time manual configuration in
the Jellyfin dashboard is required:
1. **Dashboard → Playback**.
2. Hardware acceleration: **Intel QuickSync (QSV)**.
3. VA-API device: `/dev/dri/renderD128`.
4. Enable hardware decoding for the codecs your library actually uses
(H264 at minimum; HEVC/VP9 depending on iGPU generation).
5. If any HDR content exists in the library, enable tone-mapping — this
is one of the more CPU-expensive operations Quick Sync can offload.
## Verification
Build-time (from the Mac, no SSH needed):
```
nix eval '.#nixosConfigurations.jupiter.config.systemd.services.jellyfin.serviceConfig.SupplementaryGroups' \
--extra-experimental-features 'nix-command flakes'
```
Expect `[ "video" "render" ]`.
On jupiter after `sudo nixos-rebuild switch --flake '.#jupiter'`:
```
systemctl status jellyfin
journalctl -u jellyfin -n 50 --no-pager
vainfo
```
`vainfo` should list the `iHD` driver and print supported VAEntrypoints
(VLD decode / encode profiles for H264/HEVC).
Functional check: play a file on a client that forces transcoding (or
force it manually via Jellyfin's playback quality setting), then in
Jellyfin's dashboard **Activity/Now Playing** panel confirm the
transcode reason and check that CPU usage on jupiter (`htop`) stays low
during playback rather than pegging a core — Quick Sync offload should
show up as low CPU, some GPU (`intel_gpu_top`) activity instead.
## Open items
- Exact supported codec list depends on the iGPU generation (device ID
`9a49`) — confirm via `vainfo` output once run, and enable only the
hardware decode paths it actually reports.
## Post-deploy fix: two additional runtime packages required
After the initial deploy (Task 1's `SupplementaryGroups` grant) and
enabling QSV in the dashboard, HEVC HDR playback hung indefinitely
(Direct Play worked for some titles; titles that needed a real
transcode+tonemap never produced output). Root-caused via
`journalctl -u jellyfin` and the per-session ffmpeg transcode log
(`find / -xdev -iname '*ffmpeg-transcode*'`) — two separate runtimes
were missing beyond `intel-media-driver` (which only provides VAAPI):
1. **QSV session creation failed:** `Error creating a MFX session: -9`
/ `Error initializing an MFX session: -3` on
`-init_hw_device qsv=qs@va`. VAAPI and QSV are separate runtimes on
Linux — QSV needs the oneVPL/MFX GPU implementation. Fix: added
`pkgs.vpl-gpu-rt` ("oneAPI Video Processing Library Intel GPU
implementation"; note `onevpl-intel-gpu` is the old, renamed
attribute) to `hardware.graphics.extraPackages` in
`machines/jupiter/hardware-configuration.nix`.
2. **OpenCL device creation failed:** `Failed to get number of OpenCL
platforms: -1001` (`CL_PLATFORM_NOT_FOUND_KHR`) on
`-init_hw_device opencl=ocl@va`. The `tonemap_opencl` filter jellyfin
uses for HDR→SDR tone-mapping needs a working OpenCL ICD, which
nothing installed so far provides. Fix: added
`pkgs.intel-compute-runtime` ("Intel Graphics Compute Runtime oneAPI
Level Zero and OpenCL, supporting 12th Gen and newer" — matches
jupiter's Tiger Lake/Xe iGPU) to the same `extraPackages` list.
Confirmed working end-to-end: HEVC HDR transcode with QSV encode +
OpenCL tone-map runs at `speed=2.68x` realtime on jupiter's iGPU, and
plays smoothly on Apple TV (JellyTV app).
Both packages live in `machines/jupiter/hardware-configuration.nix`
(`hardware.graphics.extraPackages`), alongside `intel-media-driver`,
rather than in the jellyfin module itself — they're iGPU runtime
capabilities, not something specific to the jellyfin service.
@@ -1,105 +0,0 @@
# mibook: choose terminal-only vs desktop at boot
## Goal
Let mibook offer a choice, each time it boots, between the normal KDE desktop
and a terminal-only ("server") mode with no graphical session — decided at
boot, without rebuilding the system.
## Background: why the first attempt failed
The first implementation booted mibook to a text console by default
(`systemd.defaultUnit = "multi-user.target"`, display manager not started) and
provided a `desktop` command to start KDE on demand. On real hardware this
locked the machine out:
- **No SSH.** mibook is WiFi-only and its WiFi credentials are stored per-user
in KWallet ("agent-owned"). NetworkManager only receives the password once a
desktop session is running, so a headless boot never joins the network and
the machine has no IP — nothing to SSH into.
- **No usable console.** The boot appeared to "hang" with no login prompt:
`NetworkManager-wait-online` stalled waiting for a network that never came
up, and the `getty` login prompt on tty1 was buried under later service
messages.
Conclusion: a headless WiFi laptop cannot be reached remotely, and the plain
console was hard to use. The design must (a) keep the desktop as the reliable
default, (b) make the terminal path a deliberate, self-sufficient choice, and
(c) not depend on the network being up.
## Behavior
- The **default GRUB entry** boots straight into KDE — unchanged from the
known-working baseline.
- A **separate GRUB entry, `mibook (terminal)`** (a NixOS *specialisation*),
boots to a text console with **autologin** for `finn`. From there the user
can work in the shell or run `desktop` to bring KDE up (via SDDM).
- The choice is made in the GRUB menu at boot — matching the original request
to "decide each time I boot."
- Booting never stalls on the network.
### Known limitation (documented, not fixed in config)
In terminal mode WiFi will not connect on its own, because the password is
stored per-user in KWallet. To reach mibook over SSH from terminal mode, the
user must first save the WiFi as a **system** connection in KDE:
network settings → the WiFi network → "All users may connect to this network".
Until then, terminal mode is local-console-only. This is a one-time manual
step outside the scope of the Nix config.
## Implementation
### `machines/mibook/configuration.nix`
- Add a NixOS specialisation `specialisation.terminal.configuration`:
- `system.nixos.tags = [ "terminal" ];` — labels the generated boot entry.
- `systemd.defaultUnit = lib.mkForce "multi-user.target";` — boots to the
text console. `graphical.target` is what pulls in the display manager (via
its embedded `Wants=display-manager.service`), so defaulting to
`multi-user.target` leaves SDDM installed but not started at boot.
- `services.getty.autologinUser = "finn";` — guarantees a usable shell on
the console instead of a login prompt that can scroll off screen.
- A `desktop` command via
`pkgs.writeShellScriptBin "desktop" "exec sudo systemctl start display-manager.service"`
in `environment.systemPackages`, to start KDE on demand.
- Add `systemd.services.NetworkManager-wait-online.enable = false;` (applies to
both the default and terminal boots) so boot never stalls waiting for the
network.
### Reverted from the first attempt
- `modules/environments/kde-desktop/default.nix` — remove the `startOnBoot`
option and its `mkMerge`/`mkIf` machinery; back to the original profile that
simply enables SDDM + Plasma 6.
- `machines/mibook/environments.nix` — remove `kde-desktop.startOnBoot = false;`
(back to just `kde-desktop.enable = true;`).
## Why a specialisation
A specialisation generates a second boot-menu entry automatically from a
modified copy of the configuration. It is the idiomatic NixOS mechanism for a
boot-time choice and avoids fragile hand-written GRUB `extraEntries` that would
need to track kernel/initrd paths across generations. The default entry remains
byte-for-byte the working desktop configuration.
## Testing / verification
- `nix build '.#nixosConfigurations.mibook.config.system.build.toplevel'`
builds both `nixos-system-mibook` and `nixos-system-mibook-terminal`.
- Verified on the built closures:
- Parent `default.target``graphical.target`; no console autologin
(identical to the pre-change baseline).
- Specialisation `default.target``multi-user.target`; tty1 getty wrapper
contains `--autologin finn`; `desktop` present in the system profile.
- `NetworkManager-wait-online` disabled in both.
- Post-`switch` manual check on mibook: default GRUB entry boots to KDE; the
`terminal` entry boots to an autologged-in console; running `desktop` there
starts SDDM and a working Plasma session.
## Trade-offs
- Autologin on the terminal console means physical access grants a shell
without a password. Acceptable for a personal laptop the user controls; the
desktop (default) boot is unaffected.
- `desktop` relies on `sudo`; the user has sudo access, so no extra config is
required.
Generated
-273
View File
@@ -1,273 +0,0 @@
{
"nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1782949081,
"narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": [
"nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1782949081,
"narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"git-hooks-nix": {
"inputs": {
"flake-compat": [
"nix"
],
"nixpkgs": [
"nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1783008725,
"narHash": "sha256-jGiy6+sxjNWXSjp25uoJuNfyH9zBK1PEDY0lVoL4ibQ=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "bca82caa46d5ec0f5d422c61fb1e30bc51313cbe",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"nix": {
"inputs": {
"flake-compat": "flake-compat",
"flake-parts": "flake-parts_2",
"git-hooks-nix": "git-hooks-nix",
"nixpkgs": "nixpkgs",
"nixpkgs-23-11": "nixpkgs-23-11",
"nixpkgs-regression": "nixpkgs-regression"
},
"locked": {
"lastModified": 1784762557,
"narHash": "sha256-R/r6jRnANV50c8F5Fz5+1Q1moab0IGWRk+cg5ME2nMY=",
"owner": "NixOS",
"repo": "nix",
"rev": "d10c84cd0cc0efdcb29cf2611caf5fbcd10fa071",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nix",
"type": "github"
}
},
"nixlib": {
"locked": {
"lastModified": 1736643958,
"narHash": "sha256-tmpqTSWVRJVhpvfSN9KXBvKEXplrwKnSZNAoNPf/S/s=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "1418bc28a52126761c02dd3d89b2d8ca0f521181",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixos-generators": {
"inputs": {
"nixlib": "nixlib",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1769813415,
"narHash": "sha256-nnVmNNKBi1YiBNPhKclNYDORoHkuKipoz7EtVnXO50A=",
"owner": "nix-community",
"repo": "nixos-generators",
"rev": "8946737ff703382fda7623b9fab071d037e897d5",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixos-generators",
"type": "github"
}
},
"nixos-hardware": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1784723954,
"narHash": "sha256-1CfD8ZUjCkTgjsneLZ/lxCHhgDfqxxE7/GX0MmsgiqA=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "a017f5b72210026af5b3ac5949f08d94380a6fbd",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixos-hardware",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1783148766,
"narHash": "sha256-H9+N+GFtsbVC8ZniHliChM7ndizxtqVZs6bnGOLM3WQ=",
"rev": "a50de1b7d8a586adc18d2395c19de7d6058e6030",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/26.05/nixos-26.05.4193.a50de1b7d8a5/nixexprs.tar.xz"
},
"original": {
"type": "tarball",
"url": "https://channels.nixos.org/nixos-26.05/nixexprs.tar.xz"
}
},
"nixpkgs-23-11": {
"locked": {
"lastModified": 1717159533,
"narHash": "sha256-oamiKNfr2MS6yH64rUn99mIZjc45nGJlj9eGth/3Xuw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1782614948,
"narHash": "sha256-ePjCwr1sNm9NYUqywL7QfK3JnlS015msC+eBu2zKlp8=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "db3f255737b94216eb71cce308e2912cf6bc2d7c",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixpkgs-regression": {
"locked": {
"lastModified": 1643052045,
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
}
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1784796856,
"narHash": "sha256-vwxWgF+Gj276WznzGb1LxGsK/39HaQwgQXiU3EkC844=",
"rev": "e2587caef70cea85dd97d7daab492899902dbf5d",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.11pre1040357.e2587caef70c/nixexprs.tar.xz"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1767892417,
"narHash": "sha256-8bW3q88CEg2u4hSP66Vf4lpbLonHz7hqDNBMcCY7E9U=",
"rev": "3497aa5c9457a9d88d71fa93a4a8368816fbeeba",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre924538.3497aa5c9457/nixexprs.tar.xz"
},
"original": {
"type": "tarball",
"url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1784707089,
"narHash": "sha256-DUedXhD2Rg8q4Xyd07Sb90eZGy4gg6W+Vl/WbLNwAZo=",
"rev": "b3fe9581c9061c749abef42b6d4ee7b7c05c33fa",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/26.05/nixos-26.05.5845.b3fe9581c906/nixexprs.tar.xz"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-26.05",
"type": "indirect"
}
},
"root": {
"inputs": {
"flake-parts": "flake-parts",
"nix": "nix",
"nixos-generators": "nixos-generators",
"nixos-hardware": "nixos-hardware",
"nixpkgs": "nixpkgs_3",
"nixpkgs-unstable": "nixpkgs-unstable"
}
}
},
"root": "root",
"version": 7
}
-43
View File
@@ -1,43 +0,0 @@
{
description = "NixOS configuration";
inputs = {
nix.url = "github:NixOS/nix";
nixpkgs.url = "nixpkgs/nixos-26.05";
nixpkgs-unstable.url = "nixpkgs/nixos-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
nixos-hardware.url = "github:NixOS/nixos-hardware";
nixos-generators = {
url = "github:nix-community/nixos-generators";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
inputs@{ self, flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [
./machines/configuration.nix
];
perSystem =
{
self',
inputs',
config,
pkgs,
system,
...
}:
{
# make pkgs available to all `perSystem` functions
_module.args.pkgs = import inputs.nixpkgs {
inherit system;
};
};
systems = [ "x86_64-linux" ];
};
}
-67
View File
@@ -1,67 +0,0 @@
{ self, ... }:
let
inherit (self.inputs)
nixpkgs
nixpkgs-unstable
nixos-hardware
#hyprland
;
nixosSystem = nixpkgs.lib.makeOverridable nixpkgs.lib.nixosSystem;
overlay-unstable = final: prev: {
unstable = import nixpkgs-unstable {
inherit (final) system;
config.allowUnfree = true;
};
};
customModules = import ./core/default.nix;
baseModules = [
# make flake inputs accessible in NixOS
{
_module.args.self = self;
_module.args.inputs = self.inputs;
}
{
imports = [
(
{ pkgs, ... }:
{
nixpkgs.overlays = [
overlay-unstable
(import ../pkgs)
];
nix.nixPath = [
"nixpkgs=${pkgs.path}"
];
documentation.info.enable = false;
}
)
#hyprland.nixosModules.default
];
}
../modules
# ../profiles
];
defaultModules = baseModules ++ customModules;
in
{
flake.nixosConfigurations = {
# use your hardware- model from this list: https://github.com/NixOS/nixos-hardware/blob/master/flake.nix
jupiter = nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit self; };
modules = defaultModules ++ [
# nixos-hardware.nixosModules.bmax-b7-power
./jupiter/configuration.nix
];
};
mibook = nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit self; };
modules = defaultModules ++ [
# nixos-hardware.nixosModules.mibook
./mibook/configuration.nix
];
};
};
}
-158
View File
@@ -1,158 +0,0 @@
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page
# and in the NixOS manual (accessible by running nixos-help).
{ config, pkgs, ... }:
{
imports = [
# Include the results of the hardware scan.
./hardware-configuration.nix
];
# Bootloader.
boot.loader.grub.enable = true;
boot.loader.grub.device = "/dev/sda";
boot.loader.grub.useOSProber = true;
networking.hostName = "nixos"; # Define your hostname.
# networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
# Configure network proxy if necessary
# networking.proxy.default = "http://user:password@proxy:port/";
# networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";
# Enable networking
networking.networkmanager.enable = true;
# Set your time zone.
time.timeZone = "Europe/Berlin";
# Select internationalisation properties.
i18n.defaultLocale = "en_US.UTF-8";
i18n.extraLocaleSettings = {
LC_ADDRESS = "de_DE.UTF-8";
LC_IDENTIFICATION = "de_DE.UTF-8";
LC_MEASUREMENT = "de_DE.UTF-8";
LC_MONETARY = "de_DE.UTF-8";
LC_NAME = "de_DE.UTF-8";
LC_NUMERIC = "de_DE.UTF-8";
LC_PAPER = "de_DE.UTF-8";
LC_TELEPHONE = "de_DE.UTF-8";
LC_TIME = "de_DE.UTF-8";
};
# Enable the X11 windowing system.
services.xserver.enable = true;
# Enable the GNOME Desktop Environment.
services.xserver.displayManager.gdm.enable = true;
services.xserver.desktopManager.gnome.enable = true;
# Configure keymap in X11
services.xserver = {
layout = "de";
xkbVariant = "";
};
# Configure console keymap
console.keyMap = "de";
# Enable CUPS to print documents.
services.printing.enable = true;
# Enable sound with pipewire.
hardware.pulseaudio.enable = false;
security.rtkit.enable = true;
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
# If you want to use JACK applications, uncomment this
#jack.enable = true;
# use the example session manager (no others are packaged yet so this is enabled by default,
# no need to redefine it in your config for now)
#media-session.enable = true;
};
# Enable touchpad support (enabled default in most desktopManager).
# services.xserver.libinput.enable = true;
# Define a user account. Don't forget to set a password with passwd.
users.users.finn = {
isNormalUser = true;
description = "Finn Markwitz";
extraGroups = [
"networkmanager"
"wheel"
];
packages = with pkgs; [
firefox
jetbrains.webstorm
jetbrains.goland
jetbrains.pycharm-professional
jetbrains.idea-ultimate
thunderbird
google-chrome
discord
spotify
keepassxc
nextcloud-client
zsh
oh-my-zsh
neovim
];
};
# Enable automatic login for the user.
services.xserver.displayManager.autoLogin.enable = true;
services.xserver.displayManager.autoLogin.user = "finn";
# Workaround for GNOME autologin: https://github.com/NixOS/nixpkgs/issues/103746#issuecomment-945091229
systemd.services."getty@tty1".enable = false;
systemd.services."autovt@tty1".enable = false;
# Allow unfree packages
nixpkgs.config.allowUnfree = true;
# List packages installed in system profile. To search, run:
# $ nix search wget
environment.systemPackages = with pkgs; [
vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default.
wget
git
python312
nodejs_18
];
# Some programs need SUID wrappers, can be configured further or are
# started in user sessions.
# programs.mtr.enable = true;
# programs.gnupg.agent = {
# enable = true;
# enableSSHSupport = true;
# };
# List services that you want to enable:
# Enable the OpenSSH daemon.
# services.openssh.enable = true;
# Open ports in the firewall.
# networking.firewall.allowedTCPPorts = [ ... ];
# networking.firewall.allowedUDPPorts = [ ... ];
# Or disable the firewall altogether.
# networking.firewall.enable = false;
# This value determines the NixOS release from which the default
# settings for stateful data, like file locations and database versions
# on your system were taken. Its perfectly fine and recommended to leave
# this value at the release version of the first install of this system.
# Before changing this value read the documentation for this option
# (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
system.stateVersion = "23.05"; # Did you read the comment?
}
-71
View File
@@ -1,71 +0,0 @@
{
config,
pkgs,
lib,
...
}:
{
# Packages
environment.systemPackages = with pkgs; [
bandwhich
bind # dig
borgbackup
cryptsetup
file
fwupd
fzf
gettext
git
gptfdisk
htop
jq
killall
lsof
mosh
multipath-tools # kpartx
mtr
nmap
nmon
ouch # de-/compress
pciutils
progress
pv
reptyr
rsync
screen
stress-ng
usbutils
tmux
vim
wget
whois
zip
unzip
networkmanager
lm_sensors
systemctl-tui
nixfmt-rfc-style
];
time.timeZone = "Europe/Berlin";
services.timesyncd.enable = true;
# Enable networking
networking.networkmanager.enable = true;
# Select internationalisation properties.
i18n.defaultLocale = "en_US.UTF-8";
i18n.extraLocaleSettings = {
LC_ADDRESS = "de_DE.UTF-8";
LC_IDENTIFICATION = "de_DE.UTF-8";
LC_MEASUREMENT = "de_DE.UTF-8";
LC_MONETARY = "de_DE.UTF-8";
LC_NAME = "de_DE.UTF-8";
LC_NUMERIC = "de_DE.UTF-8";
LC_PAPER = "de_DE.UTF-8";
LC_TELEPHONE = "de_DE.UTF-8";
LC_TIME = "de_DE.UTF-8";
};
}
-7
View File
@@ -1,7 +0,0 @@
[
./core.nix
./modules.nix
./network.nix
./nix.nix
./users.nix
]
-8
View File
@@ -1,8 +0,0 @@
# Generate via nix-generate
{
config,
lib,
pkgs,
...
}:
{ }
-20
View File
@@ -1,20 +0,0 @@
{
config,
lib,
pkgs,
...
}:
{
networking.networkmanager = {
enable = true;
unmanaged = [
"interface-name:br-*" # Ignore docker compose network bridges
"interface-name:docker?" # Ignore docker default bridge
"interface-name:veth*" # Ignore docker compose network devices
"interface-name:virbr?" # Ignore libvirt default bridge
];
};
}
-33
View File
@@ -1,33 +0,0 @@
{
config,
lib,
pkgs,
inputs,
...
}:
{
nix = {
daemonCPUSchedPolicy = "idle";
daemonIOSchedClass = "idle";
settings = {
auto-optimise-store = true;
};
gc = {
automatic = true;
options = "--delete-older-than 30d";
};
extraOptions = ''
experimental-features = nix-command flakes
warn-dirty = false
'';
registry = {
nixpkgs.flake = inputs.nixpkgs;
unstable.flake = inputs.nixpkgs-unstable;
};
};
nixpkgs.config.allowUnfree = true;
}
-32
View File
@@ -1,32 +0,0 @@
{
config,
pkgs,
lib,
...
}:
{
users.users.finn = {
isNormalUser = true;
home = "/home/finn";
group = "finn";
extraGroups = [
"adbusers" # adb control
"audio" # sound control
"dialout" # serial-console
"docker" # usage of `docker` socket
"input" # mouse control
"libvirtd" # kvm control
"networkmanager" # wireless configuration
"podman" # usage of `podman` socket
"video" # screen control
"wheel" # `sudo` for the user.
];
# openssh.authorizedKeys.keys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOFx6OLwL9MbkD3mnMsv+xrzZHN/rwCTgVs758SCLG0h finn@thinkman" ];
};
users.groups.finn = {
gid = 1000;
};
}
-66
View File
@@ -1,66 +0,0 @@
{
config,
pkgs,
lib,
...
}:
{
imports = [
./disks.nix
./hardware-configuration.nix
./environments.nix
./network.nix
];
networking.hostName = "jupiter";
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
boot.kernelPackages = pkgs.linuxPackages;
# Shitfuck SOnar Dotnet dependency
nixpkgs.config = {
permittedInsecurePackages = [
"aspnetcore-runtime-6.0.36"
"aspnetcore-runtime-wrapped-6.0.36"
"dotnet-sdk-6.0.428"
"dotnet-sdk-wrapped-6.0.428"
];
};
services.openssh.enable = true;
# Configure keymap in X11
services.xserver.xkb = {
layout = "de";
variant = "";
};
# Configure console keymap
console.keyMap = "de";
# Enable CUPS to print documents.
services.printing.enable = true;
services.fwupd.enable = true;
security.rtkit.enable = true;
# Try fix wifi disconnect
networking.networkmanager.wifi.powersave = false;
# Disable hibernate completely
powerManagement.enable = true;
systemd.targets."hibernate".enable = false;
systemd.targets."hybrid-sleep".enable = false;
systemd.targets."suspend-then-hibernate".enable = false;
# Optional: kernel parameter to fully disable hibernation
boot.kernelParams = [ "nohibernate" ];
system = {
stateVersion = "23.05";
autoUpgrade.enable = true;
};
}
-30
View File
@@ -1,30 +0,0 @@
{
config,
lib,
pkgs,
...
}:
#TODO: CHECK THIS OUT: https://github.com/nix-community/disko its kinda better but felix does not support it
{
fileSystems."/" =
{ device = "/dev/disk/by-uuid/8e3ac6ce-6865-49d8-b6d0-d7c008de391e";
fsType = "ext4";
};
fileSystems."/boot" =
{ device = "/dev/disk/by-uuid/F193-2A35";
fsType = "vfat";
};
fileSystems."/data/nfs" = {
device = "voyager:/";
fsType = "nfs";
options = [
"x-systemd.automount"
"noauto"
];
};
}
-75
View File
@@ -1,75 +0,0 @@
# enabled profiles
{ config, lib, ... }:
let
secrets = config.sops.secrets;
in
{
my.profiles = {
hyprland.enable = false;
zsh.enable = true;
apps = {
desktop_apps = false;
dev_apps = false;
gnome_apps = false;
};
actual.enable = false;
audiobookshelf.enable = true;
kde-desktop.enable = false;
radarr.enable = true;
docker.enable = true;
readarr.enable = true;
sonarr.enable = true;
jellyfin.enable = true;
jellyseerr.enable = true;
development.enable = true;
home-assistant.enable = true;
homepage.enable = true;
paperless = {
enable = true;
port = 28981; # Optional, to override the default port
extraConfig = {
PAPERLESS_ADMIN_USER = "finn";
PAPERLESS_OCR_LANGUAGE = "deu+eng";
};
};
prowlarr.enable = true;
};
# my.homepage.widgets = [
# {
# search = {
# provider = "google";
# target = "_blank";
# };
# }
# {
# resources = {
# cpu = true;
# memory = true;
# disk = "/";
# cacheInterval = 5000;
# };
# }
# {
# datetime = {
# text_size = "xl";
# format = {
# timeStyle = "short";
# dateStyle = "short";
# hour12 = false;
# };
# };
# }
# ];
my.hardware = {
bluetooth.enable = true;
sound.enable = false;
};
my.services = {
vpn.enable = true;
webserver.enable = false;
};
}
@@ -1,71 +0,0 @@
# Do not modify this file! It was generated by nixos-generate-config
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{
config,
lib,
pkgs,
modulesPath,
...
}:
{
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = [
"xhci_pci"
"ahci"
"nvme"
];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ];
#boot.kernelParams = [ "i915.force_probe=9a49" ];
# 9a49
nixpkgs.config.packageOverrides = pkgs: {
vaapiIntel = pkgs.vaapiIntel.override { enableHybridCodec = true; };
};
hardware.graphics = {
enable = true;
extraPackages = with pkgs; [
intel-media-driver # LIBVA_DRIVER_NAME=iHD
#vaapiIntel # LIBVA_DRIVER_NAME=i965 (older but works better for Firefox/Chromium)
libva-vdpau-driver
libvdpau-va-gl
vpl-gpu-rt # oneVPL/MFX runtime, required for QSV (h264_qsv/hevc_qsv) session creation
intel-compute-runtime # OpenCL runtime, required for tonemap_opencl (HDR tone-mapping)
];
};
hardware.logitech.wireless = {
enable = true;
enableGraphical = true;
};
fileSystems."/" =
{ device = "/dev/disk/by-uuid/8e3ac6ce-6865-49d8-b6d0-d7c008de391e";
fsType = "ext4";
};
fileSystems."/boot" =
{ device = "/dev/disk/by-uuid/F193-2A35";
fsType = "vfat";
options = [ "fmask=0077" "dmask=0077" ];
};
swapDevices = [ { device = "/dev/disk/by-uuid/7ffb2d2b-c03d-474b-8513-fee26e1e5e2f"; } ];
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
networking.useDHCP = lib.mkDefault true;
# networking.interfaces.enp3s0.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
powerManagement.cpuFreqGovernor = lib.mkDefault "performance";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
-10
View File
@@ -1,10 +0,0 @@
_: {
networking.firewall.allowedTCPPorts = [
8080 # aria
];
networking = {
domain = "jupiter.solar.internal";
search = [ "jupiter.solar.internal" ];
};
}
-88
View File
@@ -1,88 +0,0 @@
{
config,
pkgs,
lib,
...
}:
{
imports = [
./disks.nix
./hardware-configuration.nix
./environments.nix
# ./system.nix use docker here
];
networking.hostName = "mibook";
boot.loader.grub = {
enable = true;
device = "/dev/nvme0n1";
useOSProber = true;
};
# Configure keymap in X11
services.xserver.xkb = {
layout = "de";
variant = "";
};
# Configure console keymap
console.keyMap = "de";
# Enable CUPS to print documents.
services.printing.enable = true;
nixpkgs.config.allowUnfree = true;
hardware.nvidia.prime = {
sync.enable = false;
nvidiaBusId = "PCI:01:00:0";
intelBusId = "PCI:00:2:0";
};
services.openssh.enable = true;
# Don't let boot stall waiting for a network that may never come up
# (WiFi credentials live in KWallet and need a desktop session), which
# otherwise hangs the terminal boot before the login prompt appears.
systemd.services.NetworkManager-wait-online.enable = false;
# Boot-time choice: the default GRUB entry boots straight into KDE.
# A separate "terminal" entry (a NixOS specialisation) boots to a text
# console with autologin, where you can work or run `desktop` to bring
# KDE up. Pick the entry you want in the GRUB menu at boot.
#
# NOTE: in terminal mode WiFi will not connect on its own (the password
# is stored per-user in KWallet). To reach the machine over SSH from
# terminal mode, first save the WiFi as a system connection in KDE:
# network settings -> your WiFi -> "All users may connect to this network".
specialisation.terminal.configuration = {
system.nixos.tags = [ "terminal" ];
# Boot to a text console. graphical.target is what pulls in the display
# manager (via its embedded Wants=display-manager.service), so defaulting
# to multi-user.target leaves SDDM installed but not started at boot.
systemd.defaultUnit = lib.mkForce "multi-user.target";
# Guarantee a usable shell on the console (no login prompt to hunt for).
services.getty.autologinUser = "finn";
# Bring the desktop up on demand from the terminal.
environment.systemPackages = [
(pkgs.writeShellScriptBin "desktop" "exec sudo systemctl start display-manager.service")
];
};
# KDE (PowerDevil) power settings: do nothing on lid close while on AC power.
# Shipped as a system-wide default; KConfig cascades so a user's own
# ~/.config/powerdevilrc will override this if present.
environment.etc."xdg/powerdevilrc".text = ''
[AC][SuspendAndShutdown]
LidAction=0
'';
system = {
stateVersion = "23.05";
autoUpgrade.enable = true;
};
}
-15
View File
@@ -1,15 +0,0 @@
{
config,
lib,
pkgs,
...
}:
#TODO: CHECK THIS OUT: https://github.com/nix-community/disko its kinda better but felix does not support it
{
fileSystems."/" = {
device = "/dev/disk/by-uuid/46e36a07-e75a-480a-a2f1-be103c6a3881";
fsType = "ext4";
};
}
-28
View File
@@ -1,28 +0,0 @@
# enabled profiles
{ config, lib, ... }:
let
secrets = config.sops.secrets;
in
{
my.profiles = {
kde-desktop.enable = true;
zsh.enable = true;
apps = {
desktop_apps = true;
dev_apps = true;
};
development.enable = true;
docker.enable = true;
claude-code.enable = true;
};
my.hardware = {
bluetooth.enable = true;
sound.enable = false;
nvidia.enable = false;
};
my.services = {
vpn.enable = true;
};
}
@@ -1,43 +0,0 @@
# Do not modify this file! It was generated by nixos-generate-config
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{
config,
lib,
pkgs,
modulesPath,
...
}:
{
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = [
"xhci_pci"
"ahci"
"nvme"
];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ];
fileSystems."/" = {
device = "/dev/disk/by-uuid/46e36a07-e75a-480a-a2f1-be103c6a3881";
fsType = "ext4";
};
swapDevices = [ ];
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
networking.useDHCP = lib.mkDefault true;
# networking.interfaces.wlp2s0.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
powerManagement.cpuFreqGovernor = lib.mkDefault "powersave";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
-8
View File
@@ -1,8 +0,0 @@
{ ... }:
{
imports = [
./environments
./hardware
./services
];
}
-47
View File
@@ -1,47 +0,0 @@
# manages and downloads films
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.actual;
hostName = config.networking.hostName;
port = 40465;
in
{
options.my.profiles.actual = with lib; {
enable = mkEnableOption "Actual budget service";
};
config = lib.mkIf cfg.enable {
services.actual = {
enable = true;
openFirewall = true;
settings = {
port = port;
hostname = "0.0.0.0";
};
};
environment.systemPackages = with pkgs; [
actual-server
];
my.homepage.services = [
{
group = "Finance";
name = "Actual";
description = "Budgeting";
href = "http://${hostName}:${toString port}";
icon = "mdi-cash";
}
];
systemd.services.actual = {
after = [ "network-online.target" ];
};
};
}
-35
View File
@@ -1,35 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.apps;
in
{
options.my.profiles.apps = with lib; {
desktop_apps = mkEnableOption "Basic Apps";
dev_apps = mkEnableOption "Development Apps";
gnome_apps = mkEnableOption "Gnome Extentions and Configuration";
};
config = lib.mkIf cfg.desktop_apps {
users.users.finn.packages = with pkgs; [
firefox
thunderbird
google-chrome
vscode
discord
spotify
keepassxc
moonlight-qt
nextcloud-client
pipewire
wireplumber
vlc
ghostty
];
};
}
@@ -1,45 +0,0 @@
# manages and downloads films
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.audiobookshelf;
hostName = config.networking.hostName;
domain = config.networking.domain;
in
{
options.my.profiles.audiobookshelf = with lib; {
enable = mkEnableOption "Audio Book Service";
};
config = lib.mkIf cfg.enable {
services.audiobookshelf = {
enable = true;
openFirewall = true;
port = 63834;
host = "0.0.0.0";
};
environment.systemPackages = with pkgs; [
audiobookshelf
];
my.homepage.services = [
{
group = "Media";
name = "Audiobookshelf";
description = "Audiobooks and podcasts";
href = "http://${domain}:63834";
icon = "audiobookshelf.png";
}
];
systemd.services.audiobookshelf = {
after = [ "network-online.target" ];
};
};
}
@@ -1,31 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.claude-code;
in
{
options.my.profiles.claude-code = with lib; {
enable = mkEnableOption "Claude Code CLI";
};
config = lib.mkIf cfg.enable {
users.users.finn.packages = with pkgs; [
claude-code
ripgrep
fd
gh
jq
];
# Future: headless Claude Code service
# A natural next step is exposing Claude Code as a persistent background service —
# e.g. a systemd user service that accepts work via an HTTP API or Unix socket,
# triggerable over SSH or a local network endpoint. This would turn the MiBook
# into a true remote execution node without requiring an interactive session.
# See: my.profiles.claude-code.service.enable (not yet implemented)
};
}
-23
View File
@@ -1,23 +0,0 @@
{ ... }:
{
imports = [
./actual
./apps
./audiobookshelf
./claude-code
./development
./home-assistant
./hyprland
./zsh
./paperless
./prowlarr
./radarr
./docker
./homepage
./kde-desktop
./readarr
./sonarr
./jellyfin
./jellyseerr
];
}
@@ -1,40 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.development;
in
{
options.my.profiles.development = with lib; {
enable = mkEnableOption "Development Tools";
};
config = lib.mkIf cfg.enable {
users.users.finn.packages = with pkgs; [
google-chrome
vscode
zed-editor
neovim
# jetbrains.idea-ultimate
go
(python3.withPackages (
ps: with ps; [
jupyter # notebooks
matplotlib
numpy
pandas
pillow
plotly
scikit-learn
scipy
tqdm # progressbar in pandas
wheel # python development
]
))
];
};
}
-29
View File
@@ -1,29 +0,0 @@
# Docker related settings
{
config,
inputs,
lib,
options,
pkgs,
...
}:
let
cfg = config.my.profiles.docker;
in
{
options.my.profiles.docker = with lib; {
enable = mkEnableOption "docker configuration";
};
config = lib.mkIf cfg.enable {
environment.systemPackages = with pkgs; [
docker
docker-compose
];
virtualisation.docker = {
enable = true;
autoPrune.enable = true;
};
};
}
@@ -1,76 +0,0 @@
# manages home automations
{
config,
lib,
pkgs,
self,
...
}:
let
cfg = config.my.profiles.home-assistant;
hostName = config.networking.hostName;
in
{
options.my.profiles.home-assistant = with lib; {
enable = mkEnableOption "Home Automation";
};
config = lib.mkIf cfg.enable {
services.matter-server.enable = true;
services.home-assistant = {
enable = true;
openFirewall = true;
extraComponents = [
"matter"
"mobile_app"
"otbr"
"thread"
"xiaomi_miio"
"apple_tv" # Apple TV (pyatv); pair via UI PIN flow
"tuya" # Tuya/SmartLife cloud; Unistyle WLAN irrigation computer
];
};
services.home-assistant.config = {
homeassistant = {
name = "Home - Rechberg";
unit_system = "metric";
internal_url = "http://${hostName}:8123";
external_url = "http://jupiter.solar.internal:8123";
};
mobile_app = { };
automation = "!include automations.yaml";
script = "!include scripts.yaml";
scene = "!include scenes.yaml";
};
# `!include` targets must exist or HA fails at startup. Create them empty
# so HA's UI editor can write to them; `f` only acts if the file is absent.
systemd.tmpfiles.rules = [
"f /var/lib/hass/automations.yaml 0644 hass hass - []"
"f /var/lib/hass/scripts.yaml 0644 hass hass - {}"
"f /var/lib/hass/scenes.yaml 0644 hass hass - []"
];
services.openthread-border-router = {
enable = true;
package = pkgs.unstable.openthread-border-router;
openFirewall = true;
backboneInterfaces = [ "enp3s0" ];
radio.device = "/dev/serial/by-id/usb-Nabu_Casa_ZBT-2_DCB4D9149C7C-if00";
radio.baudRate = 460800;
};
my.homepage.services = [
{
group = "Services";
name = "Home Assistant";
description = "Home automation";
href = "http://${hostName}:8123";
icon = "si-homeassistant";
}
];
};
}
@@ -1,39 +0,0 @@
[
{
Developer = [
{
Github = [
{
abbr = "GitLab - Smoothcloud";
href = "https://gitlab.smoothcloud.de/";
}
];
}
];
}
{
Social = [
{
Reddit = [
{
icon = "reddit.png";
href = "https://reddit.com/";
description = "The front page of the internet";
}
];
}
];
}
{
Entertainment = [
{
YouTube = [
{
abbr = "YT";
href = "https://youtube.com/";
}
];
}
];
}
]
-110
View File
@@ -1,110 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.homepage;
dashboardPort = 8082;
dashboardHost = config.networking.domain;
dashboardUrl = "http://${dashboardHost}:${toString dashboardPort}";
manualServices = import ./manual-services.nix;
manualWidgets = import ./manual-widgets.nix;
groupedServices =
lib.foldl'
(
acc: entry:
acc
// {
${entry.group} = (acc.${entry.group} or [ ]) ++ [ entry ];
}
)
{ }
config.my.homepage.services;
homepageServices = lib.mapAttrsToList (
group: entries:
{
${group} = map (
entry:
let
service = builtins.removeAttrs entry [
"group"
"name"
];
in
{
${entry.name} = lib.filterAttrs (_: value: value != null) service;
}
) entries;
}
) groupedServices;
in
{
options.my.homepage.services = with lib; mkOption {
type = types.listOf (
types.submodule {
options = {
group = mkOption {
type = types.str;
description = "Homepage service group";
};
name = mkOption {
type = types.str;
description = "Homepage service name";
};
description = mkOption {
type = types.str;
description = "Homepage service description";
};
href = mkOption {
type = types.str;
description = "Homepage service URL";
};
icon = mkOption {
type = types.nullOr types.str;
default = null;
description = "Optional Homepage service icon";
};
};
}
);
default = [ ];
description = "Merged homepage service metadata contributed by repo modules.";
};
options.my.homepage.widgets = with lib; mkOption {
type = types.listOf types.attrs;
default = [ ];
description = "Widget definitions passed directly to services.homepage-dashboard.widgets. Each entry is an attrset like { resources = { cpu = true; memory = true; }; }.";
};
options.my.profiles.homepage = with lib; {
enable = mkEnableOption "getHomepage.dev Dashboard";
};
config = lib.mkIf cfg.enable {
services.homepage-dashboard = {
enable = true;
listenPort = dashboardPort;
allowedHosts = "${dashboardHost}:${toString dashboardPort},localhost:${toString dashboardPort},127.0.0.1:${toString dashboardPort},jupiter.solar.internal:${toString dashboardPort}";
bookmarks = import ./bookmarks.nix;
services = homepageServices ++ manualServices;
widgets = config.my.homepage.widgets ++ manualWidgets;
};
users.users.finn.packages = with pkgs; [
homepage-dashboard
];
programs.chromium.homepageLocation = dashboardUrl;
};
}
@@ -1,28 +0,0 @@
[
# Example:
{
"Services" = [
{
"Tesla - Dashboard" = {
href = "https://grafana.smoothcloud.de";
description = "Tesla Information";
icon = "tesla.png";
};
}
{
"GitLab" = {
href = "https://gitlab.smoothcloud.de";
description = "Selfhosted GitLab";
icon = "gitlab.png";
};
}
{
"Nextcloud" = {
href = "https://next.smoothcloud.de";
description = "Selfhosted Cloud Service";
icon = "nextcloud.png";
};
}
];
}
]
@@ -1,6 +0,0 @@
[
# Example:
{ resources = { cpu = true; memory = true; disk = "/"; }; }
{ datetime = { text_size = "xl"; format = { timeStyle = "short"; }; }; }
{ search = {provider = "google"; focus = true; target = "_blank"; }; }
]
-46
View File
@@ -1,46 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.hyprland;
in
{
options.my.profiles.hyprland = with lib; {
enable = mkEnableOption "hyprland env";
};
config = lib.mkIf cfg.enable {
programs.hyprland = {
enable = true;
xwayland.enable = true;
};
services.displayManager.sddm.wayland.enable = true;
services.xserver = {
layout = "de";
xkbVariant = "";
};
users.users.finn.packages = with pkgs; [
wofi
mako
kitty
hyprpaper
waybar
libsForQt5.dolphin
#libsForQt5.systemsettings
libsForQt5.polkit-kde-agent
#xdg-desktop-portal-hyprland
];
hardware = {
# Opengl
opengl.enable = true;
# Most wayland compositors need this
nvidia.modesetting.enable = true;
};
};
}
-45
View File
@@ -1,45 +0,0 @@
# manages and downloads films
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.jellyfin;
hostName = config.networking.hostName;
port = 8096;
in
{
options.my.profiles.jellyfin = with lib; {
enable = mkEnableOption "Media Service";
};
config = lib.mkIf cfg.enable {
services.jellyfin = {
enable = true;
openFirewall = true;
};
environment.systemPackages = [ pkgs.libva-utils ];
my.homepage.services = [
{
group = "Media";
name = "Jellyfin";
description = "Media server";
href = "http://${hostName}:${toString port}";
icon = "jellyfin.png";
}
];
systemd.services.jellyfin = {
after = [ "network-online.target" ];
serviceConfig.SupplementaryGroups = [
"video"
"render"
];
};
};
}
@@ -1,33 +0,0 @@
# manages and downloads films
{ config, lib, ... }:
let
cfg = config.my.profiles.jellyseerr;
hostName = config.networking.hostName;
port = 5055;
in
{
options.my.profiles.jellyseerr = with lib; {
enable = mkEnableOption "JellySeerr Media Service";
};
config = lib.mkIf cfg.enable {
services.jellyseerr = {
enable = true;
openFirewall = true;
};
my.homepage.services = [
{
group = "Media";
name = "Jellyseerr";
description = "Media requests";
href = "http://${hostName}:${toString port}";
icon = "mdi-television-guide";
}
];
systemd.services.jellyseerr = {
after = [ "network-online.target" ];
};
};
}
@@ -1,27 +0,0 @@
# KDE Desktio Environment
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.kde-desktop;
in
{
options.my.profiles.kde-desktop = with lib; {
enable = mkEnableOption "KDE Desktop Environment";
};
config = lib.mkIf cfg.enable {
services = {
displayManager.sddm.enable = true;
displayManager.sddm.wayland.enable = true;
desktopManager.plasma6.enable = true;
};
users.users.finn.packages = with pkgs; [
# Programms can be added here...
numix-icon-theme
];
};
}
@@ -1,48 +0,0 @@
# document management system
{ config, lib, ... }:
let
cfg = config.my.profiles.paperless;
hostName = config.networking.hostName;
in
{
options.my.profiles.paperless = with lib; {
enable = mkEnableOption "Paperless Server";
port = mkOption {
type = types.port;
default = 28981;
example = 8080;
description = "Internal port for webui";
};
extraConfig = mkOption {
type = types.attrs;
default = { };
example = {
PAPERLESS_OCR_LANGUAGE = "deu+eng";
};
description = "Extra configuration options";
};
};
config = lib.mkIf cfg.enable {
services.paperless = {
enable = true;
address = "0.0.0.0";
port = cfg.port;
# settings = cfg.extraConfig;
};
my.homepage.services = [
{
group = "Services";
name = "Paperless";
description = "Document management";
href = "http://${hostName}:${toString cfg.port}";
icon = "paperless-ngx.png";
}
];
networking.firewall.allowedTCPPorts = [ cfg.port ];
};
}
-51
View File
@@ -1,51 +0,0 @@
# manages indexes
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.prowlarr;
hostName = config.networking.hostName;
port = 9696;
in
# domain = config.networking.domain;
# port = 9696;
{
options.my.profiles.prowlarr = with lib; {
enable = mkEnableOption "Prowlarr for indexing";
};
config = lib.mkIf cfg.enable {
services.prowlarr = {
enable = true;
openFirewall = true;
};
my.homepage.services = [
{
group = "Media";
name = "Prowlarr";
description = "Indexer manager";
href = "http://${hostName}:${toString port}";
icon = "prowlarr.png";
}
];
# # ugly fix for service not having a homedirectory
# users.users.prowlarr = {
# isSystemUser = true;
# home = "/var/lib/prowlarr";
# group = "prowlarr";
# uid = 61654;
# };
# users.groups.prowlarr = {
# gid = 61654;
# };
systemd.services.prowlarr = {
after = [ "network-online.target" ];
};
};
}
-43
View File
@@ -1,43 +0,0 @@
# manages and downloads films
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.radarr;
hostName = config.networking.hostName;
port = 7878;
in
# domain = config.networking.domain;
# port = 7878;
{
options.my.profiles.radarr = with lib; {
enable = mkEnableOption "Sonarr for films management";
};
config = lib.mkIf cfg.enable {
services.radarr = {
enable = true;
openFirewall = true;
};
my.homepage.services = [
{
group = "Media";
name = "Radarr";
description = "Movie management";
href = "http://${hostName}:${toString port}";
icon = "radarr.png";
}
];
my.profiles.prowlarr.enable = true;
systemd.services.radarr = {
after = [ "network-online.target" ];
};
};
}
-43
View File
@@ -1,43 +0,0 @@
# manages and downloads films
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.readarr;
hostName = config.networking.hostName;
port = 8787;
in
# domain = config.networking.domain;
# port = 7878;
{
options.my.profiles.readarr = with lib; {
enable = mkEnableOption "Readarr for ebook management";
};
config = lib.mkIf cfg.enable {
services.readarr = {
enable = true;
openFirewall = true;
};
my.homepage.services = [
{
group = "Media";
name = "Readarr";
description = "Book management";
href = "http://${hostName}:${toString port}";
icon = "readarr.png";
}
];
my.profiles.prowlarr.enable = true;
systemd.services.readarr = {
after = [ "network-online.target" ];
};
};
}
-41
View File
@@ -1,41 +0,0 @@
# manages and downloads films
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.sonarr;
hostName = config.networking.hostName;
port = 8989;
in
{
options.my.profiles.sonarr = with lib; {
enable = mkEnableOption "sonarr for series management";
};
config = lib.mkIf cfg.enable {
services.sonarr = {
enable = true;
openFirewall = true;
};
my.homepage.services = [
{
group = "Media";
name = "Sonarr";
description = "Series management";
href = "http://${hostName}:${toString port}";
icon = "sonarr.png";
}
];
my.profiles.prowlarr.enable = true;
systemd.services.sonarr = {
after = [ "network-online.target" ];
};
};
}
-27
View File
@@ -1,27 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.zsh;
in
{
options.my.profiles.zsh = with lib; {
enable = mkEnableOption "zsh env";
};
config = lib.mkIf cfg.enable {
users.defaultUserShell = pkgs.zsh;
programs.zsh = {
enable = true;
ohMyZsh = {
enable = true;
theme = "agnoster";
};
};
};
}
-30
View File
@@ -1,30 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.hardware.bluetooth;
in
{
options.my.hardware.bluetooth = with lib; {
enable = mkEnableOption "bluetooth configuration";
};
config = lib.mkIf cfg.enable {
hardware.bluetooth = {
enable = true;
package = pkgs.bluez;
settings = {
General = {
Enable = "Source,Sink,Media,Socket";
};
};
};
services.blueman.enable = true;
environment.systemPackages = with pkgs; [
sony-headphones-client
];
};
}
-18
View File
@@ -1,18 +0,0 @@
# Hardware-related modules
{ ... }:
{
imports = [
./bluetooth
#./debug
#./drive-monitor
#./firmware
#./graphics
#./keychron
./nvidia
#./yubikey
./sound
#./thunderbolt
# ./wifi
];
}
-53
View File
@@ -1,53 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.hardware.nvidia;
in
{
options.my.hardware.nvidia = with lib; {
enable = mkEnableOption "NVIDIA GPU support";
};
config = lib.mkIf cfg.enable {
# Enable OpenGL
hardware.graphics = {
enable = true;
};
# Load nvidia driver for Xorg and Wayland
services.xserver.videoDrivers = ["nvidia"];
hardware.nvidia = {
# Modesetting is required.
modesetting.enable = true;
# Nvidia power management. Experimental, and can cause sleep/suspend to fail.
# Enable this if you have graphical corruption issues or application crashes after waking
# up from sleep. This fixes it by saving the entire VRAM memory to /tmp/ instead
# of just the bare essentials.
powerManagement.enable = false;
# Fine-grained power management. Turns off GPU when not in use.
# Experimental and only works on modern Nvidia GPUs (Turing or newer).
powerManagement.finegrained = false;
# Use the NVidia open source kernel module (not to be confused with the
# independent third-party "nouveau" open source driver).
# Support is limited to the Turing and later architectures. Full list of
# supported GPUs is at:
# https://github.com/NVIDIA/open-gpu-kernel-modules#compatible-gpus
# Only available from driver 515.43.04+
open = false;
# Enable the Nvidia settings menu,
# accessible via `nvidia-settings`.
nvidiaSettings = true;
# Optionally, you may need to select the appropriate driver version for your specific GPU.
package = config.boot.kernelPackages.nvidiaPackages.stable;
};
};
}
-26
View File
@@ -1,26 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.hardware.bluetooth;
in
{
options.my.hardware.sound = with lib; {
enable = mkEnableOption "Sound configuration";
};
config = lib.mkIf cfg.enable {
services.pulseaudio = {
enable = false;
support32Bit = true;
};
users.extraUsers.finn.extraGroups = [ "audio" ];
environment.systemPackages = with pkgs; [
headsetcontrol
];
};
}
-21
View File
@@ -1,21 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.hardware.wifi;
in
{
options.my.hardware.wifi = with lib; {
enable = mkEnableOption "wifi configuration";
};
config = lib.mkIf cfg.enable {
hardware.wifi = {
enable = true;
package = pkgs.rtw89-firmware;
};
};
}
-9
View File
@@ -1,9 +0,0 @@
# Service-related modules
{ ... }:
{
imports = [
./vpn
./webserver
];
}
-18
View File
@@ -1,18 +0,0 @@
# self-hosted vpn
{ config, lib, ... }:
let
cfg = config.my.services.vpn;
in
{
options.my.services.vpn = with lib; {
enable = mkEnableOption "Headscale VPN Setup";
};
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
services.tailscale.enable = true;
}
]
);
}
-247
View File
@@ -1,247 +0,0 @@
# public webserver with reverseproxy
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.services.webserver;
inherit (config.networking) domain;
virtualHostOption = lib.types.submodule {
options = {
subdomain = lib.mkOption {
type = lib.types.str;
example = "dev";
description = ''
Which subdomain, under config.networking.domain, to use
for this virtual host.
'';
};
homepage = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to expose this virtual host on homepage-dashboard.
'';
};
group = lib.mkOption {
type = lib.types.str;
default = "Web";
description = ''
Homepage service group for this virtual host.
'';
};
name = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
Optional display name for homepage-dashboard. Defaults to the subdomain.
'';
};
description = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
Optional homepage-dashboard description for this virtual host.
'';
};
icon = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Optional homepage-dashboard icon for this virtual host.
'';
};
};
port = lib.mkOption {
type = with lib.types; nullOr port;
default = null;
example = 8080;
description = ''
Which port to proxy to, through localhost, for this virtual host.
This option is incompatible with `root`.
'';
};
root = lib.mkOption {
type = with lib.types; nullOr path;
default = null;
example = "/var/www/blog";
description = ''
The root folder for this virtual host. This option is incompatible
with `port`.
'';
};
extraConfig = lib.mkOption {
type = with lib.types; nullOr lines;
example = lib.literalExpression ''
{
locations."/socket" = {
proxyPass = "http://localhost:8096/";
proxyWebsockets = true;
};
}
'';
default = null;
description = ''
Any extra configuration that should be applied to this virtual host.
'';
};
};
};
in
{
options.my.services.webserver = {
enable = lib.mkEnableOption "webserver";
virtualHosts = lib.mkOption {
type = lib.types.listOf virtualHostOption;
default = [ ];
example = lib.literalExpression ''
[
{
subdomain = "gitea";
port = 8080;
homepage.description = "Git forge";
}
{
subdomain = "dev";
root = "/var/www/dev";
homepage.description = "Static site";
}
{
subdomain = "jellyfin";
port = 8096;
homepage.group = "Media";
homepage.description = "Media server";
extraConfig = {
locations."/socket" = {
proxyPass = "http://localhost:8096/";
proxyWebsockets = true;
};
};
}
]
'';
description = ''
List of virtual hosts to set-up using default settings.
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = lib.allUnique (builtins.filter (p: p != null) (map (v: v.port) cfg.virtualHosts));
message =
let
portsWithSubdomains = builtins.filter (v: v.port != null) cfg.virtualHosts;
duplicates = lib.filter (
p: builtins.length (lib.filter (x: x.port == p.port) portsWithSubdomains) > 1
) portsWithSubdomains;
in
if duplicates == [ ] then
""
else
"Duplicate ports found in my.services.webserver.virtualHosts: "
+ builtins.concatStringsSep ", " (map (v: v.subdomain + ":" + builtins.toString v.port) duplicates);
}
];
my.homepage.services = map (
vhost:
{
group = vhost.homepage.group;
name = if vhost.homepage.name != "" then vhost.homepage.name else vhost.subdomain;
description =
if vhost.homepage.description != "" then
vhost.homepage.description
else if vhost.root != null then
"Static site"
else if vhost.port != null then
"Reverse proxied service"
else
"Web service";
href = "https://${vhost.subdomain}.${domain}";
icon = vhost.homepage.icon;
}
) (builtins.filter (vhost: vhost.homepage.enable) cfg.virtualHosts);
services = {
nginx.enable = false;
caddy = {
enable = true;
email = "jupiter@solar.internal";
globalConfig = ''
servers{
}
'';
extraConfig = ''
(compress) {
encode gzip zstd
}
(headers) {
header {
# enable CORS
Access-Control-Allow-Origin "https://${config.networking.domain}"
# disable FLoC tracking
Permissions-Policy interest-cohort=()
# enable HSTS
Strict-Transport-Security max-age=31536000;
# disable clients from sniffing the media type
X-Content-Type-Options "nosniff"
# clickjacking protection
X-Frame-Options "DENY"
# enable XSS protection
X-XSS-Protection "1; mode=block"
# referrer policy
Referrer-Policy "strict-origin-when-cross-origin"
}
}
(common) {
import headers
import compress
}
'';
virtualHosts =
let
mkVHost =
{ subdomain, ... }@args:
lib.nameValuePair "${subdomain}.${domain}" (
lib.foldl lib.recursiveUpdate { } [
{
useACMEHost = domain;
extraConfig = ''
import common
${lib.optionalString (args.root != null) ''
root * ${args.root}
file_server
''}
${lib.optionalString (args.port != null) ''
reverse_proxy localhost:${toString args.port} {
# remove CORS headers from proxied server, because duplicate headers are not allowed
# remove after new release: https://github.com/navidrome/navidrome/commit/657fe11f5327ff7a3cb6aa9308b0bb7c71eea5c6
header_down -Access-Control-Allow-Origin
}
''}
${lib.optionalString (args.extraConfig != null) args.extraConfig}
'';
}
]
);
in
lib.listToAttrs (map mkVHost cfg.virtualHosts);
};
};
networking.firewall.allowedTCPPorts = [
80
443
];
};
}
-3
View File
@@ -1,3 +0,0 @@
final: prev: {
# homer = final.callPackage ./homer { };
}