Commit 25fe4fe72ce5

Vincent Demeester <vincent@sbr.pm>
2026-04-22 09:51:10
feat: add readwise-reader triage service on okinawa
Added home-manager module with systemd timer for daily fetch/analyze/report generation served via darkhttpd. Routed through rhea traefik as reading.sbr.pm.
1 parent f44547c
Changed files (5)
home/common/services/readwise-reader.nix
@@ -0,0 +1,131 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+
+with lib;
+
+let
+  cfg = config.services.readwise-reader;
+
+  python = pkgs.python3.withPackages (ps: [
+    ps.requests
+    ps.click
+  ]);
+
+  readwise-reader-script = ../../../tools/readwise-reader/readwise-reader.py;
+
+  readwise-reader-wrapper = pkgs.writeShellScriptBin "readwise-reader-triage" ''
+    set -euo pipefail
+
+    export PATH="${
+      lib.makeBinPath [
+        pkgs.passage
+        pkgs.google-cloud-sdk
+      ]
+    }:$PATH"
+    export PASSAGE_DIR="${config.home.homeDirectory}/.local/share/passage"
+    export PASSAGE_IDENTITIES_FILE="${config.home.homeDirectory}/.local/share/passage/identities"
+    export XDG_DATA_HOME="${config.home.homeDirectory}/.local/share"
+    export GOOGLE_CLOUD_PROJECT="itpc-gcp-pnd-pe-eng-claude"
+    export GOOGLE_CLOUD_LOCATION="us-east5"
+
+    SCRIPT="${readwise-reader-script}"
+
+    echo "[$(date -Iseconds)] Starting readwise-reader triage..."
+
+    echo "โ†’ Fetching documents..."
+    ${python}/bin/python "$SCRIPT" fetch
+
+    echo "โ†’ Analyzing with ${cfg.model}..."
+    ${python}/bin/python "$SCRIPT" analyze -m ${cfg.model}
+
+    echo "โ†’ Generating report..."
+    ${python}/bin/python "$SCRIPT" report --no-open
+
+    echo "[$(date -Iseconds)] Done. Report at ${cfg.outputDir}/triage-report.html"
+  '';
+in
+{
+  options.services.readwise-reader = {
+    enable = mkEnableOption "Readwise Reader triage report generation";
+
+    model = mkOption {
+      type = types.str;
+      default = "opus";
+      description = "LLM model for analysis (opus, sonnet, gemini, gemini25)";
+    };
+
+    interval = mkOption {
+      type = types.str;
+      default = "daily";
+      description = "How often to regenerate (systemd timer format)";
+    };
+
+    outputDir = mkOption {
+      type = types.str;
+      default = "${config.home.homeDirectory}/.local/share/readwise";
+      description = "Directory containing the report output";
+    };
+
+    serve = {
+      enable = mkEnableOption "serve the triage report via darkhttpd";
+
+      port = mkOption {
+        type = types.port;
+        default = 8880;
+        description = "Port for the HTTP server";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    home.packages = [
+      readwise-reader-wrapper
+    ];
+
+    systemd.user.services.readwise-reader-triage = {
+      Unit = {
+        Description = "Readwise Reader triage: fetch, analyze, generate report";
+      };
+      Service = {
+        Type = "oneshot";
+        ExecStart = "${readwise-reader-wrapper}/bin/readwise-reader-triage";
+        # Give enough time for LLM analysis
+        TimeoutStartSec = "30min";
+      };
+    };
+
+    systemd.user.timers.readwise-reader-triage = {
+      Unit = {
+        Description = "Timer for Readwise Reader triage report";
+      };
+      Timer = {
+        OnCalendar = cfg.interval;
+        Persistent = true;
+        # Randomize to avoid hitting API limits simultaneously with other services
+        RandomizedDelaySec = "15min";
+      };
+      Install = {
+        WantedBy = [ "timers.target" ];
+      };
+    };
+
+    systemd.user.services.readwise-reader-serve = mkIf cfg.serve.enable {
+      Unit = {
+        Description = "Serve Readwise Reader triage report";
+        After = [ "readwise-reader-triage.service" ];
+      };
+      Service = {
+        ExecStart = "${pkgs.darkhttpd}/bin/darkhttpd ${cfg.outputDir} --port ${toString cfg.serve.port} --no-listing";
+        Restart = "always";
+        RestartSec = 5;
+      };
+      Install = {
+        WantedBy = [ "default.target" ];
+      };
+    };
+  };
+}
systems/okinawa/extra.nix
@@ -261,6 +261,7 @@
     5000 # Harmonia binary cache
     5555 # OpenCode web
     8090 # llama-server
+    8880 # readwise-reader triage report
     9000 # Prometheus node exporter
   ];
 
systems/okinawa/home.nix
@@ -26,6 +26,7 @@ in
     ../../home/common/services/gcal-to-org.nix
     ../../home/common/services/goimapnotify.nix
     ../../home/common/services/mail-monitor.nix
+    ../../home/common/services/readwise-reader.nix
     ../../home/common/services/readwise-sync.nix
     ../../home/common/services/redhat.nix
     ../../home/common/shell/gh.nix
@@ -116,6 +117,17 @@ in
     interval = "daily";
   };
 
+  # Readwise Reader triage report
+  services.readwise-reader = {
+    enable = true;
+    model = "opus";
+    interval = "daily";
+    serve = {
+      enable = true;
+      port = 8880;
+    };
+  };
+
   # ntfy notification subscriber
   # disabled: auth token expired, causes ~7k retry log lines per boot
   # systemd.user.services.ntfy-subscriber = {
systems/rhea/extra.nix
@@ -349,6 +349,7 @@ in
                 opencode = mkRouter "opencode" [ "opencode.sbr.pm" ];
                 # llama-server on okinawa (VPN-only, OpenAI-compatible API)
                 llm = mkRouter "llm" [ "llm.sbr.pm" ];
+                reading = mkRouter "reading" [ "reading.sbr.pm" ];
                 # SearXNG metasearch engine on sakhalin
                 search = mkRouter "search" [
                   "search.sbr.pm"
@@ -378,6 +379,7 @@ in
                 lidarr = mkService "http://${builtins.head globals.machines.aion.net.ips}:8686";
                 opencode = mkService "http://${builtins.head globals.machines.okinawa.net.vpn.ips}:5555";
                 llm = mkService "http://${builtins.head globals.machines.okinawa.net.vpn.ips}:8090";
+                reading = mkService "http://${builtins.head globals.machines.okinawa.net.vpn.ips}:8880";
                 search = mkService "http://${builtins.head globals.machines.sakhalin.net.vpn.ips}:8090";
               };
             middlewares =
globals.nix
@@ -674,6 +674,8 @@ _: {
     opencode.host = "rhea";
     # llama-server on okinawa (routed through rhea/traefik)
     llm.host = "rhea";
+    # Readwise Reader triage report on okinawa (routed through rhea/traefik)
+    reading.host = "rhea";
     # SearXNG metasearch engine on aomi (routed through rhea/traefik)
     search = {
       host = "rhea";