Initial scaffold: NixOS + SvelteKit dashboard + Python renderer

This commit is contained in:
Ruben Hensen
2026-06-07 18:15:06 +02:00
commit 50256fa384
25 changed files with 4491 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
{ config, lib, pkgs, self, ... }:
let
cfg = config.eink.dashboard;
dashboardSrc = self + "/dashboard";
dashboardBuild = pkgs.buildNpmPackage {
pname = "eink-dashboard";
version = "0.1.0";
src = dashboardSrc;
# Re-generate this hash with:
# nix run nixpkgs#prefetch-npm-deps -- dashboard/package-lock.json
npmDepsHash = "sha256-UticV62t7/CYZ1nVZPNxUZn38WhQ4mHmYT3V90D01OU=";
buildPhase = ''
runHook preBuild
npm run build
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out
cp -r build package.json $out/
cp -r node_modules $out/
runHook postInstall
'';
};
in
{
options.eink.dashboard = {
enable = lib.mkEnableOption "SvelteKit e-paper dashboard";
port = lib.mkOption {
type = lib.types.port;
default = 3000;
};
caldavUrl = lib.mkOption {
type = lib.types.str;
default = "https://mail.rubenhensen.nl/dav/cal/";
description = "Base URL of the CalDAV server. Stalwart serves CalDAV at /dav/cal/, distinct from CardDAV at /dav/card/.";
};
caldavUser = lib.mkOption {
type = lib.types.str;
default = "ruben@rubenhensen.nl";
};
};
config = lib.mkIf cfg.enable {
systemd.services.eink-dashboard = {
description = "E-paper dashboard (SvelteKit)";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
HOST = "127.0.0.1";
PORT = toString cfg.port;
CALDAV_URL = cfg.caldavUrl;
CALDAV_USER = cfg.caldavUser;
NODE_ENV = "production";
};
serviceConfig = {
# CalDAV password loaded as a systemd credential. Provision the
# encrypted file under /var/lib/eink/secrets/caldav-password
# (or use sops-nix). See README.
LoadCredential = [ "caldav-password:/var/lib/eink/secrets/caldav-password" ];
DynamicUser = true;
StateDirectory = "eink-dashboard";
ExecStart = "${pkgs.nodejs}/bin/node ${dashboardBuild}/build/index.js";
Restart = "always";
RestartSec = "5s";
};
};
};
}
+79
View File
@@ -0,0 +1,79 @@
{ config, lib, pkgs, self, ... }:
let
cfg = config.eink.renderer;
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
pillow
numpy
playwright
requests
# The Waveshare 12.48" lib is not packaged in nixpkgs; we vendor it
# under renderer/waveshare/ and import it as a local module.
spidev
rpi-gpio
]);
in
{
options.eink.renderer = {
enable = lib.mkEnableOption "E-paper renderer (Playwright + Pillow + Waveshare driver)";
dashboardUrl = lib.mkOption {
type = lib.types.str;
default = "http://127.0.0.1:3000";
};
refreshSeconds = lib.mkOption {
type = lib.types.int;
default = 300;
description = "Seconds between full panel refreshes. Below 180 will wear the panel.";
};
panel = lib.mkOption {
type = lib.types.enum [ "bw" "bwr" ];
default = "bw";
description = "Panel variant: bw = 12.48\" B/W, bwr = 12.48\" Module (B) B/W/Red.";
};
};
config = lib.mkIf cfg.enable {
# Playwright needs a Chromium and its system deps. Use the nixpkgs one
# so the binary path is stable across rebuilds (not the npm-downloaded
# browser, which won't find its sandbox libs on NixOS).
systemd.services.eink-renderer = {
description = "E-paper renderer loop";
after = [ "eink-dashboard.service" "network-online.target" ];
wants = [ "eink-dashboard.service" ];
wantedBy = [ "multi-user.target" ];
environment = {
DASHBOARD_URL = cfg.dashboardUrl;
REFRESH_SECONDS = toString cfg.refreshSeconds;
PANEL_VARIANT = cfg.panel;
PLAYWRIGHT_BROWSERS_PATH = "${pkgs.playwright-driver.browsers}";
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1";
PYTHONPATH = "${self}/renderer";
};
serviceConfig = {
Type = "simple";
ExecStart = "${pythonEnv}/bin/python ${self}/renderer/render.py";
Restart = "always";
RestartSec = "10s";
# The Waveshare lib needs raw SPI + GPIO. Run as a dedicated user
# in the spi/gpio groups rather than root.
User = "eink";
Group = "eink";
SupplementaryGroups = [ "spi" "gpio" ];
};
};
users.users.eink = {
isSystemUser = true;
group = "eink";
extraGroups = [ "spi" "gpio" ];
};
users.groups.eink = {};
};
}