From 934c02609acdae08f920a3928a9715f6f1c0a5e5 Mon Sep 17 00:00:00 2001 From: yukkop Date: Sat, 23 May 2026 19:40:36 +0000 Subject: [PATCH] feat: `hectic-lab`: mechabellum for lismy --- flake.lock | 21 ++++ flake.nix | 6 ++ nixos/system/hectic-lab/hectic-lab.nix | 1 + nixos/system/hectic-lab/mechabellum.nix | 137 ++++++++++++++++++++++++ 4 files changed, 165 insertions(+) create mode 100644 nixos/system/hectic-lab/mechabellum.nix diff --git a/flake.lock b/flake.lock index e005e44..5901c74 100644 --- a/flake.lock +++ b/flake.lock @@ -668,6 +668,26 @@ "type": "github" } }, + "mechabellum-replay-analysis": { + "inputs": { + "nixpkgs": [ + "nixpkgs-fixed" + ] + }, + "locked": { + "lastModified": 1779564179, + "narHash": "sha256-rcR0Sq6782vF5cilXvZJHUG/MMBG6JtHoMXiuBYlfEs=", + "ref": "refs/heads/master", + "rev": "087d4dfd1d8517bc32b55404125ad3ca5746ed6f", + "revCount": 101, + "type": "git", + "url": "ssh://git@github.com/LysmiMx/mechabellum-replay-analysis.git" + }, + "original": { + "type": "git", + "url": "ssh://git@github.com/LysmiMx/mechabellum-replay-analysis.git" + } + }, "nix-minecraft": { "inputs": { "flake-compat": "flake-compat_3", @@ -962,6 +982,7 @@ "home-manager": "home-manager", "hyprland": "hyprland", "impermanence": "impermanence", + "mechabellum-replay-analysis": "mechabellum-replay-analysis", "nix-minecraft": "nix-minecraft", "nixos-anywhere": "nixos-anywhere", "nixos-hardware": "nixos-hardware", diff --git a/flake.nix b/flake.nix index 214cd4c..5a8f320 100644 --- a/flake.nix +++ b/flake.nix @@ -58,6 +58,12 @@ url = "git+ssh://git@github.com/liquizz/hectic-landing.git"; inputs.nixpkgs.follows = "nixpkgs-fixed"; }; + mechabellum-replay-analysis = { + # NOTE(yukkop): private repo - SSH access required. + # Only evaluated when nixosConfigurations."hectic-lab|x86_64-linux" is built. + url = "git+ssh://git@github.com/LysmiMx/mechabellum-replay-analysis.git"; + inputs.nixpkgs.follows = "nixpkgs-fixed"; + }; }; outputs = { diff --git a/nixos/system/hectic-lab/hectic-lab.nix b/nixos/system/hectic-lab/hectic-lab.nix index 460cc76..05c30b5 100644 --- a/nixos/system/hectic-lab/hectic-lab.nix +++ b/nixos/system/hectic-lab/hectic-lab.nix @@ -31,6 +31,7 @@ in { inputs.hectic-landing.nixosModules.hectic-landing (import ./containers.nix { inherit flake self inputs; }) + (import ./mechabellum.nix { inherit flake self inputs domain sslOpts; }) (import (./. + "/sentinèlla.nix") { inherit flake self inputs domain sslOpts; }) ]; diff --git a/nixos/system/hectic-lab/mechabellum.nix b/nixos/system/hectic-lab/mechabellum.nix new file mode 100644 index 0000000..10773b9 --- /dev/null +++ b/nixos/system/hectic-lab/mechabellum.nix @@ -0,0 +1,137 @@ +{ + inputs, + flake, + self, + domain, + sslOpts, + ... +}: { + pkgs, + lib, + ... +}: let + system = pkgs.stdenv.hostPlatform.system; + + mechDomain = "mechabellum.${domain}"; + apiHost = "127.0.0.1"; + apiPort = 8010; + + mechPackages = inputs.mechabellum-replay-analysis.packages.${system}; + + mechabellumBackend = pkgs.python312.withPackages (_: [ + mechPackages.backend + ]); + + mechabellumFrontend = mechPackages.frontend.overrideAttrs (_: { + VITE_API_BASE_URL = "https://${mechDomain}"; + VITE_PUBLIC_APP_URL = "https://${mechDomain}"; + }); + + stateDir = "/var/lib/mechabellum"; + staticDir = "${stateDir}/static"; +in { + systemd.tmpfiles.rules = [ + "d ${stateDir} 0750 root root -" + "d ${stateDir}/replays 0750 root root -" + "d ${stateDir}/analysis_batches 0750 root root -" + "d ${stateDir}/analysis_reports 0750 root root -" + "d ${staticDir} 0755 root root -" + ]; + + systemd.services.mechabellum-api = { + description = "Mechabellum Replay Analysis API"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + unitConfig = { + ConditionPathExists = [ + "${staticDir}/unit_id_to_name.json" + "${staticDir}/unit_footprints.json" + ]; + }; + serviceConfig = { + Type = "simple"; + ExecStart = '' + ${mechabellumBackend}/bin/uvicorn \ + mechabellum_replay.backend.app:app \ + --host ${apiHost} \ + --port ${builtins.toString apiPort} + ''; + WorkingDirectory = stateDir; + StateDirectory = "mechabellum"; + Restart = "always"; + RestartSec = "5s"; + DynamicUser = true; + ProtectSystem = "strict"; + ProtectHome = true; + NoNewPrivileges = true; + ReadWritePaths = [ stateDir ]; + }; + environment = { + DATA_DIR = stateDir; + STATIC_DATA_DIR = staticDir; + CORS_ALLOWED_ORIGINS = "https://${mechDomain}"; + }; + }; + + systemd.services.mechabellum-worker = { + description = "Mechabellum Replay Analysis worker"; + after = [ "network-online.target" "mechabellum-api.service" ]; + wants = [ "network-online.target" "mechabellum-api.service" ]; + wantedBy = [ "multi-user.target" ]; + unitConfig = { + ConditionPathExists = [ + "${staticDir}/unit_id_to_name.json" + "${staticDir}/unit_footprints.json" + ]; + }; + serviceConfig = { + Type = "simple"; + ExecStart = '' + ${mechabellumBackend}/bin/python -m mechabellum_replay.backend.worker + ''; + WorkingDirectory = stateDir; + StateDirectory = "mechabellum"; + Restart = "always"; + RestartSec = "5s"; + DynamicUser = true; + ProtectSystem = "strict"; + ProtectHome = true; + NoNewPrivileges = true; + ReadWritePaths = [ stateDir ]; + }; + environment = { + DATA_DIR = stateDir; + STATIC_DATA_DIR = staticDir; + }; + }; + + services.nginx.virtualHosts."${mechDomain}" = sslOpts // { + forceSSL = true; + root = mechabellumFrontend; + + locations."/api/" = { + proxyPass = "http://${apiHost}:${builtins.toString apiPort}/api/"; + extraConfig = '' + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + ''; + }; + + locations."/" = { + tryFiles = "$uri $uri/ /index.html"; + }; + }; + + warnings = [ + '' + mechabellum.${domain} was enabled, but the upstream repo does not package + data/static/unit_id_to_name.json or data/static/unit_footprints.json. + Copy those files into ${staticDir} on the server before starting the API + and worker units. + '' + ]; +}