Commit 86f01761955f
Changed files (7)
systems
aion
common
services
kyushu
rhea
sakhalin
systems/aion/extra.nix
@@ -24,6 +24,7 @@
host = "rhea.sbr.pm";
user = "vincent";
paths = [
+ "/neo/audiobooks"
"/neo/documents"
"/neo/music"
"/neo/pictures"
systems/common/services/homepage.nix
@@ -1,4 +1,9 @@
-{ lib, globals, ... }:
+{
+ lib,
+ config,
+ globals,
+ ...
+}:
let
rheaIPs = globals.machines.rhea.net.ips;
rheaVPNIPs = globals.machines.rhea.net.vpn.ips;
@@ -65,6 +70,8 @@ in
description = "Media Server";
href = "https://jellyfin.sbr.pm";
icon = "jellyfin.png";
+ ping = "https://jellyfin.sbr.pm";
+ statusStyle = "dot";
};
}
{
@@ -72,13 +79,26 @@ in
description = "Media Requests";
href = "https://jellyseerr.sbr.pm";
icon = "jellyseerr.png";
+ ping = "https://jellyseerr.sbr.pm";
+ statusStyle = "dot";
};
}
{
Navidrome = {
description = "Music Streaming";
- href = "https://navidrome.sbr.pm";
+ href = "https://music.sbr.pm";
icon = "navidrome.png";
+ ping = "https://music.sbr.pm";
+ statusStyle = "dot";
+ };
+ }
+ {
+ Audiobookshelf = {
+ description = "Podcasts & Audiobooks";
+ href = "https://podcasts.sbr.pm";
+ icon = "audiobookshelf.png";
+ ping = "https://podcasts.sbr.pm";
+ statusStyle = "dot";
};
}
{
@@ -86,6 +106,8 @@ in
description = "Photo Management";
href = "https://immich.sbr.pm";
icon = "immich.png";
+ ping = "https://immich.sbr.pm";
+ statusStyle = "dot";
};
}
];
@@ -97,6 +119,13 @@ in
description = "TV Shows";
href = "https://sonarr.sbr.pm";
icon = "sonarr.png";
+ ping = "https://sonarr.sbr.pm";
+ statusStyle = "dot";
+ widget = {
+ type = "sonarr";
+ url = "https://sonarr.sbr.pm";
+ key = "{{HOMEPAGE_FILE_SONARR_KEY}}";
+ };
};
}
{
@@ -104,6 +133,13 @@ in
description = "Movies";
href = "https://radarr.sbr.pm";
icon = "radarr.png";
+ ping = "https://radarr.sbr.pm";
+ statusStyle = "dot";
+ widget = {
+ type = "radarr";
+ url = "https://radarr.sbr.pm";
+ key = "{{HOMEPAGE_FILE_RADARR_KEY}}";
+ };
};
}
{
@@ -111,6 +147,13 @@ in
description = "Music";
href = "https://lidarr.sbr.pm";
icon = "lidarr.png";
+ ping = "https://lidarr.sbr.pm";
+ statusStyle = "dot";
+ widget = {
+ type = "lidarr";
+ url = "https://lidarr.sbr.pm";
+ key = "{{HOMEPAGE_FILE_LIDARR_KEY}}";
+ };
};
}
{
@@ -118,6 +161,8 @@ in
description = "Subtitles";
href = "https://bazarr.sbr.pm";
icon = "bazarr.png";
+ ping = "https://bazarr.sbr.pm";
+ statusStyle = "dot";
};
}
{
@@ -125,6 +170,8 @@ in
description = "Indexer Manager";
href = "https://prowlarr.sbr.pm";
icon = "prowlarr.png";
+ ping = "https://prowlarr.sbr.pm";
+ statusStyle = "dot";
};
}
{
@@ -132,6 +179,8 @@ in
description = "Torrent Client";
href = "https://transmission.sbr.pm";
icon = "transmission.png";
+ ping = "https://transmission.sbr.pm";
+ statusStyle = "dot";
};
}
];
@@ -143,6 +192,8 @@ in
description = "Monitoring";
href = "https://grafana.sbr.pm";
icon = "grafana.png";
+ ping = "https://grafana.sbr.pm";
+ statusStyle = "dot";
};
}
{
@@ -150,6 +201,8 @@ in
description = "Uptime Monitoring";
href = "https://healthchecks.sbr.pm";
icon = "healthchecks.png";
+ ping = "https://healthchecks.sbr.pm";
+ statusStyle = "dot";
};
}
{
@@ -157,6 +210,8 @@ in
description = "Home Automation";
href = "https://home.sbr.pm";
icon = "home-assistant.png";
+ ping = "https://home.sbr.pm";
+ statusStyle = "dot";
};
}
{
@@ -164,6 +219,8 @@ in
description = "Reverse Proxy";
href = "https://traefik.sbr.pm";
icon = "traefik.png";
+ ping = "https://traefik.sbr.pm";
+ statusStyle = "dot";
};
}
];
@@ -175,6 +232,8 @@ in
description = "Document Management";
href = "https://paperless.sbr.pm";
icon = "paperless.png";
+ ping = "https://paperless.sbr.pm";
+ statusStyle = "dot";
};
}
{
@@ -182,6 +241,8 @@ in
description = "Workflow Automation";
href = "https://n8n.sbr.pm";
icon = "n8n.png";
+ ping = "https://n8n.sbr.pm";
+ statusStyle = "dot";
};
}
{
@@ -189,6 +250,17 @@ in
description = "Offline Wikipedia";
href = "https://kiwix.sbr.pm";
icon = "kiwix.png";
+ ping = "https://kiwix.sbr.pm";
+ statusStyle = "dot";
+ };
+ }
+ {
+ Linkwarden = {
+ description = "Bookmark Manager";
+ href = "https://links.sbr.pm";
+ icon = "linkwarden.png";
+ ping = "https://links.sbr.pm";
+ statusStyle = "dot";
};
}
];
@@ -206,14 +278,102 @@ in
}
];
}
+ {
+ "SourceHut" = [
+ {
+ abbr = "SH";
+ href = "https://sr.ht/~vdemeester";
+ }
+ ];
+ }
+ {
+ Codeberg = [
+ {
+ abbr = "CB";
+ href = "https://codeberg.org/vdemeester";
+ }
+ ];
+ }
+ ];
+ }
+ {
+ NixOS = [
{
"NixOS Search" = [
{
- abbr = "NX";
+ abbr = "NS";
href = "https://search.nixos.org";
}
];
}
+ {
+ "NixOS Packages" = [
+ {
+ abbr = "NP";
+ href = "https://search.nixos.org/packages";
+ }
+ ];
+ }
+ {
+ "NixOS Options" = [
+ {
+ abbr = "NO";
+ href = "https://search.nixos.org/options";
+ }
+ ];
+ }
+ {
+ "Home Manager Options" = [
+ {
+ abbr = "HM";
+ href = "https://home-manager-options.extranix.com";
+ }
+ ];
+ }
+ {
+ "NixOS Wiki" = [
+ {
+ abbr = "NW";
+ href = "https://wiki.nixos.org";
+ }
+ ];
+ }
+ ];
+ }
+ {
+ Tools = [
+ {
+ "Claude Code" = [
+ {
+ abbr = "CC";
+ href = "https://claude.ai/code";
+ }
+ ];
+ }
+ {
+ "QMK Configurator" = [
+ {
+ abbr = "QMK";
+ href = "https://config.qmk.fm";
+ }
+ ];
+ }
+ {
+ "QMK Docs" = [
+ {
+ abbr = "QD";
+ href = "https://docs.qmk.fm";
+ }
+ ];
+ }
+ {
+ "Keymap Drawer" = [
+ {
+ abbr = "KD";
+ href = "https://caksoylar.github.io/keymap-drawer";
+ }
+ ];
+ }
];
}
{
@@ -234,6 +394,14 @@ in
}
];
}
+ {
+ "Tekton Docs" = [
+ {
+ abbr = "TK";
+ href = "https://tekton.dev/docs/";
+ }
+ ];
+ }
];
}
];
@@ -255,11 +423,22 @@ in
];
};
+ # Create homepage group for secret file access
+ users.groups.homepage = { };
+
# Open firewall for local access
networking.firewall.allowedTCPPorts = [ 3001 ];
# Allow requests from all rhea domains and IPs (from globals.nix)
- systemd.services.homepage-dashboard.environment = {
- HOMEPAGE_ALLOWED_HOSTS = lib.mkForce allowedHosts;
+ systemd.services.homepage-dashboard = {
+ environment = {
+ HOMEPAGE_ALLOWED_HOSTS = lib.mkForce allowedHosts;
+ HOMEPAGE_FILE_SONARR_KEY = config.age.secrets."exportarr-sonarr-apikey".path;
+ HOMEPAGE_FILE_RADARR_KEY = config.age.secrets."exportarr-radarr-apikey".path;
+ HOMEPAGE_FILE_LIDARR_KEY = config.age.secrets."exportarr-lidarr-apikey".path;
+ };
+ serviceConfig = {
+ SupplementaryGroups = [ "homepage" ];
+ };
};
}
systems/common/services/linkwarden.nix
@@ -0,0 +1,60 @@
+{ pkgs, ... }:
+{
+ # Linkwarden - Self-hosted collaborative bookmark manager
+ # https://linkwarden.app/
+ #
+ # Replacement for Omnivore (which shut down in November 2024)
+ # Features: Full-page preservation, reader view, annotations, AI tagging
+
+ services.linkwarden = {
+ enable = true;
+
+ # Network configuration
+ host = "0.0.0.0";
+ port = 3002;
+
+ # Storage
+ storageLocation = "/var/lib/linkwarden";
+ cacheLocation = "/var/cache/linkwarden";
+
+ # Database (auto-configured PostgreSQL)
+ database = {
+ createLocally = true;
+ name = "linkwarden";
+ user = "linkwarden";
+ };
+
+ # Allow user registration
+ enableRegistration = true;
+
+ # Secret files
+ # TODO: Move to agenix for production
+ secretFiles.NEXTAUTH_SECRET = "${pkgs.writeText "nextauth-secret" ''
+ changeme-replace-with-agenix-secret-in-production
+ ''}";
+
+ # Environment variables
+ environment = {
+ PAGINATION_TAKE_COUNT = "24";
+ AUTOSCROLL_TIMEOUT = "30";
+ RE_ARCHIVE_LIMIT = "5";
+ # STORAGE_FOLDER is set automatically by the module
+ # Disable telemetry for privacy
+ NEXT_PUBLIC_DISABLE_REGISTRATION = "false";
+ };
+ };
+
+ # Ensure PostgreSQL is configured
+ services.postgresql = {
+ ensureDatabases = [ "linkwarden" ];
+ ensureUsers = [
+ {
+ name = "linkwarden";
+ ensureDBOwnership = true;
+ }
+ ];
+ };
+
+ # Open firewall for local access (Traefik will proxy)
+ networking.firewall.allowedTCPPorts = [ 3002 ];
+}
systems/kyushu/hardware.nix
@@ -15,6 +15,21 @@
};
# NFS mounts from rhea
+ fileSystems."/net/rhea/audiobooks" = {
+ device = "rhea.sbr.pm:/audiobooks"; # NFSv4: path relative to fsid=0 (/neo)
+ fsType = "nfs";
+ options = [
+ "nfsvers=4.2" # Use NFSv4.2 for best performance
+ "x-systemd.automount" # Lazy-mount on first access
+ "noauto" # Don't mount at boot
+ "x-systemd.idle-timeout=600" # Auto-unmount after 10 min idle
+ "soft" # Don't hang if server unavailable
+ "timeo=14" # Timeout after 1.4s (14 * 0.1s)
+ "retrans=2" # Retry twice before timing out
+ "_netdev" # Wait for network before mounting
+ ];
+ };
+
fileSystems."/net/rhea/music" = {
device = "rhea.sbr.pm:/music"; # NFSv4: path relative to fsid=0 (/neo)
fsType = "nfs";
systems/rhea/extra.nix
@@ -21,18 +21,28 @@
age.secrets."exportarr-sonarr-apikey" = {
file = ../../secrets/rhea/exportarr-sonarr-apikey.age;
+ mode = "440";
+ group = "homepage";
};
age.secrets."exportarr-radarr-apikey" = {
file = ../../secrets/rhea/exportarr-radarr-apikey.age;
+ mode = "440";
+ group = "homepage";
};
age.secrets."exportarr-lidarr-apikey" = {
file = ../../secrets/rhea/exportarr-lidarr-apikey.age;
+ mode = "440";
+ group = "homepage";
};
age.secrets."exportarr-prowlarr-apikey" = {
file = ../../secrets/rhea/exportarr-prowlarr-apikey.age;
+ mode = "440";
+ group = "homepage";
};
age.secrets."exportarr-bazarr-apikey" = {
file = ../../secrets/rhea/exportarr-bazarr-apikey.age;
+ mode = "440";
+ group = "homepage";
};
users.users.vincent.linger = true;
@@ -134,8 +144,11 @@
port = 4533;
altHosts = [ "music.sbr.pm" ];
};
+ audiobookshelf = {
+ port = 13378;
+ altHosts = [ "podcasts.sbr.pm" ];
+ };
homepage.port = 3001;
- healthchecks.port = 8000;
};
# Generate routers for local services
@@ -222,6 +235,10 @@
paperless = mkRouter "paperless" [ "paperless.sbr.pm" ];
grafana = mkRouter "grafana" [ "grafana.sbr.pm" ];
dav = mkRouter "dav" [ "dav.sbr.pm" ];
+ linkwarden = mkRouter "linkwarden" [
+ "linkwarden.sbr.pm"
+ "links.sbr.pm"
+ ];
# Traefik dashboard
traefik-dashboard = {
rule = "Host(`traefik.sbr.pm`)";
@@ -239,6 +256,7 @@
n8n = mkService "http://${builtins.head globals.machines.sakhalin.net.ips}:5678";
paperless = mkService "http://${builtins.head globals.machines.sakhalin.net.ips}:8000";
grafana = mkService "http://${builtins.head globals.machines.sakhalin.net.ips}:3000";
+ linkwarden = mkService "http://${builtins.head globals.machines.sakhalin.net.ips}:3002";
navidrome = mkService "http://${builtins.head globals.machines.aion.net.ips}:4533";
dav = mkService "http://${builtins.head globals.machines.athena.net.ips}:80";
};
@@ -299,6 +317,19 @@
# };
samba.settings = {
global."server string" = "Rhea";
+ audiobooks = {
+ path = "/neo/audiobooks";
+ public = "yes";
+ browseable = "yes";
+ "read only" = "no";
+ "guest ok" = "yes";
+ writable = "yes";
+ comment = "audiobooks";
+ "create mask" = "0644";
+ "directory mask" = "0755";
+ "force user" = "vincent";
+ "force group" = "users";
+ };
backup = {
path = "/neo/backup";
public = "yes";
@@ -386,12 +417,13 @@
statdPort = 4000;
exports = ''
/neo 192.168.1.0/24(rw,fsid=0,no_subtree_check) 10.100.0.0/24(rw,fsid=0,no_subtree_check)
- /neo/backup 192.168.1.0/24(rw,fsid=1,no_subtree_check) 10.100.0.0/24(rw,fsid=1,no_subtree_check)
- /neo/documents 192.168.1.0/24(rw,fsid=2,no_subtree_check) 10.100.0.0/24(rw,fsid=2,no_subtree_check)
- /neo/downloads 192.168.1.0/24(rw,fsid=3,no_subtree_check) 10.100.0.0/24(rw,fsid=3,no_subtree_check)
- /neo/music 192.168.1.0/24(rw,fsid=4,no_subtree_check) 10.100.0.0/24(rw,fsid=4,no_subtree_check)
- /neo/pictures 192.168.1.0/24(rw,fsid=5,no_subtree_check) 10.100.0.0/24(rw,fsid=5,no_subtree_check)
- /neo/videos 192.168.1.0/24(rw,fsid=6,no_subtree_check) 10.100.0.0/24(rw,fsid=6,no_subtree_check)
+ /neo/audiobooks 192.168.1.0/24(rw,fsid=1,no_subtree_check) 10.100.0.0/24(rw,fsid=1,no_subtree_check)
+ /neo/backup 192.168.1.0/24(rw,fsid=2,no_subtree_check) 10.100.0.0/24(rw,fsid=2,no_subtree_check)
+ /neo/documents 192.168.1.0/24(rw,fsid=3,no_subtree_check) 10.100.0.0/24(rw,fsid=3,no_subtree_check)
+ /neo/downloads 192.168.1.0/24(rw,fsid=4,no_subtree_check) 10.100.0.0/24(rw,fsid=4,no_subtree_check)
+ /neo/music 192.168.1.0/24(rw,fsid=5,no_subtree_check) 10.100.0.0/24(rw,fsid=5,no_subtree_check)
+ /neo/pictures 192.168.1.0/24(rw,fsid=6,no_subtree_check) 10.100.0.0/24(rw,fsid=6,no_subtree_check)
+ /neo/videos 192.168.1.0/24(rw,fsid=7,no_subtree_check) 10.100.0.0/24(rw,fsid=7,no_subtree_check)
'';
};
immich = {
@@ -403,34 +435,13 @@
postgresql = {
ensureDatabases = [
"immich"
- "healthchecks"
];
ensureUsers = [
{
name = "vincent";
}
- {
- name = "healthchecks";
- }
];
};
- healthchecks = {
- enable = true;
- user = "healthchecks";
- group = "healthchecks";
- listenAddress = "127.0.0.1";
- port = 8000;
- settings = {
- ALLOWED_HOSTS = [ "healthchecks.sbr.pm" ];
- SITE_ROOT = "https://healthchecks.sbr.pm";
- SITE_NAME = "Healthchecks";
- DEBUG = false;
- DB = "postgres";
- DB_HOST = "/run/postgresql";
- DB_NAME = "healthchecks";
- DB_USER = "healthchecks";
- };
- };
jellyfin = {
enable = true;
user = "vincent";
@@ -441,6 +452,14 @@
enable = true;
openFirewall = true;
};
+ audiobookshelf = {
+ enable = true;
+ port = 13378;
+ host = "0.0.0.0";
+ user = "vincent";
+ group = "users";
+ openFirewall = true;
+ };
aria2 = {
# FIXME: make sure aria2 runs as user vincent
enable = true;
@@ -545,6 +564,7 @@
};
# Grant vincent ownership and superuser privileges for the immich database
+ # Grant healthchecks user permissions for the healthchecks database
systemd.services.postgresql.postStart = lib.mkAfter ''
PSQL="${config.services.postgresql.package}/bin/psql --port=${toString config.services.postgresql.settings.port}"
$PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname = 'vincent'" | grep -q 1 || $PSQL -tAc "CREATE ROLE vincent WITH LOGIN SUPERUSER"
systems/sakhalin/extra.nix
@@ -12,6 +12,7 @@
../common/services/docker.nix
../common/desktop/binfmt.nix # TODO: move to something else than desktop
../common/services/prometheus-exporters-node.nix
+ ../common/services/linkwarden.nix
];
# TODO make it an option ? (otherwise I'll add it for all)
globals.nix
@@ -524,13 +524,19 @@ _: {
aliases = [ "s" ];
};
homepage.host = "rhea";
+ # Linkwarden bookmark manager (runs on sakhalin, proxied via rhea/Traefik)
+ linkwarden = {
+ host = "rhea";
+ aliases = [ "links" ];
+ };
# Traefik dashboard
traefik.host = "rhea";
- # Healthchecks monitoring
- healthchecks.host = "rhea";
# Music streaming on aion (routed through rhea/traefik)
music.host = "rhea";
navidrome.host = "rhea";
+ # Podcast and audiobook management on aion (routed through rhea/traefik)
+ audiobookshelf.host = "rhea";
+ podcasts.host = "rhea";
# WebDAV on athena (routed through rhea/traefik)
dav.host = "rhea";
# MQTT on demeter (routed through rhea/traefik)