self: { config, lib, pkgs, ... }: let cfg = config.services.flowtrack; database = "/var/lib/flowtrack/flowtrack.sqlite3"; hardening = { User = "flowtrack"; Group = "flowtrack"; StateDirectory = "flowtrack"; StateDirectoryMode = "0700"; UMask = "0077"; NoNewPrivileges = true; PrivateTmp = true; PrivateDevices = true; ProtectSystem = "strict"; ProtectHome = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; }; userFlags = lib.concatMapStringsSep " " (user: "--editor ${lib.escapeShellArg user}") cfg.editors + " " + lib.concatMapStringsSep " " (user: "--viewer ${lib.escapeShellArg user}") cfg.viewers; pushFlag = lib.optionalString (cfg.push.contact != null) " --push-contact ${lib.escapeShellArg cfg.push.contact}"; in { options.services.flowtrack = { enable = lib.mkEnableOption "the Flowtrack period tracker"; package = lib.mkOption { type = lib.types.package; default = self.packages.${pkgs.stdenv.hostPlatform.system}.default; defaultText = lib.literalExpression "flowtrack.packages.${pkgs.stdenv.hostPlatform.system}.default"; description = "The flowtrack package to run."; }; port = lib.mkOption { type = lib.types.port; default = 8093; description = "Loopback TCP port used by the tracker."; }; editors = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; description = "Remote-User names that own and edit the data."; }; viewers = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; description = "Remote-User names with read-only access."; }; push.contact = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "https://flowtrack.example.org"; description = "VAPID contact URL (https: or mailto:) sent to push services; null disables push notifications."; }; push.time = lib.mkOption { type = lib.types.str; default = "*-*-* 09:00:00"; example = "*-*-* 09:00:00 Europe/London"; description = "systemd OnCalendar expression for the daily reminder check."; }; backup = { directory = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "Directory for nightly database snapshots; null disables backups."; }; keep = lib.mkOption { type = lib.types.ints.positive; default = 30; description = "Number of nightly snapshots to keep."; }; }; }; config = lib.mkIf cfg.enable (lib.mkMerge [ { users.users.flowtrack = { isSystemUser = true; group = "flowtrack"; description = "Flowtrack service user"; }; users.groups.flowtrack = { }; systemd.services.flowtrack = { description = "Flowtrack period tracker"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; serviceConfig = hardening // { ExecStart = "${cfg.package}/bin/flowtrack --host 127.0.0.1 --port ${toString cfg.port} --database ${database} ${userFlags}${pushFlag}"; Restart = "on-failure"; RestartSec = 3; }; }; } (lib.mkIf (cfg.push.contact != null) { systemd.services.flowtrack-notify = { description = "Send due Flowtrack reminders"; after = [ "network-online.target" "flowtrack.service" ]; wants = [ "network-online.target" ]; serviceConfig = hardening // { Type = "oneshot"; ExecStart = "${cfg.package}/bin/flowtrack-notify daily --database ${database} --contact ${lib.escapeShellArg cfg.push.contact}"; }; }; systemd.timers.flowtrack-notify = { wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = cfg.push.time; Persistent = true; }; }; }) (lib.mkIf (cfg.backup.directory != null) { systemd.tmpfiles.rules = [ "d ${cfg.backup.directory} 0700 flowtrack flowtrack -" ]; systemd.services.flowtrack-backup = { description = "Snapshot the Flowtrack database"; unitConfig.RequiresMountsFor = [ cfg.backup.directory ]; serviceConfig = hardening // { Type = "oneshot"; ExecStart = "${cfg.package}/bin/flowtrack --database ${database} --backup-to ${cfg.backup.directory} --keep ${toString cfg.backup.keep}"; ReadWritePaths = [ cfg.backup.directory ]; }; }; systemd.timers.flowtrack-backup = { wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = "*-*-* 03:15:00"; Persistent = true; }; }; }) ]); }