From 16ce3aebd9e49e9b56a69c0024ef4c80b9ab5a77 Mon Sep 17 00:00:00 2001 From: TheGeneralist <180094941+thegeneralist01@users.noreply.github.com> Date: Tue, 17 Feb 2026 07:03:41 +0100 Subject: [PATCH 1/2] add calorie-tracker as external flake service --- flake.lock | 98 ++++++++-- flake.nix | 6 +- .../calorie-tracker/default.nix | 97 ++++++++++ .../calorie-tracker/schema.sql | 180 ++++++++++++++++++ hosts/thegeneralist-central/configuration.nix | 1 + hosts/thegeneralist-central/site.nix | 3 +- 6 files changed, 370 insertions(+), 15 deletions(-) create mode 100644 hosts/thegeneralist-central/calorie-tracker/default.nix create mode 100644 hosts/thegeneralist-central/calorie-tracker/schema.sql diff --git a/flake.lock b/flake.lock index 3cc58f5..7de845f 100644 --- a/flake.lock +++ b/flake.lock @@ -44,6 +44,25 @@ "type": "github" } }, + "calorie-tracker": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1771307893, + "narHash": "sha256-wHBIQSFjo/KWcY9Xvq30wsVAaNGEgirwebFQjqujPiU=", + "ref": "refs/heads/master", + "rev": "977eeccd7f2702ae85ff562b4f8bd39c3cbb1898", + "revCount": 1, + "type": "git", + "url": "file:///home/thegeneralist/calorie-tracker" + }, + "original": { + "type": "git", + "url": "file:///home/thegeneralist/calorie-tracker" + } + }, "fenix": { "inputs": { "nixpkgs": [ @@ -117,10 +136,28 @@ "type": "github" } }, + "flake-utils_3": { + "inputs": { + "systems": "systems_4" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "ghostty": { "inputs": { "flake-compat": "flake-compat", - "flake-utils": "flake-utils", + "flake-utils": "flake-utils_2", "nixpkgs-stable": "nixpkgs-stable", "nixpkgs-unstable": "nixpkgs-unstable", "zig": "zig", @@ -233,11 +270,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1755186698, - "narHash": "sha256-wNO3+Ks2jZJ4nTHMuks+cxAiVBGNuEBXsT29Bz6HASo=", + "lastModified": 1771008912, + "narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "fbcf476f790d8a217c3eab4e12033dc4a0f6d23c", + "rev": "a82ccc39b39b621151d6732718e3e250109076fa", "type": "github" }, "original": { @@ -280,6 +317,22 @@ } }, "nixpkgs_2": { + "locked": { + "lastModified": 1755186698, + "narHash": "sha256-wNO3+Ks2jZJ4nTHMuks+cxAiVBGNuEBXsT29Bz6HASo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "fbcf476f790d8a217c3eab4e12033dc4a0f6d23c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { "locked": { "lastModified": 1769789167, "narHash": "sha256-kKB3bqYJU5nzYeIROI82Ef9VtTbu4uA3YydSk/Bioa8=", @@ -297,23 +350,27 @@ }, "readlater-bot": { "inputs": { - "flake-utils": "flake-utils_2", - "nixpkgs": "nixpkgs_2" + "flake-utils": "flake-utils_3", + "nixpkgs": "nixpkgs_3" }, "locked": { - "lastModified": 1770458808, - "narHash": "sha256-Fs/DwFxitReM7PuN1aee8pcmRzST7wzX7WLeBK/lOAI=", - "path": "/home/thegeneralist/infofeeder-bot", - "type": "path" + "lastModified": 1771250778, + "narHash": "sha256-lmWwzbuMer8vjGXh37p79dSctaFNjTBW6Cp0T5R+ZiE=", + "ref": "refs/heads/master", + "rev": "181c03915b93d21c5d15a1375d3bc621b2992700", + "revCount": 17, + "type": "git", + "url": "file:///home/thegeneralist/infofeeder-bot" }, "original": { - "path": "/home/thegeneralist/infofeeder-bot", - "type": "path" + "type": "git", + "url": "file:///home/thegeneralist/infofeeder-bot" } }, "root": { "inputs": { "agenix": "agenix", + "calorie-tracker": "calorie-tracker", "fenix": "fenix", "ghostty": "ghostty", "home-manager": "home-manager", @@ -321,7 +378,7 @@ "homebrew-core": "homebrew-core", "nix-darwin": "nix-darwin", "nix-homebrew": "nix-homebrew", - "nixpkgs": "nixpkgs", + "nixpkgs": "nixpkgs_2", "readlater-bot": "readlater-bot" } }, @@ -387,6 +444,21 @@ "type": "github" } }, + "systems_4": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, "zig": { "inputs": { "flake-compat": [ diff --git a/flake.nix b/flake.nix index e72df1d..40029a4 100644 --- a/flake.nix +++ b/flake.nix @@ -43,7 +43,11 @@ }; readlater-bot = { - url = "path:/home/thegeneralist/infofeeder-bot"; + url = "git+file:///home/thegeneralist/infofeeder-bot"; + }; + + calorie-tracker = { + url = "git+file:///home/thegeneralist/calorie-tracker"; }; }; diff --git a/hosts/thegeneralist-central/calorie-tracker/default.nix b/hosts/thegeneralist-central/calorie-tracker/default.nix new file mode 100644 index 0000000..3868f03 --- /dev/null +++ b/hosts/thegeneralist-central/calorie-tracker/default.nix @@ -0,0 +1,97 @@ +{ pkgs, inputs, ... }: +let + sourceDir = "${inputs.calorie-tracker}"; + appDir = "/var/lib/calorie-tracker/app"; + dataDir = "/var/lib/calorie-tracker"; + port = 4322; + + acmeDomain = "thegeneralist01.com"; + domain = "calorie.${acmeDomain}"; + + ssl = { + forceSSL = true; + quic = true; + useACMEHost = acmeDomain; + }; +in +{ + systemd.services.calorie-tracker = { + description = "Calorie Tracker"; + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + + environment = { + NODE_ENV = "production"; + HOST = "127.0.0.1"; + PORT = toString port; + DATABASE_URL = "file:${dataDir}/dev.db"; + PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING = "1"; + + PRISMA_FMT_BINARY = "${pkgs.prisma-engines}/bin/prisma-fmt"; + PRISMA_SCHEMA_ENGINE_BINARY = "${pkgs.prisma-engines}/bin/schema-engine"; + PRISMA_QUERY_ENGINE_BINARY = "${pkgs.prisma-engines}/bin/query-engine"; + PRISMA_QUERY_ENGINE_LIBRARY = "${pkgs.prisma-engines}/lib/libquery_engine.node"; + }; + + path = with pkgs; [ + bash + coreutils + gnused + nodejs_22 + prisma + prisma-engines + rsync + sqlite + ]; + + preStart = '' + mkdir -p ${appDir} + rsync -a --delete --exclude ".git" --exclude "node_modules" --exclude "dist" --exclude ".astro" ${sourceDir}/ ${appDir}/ + + cd ${appDir} + + if [ ! -f .env ]; then + cp .env.example .env + fi + + sed -i 's#^DATABASE_URL=.*#DATABASE_URL="file:${dataDir}/dev.db"#' .env + + if [ ! -d node_modules ] || [ ! -d node_modules/@astrojs/node ] || [ ! -d node_modules/server-destroy ]; then + npm ci --no-fund --no-audit + fi + + sqlite3 "${dataDir}/dev.db" < ${./schema.sql} + + npm run prisma:generate + if [ ! -f dist/server/entry.mjs ]; then + npm run build + fi + ''; + + serviceConfig = { + Type = "simple"; + User = "thegeneralist"; + Group = "users"; + StateDirectory = "calorie-tracker"; + StateDirectoryMode = "0750"; + WorkingDirectory = appDir; + ExecStart = "${pkgs.nodejs_22}/bin/node ${appDir}/dist/server/entry.mjs"; + KillMode = "mixed"; + Restart = "always"; + RestartSec = 5; + }; + }; + + # services.nginx.virtualHosts.${domain} = ssl // { + # locations."/" = { + # proxyPass = "http://127.0.0.1:${toString port}"; + # proxyWebsockets = true; + # recommendedProxySettings = true; + # extraConfig = '' + # proxy_read_timeout 300s; + # proxy_send_timeout 300s; + # ''; + # }; + # }; +} diff --git a/hosts/thegeneralist-central/calorie-tracker/schema.sql b/hosts/thegeneralist-central/calorie-tracker/schema.sql new file mode 100644 index 0000000..7c2c1a8 --- /dev/null +++ b/hosts/thegeneralist-central/calorie-tracker/schema.sql @@ -0,0 +1,180 @@ +PRAGMA foreign_keys = ON; + +CREATE TABLE IF NOT EXISTS "User" ( + "id" TEXT NOT NULL PRIMARY KEY, + "username" TEXT NOT NULL UNIQUE, + "email" TEXT NOT NULL UNIQUE, + "passwordHash" TEXT NOT NULL, + "isAdmin" INTEGER NOT NULL DEFAULT 0, + "emailVerifiedAt" DATETIME, + "locale" TEXT NOT NULL DEFAULT 'en', + "dayCutoffMinutes" INTEGER NOT NULL DEFAULT 0, + "weekStartsOn" INTEGER NOT NULL DEFAULT 1, + "scheduledDeletionAt" DATETIME, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS "Session" ( + "id" TEXT NOT NULL PRIMARY KEY, + "tokenHash" TEXT NOT NULL UNIQUE, + "rememberDevice" INTEGER NOT NULL DEFAULT 1, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastActiveAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expiresAt" DATETIME NOT NULL, + "recentAuthAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT NOT NULL, + FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE +); +CREATE INDEX IF NOT EXISTS "Session_userId_idx" ON "Session"("userId"); + +CREATE TABLE IF NOT EXISTS "Goal" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL UNIQUE, + "type" TEXT NOT NULL DEFAULT 'MAINTAIN_WEIGHT', + "calorieFormula" TEXT NOT NULL DEFAULT 'MIFFLIN_ST_JEOR', + "dailyCalorieTarget" INTEGER, + "proteinGramsTarget" INTEGER, + "carbsGramsTarget" INTEGER, + "fatGramsTarget" INTEGER, + "adaptiveSuggestions" INTEGER NOT NULL DEFAULT 1, + "requiresApproval" INTEGER NOT NULL DEFAULT 1, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "UserSettings" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL UNIQUE, + "waterGoalLiters" REAL NOT NULL DEFAULT 2, + "useMetricDistance" INTEGER NOT NULL DEFAULT 1, + "precisionMode" TEXT NOT NULL DEFAULT 'BASIC', + "weekStartConfigurable" INTEGER NOT NULL DEFAULT 1, + "remindersEnabled" INTEGER NOT NULL DEFAULT 1, + "reminderQuietHoursStart" TEXT DEFAULT '22:00', + "reminderQuietHoursEnd" TEXT DEFAULT '07:00', + "pwaInstallPromptDismissed" INTEGER NOT NULL DEFAULT 0, + "pwaUpdateToastEnabled" INTEGER NOT NULL DEFAULT 1, + "aiPhotoEstimationEnabled" INTEGER NOT NULL DEFAULT 0, + "aiPhotoConsentAt" DATETIME, + "aiPhotoConsentPolicy" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "Product" ( + "id" TEXT NOT NULL PRIMARY KEY, + "ownerUserId" TEXT, + "name" TEXT NOT NULL, + "brand" TEXT, + "barcode" TEXT, + "qrCode" TEXT, + "region" TEXT, + "packageSizeLabel" TEXT, + "servingSizeLabel" TEXT, + "calories" REAL, + "protein" REAL, + "carbs" REAL, + "fat" REAL, + "source" TEXT NOT NULL DEFAULT 'MANUAL', + "isAiEstimated" INTEGER NOT NULL DEFAULT 0, + "isGlobal" INTEGER NOT NULL DEFAULT 0, + "publicationStatus" TEXT NOT NULL DEFAULT 'LOCAL_ONLY', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "publishedAt" DATETIME, + FOREIGN KEY ("ownerUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE +); +CREATE INDEX IF NOT EXISTS "Product_ownerUserId_idx" ON "Product"("ownerUserId"); +CREATE INDEX IF NOT EXISTS "Product_isGlobal_publicationStatus_idx" ON "Product"("isGlobal", "publicationStatus"); +CREATE INDEX IF NOT EXISTS "Product_barcode_brand_region_idx" ON "Product"("barcode", "brand", "region"); + +CREATE TABLE IF NOT EXISTS "ProductSubmission" ( + "id" TEXT NOT NULL PRIMARY KEY, + "productId" TEXT NOT NULL, + "submittedById" TEXT NOT NULL, + "reviewedById" TEXT, + "labelPhotoUrl" TEXT, + "status" TEXT NOT NULL DEFAULT 'PENDING', + "reason" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "reviewedAt" DATETIME, + FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY ("submittedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY ("reviewedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE +); +CREATE INDEX IF NOT EXISTS "ProductSubmission_productId_status_idx" ON "ProductSubmission"("productId", "status"); + +CREATE TABLE IF NOT EXISTS "ProductContribution" ( + "id" TEXT NOT NULL PRIMARY KEY, + "productId" TEXT NOT NULL, + "contributorId" TEXT NOT NULL, + "payloadJson" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'PENDING', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "reviewedAt" DATETIME, + "reviewedById" TEXT, + FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY ("contributorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE +); +CREATE INDEX IF NOT EXISTS "ProductContribution_productId_status_idx" ON "ProductContribution"("productId", "status"); + +CREATE TABLE IF NOT EXISTS "ModerationAuditLog" ( + "id" TEXT NOT NULL PRIMARY KEY, + "actorUserId" TEXT NOT NULL, + "targetType" TEXT NOT NULL, + "targetId" TEXT NOT NULL, + "action" TEXT NOT NULL, + "reason" TEXT, + "beforeJson" TEXT, + "afterJson" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("actorUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE +); +CREATE INDEX IF NOT EXISTS "ModerationAuditLog_targetType_targetId_idx" ON "ModerationAuditLog"("targetType", "targetId"); + +CREATE TABLE IF NOT EXISTS "MealEntry" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "productId" TEXT, + "mealType" TEXT NOT NULL, + "consumedAt" DATETIME NOT NULL, + "quantityValue" REAL NOT NULL, + "quantityUnit" TEXT NOT NULL, + "calories" REAL NOT NULL, + "protein" REAL NOT NULL, + "carbs" REAL NOT NULL, + "fat" REAL NOT NULL, + "snapshotJson" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE SET NULL ON UPDATE CASCADE +); +CREATE INDEX IF NOT EXISTS "MealEntry_userId_consumedAt_idx" ON "MealEntry"("userId", "consumedAt"); + +CREATE TABLE IF NOT EXISTS "WaterEntry" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "amountMl" INTEGER NOT NULL, + "consumedAt" DATETIME NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE +); +CREATE INDEX IF NOT EXISTS "WaterEntry_userId_consumedAt_idx" ON "WaterEntry"("userId", "consumedAt"); + +CREATE TABLE IF NOT EXISTS "ActivityEntry" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "method" TEXT NOT NULL, + "durationMinutes" INTEGER, + "distanceKm" REAL, + "intensity" REAL, + "caloriesBurned" REAL NOT NULL, + "startedAt" DATETIME NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE +); +CREATE INDEX IF NOT EXISTS "ActivityEntry_userId_startedAt_idx" ON "ActivityEntry"("userId", "startedAt"); diff --git a/hosts/thegeneralist-central/configuration.nix b/hosts/thegeneralist-central/configuration.nix index 474339a..ba6f7e7 100644 --- a/hosts/thegeneralist-central/configuration.nix +++ b/hosts/thegeneralist-central/configuration.nix @@ -17,6 +17,7 @@ ./cache ./archive ./forgejo + ./calorie-tracker ]; age.secrets.password.file = ./password.age; diff --git a/hosts/thegeneralist-central/site.nix b/hosts/thegeneralist-central/site.nix index 538f1ad..a45a3ab 100644 --- a/hosts/thegeneralist-central/site.nix +++ b/hosts/thegeneralist-central/site.nix @@ -1,7 +1,7 @@ { config, pkgs, ... }: let domain = "thegeneralist01.com"; - family_domain = builtins.getEnv "FAMILY_DOMAIN"; + family_domain = builtins.getEnv "FAMILY_DOMAIN"; ssl = { quic = true; @@ -101,6 +101,7 @@ in "thegeneralist01.com" = "http://localhost:80"; "www.thegeneralist01.com" = "http://localhost:80"; "cache.thegeneralist01.com" = "http://localhost:80"; + "calorie.thegeneralist01.com" = "http://localhost:4322"; "git.thegeneralist01.com" = "http://localhost:3000"; }; default = "http_status:404"; From 37979340d9a950d0c82a27897fba1069ad5dec83 Mon Sep 17 00:00:00 2001 From: TheGeneralist <180094941+thegeneralist01@users.noreply.github.com> Date: Tue, 17 Feb 2026 07:49:12 +0100 Subject: [PATCH 2/2] fix calorie-tracker service prestart and build flow Run prestart from the state directory, normalize write permissions after rsync, install required dev dependencies for Astro build checks, and force clean dist rebuilds to prevent stale artifact module errors. --- flake.lock | 8 ++++---- .../calorie-tracker/default.nix | 14 ++++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/flake.lock b/flake.lock index 7de845f..1c41f87 100644 --- a/flake.lock +++ b/flake.lock @@ -50,11 +50,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1771307893, - "narHash": "sha256-wHBIQSFjo/KWcY9Xvq30wsVAaNGEgirwebFQjqujPiU=", + "lastModified": 1771309191, + "narHash": "sha256-Y83E1JrimzUPQ1a7FpOd9dIQFJFucbV4lYPI7ik3Piw=", "ref": "refs/heads/master", - "rev": "977eeccd7f2702ae85ff562b4f8bd39c3cbb1898", - "revCount": 1, + "rev": "8cf458087edb5f2ff7e3563d4bb6e489c3264d1d", + "revCount": 2, "type": "git", "url": "file:///home/thegeneralist/calorie-tracker" }, diff --git a/hosts/thegeneralist-central/calorie-tracker/default.nix b/hosts/thegeneralist-central/calorie-tracker/default.nix index 3868f03..9864eb2 100644 --- a/hosts/thegeneralist-central/calorie-tracker/default.nix +++ b/hosts/thegeneralist-central/calorie-tracker/default.nix @@ -48,6 +48,7 @@ in preStart = '' mkdir -p ${appDir} rsync -a --delete --exclude ".git" --exclude "node_modules" --exclude "dist" --exclude ".astro" ${sourceDir}/ ${appDir}/ + chmod -R u+rwX ${appDir} cd ${appDir} @@ -57,16 +58,15 @@ in sed -i 's#^DATABASE_URL=.*#DATABASE_URL="file:${dataDir}/dev.db"#' .env - if [ ! -d node_modules ] || [ ! -d node_modules/@astrojs/node ] || [ ! -d node_modules/server-destroy ]; then - npm ci --no-fund --no-audit + if [ ! -d node_modules ] || [ ! -d node_modules/@astrojs/node ] || [ ! -d node_modules/server-destroy ] || [ ! -d node_modules/@vite-pwa/astro ] || [ ! -d node_modules/@astrojs/check ]; then + npm ci --include=dev --no-fund --no-audit fi sqlite3 "${dataDir}/dev.db" < ${./schema.sql} npm run prisma:generate - if [ ! -f dist/server/entry.mjs ]; then - npm run build - fi + rm -rf dist + npm run build ''; serviceConfig = { @@ -75,7 +75,9 @@ in Group = "users"; StateDirectory = "calorie-tracker"; StateDirectoryMode = "0750"; - WorkingDirectory = appDir; + # Keep working dir on the guaranteed StateDirectory path so preStart can + # create ${appDir} on first boot/deploy before ExecStart runs. + WorkingDirectory = dataDir; ExecStart = "${pkgs.nodejs_22}/bin/node ${appDir}/dist/server/entry.mjs"; KillMode = "mixed"; Restart = "always";