Files
pirats/flake.nix
Tim McCarthy ccb32e7103 Periodic purge of old finished/inactive games
New maintenance.py runs a background asyncio loop (started from the app
lifespan) that purges games which either finished more than PIRATS_PURGE_
FINISHED_DAYS (default 14) ago or have had no activity for PIRATS_PURGE_
INACTIVE_DAYS (default 30). "Activity" is a game's most-recent GameEvent
timestamp; games with no events are undatable and left alone.

Each purge is logged with the crew name, uuid, reason, idle days and an
estimated byte footprint; every run that removes anything VACUUMs the SQLite
file and logs the disk space actually reclaimed. Child rows go via the ORM
cascade already declared on Game's relationships.

Configurable via PIRATS_PURGE_ENABLED / _INTERVAL_HOURS / _FINISHED_DAYS /
_INACTIVE_DAYS, exposed as services.pirats.purge.* in the NixOS module and
documented in the README. Unit test covers the selection logic and cascade;
smoke-tested the VACUUM/reclaim path against a file DB.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:47:44 -07:00

209 lines
7.3 KiB
Nix

{
description = "Rats with Gats Remote Play Web App";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs, ... }:
let
# Systems to support
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
# Helper to generate an attrset for all supported systems
forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f system);
in
{
packages = forAllSystems (system:
let
pkgs = nixpkgs.legacyPackages.${system};
python = pkgs.python3;
# Build the Svelte frontend
frontend = pkgs.buildNpmPackage {
pname = "pirats-frontend";
version = "0.1.0";
src = ./frontend;
npmDepsHash = "sha256-S2CXmFgovGD40wS4bTsiVF84J3zEQK6qrg7EPy+ojPY=";
installPhase = ''
mkdir -p $out
cp -r dist $out/
'';
};
in
{
default = python.pkgs.buildPythonApplication {
pname = "pirats";
version = "0.1.0";
src = ./.;
format = "pyproject";
nativeBuildInputs = with python.pkgs; [
setuptools
wheel
];
propagatedBuildInputs = with python.pkgs; [
alembic
fastapi
sqlmodel
uvicorn
websockets
python-multipart
httpx
];
nativeCheckInputs = with python.pkgs; [
pytestCheckHook
];
preBuild = ''
mkdir -p src/pirats/static
cp -r ${frontend}/dist/* src/pirats/static/
'';
pythonImportsCheck = [ "pirats" ];
};
pirats = self.packages.${system}.default;
}
);
devShells = forAllSystems (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
default = pkgs.mkShell {
inputsFrom = [ self.packages.${system}.default ];
packages = with pkgs; [
pkgs.python3.pkgs.pytest
pkgs.nodejs
];
};
}
);
# NixOS Module to run the application as a service
nixosModules.default = { config, lib, pkgs, ... }:
let
cfg = config.services.pirats;
in
{
options.services.pirats = {
enable = lib.mkEnableOption "Rats with Gats remote play service";
package = lib.mkOption {
type = lib.types.package;
default = self.packages.${pkgs.system}.default;
description = "The pirats package to use.";
};
host = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "The host address to bind to.";
};
port = lib.mkOption {
type = lib.types.port;
default = 8000;
description = "The port to listen on.";
};
databasePath = lib.mkOption {
type = lib.types.str;
default = "/var/lib/pirats/rats_with_gats.db";
description = "Path to the SQLite database file.";
};
logFile = lib.mkOption {
type = lib.types.str;
default = "/var/log/pirats/pirats.log";
description = ''
Path to the server log file (rotating, 10 MB x 5 backups). stderr is
also captured by the systemd journal. The default lives under the
service's LogsDirectory (/var/log/pirats), which the sandboxed
DynamicUser can write to; a custom path elsewhere must be made
writable by the service separately.
'';
};
logLevel = lib.mkOption {
type = lib.types.enum [ "debug" "info" "warning" "error" "critical" ];
default = "info";
description = "Minimum severity of log messages to record.";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open ports in the firewall for the service.";
};
devModeDefault = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether new games start with Dev Mode (testing shortcuts) enabled.";
};
purge = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Periodically purge old finished/inactive games.";
};
intervalHours = lib.mkOption {
type = lib.types.numbers.positive;
default = 24;
description = "How often (in hours) the purge task runs.";
};
finishedDays = lib.mkOption {
type = lib.types.numbers.positive;
default = 14;
description = "Purge games that finished more than this many days ago.";
};
inactiveDays = lib.mkOption {
type = lib.types.numbers.positive;
default = 30;
description = "Purge games with no activity for this many days.";
};
};
};
config = lib.mkIf cfg.enable {
networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall [ cfg.port ];
systemd.services.pirats = {
description = "Rats with Gats Web Application";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
DATABASE_URL = "sqlite:///${cfg.databasePath}";
# Unset means "local checkout" and defaults Dev Mode on, so the
# service must always set it explicitly.
PIRATS_DEV_MODE = lib.boolToString cfg.devModeDefault;
PIRATS_LOG_FILE = cfg.logFile;
PIRATS_LOG_LEVEL = cfg.logLevel;
PIRATS_PURGE_ENABLED = lib.boolToString cfg.purge.enable;
PIRATS_PURGE_INTERVAL_HOURS = toString cfg.purge.intervalHours;
PIRATS_PURGE_FINISHED_DAYS = toString cfg.purge.finishedDays;
PIRATS_PURGE_INACTIVE_DAYS = toString cfg.purge.inactiveDays;
};
serviceConfig = {
ExecStart = "${cfg.package}/bin/pirats --host ${cfg.host} --port ${toString cfg.port}";
Restart = "always";
# Sandboxing and security
DynamicUser = true;
StateDirectory = "pirats";
# Creates /var/log/pirats owned by the dynamic user and adds it to
# ReadWritePaths, so the default logFile under it is writable
# despite ProtectSystem=strict.
LogsDirectory = "pirats";
WorkingDirectory = "/var/lib/pirats";
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
};
};
};
};
};
}