From a2134fda785d6633cd6bb79838ef1267849862cb Mon Sep 17 00:00:00 2001 From: Kieran Klukas Date: Thu, 19 Feb 2026 20:46:59 -0500 Subject: [PATCH] feat: add cedarlogic deploy --- machines/terebithia/default.nix | 10 ++ modules/nixos/services/cedarlogic.nix | 221 ++++++++++++++++++++++++++ secrets/cedarlogic.age | Bin 0 -> 787 bytes secrets/secrets.nix | 3 + 4 files changed, 234 insertions(+) create mode 100644 modules/nixos/services/cedarlogic.nix create mode 100644 secrets/cedarlogic.age diff --git a/machines/terebithia/default.nix b/machines/terebithia/default.nix index 3c90810..137ccc8 100644 --- a/machines/terebithia/default.nix +++ b/machines/terebithia/default.nix @@ -165,6 +165,10 @@ owner = "canvas-mcp"; mode = "0400"; }; + cedarlogic = { + file = ../../secrets/cedarlogic.age; + owner = "cedarlogic"; + }; "restic/env".file = ../../secrets/restic/env.age; "restic/repo".file = ../../secrets/restic/repo.age; @@ -553,6 +557,12 @@ }; }; + atelier.services.cedarlogic = { + enable = true; + domain = "cedarlogic.dunkirk.sh"; + secretsFile = config.age.secrets.cedarlogic.path; + }; + services.caddy.virtualHosts."terebithia.dunkirk.sh" = { extraConfig = '' tls { diff --git a/modules/nixos/services/cedarlogic.nix b/modules/nixos/services/cedarlogic.nix new file mode 100644 index 0000000..f79b0d0 --- /dev/null +++ b/modules/nixos/services/cedarlogic.nix @@ -0,0 +1,221 @@ +# CedarLogic - Web-based circuit simulator +# +# Custom module (not mkService) because: +# - App lives in web/ subdirectory of the repo +# - Needs a Vite build step before serving +# - Multi-port: API (3000), Hocuspocus WS (3001), Cursor WS (3002) +# - Caddy needs path-based routing to different backends + +{ config, lib, pkgs, ... }: + +let + cfg = config.atelier.services.cedarlogic; + appDir = "${cfg.dataDir}/app"; + webDir = "${appDir}/web"; +in +{ + options.atelier.services.cedarlogic = { + enable = lib.mkEnableOption "CedarLogic circuit simulator"; + + domain = lib.mkOption { + type = lib.types.str; + description = "Domain to serve CedarLogic on"; + }; + + dataDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/cedarlogic"; + description = "Directory to store CedarLogic data"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 3100; + description = "Port for the HTTP API server"; + }; + + wsPort = lib.mkOption { + type = lib.types.port; + default = 3101; + description = "Port for the Hocuspocus WebSocket server"; + }; + + cursorPort = lib.mkOption { + type = lib.types.port; + default = 3102; + description = "Port for the cursor relay WebSocket server"; + }; + + secretsFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to secrets file (GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, JWT_SECRET)"; + }; + + deploy = { + repository = lib.mkOption { + type = lib.types.str; + default = "https://github.com/taciturnaxolotl/CedarLogic"; + description = "Git repository URL"; + }; + + branch = lib.mkOption { + type = lib.types.str; + default = "web"; + description = "Git branch to deploy"; + }; + }; + }; + + config = lib.mkIf cfg.enable { + # User and group + users.groups.services = {}; + + users.users.cedarlogic = { + isSystemUser = true; + group = "cedarlogic"; + extraGroups = [ "services" ]; + home = cfg.dataDir; + createHome = true; + shell = pkgs.bash; + }; + + users.groups.cedarlogic = {}; + + # Caddy needs to read static files from the dist directory + users.users.caddy.extraGroups = [ "cedarlogic" "services" ]; + + # Allow cedarlogic user to restart its own service (for SSH deploys) + security.sudo.extraRules = [ + { + users = [ "cedarlogic" ]; + commands = [ + { + command = "/run/current-system/sw/bin/systemctl restart cedarlogic.service"; + options = [ "NOPASSWD" ]; + } + ]; + } + ]; + + # Systemd service + systemd.services.cedarlogic = { + description = "CedarLogic circuit simulator"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + path = [ pkgs.git pkgs.openssh pkgs.unstable.bun ]; + + preStart = '' + set -e + + # Clone if not present + if [ ! -d ${appDir}/.git ]; then + ${pkgs.git}/bin/git clone -b ${cfg.deploy.branch} ${cfg.deploy.repository} ${appDir} + fi + + cd ${webDir} + + # Install dependencies + if [ -f package.json ]; then + ${pkgs.unstable.bun}/bin/bun install + fi + + # Generate gate definitions from XML + ${pkgs.unstable.bun}/bin/bun run parse-gates + + # Build client (Vite) + ${pkgs.unstable.bun}/bin/bun run build + ''; + + serviceConfig = { + Type = "exec"; + User = "cedarlogic"; + Group = "cedarlogic"; + # Don't set WorkingDirectory — preStart needs to run before + # the repo is cloned, and systemd applies it to all stages. + # Instead, ExecStart cd's into webDir. + EnvironmentFile = lib.mkIf (cfg.secretsFile != null) cfg.secretsFile; + Environment = [ + "NODE_ENV=production" + "PORT=${toString cfg.port}" + "WS_PORT=${toString cfg.wsPort}" + "CURSOR_PORT=${toString cfg.cursorPort}" + "DATABASE_PATH=${cfg.dataDir}/data/cedarlogic.db" + "GOOGLE_REDIRECT_URI=https://${cfg.domain}/auth/google/callback" + ]; + ExecStart = "${pkgs.bash}/bin/bash -c 'cd ${webDir} && exec ${pkgs.unstable.bun}/bin/bun run src/server/index.ts'"; + Restart = "on-failure"; + RestartSec = "10s"; + TimeoutStartSec = "120s"; + + StateDirectory = "cedarlogic"; + StateDirectoryMode = "0755"; + + UMask = "0022"; + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + }; + + serviceConfig.ExecStartPre = [ + "!${pkgs.writeShellScript "cedarlogic-setup" '' + mkdir -p ${webDir} + mkdir -p ${cfg.dataDir}/data + chown -R cedarlogic:services ${cfg.dataDir} + chmod -R g+rwX ${cfg.dataDir} + ''}" + ]; + }; + + systemd.tmpfiles.rules = [ + "d ${appDir} 0755 cedarlogic services -" + "d ${cfg.dataDir}/data 0755 cedarlogic services -" + ]; + + # Caddy - path-based routing to 3 backends + static file serving + services.caddy.virtualHosts.${cfg.domain} = { + extraConfig = '' + tls { + dns cloudflare {env.CLOUDFLARE_API_TOKEN} + } + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" + } + + # Hocuspocus WebSocket (Yjs collaboration) + handle /ws { + reverse_proxy localhost:${toString cfg.wsPort} + } + + # Cursor relay WebSocket + handle /cursor-ws { + reverse_proxy localhost:${toString cfg.cursorPort} + } + + # API and auth routes + handle /api/* { + reverse_proxy localhost:${toString cfg.port} + } + handle /auth/* { + reverse_proxy localhost:${toString cfg.port} + } + + # Static files (Vite build output + WASM) + handle { + root * ${webDir}/dist + try_files {path} /index.html + file_server + } + ''; + }; + + # Backup config + atelier.backup.services.cedarlogic = { + paths = [ "${cfg.dataDir}/data" ]; + exclude = [ "*.log" ]; + preBackup = "systemctl stop cedarlogic"; + postBackup = "systemctl start cedarlogic"; + }; + }; +} diff --git a/secrets/cedarlogic.age b/secrets/cedarlogic.age new file mode 100644 index 0000000000000000000000000000000000000000..ade3e3f7598558b89e1e514211c1ebc3825ca637 GIT binary patch literal 787 zcmYdHPt{G$OD?J`D9Oyv)5|YP*Do{V(zR14F3!*`Do#{zDNJ@Z2;_?NtjsV7Pm6N$ zHVij#D@={>a}6&oH8RdMG4T&}w#f4jE=_kgFVA9mx z^7i!2GpovS%#R35Ew=PdPAt#J&kZvz@^v!tFo z4l(nJGR-VDNKf%{jIwkyDX7fP@h=W{HFWZJGu00>D|NHTHSu*1HQ?gX)zwufbaW|C z_Q*F6EHqBa_p{8-b@ekT^h>u$aZfRIan-IU3(Pl33(Yb0_qX6W)8%ucJi6^pPHe~B zR70gIm9m_RkCHmAHeElL{&@L!)__+P?@A{9{I1$#D}Ux)$@FN)r%R2^_nTQY>KM%q zcG;z$(5%y9qqp{pTb;o~^M~r9W>;p4<>hgBYH4aks0QV~SX_VVkkyP3gIj<8toPPi zea$^_y{G%6dYw7jEf2ueq^W7Bypf(?M literal 0 HcmV?d00001 diff --git a/secrets/secrets.nix b/secrets/secrets.nix index 31eee4a..5173362 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -88,4 +88,7 @@ in "canvas-mcp-dkim.age".publicKeys = [ kierank ]; + "cedarlogic.age".publicKeys = [ + kierank + ]; } -- 2.51.2