{ description = "Methanium Status"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; rust-overlay.url = "github:oxalica/rust-overlay"; }; outputs = { self, nixpkgs, rust-overlay, ... }: let systems = [ "aarch64-darwin" "aarch64-linux" "x86_64-darwin" "x86_64-linux" ]; forAllSystems = nixpkgs.lib.genAttrs systems; pkgsFor = system: import nixpkgs { inherit system; overlays = [ rust-overlay.overlays.default ]; }; in { packages = forAllSystems ( system: let pkgs = pkgsFor system; root = toString ./.; rustToolchain = pkgs.rust-bin.stable.latest.default.override { targets = [ "wasm32-unknown-unknown" ]; }; source = pkgs.lib.cleanSourceWith { src = ./.; filter = path: _type: let relative = pkgs.lib.removePrefix "${root}/" (toString path); in !(pkgs.lib.hasPrefix ".git/" relative) && !(pkgs.lib.hasInfix "node_modules" relative) && !(pkgs.lib.hasInfix "target/" relative) && !(pkgs.lib.hasInfix "apps/frontend/dist/" relative) && relative != "result"; }; frontendNativeBuildInputs = [ pkgs.cacert pkgs.git pkgs.nodejs pkgs.pnpm pkgs.wasm-pack rustToolchain ]; frontendDeps = pkgs.stdenvNoCC.mkDerivation { pname = "methanium-status-frontend-deps"; version = "0.1.0"; src = source; nativeBuildInputs = frontendNativeBuildInputs; outputHashAlgo = "sha256"; outputHashMode = "recursive"; outputHash = "sha256-hyNuty4/6MvRxnGIOVsqdUC0B1+LL4yGDjNUkDAUDPY="; dontFixup = true; __structuredAttrs = true; unsafeDiscardReferences.out = true; buildPhase = '' runHook preBuild export HOME="$TMPDIR/home" export CARGO_HOME="$TMPDIR/cargo" mkdir -p "$HOME" pnpm install --frozen-lockfile patchShebangs node_modules pnpm build runHook postBuild ''; installPhase = '' runHook preInstall mkdir -p "$out/apps/frontend" cp -a node_modules "$out/node_modules" cp -a apps/frontend/node_modules "$out/apps/frontend/node_modules" cp -a "$CARGO_HOME" "$out/cargo" cp -a "$HOME/.cache/.wasm-pack" "$out/wasm-pack" runHook postInstall ''; }; frontend = pkgs.stdenvNoCC.mkDerivation { pname = "methanium-status-frontend"; version = "0.1.0"; src = source; nativeBuildInputs = frontendNativeBuildInputs; buildPhase = '' runHook preBuild cp -a ${frontendDeps}/node_modules . cp -a ${frontendDeps}/apps/frontend/node_modules apps/frontend/node_modules cp -a ${frontendDeps}/cargo "$TMPDIR/cargo" chmod -R u+w node_modules apps/frontend/node_modules "$TMPDIR/cargo" export HOME="$TMPDIR/home" export CARGO_HOME="$TMPDIR/cargo" export CARGO_NET_OFFLINE=true mkdir -p "$HOME/.cache" cp -a ${frontendDeps}/wasm-pack "$HOME/.cache/.wasm-pack" chmod -R u+w "$HOME/.cache/.wasm-pack" pnpm build runHook postBuild ''; installPhase = '' runHook preInstall cp -r apps/frontend/dist "$out" runHook postInstall ''; }; backend = pkgs.rustPlatform.buildRustPackage { pname = "methanium-status"; version = "0.1.0"; src = source; cargoRoot = "apps/backend"; buildAndTestSubdir = "apps/backend"; MTP_TYPE_MAPS = "${source}/apps/frontend/type-maps.yaml"; cargoLock = { lockFile = ./apps/backend/Cargo.lock; outputHashes = { "mtp-0.2.0" = "sha256-xfF1kRp0kDrGgTcQWN7nZHKgxXizZh4t14uHsS/MYSk="; }; }; nativeBuildInputs = [ pkgs.cmake ]; }; in { default = frontend; inherit backend frontend; } ); devShells = forAllSystems ( system: let pkgs = pkgsFor system; rustToolchain = pkgs.rust-bin.stable.latest.default.override { extensions = [ "clippy" "rust-src" "rustfmt" ]; targets = [ "wasm32-unknown-unknown" ]; }; in { default = pkgs.mkShell { packages = [ pkgs.cmake pkgs.curl pkgs.nodejs pkgs.openssl pkgs.pnpm pkgs.wasm-pack rustToolchain ]; }; } ); checks = forAllSystems (system: { inherit (self.packages.${system}) backend frontend; }); nixosModules.default = { config, lib, pkgs, ... }: let cfg = config.services.methanium-status; acmeHost = if cfg.acmeHost == null then cfg.hostName else cfg.acmeHost; serviceType = lib.types.submodule { options = { id = lib.mkOption { type = lib.types.str; description = "Stable service identifier."; }; name = lib.mkOption { type = lib.types.str; description = "Service display name."; }; url = lib.mkOption { type = lib.types.str; description = "HTTP or HTTPS URL to check."; }; checkHttp3 = lib.mkOption { type = lib.types.bool; default = false; description = "Whether this service must also pass an HTTP/3-only check."; }; }; }; categoryType = lib.types.submodule { options = { id = lib.mkOption { type = lib.types.str; description = "Stable category identifier."; }; name = lib.mkOption { type = lib.types.str; description = "Category display name."; }; logo = lib.mkOption { type = lib.types.path; description = "Local SVG, PNG, WebP, or JPEG logo file."; }; services = lib.mkOption { type = lib.types.listOf serviceType; default = [ ]; description = "Services shown and monitored in this category."; }; }; }; configFile = (pkgs.formats.json { }).generate "methanium-status.json" { public_address = cfg.publicAddress; public_port = cfg.publicPort; admin_address = cfg.adminAddress; admin_port = cfg.adminPort; check_interval_seconds = cfg.checkInterval; timeout_seconds = cfg.timeout; certificate = "/var/lib/acme/${acmeHost}/fullchain.pem"; private_key = "/var/lib/acme/${acmeHost}/key.pem"; frontend_dir = toString cfg.frontendPackage; state_dir = "/var/lib/${cfg.stateDirectory}"; curl = "${pkgs.curl}/bin/curl"; categories = map (category: { inherit (category) id name; logo = toString category.logo; services = map (service: { inherit (service) id name url; check_http3 = service.checkHttp3; }) category.services; }) cfg.categories; }; in { options.services.methanium-status = { enable = lib.mkEnableOption "Methanium Status monitoring and status page"; package = lib.mkPackageOption self.packages.${pkgs.stdenv.hostPlatform.system} "backend" { }; frontendPackage = lib.mkPackageOption self.packages.${pkgs.stdenv.hostPlatform.system} "frontend" { }; hostName = lib.mkOption { type = lib.types.str; default = "status.methanium.net"; description = "Public status page hostname."; }; acmeHost = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "ACME certificate name, defaulting to hostName."; }; publicAddress = lib.mkOption { type = lib.types.str; default = "0.0.0.0"; description = "Address on which the UDP MTP server listens."; }; publicPort = lib.mkOption { type = lib.types.port; default = 443; description = "Shared nginx TCP and MTP UDP public port."; }; adminAddress = lib.mkOption { type = lib.types.str; default = "127.0.0.1"; description = "Address for the unauthenticated incident editor."; }; adminPort = lib.mkOption { type = lib.types.port; default = 8081; description = "TCP port for the unauthenticated incident editor."; }; checkInterval = lib.mkOption { type = lib.types.ints.positive; default = 300; description = "Seconds between service checks."; }; timeout = lib.mkOption { type = lib.types.ints.positive; default = 15; description = "Per-check timeout in seconds."; }; stateDirectory = lib.mkOption { type = lib.types.str; default = "methanium-status"; description = "systemd state directory name for incident history."; }; openFirewall = lib.mkOption { type = lib.types.bool; default = false; description = "Open the public TCP and UDP port in the firewall."; }; openAdminFirewall = lib.mkOption { type = lib.types.bool; default = false; description = "Open the unauthenticated admin TCP port. Use with care."; }; categories = lib.mkOption { type = lib.types.listOf categoryType; description = "Status categories and checked services."; default = [ { id = "methanium"; name = "Methanium"; logo = ./apps/frontend/public/methanium.svg; services = [ { id = "methanium-homepage"; name = "Homepage"; url = "https://methanium.net"; } { id = "methanium-ui"; name = "Methanium UI"; url = "https://ui.methanium.net"; } { id = "vector-verdict"; name = "Vector Verdict"; url = "https://vv.methanium.net"; } { id = "methanium-git"; name = "Git"; url = "https://git.methanium.net"; } { id = "methanium-legal"; name = "Legal Page"; url = "https://legal.methanium.net"; } ]; } { id = "tensamin"; name = "Tensamin"; logo = ./apps/frontend/public/tensamin.svg; services = [ { id = "tensamin-homepage"; name = "Homepage"; url = "https://tensamin.net"; } { id = "tensamin-app"; name = "Web App"; url = "https://app.tensamin.net"; } { id = "tensamin-dev"; name = "Web App (Dev)"; url = "https://dev.tensamin.net"; } { id = "tensamin-docs"; name = "Docs"; url = "https://docs.tensamin.net"; } { id = "tensamin-omega"; name = "Omega"; url = "https://omega.tensamin.net"; checkHttp3 = true; } ]; } ]; }; }; config = lib.mkIf cfg.enable { users.groups.methanium-status = { }; users.users.methanium-status = { isSystemUser = true; group = "methanium-status"; }; users.users.nginx.extraGroups = [ "methanium-status" ]; security.acme.certs.${acmeHost} = { group = "methanium-status"; reloadServices = [ "methanium-status.service" ]; }; services.nginx = { enable = true; virtualHosts.${cfg.hostName} = { forceSSL = true; enableACME = cfg.acmeHost == null; useACMEHost = lib.mkIf (cfg.acmeHost != null) acmeHost; root = cfg.frontendPackage; listen = [ { addr = "0.0.0.0"; port = cfg.publicPort; ssl = true; } { addr = "[::]"; port = cfg.publicPort; ssl = true; } ]; extraConfig = '' add_header Alt-Svc 'h3=":${toString cfg.publicPort}"; ma=86400' always; ''; locations."/".tryFiles = "$uri $uri/ /index.html"; locations."^~ /edit".return = "404"; }; }; systemd.services.methanium-status = { description = "Methanium Status monitor"; wantedBy = [ "multi-user.target" ]; wants = [ "network-online.target" "acme-${acmeHost}.service" ]; after = [ "network-online.target" "acme-${acmeHost}.service" ]; restartTriggers = [ configFile ]; serviceConfig = { User = "methanium-status"; Group = "methanium-status"; StateDirectory = cfg.stateDirectory; ExecStart = "${cfg.package}/bin/methanium-status ${configFile}"; Restart = "on-failure"; RestartSec = 5; AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; LockPersonality = true; NoNewPrivileges = true; PrivateDevices = true; PrivateTmp = true; ProtectClock = true; ProtectControlGroups = true; ProtectHome = true; ProtectHostname = true; ProtectKernelLogs = true; ProtectKernelModules = true; ProtectKernelTunables = true; ProtectSystem = "strict"; RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; RestrictNamespaces = true; RestrictRealtime = true; SystemCallArchitectures = "native"; UMask = "0077"; }; }; networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall [ cfg.publicPort ] ++ lib.optionals cfg.openAdminFirewall [ cfg.adminPort ]; networking.firewall.allowedUDPPorts = lib.optionals cfg.openFirewall [ cfg.publicPort ]; }; }; }; }