Compare commits

..

10 Commits

Author SHA1 Message Date
c370d635e5 feat(emulation): declare RetroArch configuration 2026-10-22 13:51:33 -04:00
8756bc087f fix(adguard): disable DNS request rate limit
The private resolver grouped clients by IPv4 /24 and silently dropped requests above 20 queries per second. Disable the public-server protection so internal DNS bursts complete.
2026-10-22 13:34:58 -04:00
bb22a90053 feat(ssh): add Steam Machine target 2026-10-22 11:00:46 -04:00
5cf7109eaf docs: update homelab plans 2026-10-21 20:35:37 -04:00
4baafb7094 feat(discord): configure official client 2026-10-21 20:32:31 -04:00
0166547091 feat(steam): configure Quest VR streaming 2026-10-21 19:27:57 -04:00
1a2a4bd2bd feat(nixos): migrate AdGuard to Pikachu 2026-10-21 18:51:29 -04:00
94eb132962 fix(shell): reuse sudo session during deployment 2026-10-21 17:03:42 -04:00
8e01e1621b fix(nixos): add RomM Homepage entry 2026-10-21 14:33:36 -04:00
b4e74fc5b2 feat(zeus): add RetroArch 2026-10-21 12:05:28 -04:00
26 changed files with 917 additions and 127 deletions

View File

@@ -0,0 +1,51 @@
{
config,
lib,
registry,
...
}:
let
cfg = config.guests.adguard;
in
{
options.guests.adguard.adminPasswordHash = lib.mkOption {
type = lib.types.str;
description = "Bcrypt verifier for the AdGuard Home administrator password.";
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.address != null;
message = "guests.adguard.address must identify the AdGuard Home guest.";
}
];
registry.homepage.${config.networking.hostName}.adguard = {
tab = "System";
section = "Network";
name = "AdGuard Home";
description = "Internal DNS";
icon = "adguard-home.svg";
href = "http://${cfg.address}";
};
guests.adguard = {
interior = {
modules.services.adguardhome = {
enable = true;
inherit (cfg) adminPasswordHash;
reverseProxyAddress = registry.address.${config.networking.hostName}."reverse-proxy";
};
services.resolved.settings.Resolve.DNSStubListener = false;
};
limits = {
memory = lib.mkDefault "256M";
cpu = lib.mkDefault "100%";
tasksMax = lib.mkDefault 128;
};
};
};
}

View File

@@ -94,6 +94,14 @@ in
}
];
registry.homepage.${config.networking.hostName}.romm = {
tab = "Apps";
section = "Media";
name = "RomM";
description = "Games";
icon = "romm.png";
};
guests.romm = {
interior = {
modules.services.romm.enable = true;
@@ -156,6 +164,8 @@ in
"services/romm/igdb/client-id"
"services/romm/igdb/client-secret"
"services/romm/retroachievements/api-key"
"services/romm/screenscraper/dev/id"
"services/romm/screenscraper/dev/password"
"services/romm/screenscraper/user"
"services/romm/screenscraper/password"
"services/romm/steamgriddb/api-key"
@@ -171,6 +181,8 @@ in
content = ''
IGDB_CLIENT_ID=${config.sops.placeholder."services/romm/igdb/client-id"}
IGDB_CLIENT_SECRET=${config.sops.placeholder."services/romm/igdb/client-secret"}
SCREENSCRAPER_DEV_ID=${config.sops.placeholder."services/romm/screenscraper/dev/id"}
SCREENSCRAPER_DEV_PASSWORD=${config.sops.placeholder."services/romm/screenscraper/dev/password"}
SCREENSCRAPER_USER=${config.sops.placeholder."services/romm/screenscraper/user"}
SCREENSCRAPER_PASSWORD=${config.sops.placeholder."services/romm/screenscraper/password"}
RETROACHIEVEMENTS_API_KEY=${config.sops.placeholder."services/romm/retroachievements/api-key"}

View File

@@ -40,7 +40,6 @@
modules.desktop.enable = true;
modules.desktop.apps.firefox.uiScale = "1.25";
modules.desktop.apps.obsidian.enable = true;
modules.desktop.apps.steam.enable = true;
registry.waybar.neogaia.battery = {
order = 70;

View File

@@ -29,6 +29,14 @@
registry.address.pikachu.self = "10.23.10.102";
guests.adguard = {
enable = true;
vlan = 20;
mac = "bc:24:11:91:60:bf";
address = "10.23.20.33";
adminPasswordHash = "$2b$12$wariglnrCvJOnh4b8KnB9eDX4RdQRaaTZtyXMUOilaYSn7xTR0g1e";
};
guests.valheim = {
enable = true;
vlan = 20;

View File

@@ -47,7 +47,7 @@
modules.desktop.emulation.enable = true;
modules.desktop.apps.signal.enable = true;
modules.desktop.apps.steam.enable = true;
modules.desktop.apps.vencord.enable = true;
modules.desktop.apps.discord.enable = true;
home-manager.users.${config.user.name}.wayland.windowManager.hyprland.settings.monitor = [
{

View File

@@ -0,0 +1,23 @@
{
config,
lib,
pkgs,
util,
...
}:
# Official Discord provides chat, voice, and screen sharing.
let
user = config.user.name;
in
{
options.modules.desktop.apps.discord.enable = lib.mkEnableOption "official Discord";
config = lib.mkIf config.modules.desktop.apps.discord.enable {
home-manager.users.${user}.home.packages = [ pkgs.discord ];
registry.window-icon.${config.networking.hostName}.discord = {
class = "[Dd]iscord";
icon = util.glyph "f1ff";
};
};
}

View File

@@ -14,6 +14,7 @@
programs.steam = {
enable = true;
remotePlay.openFirewall = true;
package = pkgs.steam.override {
extraEnv.STEAM_FORCE_DESKTOPUI_SCALING = "1.5";
};
@@ -21,9 +22,10 @@
hardware.steam-hardware.enable = true;
environment.systemPackages = [
pkgs.gamescope
];
programs.gamescope = {
enable = true;
capSysNice = true;
};
registry.window-icon.${config.networking.hostName}.steam = {
class = "[Ss]team";

View File

@@ -1,24 +0,0 @@
{
config,
lib,
pkgs,
util,
...
}:
# Vesktop, a Discord client with Vencord built in.
let
user = config.user.name;
in
{
options.modules.desktop.apps.vencord.enable =
lib.mkEnableOption "Vesktop, a Discord client with Vencord built in";
config = lib.mkIf config.modules.desktop.apps.vencord.enable {
home-manager.users.${user}.home.packages = [ pkgs.vesktop ];
registry.window-icon.${config.networking.hostName}.vesktop = {
class = "[Vv]esktop";
icon = util.glyph "f1ff";
};
};
}

View File

@@ -9,7 +9,6 @@
let
cfg = config.modules.desktop.emulation.retroarch;
user = config.user.name;
systemDirectory = "/home/${user}/.config/retroarch/system";
in
{
options.modules.desktop.emulation.retroarch = {
@@ -23,7 +22,38 @@ in
};
config = lib.mkIf cfg.enable {
sops.secrets =
lib.genAttrs
[
"retroarch/romm/user"
"retroarch/romm/password"
"retroarch/retroachievements/user"
"retroarch/retroachievements/password"
]
(_: {
sopsFile = ../../../secrets + "/${config.networking.hostName}.yaml";
});
sops.templates.retroarch-credentials = {
owner = user;
mode = "0400";
content = ''
webdav_username = "${config.sops.placeholder."retroarch/romm/user"}"
webdav_password = "${config.sops.placeholder."retroarch/romm/password"}"
cheevos_username = "${config.sops.placeholder."retroarch/retroachievements/user"}"
cheevos_password = "${config.sops.placeholder."retroarch/retroachievements/password"}"
'';
};
home-manager.users.${user} = {
home.file.".config/retroarch/retroarch.cfg" = {
force = true;
text = ''
config_save_on_exit = "false"
#include "${config.sops.templates.retroarch-credentials.path}"
'';
};
home.packages = [
(pkgs.wrapRetroArch {
cores = with pkgs.libretro; [
@@ -33,7 +63,8 @@ in
settings = {
savefiles_in_content_dir = "false";
savestates_in_content_dir = "false";
system_directory = systemDirectory;
system_directory = "/home/${user}/.config/retroarch/system";
video_fullscreen = "true";
sort_savefiles_enable = "true";
sort_savestates_enable = "true";
cloud_sync_enable = "true";
@@ -45,17 +76,33 @@ in
cloud_sync_sync_system = "false";
cloud_sync_sync_mode = "0";
cloud_sync_destructive = "false";
cheevos_enable = "true";
cheevos_hardcore_mode_enable = "false";
menu_driver = "xmb";
menu_shader_pipeline = "1";
xmb_menu_color_theme = "19";
content_show_add_entry = "0";
content_show_contentless_cores = "0";
content_show_explore = "false";
content_show_favorites = "false";
content_show_history = "false";
content_show_images = "false";
content_show_playlists = "false";
content_show_video = "false";
menu_icon_thumbnails = "3";
menu_thumbnails = "0";
network_on_demand_thumbnails = "true";
netplay_nickname = user;
}
// lib.optionalAttrs (cfg.browserDirectory != "") {
rgui_browser_directory = cfg.browserDirectory;
};
})
];
};
registry.window-icon.${config.networking.hostName}.retroarch = {
class = "retroarch";
class = "com.libretro.RetroArch";
icon = util.glyph "f11b";
};
};

View File

@@ -43,6 +43,10 @@ let
"git.alexion.dev"
];
};
steammachine = {
hostName = "10.23.50.234";
user = "deck";
};
};
mkTarget = name: target: {

View File

@@ -0,0 +1,274 @@
{ config, lib, ... }:
let
cfg = config.modules.services.adguardhome;
in
{
options.modules.services.adguardhome = {
enable = lib.mkEnableOption "AdGuard Home";
adminPasswordHash = lib.mkOption {
type = lib.types.str;
description = "Bcrypt verifier for the AdGuard Home administrator password.";
};
reverseProxyAddress = lib.mkOption {
type = lib.types.str;
description = "IP address of the guest that handles internal HTTP requests.";
};
};
config = lib.mkIf cfg.enable {
services.adguardhome = {
enable = true;
mutableSettings = false;
openFirewall = true;
settings = {
http = {
pprof = {
port = 6060;
enabled = false;
};
address = "0.0.0.0:80";
session_ttl = "720h";
};
users = [
{
name = config.user.name;
password = cfg.adminPasswordHash;
}
];
auth_attempts = 5;
block_auth_min = 15;
http_proxy = "";
language = "";
theme = "auto";
dns = {
bind_hosts = [ "0.0.0.0" ];
port = 53;
anonymize_client_ip = false;
ratelimit = 0;
refuse_any = true;
upstream_dns = [
"https://dns.cloudflare.com/dns-query"
"tls://1.1.1.1"
"https://dns.google/dns-query"
"tls://8.8.8.8"
"https://dns10.quad9.net/dns-query"
"tls://9.9.9.9"
];
upstream_dns_file = "";
bootstrap_dns = [
"9.9.9.10"
"149.112.112.10"
"2620:fe::10"
"2620:fe::fe:10"
];
fallback_dns = [ ];
upstream_mode = "parallel";
fastest_timeout = "1s";
allowed_clients = [ ];
disallowed_clients = [ ];
blocked_hosts = [
"version.bind"
"id.server"
"hostname.bind"
];
trusted_proxies = [
"127.0.0.0/8"
"::1/128"
];
cache_enabled = true;
cache_size = 4194304;
cache_ttl_min = 0;
cache_ttl_max = 0;
cache_optimistic = false;
cache_optimistic_answer_ttl = "30s";
cache_optimistic_max_age = "12h";
bogus_nxdomain = [ ];
aaaa_disabled = false;
enable_dnssec = false;
edns_client_subnet = {
custom_ip = "";
enabled = false;
use_custom = false;
};
max_goroutines = 300;
handle_ddr = true;
ipset = [ ];
ipset_file = "";
bootstrap_prefer_ipv6 = false;
upstream_timeout = "10s";
private_networks = [ ];
use_private_ptr_resolvers = true;
local_ptr_upstreams = [ ];
use_dns64 = false;
dns64_prefixes = [ ];
serve_http3 = false;
use_http3_upstreams = false;
serve_plain_dns = true;
hostsfile_enabled = true;
pending_requests.enabled = true;
};
tls = {
enabled = false;
server_name = "";
force_https = false;
port_https = 443;
port_dns_over_tls = 853;
port_dns_over_quic = 853;
port_dnscrypt = 0;
dnscrypt_config_file = "";
allow_unencrypted_doh = false;
certificate_chain = "";
private_key = "";
certificate_path = "";
private_key_path = "";
strict_sni_check = false;
};
querylog = {
dir_path = "";
ignored = [ ];
interval = "2160h";
size_memory = 1000;
enabled = true;
ignored_enabled = false;
file_enabled = true;
};
statistics = {
dir_path = "";
ignored = [ ];
interval = "24h";
enabled = true;
ignored_enabled = false;
};
filters = [
{
enabled = true;
url = "https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt";
name = "AdGuard DNS filter";
id = 1;
}
{
enabled = false;
url = "https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt";
name = "AdAway Default Blocklist";
id = 2;
}
];
whitelist_filters = [ ];
user_rules = [ ];
dhcp = {
enabled = false;
interface_name = "";
local_domain_name = "lan";
dhcpv4 = {
gateway_ip = "";
subnet_mask = "";
range_start = "";
range_end = "";
lease_duration = 86400;
icmp_timeout_msec = 1000;
options = [ ];
};
dhcpv6 = {
range_start = "";
lease_duration = 86400;
ra_slaac_only = false;
ra_allow_slaac = false;
};
};
filtering = {
blocking_ipv4 = "";
blocking_ipv6 = "";
blocked_services = {
schedule.time_zone = "Local";
ids = [ ];
};
protection_disabled_until = null;
safe_search = {
enabled = false;
bing = true;
duckduckgo = true;
ecosia = true;
google = true;
pixabay = true;
yandex = true;
youtube = true;
};
blocking_mode = "default";
parental_block_host = "family-block.dns.adguard.com";
safebrowsing_block_host = "standard-block.dns.adguard.com";
rewrites = [
{
domain = "*.home";
answer = "10.23.20.32";
enabled = true;
}
{
domain = "*.alexion.dev";
answer = cfg.reverseProxyAddress;
enabled = true;
}
{
domain = "home";
answer = cfg.reverseProxyAddress;
enabled = true;
}
];
safe_fs_patterns = [ "/opt/AdGuardHome/userfilters/*" ];
safebrowsing_cache_size = 1048576;
safesearch_cache_size = 1048576;
parental_cache_size = 1048576;
cache_time = 30;
filters_update_interval = 24;
blocked_response_ttl = 10;
filtering_enabled = true;
rewrites_enabled = true;
parental_enabled = false;
safebrowsing_enabled = false;
protection_enabled = true;
};
clients = {
runtime_sources = {
whois = true;
arp = true;
rdns = true;
dhcp = true;
hosts = true;
};
persistent = [ ];
};
log = {
enabled = true;
file = "";
max_backups = 0;
max_size = 100;
max_age = 3;
compress = false;
local_time = false;
verbose = false;
};
os = {
group = "";
user = "";
rlimit_nofile = 0;
};
};
};
networking.firewall = {
allowedTCPPorts = [ 53 ];
allowedUDPPorts = [ 53 ];
};
};
}

View File

@@ -56,6 +56,13 @@ in
default = "roms/{platform}/{game}";
firmware = "bios/{platform}";
};
scan.media = [
"box2d"
"screenshot"
"title_screen"
"logo"
"manual"
];
}
);

View File

@@ -47,6 +47,8 @@ or begin
return $status
end
set remote_nar
if test (count $missing_paths) -gt 0
set nar_file (mktemp --suffix=.nar)
or return $status
@@ -70,26 +72,23 @@ if test (count $missing_paths) -gt 0
return $scp_status
end
echo "deploy-to: importing closure on $host" >&2
ssh -t $host "sudo sh -c 'nix-store --import < $remote_nar' && rm -f $remote_nar"
or begin
echo "deploy-to: closure import failed for $host" >&2
return $status
end
echo "deploy-to: importing the closure, recording $system_path, and activating it on $host" >&2
else
echo "deploy-to: $host already has the full closure for $system_path" >&2
echo "deploy-to: recording $system_path and activating it on $host" >&2
end
echo "deploy-to: recording $system_path as the system profile on $host" >&2
ssh -t $host sudo nix-env --profile /nix/var/nix/profiles/system --set $system_path
or begin
echo "deploy-to: system profile update failed for $host" >&2
return $status
end
set remote_script (
string join '; ' \
'system_path=$1' \
'remote_nar=$2' \
'if test -n "$remote_nar"; then nix-store --import < "$remote_nar"; rm -f "$remote_nar"; fi' \
'nix-env --profile /nix/var/nix/profiles/system --set "$system_path"' \
'"$system_path/bin/switch-to-configuration" switch'
)
echo "deploy-to: activating $system_path on $host" >&2
ssh -t $host sudo "$system_path/bin/switch-to-configuration" switch
ssh -t $host sudo sh -e -c (string escape -- $remote_script) sh (string escape -- $system_path $remote_nar)
or begin
echo "deploy-to: activation failed for $host" >&2
echo "deploy-to: deployment failed for $host" >&2
return $status
end

View File

@@ -15,6 +15,9 @@ services:
steamgriddb:
api-key: ENC[AES256_GCM,data:+kBRieIB/dHbvQLbH2gQTQh2j6DQhybVWEGkSCRZrZY=,iv:3h6dhfiV7HfaZhMv93x8+7F3utzYG6fIbTDScA14PVI=,tag:Wo24YnvsIxe7iMRLyxrj2w==,type:str]
screenscraper:
dev:
id: ENC[AES256_GCM,data:nVPM7ijeVQ==,iv:noknaNqxnTx3E8cKtwY9MVkg7Xeq0C1O+7EpdXvz90g=,tag:f/iHVW1Tu93J3VdvURC7Gg==,type:str]
password: ENC[AES256_GCM,data:6v+we9O7PXExTmFZ,iv:DUEj1pei0ryh4AzTRrB9YDVArLCMvyKPbNfvB2IiyzA=,tag:nPPqB/tbsRqv9W7zFNOsdg==,type:str]
user: ENC[AES256_GCM,data:7hGcFF4xqg==,iv:SFOdYej94Thfn+Te0kXsC9Kq568A1dsnaR+JB90MgBQ=,tag:inIWuFY7f5Tvv8EyV71dYA==,type:str]
password: ENC[AES256_GCM,data:eUWwDDQUSytm/6QZlG5XjbSi41nsBQbm4w==,iv:82S2a3rAVjaF0CXcjXUXErNHMY7MEkjpDL7EVX9cIHw=,tag:uGuVJCv8ThsaNnA0C0WR2Q==,type:str]
sops:
@@ -37,7 +40,7 @@ sops:
dyjzy0/9Ui1IIXH36AImO5hiEZcrGePV2qPLLQc7ZLngC1jlJuEKJA==
-----END AGE ENCRYPTED FILE-----
recipient: age1wf5s0n0tgt6ld2ysgu9dc67mj8ylwecgl4utzg7hqwy3kut9zyms7aglmh
lastmodified: "2026-10-20T01:27:06Z"
mac: ENC[AES256_GCM,data:NCt6e/ctZwaBADfrhu1RXtILlClbHEqOAnIu8rE/zMPSwMIPB/kTtfd/ujf+1iRszoHaX4Se1xc0wz1th6NVToFVNarxBBgTk4JMUU8gdMweOjmwlh3GXU3ZcYY/xAXxWF1hxyXSEW78j3S2844H9XXENAq/Bf3mjdCYQ/qr/d4=,iv:4nFyBSmRvF4zSAwvL+xccMwtWrVbHtFKNoTb7f3rSH4=,tag:eCV/7P0TjyMZXKcWBUPXQA==,type:str]
lastmodified: "2026-10-22T16:11:38Z"
mac: ENC[AES256_GCM,data:pkDbhEFKC8fl4SzF3U7F0jqzil9CB803/vIV2mKIDO1MXC8GAOGPW3Q7qfHkCXFkVmXvBvowmJYdR3d/zMO3L4XJPRYR+NpUEpsHjRLq/uSJszWMeCjXQA8DIZxT2fWcp1FGcdpHecrs9QkWTy1OtSDGD+xr0WlHTbLX7bTrtfo=,iv:OfhVrGco4/gBE3V8U7CKDfBGvaCizcEq6x6p/y4X7NY=,tag:/+G5ojgzOlhXW2QM/irWOA==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.3

View File

@@ -1,6 +1,13 @@
ssh-host-ed25519-key: ENC[AES256_GCM,data:NMlzM7E9bXUhNqIbtyERuuPShnlx00ftHyRnyMiLSSzp1R/80n8Hdvv1q3uNmZdSgmBYPKdM34fAHuXgwREBcWlqMXptUNnXMngtqN9vxYcfWxJfcN67Fsw2xG8v1h/L0vqwPSn24RAeB6HTExqvr/5egrfQX0yylWytZLMbSIUeAsY+GGD67TDp3W7RdMfXKDHgvRubRR2YdHu8BNQ+BXgzOWCqAbrnHAk0AUaeUloGYr72+zBAhXmWfrXBJjJoU1H7CZELNtcFYczC0wgKUoTTdx9MiPGN2SEx5hCkqOd6iT4MyuY6ZM0SuXMwl7Lw/U12wOzhqZy/1orzQcyRNzcDYcBepAknth/08631fEX4PJdjzZMnI1VTdGSHs/Adl3PQoXv/WGY+QWc6Pb2I+PyAQTCkX9eCr/Mf+VFnOzu0mDBSK/8i1dGlf1lBul4o9L+x/Gz0Fj5kaA1SJdbqCcgKPtEDvLjvCvwYh215BNXv2UCyIrcYf1Y712/sAAcrHmnViocBbTo/m3FkbdGX,iv:jwHCjzmPWv1IXpTonmohIz7LftotOTG0utynf1PKobQ=,tag:1RjXmbJAPvuSUdtCuNXTlg==,type:str]
ssh-host-rsa-key: ENC[AES256_GCM,data:Xe0cw0U+ydMzaBbLg+P1JfLQq7uRstJRftpKeMnTSawKmWLYY9sAKRo76YykiYoiQSgb9R+LG/bgM6WnlcqvwVqGhozVnTGE9/ueRWgn4pB9sbT3bRB5F5uQFHJvH3wHjOlUYbllxBVQfE90M/13UQYMKSJEJ9ROdYo3Jpe1lGSTnQi4DKjEjQ2NmL4YEKKgiVkHhsujodnd54l8NMfQTsr0Dd8DpCwxKMriu0dxgJ7Io/v5hsw+tIIB+XXf6nRtkjUJlUylVl4qsZuuqXtAipVtq1g3gIf4J4Gl2q+s4TH531RPhp91dFS5Q4IEJJOOH3pt8ZkUXOZeJ+ilZKr/UV3evNTFq62tzUdMUMauQlggHJrrM+ch7wOmL2E0XCgV5Ld9og6CB79LqqK1lXwRJkFMJsiosTO26/A7YRFkvjFHVRksMHo7Z7LLe+r4CIm1uh/+3f3YSt66BIck1lMmIfmpWIq5KeEhUDKMJU5NQyCk6DTWlyCI5Mvoz2utKV2n3MkoL5X93zR5mdk7M+LXtDe6UE8WUeDABfOd9yWPYJO3IM+0eNZpXN+2jIBPpzWGAjI+Ho3jdANYOOaP3TnIL7iYInLJhbsyfDFlqQKhpcOl6HVvkgXpzABjpYBCpvZAJkqgxXByZyFOJP2sU8mUCoPDuIREZ/OYzTa39c7Nb33FhDECkR/DHo6CI91H642phqe+tv6fOHc6lE8aylDpNB/+ecAKYsKGsUrCZ9fjDt6wuNlL6q0jmktEax30nKnIbK93RuKnsQD1tMqGKubqCLOFux8Eq4XUu0xG/uG2x/UDm1mMguBcEvzLOZ+QmKuC/X2kUlzyhYuADukUqAaWftPjwdnlcBwJ/gZWHDUzw1pXv0VvPyhyFB7xHZfBUVdkTTZmKyQUTNmUUKVCMk8FE4l/LtQY1UizE9Yf3xBPEIbZgldlmUErnVXVOgr8ndoH7/9p7PqwDLNrWZeLplCSiNEDKbtx9F570RU5rgL1lAg3PvsMLsNc+4Oyeb6IE7THvUtFclxbkvDpLm6uNVnHjrUxzd5s+Lb3PXStM3p18Ki+dCbYQStEicod+BVuzYwzIw86tujI9s9aKWinqdffX4/hgB3Yhljg3pThPOxuHCei2RKntDrnqtoRnHF05n2GwIMegtnhlcGmbE+TiTxT6fI+9ewZ/f0/QEd/mLKpdOvxrA/ezxj77Vs0PaeWti/rL0WV7jqa/xxt6F2vN8QgetDomyITtTZxCy5z8n7xJUhXbk31ImlNJVZTXeH1eKGMet6XrYDzsByHzPmt30rFQR8iAfGIDjmGiCwqG24AMH4Mc1UhZ9T8k54F6hWifRRZN99fQf4qb+EhxJe+PfAkOHaoJFsfRulOGpUeTFjIiz7V3ndlFY0oMO7GRKJ1rs5xxINacXU4heZoQmLxeXfZokKgg8jxcqndaz8NBpJyU9b+eHvEj2HXFrinUUOUB9zEl1pFy4ljFTESEVVVs9RawTkM1jKQq4xnYKcHaR7ebqCwrOH/KuurIDNQm6VVue1wUQjDD1qe5sZ2S7d1VrphgSj5O05S+3zdC94UxMWPDjZoo+e31qVO8mIqDqb2qvttdz5ia6vTY4M9FZup2SqiF5r3VI3TW+rntSdwoBOT4AEWjRjUDGneggAu9nKe8N0l3abxvO5iC+vE2TF/2/dIiVTpCaMbpMVEUPGOlWIrgZ0xraDVRl+f39w8JkwlSBTAbWlBsrypGjDzMPhXmrsS2/bbQQDG2GVF+6kJuMWMrbDL14+MlhkyHA2DqE1S9hsP2+WHRSePo/J+SKzaiOonnZJv+XnF+mZSK/CupLjaHkYs0DgiWTZFCdM9cYIONUlUBaGvVHuB+ku21zfNjzHieppxe0Mf9z2D+L9vu1tJO5V9X6msnkWwvgRmugwRpGS00vutMFVrwlsVJhSr3D3CBTGpAmsAQdp9uKbAOrdoD42sBka7N75GYuxxZA7PFsRfEZioaPAjZ80QoBxtYe/G25MmWj3QY516H9pNINo7iZtaCqwxhdwe6dlcm7eFk7l0QJRYklTxhtMWWh5ExvBidRG4A47LJ6XLOIh5PjYRbxMYlbhH0bChCAip2957pZqawcC4s6/7WIHmh1t2MRVznN/x4NV1rVn8nkN3Ch+u/atWe1Q9+t8A16n/Yu7iQtplHnRv3PQxg+x2edefLqDzCpS4q11PrWMWHtbv/VA4NjduVylqCMOBUKGI5VYMq35cxMyuOm0AVHxJcPLNR+hUI3ZkREf9JawzaHxf3EfDfN8opiwbeQasbq53845sbiynuwg6JRuznJhD3U+rZ8hbsDUjFOgGS225G8sUxyYwK45BgXTrjv1I56qL6CIyybsJeP4tT2TTKf9bYwL9/UL41nDi3ceIBXCZYjzz13H3D6wpfMxoUNwIeGS00viUoxnKen9XHefOZEHyVGnMcCu7ehT4BTwaOnlEGWYYLax1AP0Tyr0zqEVQMi55PSRMIOD3LSXR/qPGOyeHSMEbbDUC11zhXrXEv1kFiCzqKDA7qA6mz+J/rsMJ4xXzBEsiQlI833a+LJkX1rR/a7llhTDr49IxpSFa8cns3lIkbTXhVx99DJUW0F2sIlyq9kjVeIY1lnvAiAWi/d+5bxo/xudSob+U993CkJbqDcNE0GU2EB4BbB3s1zGeCqDaQ2E/27uCr/7EId/eQkuo60HsAoTugTZeLI3xFbNkJTn92PyA33ki/nAI0wBm8Inv/yGj6f7IXOJEVYwb2LETUbEJ1gr+VJd+qYx+ejzS2NCTrONl7AdpPv/GSzKyvQ3/JH6CWU7iOZdRFRsi1Y9MB6x54hPbtaEAusAG8IEFSmnHomx5tEL01gsiYsG8B6v30/Yq2UF+tNcjdgLspuhMMpJY9WfeGFg7gazKfSvKxuFgUYYkDQmr2Eqq7jhVvI2vZHQZ8nvn2AGPyxJjuFxeoVAHGQ/UZILiuPaBYYzXSEB7KUDekkh6HaU1mMW0SWxu1K0xYDWYMelIClMRND65Ll4rjfF9xeC9Ft6htD4MFYij70PPPogfh7qIRa1MJb+Lq6YSFj48RMV+S62w1pojoqjmj6n2PhSrOrXyXCqrJHiRBshnzbkpmYH+pQNrg2JojrpZYRKxOJ9Hr0y1DZKPjTcrbuqhiKSNpdMsiK9pkOx56ps5ZCTz86G0HTMxk087xjOgAow4votKXRUFSwZaysfhCKqbiEfKrF37oYyxo86rKVWEA8Dn2MFgdZskFe1nlOszGm78oeu3o9JazfdpXZcg/y3eGuOs0aCEvycVd+EnVJKsztqoLr5eFUY2IlxvUh3ZeXKeQoJaDQek9eM14rQRb6UcbiervIYyWFG8DW2Ryxg7caRClY22sH7iyr0AHrIPVpCBNOsehSsvgPsKih9UqSpsu3ukzITG1q2cEPO6PuE/l0C0hYPsoXK41DA+1hXT1j+D0xYe5AHH3XMMav7p9y7iwZaMvCd+ohVoy+/a2LYKqpLgZq1gOdgD72ES+jnHqLwVMJsAcVnuhku3ENrUNPsxIgimpIS/m9ZeKfGu0pjlxjV87aTAzNDA59kS7XodceRZuou+OTViUVaL/8yKWtDXwLFttakuG6F78hPPp+pe+z41iqUiIAeCbZYwQXnVmc1jGp/qq10PUe5VFf2G53Lz7NCJidtKb0ydbVO3vgf+Lny4lBgXK1sKEdrTw0cOmTpTfqiE7UhRMKUn7UYEsEnX/GP+HQQBEYGA9QJE4INbU7054iSZRdAEpByt15TarSZk7M5jhernXp/vtdD+nekUmWvkxfsgOxLRuT5T2IpxxcS5Ogq5eTVfkSYMPOe1gzEF673BNOr+jKcoU4uKh1BA3MpIoBo++zp/iu6V0GXmmrl9tBv5m2/EAxcoMxS/+RB8hpXsftOPBGI5YxF74unBLeGNkcnELfhAnALpmb27x0x6n39ZpdqQqe6PbEw0K7WCzb4J6ObEuOsJS5yvJZEyxRTJyV0wqy+p2EDHjdl7QDZ6mhE0W4dIMfZO9yzJrNT9ya2rLKtJZsXoVhCCBeUrT1bcpFOotIJr/CmH6G3gAcg9u4e4c5WcHYA/pCwCen+2tsfNHcuYU3S5n21+IamEXZG4Gi0KteSYSV35T660AIlx/N21kT8m6QOSHmQRrd99/JR9C9667k5TMFbTJuSoZQ/bAtw2BmTukquia1WW172niVXNddelwakpjmbHa7b16r3yPv4EIqLz2911RdacjGcTQXmChwrlrMzKtLeLINtnLQytGWUh6uo34wcxgn70qsmArXDaXJXTeJrQ0E9v473mg31jx7Ty3SbQLs55vaRI4I2HicmjX3zOvafILc6NnQZ7C8i0bCpojjXE4Iisa4e5IN4o1J+MBHLaWl3PcqrsorFoacfa47YKPsb29+e6vkxdx77JFRDr2fWJOFrrpt9VGkB8X7n7D1azmWuS9/T8Zb6cGWMhdGY3YFq15qjsxT+NNfx2szbi,iv:i1s5hIhvWr+0Oood4Kftw+zH1sHoyKG7PnCME9zxATM=,tag:/83WXGYbnxRDjJZMB3rQtA==,type:str]
ssh-user-ed25519-key: ENC[AES256_GCM,data:xu3YkQnuaVC2YWGPrXfxbgY3PruxfwKMaeBSwyf6HiGOQFn3rlBpcfjtktBayVJkQzdhjK0sMqEBpvljrIOKRDdUC13jCXlvbT9D9P4AtGjzdLO672WZxZ/e+B2jtJGP0qfOo9SSPgsWijbNTy0SXkQoaO5zQe3HUWSH0CSNiNtY88YRodIDcJQVZrwVJndGsHnC5ELVZ60sdzh7rZQeKz+cvigMDpkORcs3QhUe9PFpsdl9emjSlWq+8zJVas6h2mp+ajgGMl8qrb+9mVJMDQIpEmW/HEQIHYLAuIPxyZ43iIeQZC5oPYy518tGZNJUC3KHceTQJgGWHPM707TU1/M7/W47i79TvjWccyQpIv9SQ7G9RJXhzMD15judb5cyQfwobqJ0w4VjTxao3hESZUho8ZMFAyNtXf7IqUgKMmbJ+o4pZTS8Mqyp0ZfjddP7FxnHkPiORmxQ3v2fGqtGTlJ4ODZO/vYZTmwsampWJA2J9FkMihqggI74yyoS8006mkY9kNpeTzNUeF5flFS/,iv:op7zSdx7335l8n/Q3pCM/9enb00Hbs+m3F5NUynNUXw=,tag:F5AE+gB1H1iVXSQWaliWKg==,type:str]
retroarch:
romm:
user: ENC[AES256_GCM,data:Jc1/7lhAhw==,iv:1yFG+9LFbyOHT+xQ3+ZlIiiG4VY7h/FKlgKzgTLV2FA=,tag:6nyway08U8DxFISZQnpmTA==,type:str]
password: ENC[AES256_GCM,data:10KB8nD2HN2FQhWGMoCwxUjzwxA=,iv:xxge3Y7N3T7B7OBIBs7PtmuPXQfrda/bU4LxldGjzuE=,tag:J0Ik7KNZH5mAlHeidZjSYg==,type:str]
retroachievements:
user: ENC[AES256_GCM,data:G6+e2BW8,iv:BJD3nFLWyWX8pFFCKZENOSi6FxCy+BAXOmgdjrCCIak=,tag:yb3VNgBQEN5x7Guqlh8eyg==,type:str]
password: ENC[AES256_GCM,data:kOfDyJK+3WxGDFhu,iv:q/lWpCuuCeyC61QwYQ85oYrXxtiVGyF3UccOVkXt8+w=,tag:k2pwo7R6nmNLiTUp7f70nA==,type:str]
sops:
age:
- enc: |
@@ -21,7 +28,7 @@ sops:
ZQg0XgccFlZjIBjp5jg2rDq0qG/tFkvIuX9C3udSxyGX+1MRutyhpw==
-----END AGE ENCRYPTED FILE-----
recipient: age199atsykyh0hp0yuxymznxs0mqv4ram9ttaddk6jrsjjc43rmvu7snh9dq8
lastmodified: "2026-09-05T00:59:23Z"
mac: ENC[AES256_GCM,data:zYZoZ3vxcIHTJu2rdEqhsmkCz0ylRo6xXTEQnfBbyeAOP0QD72czo0VfUPHVcZVnLhT3yfwMXjotM2ScwTkUinouOXdW0+jIgHd9U3Q949an0IJGNJVljd3/rN5SknEoTqbA/GRk/uTNUu4A1usWqrp1gSPHFxzALKg/CFJoIag=,iv:SXamTie6plAiFOlZ2rGkpAlRYPzAKfqht0V+stt23jE=,tag:w8xq4orRj9SbDaZ+Y46Epw==,type:str]
lastmodified: "2026-10-22T16:53:08Z"
mac: ENC[AES256_GCM,data:bfXTV8SOfZ88nofmvrzU6CXa/apXHsMvLh0yz3oI9QWyvVmBzW8iv5oUmItQ/GBBM3t3WkFwqqo9sHmImT0GOOg4C8DUUwn/3DP1ehOA6xy/ZDBGNmVZuwqM0boJMRkJBeIYoDX5bChRo7Eg+hMPconLCoqmfZhaIldy6IMOFlY=,iv:4gzsrRKlbcqgkugRdXvtH7KKCUh03oYZyokTtGV8egQ=,tag:+mgqrd+j9pvyFgTwZj5CEw==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.3

View File

@@ -5,7 +5,7 @@
"livePreview": false,
"readableLineLength": false,
"strictLineBreaks": true,
"propertiesInDocument": "visible",
"propertiesInDocument": "hidden",
"showLineNumber": true,
"autoPairMarkdown": false,
"useTab": false,

View File

@@ -4,21 +4,21 @@
"type": "split",
"children": [
{
"id": "81c4860f495113bf",
"id": "880908ac22302761",
"type": "tabs",
"children": [
{
"id": "daab69fc3faab88f",
"id": "10ea163afc7dbc62",
"type": "leaf",
"state": {
"type": "markdown",
"state": {
"file": "00 INBOX/credits.md",
"file": "TODO.md",
"mode": "source",
"source": true
},
"icon": "lucide-file",
"title": "credits"
"title": "TODO"
}
}
]
@@ -91,7 +91,7 @@
"state": {
"type": "localgraph",
"state": {
"file": "00 INBOX/credits.md",
"file": "TODO.md",
"options": {
"collapse-filter": true,
"search": "",
@@ -217,34 +217,34 @@
"table-editor-obsidian:Advanced Tables Toolbar": false
}
},
"active": "daab69fc3faab88f",
"active": "10ea163afc7dbc62",
"lastOpenFiles": [
"00 INBOX/ai/tldr-design.md",
"00 INBOX/ai/adguard-pikachu-migration-design-tree.md",
"00 INBOX/ai/caps-escape-design.md",
"03 wrk/tasks/0016-romm-manual-rom-migration.md",
"03 wrk/tasks/0015-romm-5-4-package.md",
"00 INBOX/ai/retroarch-zeus-design-tree.md",
"TODO.md",
"00 INBOX/ai/romm-5-4-deployment-changes-design-tree.md",
"00 INBOX/ai/romm-migration-design-tree.md",
"00 INBOX/ai/romm-5-4-package-design-tree.md",
"00 INBOX/ai/steamvr-quest-3-design-tree.md",
"AGENTS.md",
"03 wrk/homelab.md",
"00 INBOX/ai/nixos-agents-style-scope-design-tree.md",
"03 wrk/tasks/0014-romm-guest.md",
"00 INBOX/ai/tldr-design-tree.md",
"00 INBOX/ai/tldr-design.md",
"00 INBOX/ai/valheim-items.md",
"03 wrk/tasks/0013-oidc-provider.md",
"03 wrk/tasks/0002-nixos-refactor.md",
"03 wrk/tasks/0011-gitea-axi-rust-migration.md",
"00 INBOX/credits.md",
"00 INBOX/ai/grill-skill-design.md",
"03 wrk/tasks/0012-gitea-axi-pr-merge-guardrail.md",
"03 wrk/tasks/0011-gitea-axi-rust-migration.md",
"00 INBOX/ai/gitea-axi-pr-merge-guardrail.md",
"00 INBOX/ai/gitea-axi-rust-migration.md",
"00 INBOX/valheim progression.md",
"03 wrk/tasks/0010-fix-main-monitor-detection.md",
"00 INBOX/credits.md",
"03 wrk/tasks/0009-bazel-project-scaffolding.md",
"03 wrk/tasks/0005-rust-cli-scaffolding.md",
"03 wrk/tasks/0008-vim-obsidian-markdown-renderer.md",
"03 wrk/tasks/0002-nixos-refactor.md",
"TODO.md",
"technical-writing.md",
"dictionary.md",
"03 wrk/tasks/0007-obsidian-source-notes.md",
"03 wrk/tasks/0004-dictionary-cli.md",
"03 wrk/tasks/0006-wallpaper-randomizer.md",
"03 wrk/tasks/0003-whisper-dictation.md",
"03 wrk/monorepo.md",
"03 wrk/tasks/0001-todo-cli.md",
"03 wrk/productivity.md",
"03 wrk/tasks",
"00 INBOX/let order name resolution.md~",
"00 INBOX/nix name resolution.md~",

View File

@@ -0,0 +1,110 @@
# AdGuard migration to Pikachu: design tree
## Research facts
- The pinned Nixpkgs revision provides `adguardhome` version `0.107.79`.
- NixOS provides the `services.adguardhome` module.
- The module runs AdGuard Home as a hardened dynamic systemd user.
- It stores state in `/var/lib/AdGuardHome`.
- It grants `CAP_NET_BIND_SERVICE` for DNS port 53.
- It grants `CAP_NET_RAW` when AdGuard Home DHCP is enabled.
- The module supports an arbitrary YAML configuration through `settings`.
- The module supports fully declarative operation with `mutableSettings = false`.
- In fully declarative operation, the module replaces `/var/lib/AdGuardHome/AdGuardHome.yaml` on each service start.
- Fully declarative operation requires `dns.bootstrap_dns` as a list.
- Declarative settings pass an AdGuard configuration check during the build.
- `mutableSettings = true` merges declared settings into UI changes at service start.
- `mutableSettings = false` replaces the state configuration on every service start.
- `openFirewall` opens only the web interface port.
- The migration must explicitly open DNS ports 53/TCP and 53/UDP when clients need them.
- Pikachu has address `10.23.10.102` on its untagged network.
- Pikachu carries VLAN 20 and creates bridge `br-vlan20`.
- The guest framework runs each guest in its own systemd-nspawn container.
- Each guest attaches to a host VLAN bridge and receives an address through DHCP.
- Pikachu currently configures only VLAN 20 for guests.
- The repository has no AdGuard configuration, Proxmox inventory, or DHCP-provider configuration.
- The current DHCP provider remains unestablished.
- The current Proxmox AdGuard container uses MAC `bc:24:11:91:60:bf`.
- The current Proxmox AdGuard container uses address `10.23.20.33` on VLAN 20.
- This environment has no Proxmox SSH target or local Proxmox control utility.
- The current AdGuard configuration listens on all guest addresses at DNS port 53.
- The current AdGuard configuration has no allowed-client or disallowed-client restrictions.
- The current AdGuard configuration uses Cloudflare, Google, and Quad9 encrypted upstreams in parallel.
- The current AdGuard configuration disables DNSSEC.
- The current AdGuard configuration defines three local rewrites.
- The repository configures no host backup system or monitoring system.
- Hosts update through explicit `nixos-rebuild switch --flake` commands.
- The working tree has unrelated operator changes.
## Service tradeoffs
| Service | NixOS support | Domain filtering | Administration UI | DHCP | Encrypted DNS | Main tradeoff |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| AdGuard Home | `services.adguardhome`; checked YAML and mutable or immutable state | Native block lists, rules, and per-client policy | Native | Native | Upstream and client-facing encrypted DNS | Direct migration with the closest state and feature match. UI state conflicts with fully declarative operation unless configuration is immutable. |
| Blocky | `services.blocky`; checked declarative YAML | Native lists, allow lists, rewrites, and per-client groups | No UI; REST API and metrics only | No | Encrypted upstreams and client-facing DoH, DoT, and DoQ | Strong Git-managed configuration and low operational surface. It lacks an interactive administration interface and DHCP. |
| Pi-hole | `services.pihole-ftl` plus `services.pihole-web`; declarative TOML, lists, and dashboard | Native gravity lists, allow lists, regex rules, and groups | Native | Native | Use a companion for encrypted upstream DNS | Familiar filtering product with a dashboard. The migration changes product data and uses more coupled services. |
| Technitium DNS Server | `services.technitium-dns-server`; service and firewall options only | Native block lists and DNS applications | Native | Native | Upstream and client-facing encrypted DNS | Broadest DNS feature set, including authoritative DNS. NixOS does not expose its runtime configuration declaratively. |
| Unbound | `services.unbound`; structured server and free-form settings | Local zones or response-policy zones require custom policy management | No | No | DNS-over-TLS support | Mature validating recursive resolver. Filtering and user operations require separate design. |
| dnscrypt-proxy | `services.dnscrypt-proxy`; declarative TOML | Local block and allow rules, without a list-management UI | No | No | Encrypted upstream DNS, including DNSCrypt and DoH | Strong upstream privacy and resolver selection. It is not a full network filtering appliance. |
| Knot Resolver | `services.kresd`; structured listener settings and extra Lua | Custom policy modules | No | No | Client-facing DoH and TLS listeners | Flexible high-performance recursive resolver. Filtering requires resolver expertise and custom policy. |
| SmartDNS | `services.smartdns`; free-form settings | Domain and address rules | No NixOS-integrated UI | No | Encrypted upstream and client-facing DNS | Optimizes upstream selection and latency. It offers less complete filtering management than dedicated blockers. |
`Native` means that the product provides the feature without a companion service.
## Decision tree
- **D1 — DNS service product** [settled]
- Retain AdGuard Home on Pikachu.
- Constraint: Preserve AdGuard Home as the DNS service product.
- **D2A — AdGuard Home role** [settled]
- Provides DNS filtering and resolution only.
- Constraint: The existing DHCP provider continues to assign addresses and DHCP options.
- **D3A — Execution placement** [settled]
- Runs in a dedicated guest on Pikachu's existing VLAN 20.
- Constraint: Isolate AdGuard Home from the Pikachu host.
- **D4A — Guest network identity** [settled]
- Reuses MAC `bc:24:11:91:60:bf` and address `10.23.20.33` on VLAN 20.
- Constraint: Stop the Proxmox container before the guest starts.
- **D5A — Network exposure** [settled]
- Listens on all guest addresses at port 53.
- Accepts requests from every reachable internal device.
- **D6A — Resolver policy** [settled]
- Preserves the existing upstream resolvers and parallel resolver mode.
- Preserves disabled DNSSEC and the three local rewrites.
- **D7A — Configuration authority** [settled]
- Uses fully declarative Nix configuration.
- Constraint: Set `services.adguardhome.mutableSettings = false`.
- Constraint: Declare `dns.bootstrap_dns` as a list.
- Constraint: Web UI changes do not persist.
- Constraint: Match existing service behavior except for the administrator password.
- **D8A — Migration data** [settled]
- Migrate functional DNS configuration only.
- Constraint: Do not migrate query history or statistics.
- **D9A — Operations** [settled]
- Declares a newly generated bcrypt verifier in Nix.
- Constraint: Keep the plaintext password outside the repository and Nix store.
- Constraint: The verifier is readable from the Nix store.
- **D10A — Cutover and rollback** [settled]
- Uses a planned atomic cutover with rollback.
- Stop the Proxmox container before starting the guest.
- Validate the guest before restoring the Proxmox container during rollback.
- **D11A — Runtime-state backup** [settled]
- Do not back up rebuildable AdGuard Home runtime state.
- **D12A — Monitoring** [settled]
- Do not add DNS availability monitoring.
- **D13A — Upgrade ownership** [settled]
- The normal flake update and explicit NixOS rebuild workflow owns upgrades.
- Replace AdGuard Home. [not selected]
- **D2B — Replacement product** [not applicable]
- Choose Blocky, Pi-hole, Technitium, or a composed resolver.
## Frontier
None.
## Sources
- Pinned Nixpkgs module: `nixos/modules/services/networking/adguardhome.nix`.
- Pinned Nixpkgs modules: `blocky.nix`, `pihole-ftl.nix`, `pihole-web.nix`, `technitium-dns-server.nix`, `unbound.nix`, `dnscrypt-proxy.nix`, `knot-resolver.nix`, and `smartdns.nix`.
- AdGuard Home README, release `v0.107.79`.
- Technitium DNS Server README, current upstream.

View File

@@ -1,39 +0,0 @@
# Caps Escape Design
## Facts
- Commit `250b0b71f` removed `services.xserver.xkb.options` from the shared host module.
- The removed option was `caps:escape_shifted_capslock`.
- That XKB option maps Caps Lock to Escape.
- Shift+Caps Lock still enables Caps Lock.
- The shared host module configures Neogaia, Zeus, and Pikachu.
- Zeus has QMK firmware that provides equivalent keyboard behavior.
- Neogaia and Zeus enable `modules.host.cachyos` and `modules.desktop`.
- Pikachu enables neither module.
- `configs/nixos/hosts/neogaia/default.nix` directly sets Neogaia-specific hardware, networking, desktop application, and Waybar settings.
- The current configuration has no Caps Lock or Escape remapping.
- This session runs on Zeus, not Neogaia.
## Design Tree
- Restore Caps Lock behavior
- Scope the configuration
- Configure Neogaia only. ✓
- Select the mapping behavior.
- Restore `caps:escape_shifted_capslock`. ✓
- Set `services.xserver.xkb.options` in `configs/nixos/hosts/neogaia/default.nix`. ✓
- Define validation for Neogaia.
- Run static checks and manually verify both keys after deployment. ✓
- Stop after source and static validation. ✓
## Settled Decisions
1. Configure the mapping on Neogaia only.
2. Restore `caps:escape_shifted_capslock`.
3. Set `services.xserver.xkb.options` directly in Neogaia's host configuration.
4. Run static checks and manually verify both keys on Neogaia after deployment.
5. Stop after source and static validation.
## Frontier
None.

View File

@@ -0,0 +1,98 @@
---
date-created: 2026-10-21 08:47
date-modified: 2026-10-21 09:26
---
# RetroArch on Zeus design tree
## Facts
- Zeus is an `x86_64-linux` desktop with AMD graphics, 32-bit graphics support, Bluetooth, Hyprland, and Home Manager.
- Nixpkgs provides `pkgs.retroarch.withCores` for an explicit set of Libretro cores.
- Nixpkgs provides `pkgs.retroarch-free` and `pkgs.retroarch-full` for broad core sets.
- The packaged RetroArch version is 1.22.2 and includes Cloud Sync on Linux.
- Linux Cloud Sync uses a WebDAV endpoint.
- RomM requires RetroArch 1.22 or newer for this integration.
- RomM documents configuration synchronization as part of its Cloud Sync setup.
- Cloud Sync supports saves, configuration, thumbnails, and system files as separate categories.
- RomM matches a save or state to a ROM by its file name without the extension.
- Local ROM files must use the same names as their RomM library records.
- RomM 5.4 provides a WebDAV endpoint for RetroArch Cloud Sync.
- The endpoint is `https://games.alexion.dev/api/sync/retroarch/`.
- RomM stores its ROMs and BIOS files on Pikachu.
## Constraints
- Install RetroArch on Zeus.
- Start with PlayStation and Sega Saturn cores.
- Evaluate RetroArch Cloud Sync.
- Keep package selection declarative.
- Keep credentials out of the Nix store and version control.
## Decision tree
- D1 — Initial core selection [settled]
- Use `beetle-psx-hw` for PlayStation.
- Use `beetle-saturn` for Sega Saturn.
- D4 — Core package viability [settled]
- Both core packages support `x86_64-linux`.
- Neither package is marked broken or unfree.
- D5 — Package composition [settled]
- Build a RetroArch wrapper with `beetle-psx-hw` and `beetle-saturn` only.
- D2 — Cloud Sync data scope [settled]
- Sync saves, states, and configuration.
- Do not sync thumbnails or system files.
- D6 — Sync endpoint [settled]
- Use RomM's RetroArch WebDAV endpoint.
- D7 — Sync identity [settled]
- Use the existing RomM account.
- D15 — Credential injection [settled]
- Store the RomM password in RetroArch's writable user configuration.
- D8 — Synchronization mode [settled]
- Synchronize automatically at startup and after play sessions.
- D16 — Launch order [settled]
- Do not add a game-launching frontend.
- Start RetroArch before loading local ROM content.
- D9 — Conflict and destructive-sync policy [settled]
- Use non-destructive synchronization.
- RetroArch backs up a local file when RomM reports its deletion.
- D3 — ROM and firmware access [settled]
- Keep ROMs and BIOS files locally on Zeus.
- D10 — Mount or replication method [settled]
- Do not configure a remote mount or replication method.
- D11 — RetroArch directory settings [settled]
- Use `~/.local/share/retroarch/`.
- Use `roms/`, `system/`, `saves/`, and `states/` subdirectories.
- Keep saves and states outside the content directory.
- Sort saves and states into per-core folders.
- D12 — User-configuration policy [settled]
- Nix declares the package, cores, and directory defaults.
- RetroArch manages and synchronizes its writable configuration.
- D13 — User-facing integration [settled]
- Do not add a user-facing integration beyond RetroArch's desktop entry.
- D14 — Validation [settled]
- Verify the NixOS evaluation and a manual Cloud Sync round trip.
## Frontier
No decisions remain open.
## Decision log
- D13: Do not add a user-facing integration beyond RetroArch's desktop entry.
- D16: Do not add a game-launching frontend. Start RetroArch before loading local ROM content.
- D14: Verify the NixOS evaluation and a manual Cloud Sync round trip.
- D12: Nix declares the package, cores, and directory defaults. RetroArch manages and synchronizes its writable configuration.
- D11: Use `~/.local/share/retroarch/` with `roms/`, `system/`, `saves/`, and `states/` subdirectories.
- D9: Use non-destructive synchronization.
- D8: Synchronize automatically at startup and after play sessions.
- D15: Store the RomM password in RetroArch's writable user configuration.
- D5: Build a RetroArch wrapper with `beetle-psx-hw` and `beetle-saturn` only.
- D3: Keep ROMs and BIOS files locally on Zeus.
- D10: Do not configure a remote mount or replication method.
- D7: Use the existing RomM account.
- D7 note: RomM accepts API client tokens as Bearer authentication. RetroArch Cloud Sync only supports WebDAV Basic authentication.
- D6: Use RomM's RetroArch WebDAV endpoint.
- D2: Sync saves, states, and configuration. Do not sync thumbnails or system files.
- D11: Keep saves and states outside the content directory. Sort them into per-core folders.
- D1: Use `beetle-psx-hw` for PlayStation and `beetle-saturn` for Sega Saturn.
- D4: Both selected core packages support Zeus and have no broken or unfree package status.

View File

@@ -0,0 +1,107 @@
---
date-created: 2026-10-20 19:15
date-modified: 2026-10-20 19:20
---
# RomM 5.4 deployment changes
## Known facts
- The deployed Nixpkgs package is RomM 5.2.0.
- RomM 5.4.0 requires Python 3.14 or later.
- The pinned Nixpkgs contains Python 3.14.
- The pinned Python package set lacks some RomM 5.4.0 dependency versions.
- For example, it provides RQ 2.10 while RomM requires RQ 2.12 or later.
- In this design, upstream means the Nixpkgs repository.
- Nixpkgs upstream still packages RomM 5.2.0.
- Two open Nixpkgs pull requests update RomM to 5.4.0.
- Pull request 574089 adds a required compatibility patch for Nixpkgs Redis 8.0.1.
- Pull request 574089 has not run the NixOS module test.
- The existing `romm` derivation already uses Python 3.14 from the pin.
- The package can override the current derivation without copying it.
- The override evaluates successfully with the RomM 5.4.0 source and frontend hashes.
- RomM reads `config.yml` from `/var/lib/romm/config/config.yml`.
- Systemd tmpfiles `L+` replaces that path with a generated Nix-store symlink.
- NixOS modules use `L+` symlinks for declaratively managed files in persistent application directories.
- The existing Nixpkgs RomM NixOS test starts the application with PostgreSQL, Redis, nginx, and the filesystem watcher in a local VM.
- The test does not deploy to Pikachu.
- The automated update changes only the version, source hash, and frontend dependency hash.
- The manual update also removes a Redis maintenance-notification import and argument.
- RomM does not require the `rom-converto` executable while `ROM_CONVERTO_ENABLED` is false.
- As checked on 2026-10-20, both pull requests remain open.
- Neither pull request has a review or a discussion comment.
- Both pull requests request review from the RomM package maintainers.
- Both pull requests pass evaluation and lint checks.
- Their package-test checks have a neutral result.
- The Darwin package-test jobs remain queued.
- RomM 5.3.0 added a required `filesystem.structure` declaration.
- RomM will not start without that declaration.
- The existing mounts match Structure A.
- Structure A maps ROMs to `roms/{platform}/{game}`.
- Structure A maps firmware to `bios/{platform}`.
- RomM 5.4.0 enables `emulatorjs.auto_save_sync` by default.
- RomM 5.4.0 adds optional in-place library conversion through `rom-converto`.
- Library conversion requires `ROM_CONVERTO_ENABLED=true`.
- Conversion deletes original files after conversion.
- The task requires declarative `config.yml` management.
- The task requires disabled browser save synchronization.
- The task requires disabled library conversion.
- The task retains the IGDB, ScreenScraper, and SteamGridDB configuration.
- The task reuses the Nixpkgs `services.romm` module.
## Sources
- [RomM 5.3.0 release](https://github.com/rommapp/romm/releases/tag/5.3.0)
- [RomM 5.4.0 release](https://github.com/rommapp/romm/releases/tag/5.4.0)
- [RomM 5.4.0 configuration example](https://github.com/rommapp/romm/blob/5.4.0/examples/config.example.yml)
- [RomM 5.4.0 Python requirement](https://github.com/rommapp/romm/blob/5.4.0/pyproject.toml)
- [[0015-romm-5-4-package]]
## Decision tree
- RomM 5.4 deployment
- Package runtime
- Settled: Build the local package with Python 3.14.
- Package source
- Settled: Maintain a local package.
- Constraint: Do not rely on either unreviewed Nixpkgs pull request.
- Package exposure
- Settled: Expose the package as `pkgs.alexion.romm`.
- Library layout
- Settled: Manage `config.yml` declaratively with Nix.
- Configuration materialization
- Settled: Replace the configuration file with a Nix-store symlink on activation.
- Settled: Declare Structure A.
- Settled: Set `filesystem.structure.default` to `roms/{platform}/{game}`.
- Settled: Set `filesystem.structure.firmware` to `bios/{platform}`.
- File safety
- Settled: Keep library conversion disabled.
- Settled: Do not enable `ROM_CONVERTO_ENABLED`.
- Browser emulation
- Settled: Set `emulatorjs.auto_save_sync` to `false`.
- Existing integration
- Settled: Reuse `services.romm`.
- Settled: Keep IGDB, ScreenScraper, and SteamGridDB configuration unchanged.
- Validation
- Settled: Build the local package and the Pikachu configuration.
- Settled: Do not run a local NixOS VM test.
- Settled: The operator validates the deployment on Pikachu.
## Settled decisions
- Build a local RomM 5.4.0 package.
- Do not rely on either unreviewed Nixpkgs pull request.
- Expose the package as `pkgs.alexion.romm`.
- Reuse `services.romm`.
- Generate `config.yml` with Nix.
- Replace `config.yml` with a Nix-store symlink on activation.
- Declare Structure A with `roms/{platform}/{game}` and `bios/{platform}`.
- Keep `ROM_CONVERTO_ENABLED` disabled.
- Set `emulatorjs.auto_save_sync` to `false`.
- Keep IGDB, ScreenScraper, and SteamGridDB configuration unchanged.
- Build the local package and Pikachu configuration.
- Do not run a local NixOS VM test.
- Let the operator validate the deployment on Pikachu.
## Frontier
No in-scope decisions remain.

View File

@@ -0,0 +1,109 @@
---
date-created: 2026-10-20 17:45
date-modified: 2026-10-20 17:52
---
# SteamVR to Meta Quest 3 design tree
## Goal
Stream PCVR from Zeus to a Meta Quest 3.
The design must produce a repeatable and diagnosable SteamVR session.
## Observed facts
- Zeus runs NixOS with a CachyOS kernel.
- Zeus uses an AMD Radeon RX 9070-series GPU with the `amdgpu` driver.
- Zeus runs Hyprland on Wayland.
- Zeus has Steam and Gamescope configured.
- SteamVR 2.17.10 is installed.
- SteamVR includes Valve's Linux `vrlink` driver.
- A previous `vrlink` session detected the Quest 3 and its Touch controllers.
- That session selected 90 Hz from the Quest's 72, 80, 90, and 120 Hz modes.
- SteamVR setup lacks `cap_sys_nice` on `vrcompositor-launcher`.
- NixOS can declare host packages, permissions, services, and diagnostics.
- Steam owns the Quest pairing and SteamVR user settings.
- Zeus connects by Ethernet at `10.23.50.221`.
- The local Wi-Fi scan finds the `Tracion` 5 GHz network at 5220 MHz.
- The scan reports a 1170 Mbit/s link rate.
- The operator has no external tracked devices.
## Decision tree
- **D1: Host platform commitment** — Settled: Linux-native on the current NixOS installation.
- Linux-native on the current NixOS installation.
- **D2: Transport** — Blocked by D1
- Wireless LAN. — Settled: primary transport.
- **D3: Streaming stack** — Settled: SteamVR with its built-in VRLink driver.
- SteamVR with its built-in VRLink driver.
- **D3a: SteamVR acceptance criteria** — Settled: reliable session test.
- Run a SteamVR game for 30 minutes at 90 Hz.
- Verify visuals, audio, and Touch controllers.
- Verify one reconnect.
- ALVR only after SteamVR fails the agreed acceptance criteria.
- **D4: Wi-Fi topology** — Settled: keep the current access point.
- Keep the current access point.
- Add a dedicated access point.
- Use a direct Wi-Fi link.
- **D5: Session performance target** — Settled: selectable refresh-rate profiles.
- Selectable refresh-rate profiles.
- **D5a: Default refresh rate** — Settled: 90 Hz.
- **D5b: Included refresh rates** — Settled: 72, 80, 90, and 120 Hz.
- **D6: Network isolation** — Settled: existing trusted LAN.
- Existing trusted LAN.
- Dedicated VR VLAN or SSID.
- **D7: Controller and tracking scope** — Settled: Quest Touch controllers only.
- Quest Touch controllers only.
- Also support external tracked devices.
- **D8: Launch workflow** — Settled: automatic service and headset connection.
- Start SteamVR when Steam Link connects from the Quest.
- **D9: Diagnostics and recovery** — Settled: capture health checks and logs.
- Capture SteamVR logs and health checks.
- Recover failed sessions manually.
- USB tether.
- **D10: USB streaming stack** — Blocked by D2
- ALVR over USB.
- Meta Quest Link through a Windows installation.
- **D11: USB role** — Blocked by D2
- Primary transport.
- Fallback transport.
- Both wireless LAN and USB tether.
- **D12: Primary transport** — Blocked by D2
- **D13: Fallback behavior** — Blocked by D12
- **D14: System integration** — Settled: hybrid configuration.
- NixOS manages system integration.
- Steam manages Quest pairing and user settings.
- Dual-boot or replace NixOS with Windows.
- **D15: Windows streaming stack** — Blocked by D1
- Steam Link.
- Virtual Desktop.
- Meta Quest Link or Air Link.
- ALVR.
- **D16: Windows installation scope** — Blocked by D1
- Dedicated VR installation.
- General-use Windows installation.
- **D2: Transport** — Blocked by D1
- Keep NixOS and add a separate Windows VR host.
- **D17: Host placement** — Blocked by D1
- **D15: Windows streaming stack** — Blocked by D17
- **D2: Transport** — Blocked by D17
## Frontier
None.
## Settled decisions
- **D1: Host platform commitment** — Linux-native on the current NixOS installation.
- **D2: Transport** — Wireless LAN as the primary transport.
- **D4: Wi-Fi topology** — Keep the current access point.
- **D5: Session performance target** — Selectable profiles at 72, 80, 90, and 120 Hz.
- **D5a: Default refresh rate** — 90 Hz.
- **D5b: Included refresh rates** — 72, 80, 90, and 120 Hz.
- **D6: Network isolation** — Existing trusted LAN.
- **D7: Controller and tracking scope** — Quest Touch controllers only.
- **D3: Streaming stack** — SteamVR with its built-in VRLink driver.
- **D3a: SteamVR acceptance criteria** — 30-minute session at 90 Hz, with visuals, audio, Touch controllers, and one reconnect.
- **D8: Launch workflow** — Start SteamVR when Steam Link connects from the Quest.
- **D9: Diagnostics and recovery** — Capture health checks and logs. Recover failed sessions manually.
- **D14: System integration** — NixOS manages system integration. Steam manages Quest pairing and user settings.

0
docs/03 wrk/homelab.md Normal file
View File

View File

@@ -1,3 +1,7 @@
---
date-created: 2026-10-19 02:39
date-modified: 2026-10-20 17:45
---
# Brain Agent Instructions
This repository is an Obsidian vault for an execution-focused second brain.
@@ -32,6 +36,3 @@ Include negative decisions only when they prevent a likely, harmful, or repeated
Remove template instruction text when writing a note from a template.
## Gotchas

View File

@@ -51,11 +51,8 @@
- lidarr (requires raichu)
- immich
- romm
- gitea
- nginxproxymanager (gradual migration to nginx guest)
- adguard
- manage DNS rewrites for local service names such as `home`
## Raichu Proxmox -> Nixos
@@ -110,8 +107,3 @@
- Investigate Rust Bazel target structure.
Prefer clear per-file dependency edges, but `rust_library` maps each target to a crate.
Decide whether wrapper rules or another pattern can improve graph clarity without forcing artificial crate boundaries.
## Nix Refactor
Review Nix files in entrypoint order: