diff --git a/Dockerfile b/Dockerfile index eddcc58..3a6c7ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,9 +11,11 @@ FROM node:22-alpine WORKDIR /app -# Server-Dependencies installieren +# Server-Dependencies installieren (better-sqlite3 braucht Build-Tools) COPY server/package*.json ./ -RUN npm ci --production +RUN apk add --no-cache python3 make g++ && \ + npm ci --production && \ + apk del python3 make g++ # Server-Code kopieren COPY server/ ./ @@ -33,4 +35,4 @@ ENV PORT=3000 EXPOSE 3000 -CMD ["node", "--experimental-sqlite", "src/index.js"] +CMD ["node", "src/index.js"] diff --git a/client/src/components/Budget/BudgetPanel.jsx b/client/src/components/Budget/BudgetPanel.jsx index a7323ad..c8898f0 100644 --- a/client/src/components/Budget/BudgetPanel.jsx +++ b/client/src/components/Budget/BudgetPanel.jsx @@ -190,7 +190,7 @@ export default function BudgetPanel({ tripId }) { const handleAddCategory = () => { if (!newCategoryName.trim()) return addBudgetItem(tripId, { name: t('budget.defaultEntry'), category: newCategoryName.trim(), total_price: 0 }) - setNewCategoryName(''); setShowAddCategory(false) + setNewCategoryName('') } const th = { padding: '6px 8px', textAlign: 'center', fontSize: 11, fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em', borderBottom: '2px solid var(--border-primary)', whiteSpace: 'nowrap', background: 'var(--bg-secondary)' } diff --git a/server/package-lock.json b/server/package-lock.json index a0506e3..47771d4 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,15 +1,16 @@ { "name": "nomad-server", - "version": "2.5.1", + "version": "2.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nomad-server", - "version": "2.5.1", + "version": "2.5.2", "dependencies": { "archiver": "^6.0.1", "bcryptjs": "^2.4.3", + "better-sqlite3": "^12.8.0", "cors": "^2.8.5", "dotenv": "^16.4.1", "express": "^4.18.3", @@ -217,12 +218,46 @@ "bare-path": "^3.0.0" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", "license": "MIT" }, + "node_modules/better-sqlite3": { + "version": "12.8.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -236,6 +271,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -292,6 +347,30 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -387,6 +466,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/compress-commons": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-5.0.3.tgz", @@ -540,6 +625,30 @@ "ms": "2.0.0" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -559,6 +668,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -648,6 +766,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -702,6 +829,15 @@ "bare-events": "^2.7.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", @@ -754,6 +890,12 @@ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", "license": "MIT" }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -803,6 +945,12 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "11.3.4", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", @@ -884,6 +1032,12 @@ "node": ">= 0.4" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", @@ -1037,6 +1191,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -1061,6 +1235,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1342,6 +1522,18 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", @@ -1379,6 +1571,12 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -1404,6 +1602,12 @@ "node": ">= 6.0.0" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -1413,6 +1617,18 @@ "node": ">= 0.6" } }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-cron": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", @@ -1581,6 +1797,33 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -1607,6 +1850,16 @@ "dev": true, "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qs": { "version": "6.14.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", @@ -1646,6 +1899,21 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -1870,6 +2138,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1920,6 +2233,15 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1933,6 +2255,34 @@ "node": ">=4" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tar-stream": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", @@ -2001,6 +2351,18 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/server/package.json b/server/package.json index 24efe61..2fac206 100644 --- a/server/package.json +++ b/server/package.json @@ -3,12 +3,13 @@ "version": "2.5.2", "main": "src/index.js", "scripts": { - "start": "node --experimental-sqlite src/index.js", + "start": "node src/index.js", "dev": "nodemon src/index.js" }, "dependencies": { "archiver": "^6.0.1", "bcryptjs": "^2.4.3", + "better-sqlite3": "^12.8.0", "cors": "^2.8.5", "dotenv": "^16.4.1", "express": "^4.18.3", diff --git a/server/reset-admin.js b/server/reset-admin.js index 7b34472..41a2ccc 100644 --- a/server/reset-admin.js +++ b/server/reset-admin.js @@ -1,9 +1,9 @@ const path = require('path'); -const { DatabaseSync } = require('node:sqlite'); +const Database = require('better-sqlite3'); const bcrypt = require('bcryptjs'); const dbPath = path.join(__dirname, 'data/travel.db'); -const db = new DatabaseSync(dbPath); +const db = new Database(dbPath); const hash = bcrypt.hashSync('admin123', 10); const existing = db.prepare('SELECT id FROM users WHERE email = ?').get('admin@admin.com'); diff --git a/server/src/db/database.js b/server/src/db/database.js index bf212df..d03f120 100644 --- a/server/src/db/database.js +++ b/server/src/db/database.js @@ -1,4 +1,4 @@ -const { DatabaseSync } = require('node:sqlite'); +const Database = require('better-sqlite3'); const path = require('path'); const fs = require('fs'); const bcrypt = require('bcryptjs'); @@ -19,7 +19,7 @@ function initDb() { _db = null; } - _db = new DatabaseSync(dbPath); + _db = new Database(dbPath); _db.exec('PRAGMA journal_mode = WAL'); _db.exec('PRAGMA busy_timeout = 5000'); _db.exec('PRAGMA foreign_keys = ON'); @@ -309,53 +309,79 @@ function initDb() { CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); `); - // Migrations + // Versioned migrations — each runs exactly once + _db.exec('CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)'); + const versionRow = _db.prepare('SELECT version FROM schema_version').get(); + let currentVersion = versionRow?.version ?? 0; + + // Existing DBs already have all pre-v2.5.2 columns — detect and skip + if (currentVersion === 0) { + const hasLastLogin = _db.prepare( + "SELECT 1 FROM pragma_table_info('users') WHERE name = 'last_login'" + ).get(); + if (hasLastLogin) { + // DB was already fully migrated by the old try/catch pattern + currentVersion = 19; + _db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(currentVersion); + console.log('[DB] Existing database detected, setting schema version to', currentVersion); + } else { + _db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(0); + } + } + const migrations = [ - `ALTER TABLE users ADD COLUMN unsplash_api_key TEXT`, - `ALTER TABLE users ADD COLUMN openweather_api_key TEXT`, - `ALTER TABLE places ADD COLUMN duration_minutes INTEGER DEFAULT 60`, - `ALTER TABLE places ADD COLUMN notes TEXT`, - `ALTER TABLE places ADD COLUMN image_url TEXT`, - `ALTER TABLE places ADD COLUMN transport_mode TEXT DEFAULT 'walking'`, - `ALTER TABLE days ADD COLUMN title TEXT`, - `ALTER TABLE reservations ADD COLUMN status TEXT DEFAULT 'pending'`, - `ALTER TABLE trip_files ADD COLUMN reservation_id INTEGER REFERENCES reservations(id) ON DELETE SET NULL`, - `ALTER TABLE reservations ADD COLUMN type TEXT DEFAULT 'other'`, - `ALTER TABLE trips ADD COLUMN cover_image TEXT`, - `ALTER TABLE day_notes ADD COLUMN icon TEXT DEFAULT '📝'`, - `ALTER TABLE trips ADD COLUMN is_archived INTEGER DEFAULT 0`, - `ALTER TABLE categories ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE SET NULL`, - `ALTER TABLE users ADD COLUMN avatar TEXT`, - `ALTER TABLE users ADD COLUMN oidc_sub TEXT`, - `ALTER TABLE users ADD COLUMN oidc_issuer TEXT`, - `ALTER TABLE users ADD COLUMN last_login DATETIME`, + // 1–18: ALTER TABLE additions + () => _db.exec('ALTER TABLE users ADD COLUMN unsplash_api_key TEXT'), + () => _db.exec('ALTER TABLE users ADD COLUMN openweather_api_key TEXT'), + () => _db.exec('ALTER TABLE places ADD COLUMN duration_minutes INTEGER DEFAULT 60'), + () => _db.exec('ALTER TABLE places ADD COLUMN notes TEXT'), + () => _db.exec('ALTER TABLE places ADD COLUMN image_url TEXT'), + () => _db.exec("ALTER TABLE places ADD COLUMN transport_mode TEXT DEFAULT 'walking'"), + () => _db.exec('ALTER TABLE days ADD COLUMN title TEXT'), + () => _db.exec("ALTER TABLE reservations ADD COLUMN status TEXT DEFAULT 'pending'"), + () => _db.exec('ALTER TABLE trip_files ADD COLUMN reservation_id INTEGER REFERENCES reservations(id) ON DELETE SET NULL'), + () => _db.exec("ALTER TABLE reservations ADD COLUMN type TEXT DEFAULT 'other'"), + () => _db.exec('ALTER TABLE trips ADD COLUMN cover_image TEXT'), + () => _db.exec("ALTER TABLE day_notes ADD COLUMN icon TEXT DEFAULT '📝'"), + () => _db.exec('ALTER TABLE trips ADD COLUMN is_archived INTEGER DEFAULT 0'), + () => _db.exec('ALTER TABLE categories ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE SET NULL'), + () => _db.exec('ALTER TABLE users ADD COLUMN avatar TEXT'), + () => _db.exec('ALTER TABLE users ADD COLUMN oidc_sub TEXT'), + () => _db.exec('ALTER TABLE users ADD COLUMN oidc_issuer TEXT'), + () => _db.exec('ALTER TABLE users ADD COLUMN last_login DATETIME'), + // 19: budget_items table rebuild (NOT NULL → nullable persons) + () => { + const schema = _db.prepare("SELECT sql FROM sqlite_master WHERE name = 'budget_items'").get(); + if (schema?.sql?.includes('NOT NULL DEFAULT 1')) { + _db.exec(` + CREATE TABLE budget_items_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE, + category TEXT NOT NULL DEFAULT 'Sonstiges', + name TEXT NOT NULL, + total_price REAL NOT NULL DEFAULT 0, + persons INTEGER DEFAULT NULL, + days INTEGER DEFAULT NULL, + note TEXT, + sort_order INTEGER DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + INSERT INTO budget_items_new SELECT * FROM budget_items; + DROP TABLE budget_items; + ALTER TABLE budget_items_new RENAME TO budget_items; + `); + } + }, + // Future migrations go here (append only, never reorder) ]; - // Recreate budget_items to allow NULL persons (SQLite can't ALTER NOT NULL) - try { - const hasNotNull = _db.prepare("SELECT sql FROM sqlite_master WHERE name = 'budget_items'").get() - if (hasNotNull?.sql?.includes('NOT NULL DEFAULT 1')) { - _db.exec(` - CREATE TABLE budget_items_new ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE, - category TEXT NOT NULL DEFAULT 'Sonstiges', - name TEXT NOT NULL, - total_price REAL NOT NULL DEFAULT 0, - persons INTEGER DEFAULT NULL, - days INTEGER DEFAULT NULL, - note TEXT, - sort_order INTEGER DEFAULT 0, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - ); - INSERT INTO budget_items_new SELECT * FROM budget_items; - DROP TABLE budget_items; - ALTER TABLE budget_items_new RENAME TO budget_items; - `) + if (currentVersion < migrations.length) { + for (let i = currentVersion; i < migrations.length; i++) { + console.log(`[DB] Running migration ${i + 1}/${migrations.length}`); + migrations[i](); } - } catch (e) { /* table doesn't exist yet or already migrated */ } - for (const sql of migrations) { - try { _db.exec(sql); } catch (e) { /* column already exists */ } + _db.prepare('UPDATE schema_version SET version = ?').run(migrations.length); + console.log(`[DB] Migrations complete — schema version ${migrations.length}`); } // First registered user becomes admin — no default admin seed needed diff --git a/server/src/index.js b/server/src/index.js index 93493ed..26685fc 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -135,4 +135,25 @@ const server = app.listen(PORT, () => { setupWebSocket(server); }); +// Graceful shutdown +function shutdown(signal) { + console.log(`\n${signal} received — shutting down gracefully...`); + scheduler.stop(); + server.close(() => { + console.log('HTTP server closed'); + const { closeDb } = require('./db/database'); + closeDb(); + console.log('Shutdown complete'); + process.exit(0); + }); + // Force exit after 10s if connections don't close + setTimeout(() => { + console.error('Forced shutdown after timeout'); + process.exit(1); + }, 10000); +} + +process.on('SIGTERM', () => shutdown('SIGTERM')); +process.on('SIGINT', () => shutdown('SIGINT')); + module.exports = app; diff --git a/server/src/scheduler.js b/server/src/scheduler.js index 18565e0..a2040c8 100644 --- a/server/src/scheduler.js +++ b/server/src/scheduler.js @@ -120,4 +120,9 @@ function startDemoReset() { console.log('[Demo] Hourly reset scheduled (at :00 every hour)'); } -module.exports = { start, startDemoReset, loadSettings, saveSettings, VALID_INTERVALS }; +function stop() { + if (currentTask) { currentTask.stop(); currentTask = null; } + if (demoTask) { demoTask.stop(); demoTask = null; } +} + +module.exports = { start, stop, startDemoReset, loadSettings, saveSettings, VALID_INTERVALS };