feat(api): add health check and OpenAPI docs

- Implemented health check endpoint to monitor application status and components.
- Added OpenAPI specification endpoint to serve the API documentation.
- Introduced new TypeScript definitions for API paths and components.
This commit is contained in:
Sam Chau
2026-01-15 16:50:28 +10:30
parent 55c5886125
commit 456ecc298b
13 changed files with 1219 additions and 4 deletions

View File

@@ -1,6 +1,7 @@
{
"imports": {
"$lib/": "./src/lib/",
"$api/": "./src/lib/api/",
"$config": "./src/lib/server/utils/config/config.ts",
"$logger/": "./src/lib/server/utils/logger/",
"$shared/": "./src/lib/shared/",
@@ -22,7 +23,8 @@
"marked": "npm:marked@^15.0.6",
"simple-icons": "npm:simple-icons@^15.17.0",
"highlight.js": "npm:highlight.js@^11.11.1",
"croner": "npm:croner@^8.1.2"
"croner": "npm:croner@^8.1.2",
"@std/yaml": "jsr:@std/yaml@^1.0.10"
},
"tasks": {
"dev": "DENO_ENV=development PORT=6969 HOST=0.0.0.0 APP_BASE_PATH=./dist/dev PARSER_HOST=localhost PARSER_PORT=5000 deno run -A npm:vite dev",
@@ -32,7 +34,8 @@
"format": "prettier --write .",
"lint": "prettier --check . && eslint .",
"test": "APP_BASE_PATH=./dist/test deno test src/tests --allow-read --allow-write --allow-env",
"test:watch": "APP_BASE_PATH=./dist/test deno test src/tests --allow-read --allow-write --allow-env --watch"
"test:watch": "APP_BASE_PATH=./dist/test deno test src/tests --allow-read --allow-write --allow-env --watch",
"generate:api-types": "npx openapi-typescript docs/api/v1/openapi.yaml -o src/lib/api/v1.d.ts"
},
"compilerOptions": {
"lib": ["deno.window", "dom"],

129
deno.lock generated
View File

@@ -17,6 +17,7 @@
"jsr:@std/path@0.217": "0.217.0",
"jsr:@std/path@1": "1.1.2",
"jsr:@std/path@^1.1.1": "1.1.2",
"jsr:@std/yaml@^1.0.10": "1.0.10",
"npm:@deno/vite-plugin@^1.0.5": "1.0.5_vite@7.1.12__@types+node@22.19.0__picomatch@4.0.3_@types+node@22.19.0",
"npm:@eslint/compat@^1.4.0": "1.4.1_eslint@9.39.1",
"npm:@eslint/js@^9.36.0": "9.39.1",
@@ -36,6 +37,7 @@
"npm:kysely@~0.27.2": "0.27.6",
"npm:lucide-svelte@0.546": "0.546.0_svelte@5.43.3__acorn@8.15.0",
"npm:marked@^15.0.6": "15.0.12",
"npm:openapi-typescript@7": "7.10.1_typescript@5.9.3",
"npm:prettier-plugin-svelte@^3.4.0": "3.4.0_prettier@3.6.2_svelte@5.43.3__acorn@8.15.0",
"npm:prettier-plugin-tailwindcss@~0.6.14": "0.6.14_prettier@3.6.2_prettier-plugin-svelte@3.4.0__prettier@3.6.2__svelte@5.43.3___acorn@8.15.0_svelte@5.43.3__acorn@8.15.0",
"npm:prettier@^3.6.2": "3.6.2",
@@ -107,9 +109,23 @@
"dependencies": [
"jsr:@std/internal@^1.0.10"
]
},
"@std/yaml@1.0.10": {
"integrity": "245706ea3511cc50c8c6d00339c23ea2ffa27bd2c7ea5445338f8feff31fa58e"
}
},
"npm": {
"@babel/code-frame@7.28.6": {
"integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==",
"dependencies": [
"@babel/helper-validator-identifier",
"js-tokens",
"picocolors"
]
},
"@babel/helper-validator-identifier@7.28.5": {
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="
},
"@deno/vite-plugin@1.0.5_vite@7.1.12__@types+node@22.19.0__picomatch@4.0.3_@types+node@22.19.0": {
"integrity": "sha512-tLja5n4dyMhcze1NzvSs2iiriBymfBlDCZIrjMTxb9O2ru0gvmV6mn5oBD2teNw5Sd92cj3YJzKwsAs8tMJXlg==",
"dependencies": [
@@ -564,6 +580,32 @@
"@polka/url@1.0.0-next.29": {
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="
},
"@redocly/ajv@8.17.1": {
"integrity": "sha512-EDtsGZS964mf9zAUXAl9Ew16eYbeyAFWhsPr0fX6oaJxgd8rApYlPBf0joyhnUHz88WxrigyFtTaqqzXNzPgqw==",
"dependencies": [
"fast-deep-equal",
"fast-uri",
"json-schema-traverse@1.0.0",
"require-from-string"
]
},
"@redocly/config@0.22.2": {
"integrity": "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ=="
},
"@redocly/openapi-core@1.34.6": {
"integrity": "sha512-2+O+riuIUgVSuLl3Lyh5AplWZyVMNuG2F98/o6NrutKJfW4/GTZdPpZlIphS0HGgcOHgmWcCSHj+dWFlZaGSHw==",
"dependencies": [
"@redocly/ajv",
"@redocly/config",
"colorette",
"https-proxy-agent",
"js-levenshtein",
"js-yaml",
"minimatch@5.1.6",
"pluralize",
"yaml-ast-parser"
]
},
"@rollup/rollup-android-arm-eabi@4.52.5": {
"integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
"os": ["android"],
@@ -955,15 +997,21 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"bin": true
},
"agent-base@7.1.4": {
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="
},
"ajv@6.12.6": {
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dependencies": [
"fast-deep-equal",
"fast-json-stable-stringify",
"json-schema-traverse",
"json-schema-traverse@0.4.1",
"uri-js"
]
},
"ansi-colors@4.1.3": {
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="
},
"ansi-styles@4.3.0": {
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dependencies": [
@@ -1008,9 +1056,12 @@
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dependencies": [
"ansi-styles",
"supports-color"
"supports-color@7.2.0"
]
},
"change-case@5.4.4": {
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="
},
"chokidar@4.0.3": {
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dependencies": [
@@ -1029,6 +1080,9 @@
"color-name@1.1.4": {
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"colorette@1.4.0": {
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="
},
"concat-map@0.0.1": {
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
@@ -1277,6 +1331,9 @@
"fast-levenshtein@2.0.6": {
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
},
"fast-uri@3.1.0": {
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="
},
"fastq@1.19.1": {
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
"dependencies": [
@@ -1356,6 +1413,13 @@
"highlight.js@11.11.1": {
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="
},
"https-proxy-agent@7.0.6": {
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"dependencies": [
"agent-base",
"debug"
]
},
"ignore@5.3.2": {
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="
},
@@ -1372,6 +1436,9 @@
"imurmurhash@0.1.4": {
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="
},
"index-to-position@1.2.0": {
"integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="
},
"is-extglob@2.1.1": {
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
},
@@ -1397,6 +1464,12 @@
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"bin": true
},
"js-levenshtein@1.1.6": {
"integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="
},
"js-tokens@4.0.0": {
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"js-yaml@4.1.0": {
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dependencies": [
@@ -1410,6 +1483,9 @@
"json-schema-traverse@0.4.1": {
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"json-schema-traverse@1.0.0": {
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"json-stable-stringify-without-jsonify@1.0.1": {
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
},
@@ -1560,6 +1636,12 @@
"brace-expansion@1.1.12"
]
},
"minimatch@5.1.6": {
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dependencies": [
"brace-expansion@2.0.2"
]
},
"minimatch@9.0.5": {
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dependencies": [
@@ -1582,6 +1664,19 @@
"natural-compare@1.4.0": {
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
},
"openapi-typescript@7.10.1_typescript@5.9.3": {
"integrity": "sha512-rBcU8bjKGGZQT4K2ekSTY2Q5veOQbVG/lTKZ49DeCyT9z62hM2Vj/LLHjDHC9W7LJG8YMHcdXpRZDqC1ojB/lw==",
"dependencies": [
"@redocly/openapi-core",
"ansi-colors",
"change-case",
"parse-json",
"supports-color@10.2.2",
"typescript",
"yargs-parser"
],
"bin": true
},
"optionator@0.9.4": {
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"dependencies": [
@@ -1611,6 +1706,14 @@
"callsites"
]
},
"parse-json@8.3.0": {
"integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
"dependencies": [
"@babel/code-frame",
"index-to-position",
"type-fest"
]
},
"path-exists@4.0.0": {
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
},
@@ -1626,6 +1729,9 @@
"picomatch@4.0.3": {
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="
},
"pluralize@8.0.0": {
"integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="
},
"postcss-load-config@3.1.4_postcss@8.5.6": {
"integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==",
"dependencies": [
@@ -1697,6 +1803,9 @@
"readdirp@4.1.2": {
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="
},
"require-from-string@2.0.2": {
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="
},
"resolve-from@4.0.0": {
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
},
@@ -1780,6 +1889,9 @@
"strip-json-comments@3.1.1": {
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="
},
"supports-color@10.2.2": {
"integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="
},
"supports-color@7.2.0": {
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dependencies": [
@@ -1874,6 +1986,9 @@
"prelude-ls"
]
},
"type-fest@4.41.0": {
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="
},
"typescript-eslint@8.46.3_eslint@9.39.1_typescript@5.9.3_@typescript-eslint+parser@8.46.3__eslint@9.39.1__typescript@5.9.3": {
"integrity": "sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==",
"dependencies": [
@@ -1939,9 +2054,15 @@
"word-wrap@1.2.5": {
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="
},
"yaml-ast-parser@0.0.43": {
"integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="
},
"yaml@1.10.2": {
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
},
"yargs-parser@21.1.1": {
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="
},
"yocto-queue@0.1.0": {
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
},
@@ -1953,6 +2074,7 @@
"dependencies": [
"jsr:@soapbox/kysely-deno-sqlite@^2.2.0",
"jsr:@std/assert@1",
"jsr:@std/yaml@^1.0.10",
"npm:croner@^8.1.2",
"npm:highlight.js@^11.11.1",
"npm:marked@^15.0.6",
@@ -1968,7 +2090,9 @@
"npm:@sveltejs/vite-plugin-svelte@^6.2.0",
"npm:@tailwindcss/forms@~0.5.10",
"npm:@tailwindcss/vite@^4.1.13",
"npm:@types/deno@^2.5.0",
"npm:@types/node@22",
"npm:croner@^9.1.0",
"npm:eslint-config-prettier@^10.1.8",
"npm:eslint-plugin-svelte@^3.12.4",
"npm:eslint@^9.36.0",
@@ -1977,6 +2101,7 @@
"npm:kysely@0.27.6",
"npm:lucide-svelte@0.546",
"npm:marked@^15.0.6",
"npm:openapi-typescript@7",
"npm:prettier-plugin-svelte@^3.4.0",
"npm:prettier-plugin-tailwindcss@~0.6.14",
"npm:prettier@^3.6.2",

31
docs/api/v1/openapi.yaml Normal file
View File

@@ -0,0 +1,31 @@
openapi: 3.1.0
info:
title: Profilarr API
description: API for Profilarr - Profile management and sync for *arr applications
version: 1.0.0
license:
name: MIT
url: https://opensource.org/licenses/MIT
servers:
- url: /api/v1
description: API v1
tags:
- name: System
description: System health and status endpoints
paths:
/health:
$ref: './paths/system.yaml#/health'
/openapi.json:
$ref: './paths/system.yaml#/openapi'
components:
schemas:
ComponentStatus:
$ref: './schemas/common.yaml#/ComponentStatus'
HealthStatus:
$ref: './schemas/common.yaml#/HealthStatus'
HealthResponse:
$ref: './schemas/health.yaml#/HealthResponse'

View File

@@ -0,0 +1,35 @@
health:
get:
operationId: getHealth
summary: Health check
description: |
Returns the health status of the application and its components.
Status values:
- `healthy`: All components functioning normally
- `degraded`: Core functionality works but some components have issues
- `unhealthy`: Core functionality is broken
tags:
- System
responses:
'200':
description: Health check response
content:
application/json:
schema:
$ref: '../schemas/health.yaml#/HealthResponse'
openapi:
get:
operationId: getOpenApiSpec
summary: OpenAPI specification
description: Returns the OpenAPI specification for this API
tags:
- System
responses:
'200':
description: OpenAPI specification
content:
application/json:
schema:
type: object

View File

@@ -0,0 +1,15 @@
ComponentStatus:
type: string
enum:
- healthy
- degraded
- unhealthy
description: Individual component health status
HealthStatus:
type: string
enum:
- healthy
- degraded
- unhealthy
description: Overall health status

View File

@@ -0,0 +1,156 @@
DatabaseHealth:
type: object
required:
- status
- responseTimeMs
properties:
status:
$ref: './common.yaml#/ComponentStatus'
responseTimeMs:
type: number
description: Database query response time in milliseconds
message:
type: string
description: Error message if unhealthy
DatabasesHealth:
type: object
required:
- status
- total
- enabled
- cached
- disabled
properties:
status:
$ref: './common.yaml#/ComponentStatus'
total:
type: integer
description: Total number of PCD databases configured
enabled:
type: integer
description: Number of enabled databases
cached:
type: integer
description: Number of databases with compiled cache
disabled:
type: integer
description: Number of disabled databases (compilation errors)
message:
type: string
description: Additional status information
JobsHealth:
type: object
required:
- status
properties:
status:
$ref: './common.yaml#/ComponentStatus'
lastRun:
type: object
additionalProperties:
type: string
format: date-time
nullable: true
description: Last run time for each job
message:
type: string
description: Additional status information
BackupsHealth:
type: object
required:
- status
- enabled
properties:
status:
$ref: './common.yaml#/ComponentStatus'
enabled:
type: boolean
description: Whether backups are enabled
lastBackup:
type: string
format: date-time
nullable: true
description: Timestamp of last backup
count:
type: integer
description: Number of backup files
totalSizeBytes:
type: integer
description: Total size of all backups in bytes
retentionDays:
type: integer
description: Configured retention period in days
message:
type: string
description: Additional status information
LogsHealth:
type: object
required:
- status
properties:
status:
$ref: './common.yaml#/ComponentStatus'
totalSizeBytes:
type: integer
description: Total size of log files in bytes
fileCount:
type: integer
description: Number of log files
oldestLog:
type: string
format: date
nullable: true
description: Date of oldest log file
newestLog:
type: string
format: date
nullable: true
description: Date of newest log file
message:
type: string
description: Additional status information
HealthResponse:
type: object
required:
- status
- timestamp
- version
- uptime
- components
properties:
status:
$ref: './common.yaml#/HealthStatus'
timestamp:
type: string
format: date-time
description: Current server time
version:
type: string
description: Application version
uptime:
type: integer
description: Server uptime in seconds
components:
type: object
required:
- database
- databases
- jobs
- backups
- logs
properties:
database:
$ref: '#/DatabaseHealth'
databases:
$ref: '#/DatabasesHealth'
jobs:
$ref: '#/JobsHealth'
backups:
$ref: '#/BackupsHealth'
logs:
$ref: '#/LogsHealth'

317
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": {
"@deno/vite-plugin": "^1.0.5",
"@jsr/db__sqlite": "^0.12.0",
"croner": "^9.1.0",
"highlight.js": "^11.11.1",
"kysely": "0.27.6",
"lucide-svelte": "^0.546.0",
@@ -24,11 +25,13 @@
"@sveltejs/vite-plugin-svelte": "^6.2.0",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/vite": "^4.1.13",
"@types/deno": "^2.5.0",
"@types/node": "^22",
"eslint": "^9.36.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.12.4",
"globals": "^16.4.0",
"openapi-typescript": "^7.0.0",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"prettier-plugin-tailwindcss": "^0.6.14",
@@ -40,6 +43,31 @@
"vite": "^7.1.7"
}
},
"node_modules/@babel/code-frame": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
"integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.28.5",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@deno/vite-plugin": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@deno/vite-plugin/-/vite-plugin-1.0.6.tgz",
@@ -865,6 +893,82 @@
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
"license": "MIT"
},
"node_modules/@redocly/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-EDtsGZS964mf9zAUXAl9Ew16eYbeyAFWhsPr0fX6oaJxgd8rApYlPBf0joyhnUHz88WxrigyFtTaqqzXNzPgqw==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@redocly/ajv/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/@redocly/config": {
"version": "0.22.2",
"resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.2.tgz",
"integrity": "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@redocly/openapi-core": {
"version": "1.34.6",
"resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.6.tgz",
"integrity": "sha512-2+O+riuIUgVSuLl3Lyh5AplWZyVMNuG2F98/o6NrutKJfW4/GTZdPpZlIphS0HGgcOHgmWcCSHj+dWFlZaGSHw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@redocly/ajv": "^8.11.2",
"@redocly/config": "^0.22.0",
"colorette": "^1.2.0",
"https-proxy-agent": "^7.0.5",
"js-levenshtein": "^1.1.6",
"js-yaml": "^4.1.0",
"minimatch": "^5.0.1",
"pluralize": "^8.0.0",
"yaml-ast-parser": "0.0.43"
},
"engines": {
"node": ">=18.17.0",
"npm": ">=9.5.0"
}
},
"node_modules/@redocly/openapi-core/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/@redocly/openapi-core/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
@@ -1534,6 +1638,13 @@
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"license": "MIT"
},
"node_modules/@types/deno": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@types/deno/-/deno-2.5.0.tgz",
"integrity": "sha512-g8JS38vmc0S87jKsFzre+0ZyMOUDHPVokEJymSCRlL57h6f/FdKPWBXgdFh3Z8Ees9sz11qt9VWELU9Y9ZkiVw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1840,6 +1951,16 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -1857,6 +1978,16 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -1956,6 +2087,13 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/change-case": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz",
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
"dev": true,
"license": "MIT"
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -2001,6 +2139,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/colorette": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
"dev": true,
"license": "MIT"
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2017,6 +2162,15 @@
"node": ">= 0.6"
}
},
"node_modules/croner": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/croner/-/croner-9.1.0.tgz",
"integrity": "sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==",
"license": "MIT",
"engines": {
"node": ">=18.0"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2432,6 +2586,23 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
},
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@@ -2596,6 +2767,20 @@
"node": ">=12.0.0"
}
},
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -2633,6 +2818,19 @@
"node": ">=0.8.19"
}
},
"node_modules/index-to-position": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz",
"integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2692,6 +2890,23 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-levenshtein": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
"integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -3203,6 +3418,40 @@
"dev": true,
"license": "MIT"
},
"node_modules/openapi-typescript": {
"version": "7.10.1",
"resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.10.1.tgz",
"integrity": "sha512-rBcU8bjKGGZQT4K2ekSTY2Q5veOQbVG/lTKZ49DeCyT9z62hM2Vj/LLHjDHC9W7LJG8YMHcdXpRZDqC1ojB/lw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@redocly/openapi-core": "^1.34.5",
"ansi-colors": "^4.1.3",
"change-case": "^5.4.4",
"parse-json": "^8.3.0",
"supports-color": "^10.2.2",
"yargs-parser": "^21.1.1"
},
"bin": {
"openapi-typescript": "bin/cli.js"
},
"peerDependencies": {
"typescript": "^5.x"
}
},
"node_modules/openapi-typescript/node_modules/supports-color": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
"integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -3266,6 +3515,24 @@
"node": ">=6"
}
},
"node_modules/parse-json": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz",
"integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.26.2",
"index-to-position": "^1.1.0",
"type-fest": "^4.39.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -3305,6 +3572,16 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pluralize": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
"integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -3613,6 +3890,16 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -3999,6 +4286,19 @@
"node": ">= 0.8.0"
}
},
"node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -4623,6 +4923,23 @@
"node": ">=0.10.0"
}
},
"node_modules/yaml-ast-parser": {
"version": "0.0.43",
"resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz",
"integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -6,6 +6,7 @@
"dependencies": {
"@deno/vite-plugin": "^1.0.5",
"@jsr/db__sqlite": "^0.12.0",
"croner": "^9.1.0",
"highlight.js": "^11.11.1",
"kysely": "0.27.6",
"lucide-svelte": "^0.546.0",
@@ -20,11 +21,13 @@
"@sveltejs/vite-plugin-svelte": "^6.2.0",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/vite": "^4.1.13",
"@types/deno": "^2.5.0",
"@types/node": "^22",
"eslint": "^9.36.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.12.4",
"globals": "^16.4.0",
"openapi-typescript": "^7.0.0",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"prettier-plugin-tailwindcss": "^0.6.14",

200
src/lib/api/v1.d.ts vendored Normal file
View File

@@ -0,0 +1,200 @@
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/
export interface paths {
"/health": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Health check
* @description Returns the health status of the application and its components.
*
* Status values:
* - `healthy`: All components functioning normally
* - `degraded`: Core functionality works but some components have issues
* - `unhealthy`: Core functionality is broken
*/
get: operations["getHealth"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/openapi.json": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* OpenAPI specification
* @description Returns the OpenAPI specification for this API
*/
get: operations["getOpenApiSpec"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
/**
* @description Individual component health status
* @enum {string}
*/
ComponentStatus: "healthy" | "degraded" | "unhealthy";
/**
* @description Overall health status
* @enum {string}
*/
HealthStatus: "healthy" | "degraded" | "unhealthy";
HealthResponse: {
status: components["schemas"]["HealthStatus"];
/**
* Format: date-time
* @description Current server time
*/
timestamp: string;
/** @description Application version */
version: string;
/** @description Server uptime in seconds */
uptime: number;
components: {
database: components["schemas"]["DatabaseHealth"];
databases: components["schemas"]["DatabasesHealth"];
jobs: components["schemas"]["JobsHealth"];
backups: components["schemas"]["BackupsHealth"];
logs: components["schemas"]["LogsHealth"];
};
};
DatabaseHealth: {
status: components["schemas"]["ComponentStatus"];
/** @description Database query response time in milliseconds */
responseTimeMs: number;
/** @description Error message if unhealthy */
message?: string;
};
DatabasesHealth: {
status: components["schemas"]["ComponentStatus"];
/** @description Total number of PCD databases configured */
total: number;
/** @description Number of enabled databases */
enabled: number;
/** @description Number of databases with compiled cache */
cached: number;
/** @description Number of disabled databases (compilation errors) */
disabled: number;
/** @description Additional status information */
message?: string;
};
JobsHealth: {
status: components["schemas"]["ComponentStatus"];
/** @description Last run time for each job */
lastRun?: {
[key: string]: string | null;
};
/** @description Additional status information */
message?: string;
};
BackupsHealth: {
status: components["schemas"]["ComponentStatus"];
/** @description Whether backups are enabled */
enabled: boolean;
/**
* Format: date-time
* @description Timestamp of last backup
*/
lastBackup?: string | null;
/** @description Number of backup files */
count?: number;
/** @description Total size of all backups in bytes */
totalSizeBytes?: number;
/** @description Configured retention period in days */
retentionDays?: number;
/** @description Additional status information */
message?: string;
};
LogsHealth: {
status: components["schemas"]["ComponentStatus"];
/** @description Total size of log files in bytes */
totalSizeBytes?: number;
/** @description Number of log files */
fileCount?: number;
/**
* Format: date
* @description Date of oldest log file
*/
oldestLog?: string | null;
/**
* Format: date
* @description Date of newest log file
*/
newestLog?: string | null;
/** @description Additional status information */
message?: string;
};
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export interface operations {
getHealth: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Health check response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HealthResponse"];
};
};
};
};
getOpenApiSpec: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OpenAPI specification */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": Record<string, never>;
};
};
};
};
}

View File

@@ -0,0 +1,312 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from '@sveltejs/kit';
import { db } from '$db/db.ts';
import { databaseInstancesQueries } from '$db/queries/databaseInstances.ts';
import { jobsQueries } from '$db/queries/jobs.ts';
import { backupSettingsQueries } from '$db/queries/backupSettings.ts';
import { appInfoQueries } from '$db/queries/appInfo.ts';
import { getCache } from '$pcd/cache.ts';
import { config } from '$config';
import type { components } from '$api/v1.d.ts';
type HealthResponse = components['schemas']['HealthResponse'];
type ComponentStatus = components['schemas']['ComponentStatus'];
// Track startup time for uptime calculation
const startupTime = Date.now();
// Thresholds
const LOG_SIZE_WARN_BYTES = 100 * 1024 * 1024; // 100MB
const LOG_SIZE_CRITICAL_BYTES = 500 * 1024 * 1024; // 500MB
export const GET: RequestHandler = async () => {
const response: HealthResponse = {
status: 'healthy',
timestamp: new Date().toISOString(),
version: appInfoQueries.getVersion(),
uptime: Math.floor((Date.now() - startupTime) / 1000),
components: {
database: checkDatabase(),
databases: checkDatabases(),
jobs: checkJobs(),
backups: await checkBackups(),
logs: await checkLogs()
}
};
// Determine overall status
response.status = determineOverallStatus(response.components);
return json(response);
};
function determineOverallStatus(components: HealthResponse['components']): ComponentStatus {
const statuses = [
components.database.status,
components.databases.status,
components.jobs.status,
components.backups.status,
components.logs.status
];
// If database is unhealthy, everything is unhealthy
if (components.database.status === 'unhealthy') {
return 'unhealthy';
}
// If all PCD databases are unhealthy, system is unhealthy
if (components.databases.status === 'unhealthy') {
return 'unhealthy';
}
// If any component is degraded, system is degraded
if (statuses.some((s) => s === 'degraded')) {
return 'degraded';
}
return 'healthy';
}
function checkDatabase(): HealthResponse['components']['database'] {
const start = performance.now();
try {
db.queryFirst('SELECT 1');
const responseTimeMs = Math.round((performance.now() - start) * 100) / 100;
return {
status: 'healthy',
responseTimeMs
};
} catch (error) {
return {
status: 'unhealthy',
responseTimeMs: -1,
message: error instanceof Error ? error.message : 'Database query failed'
};
}
}
function checkDatabases(): HealthResponse['components']['databases'] {
try {
const allDatabases = databaseInstancesQueries.getAll();
const enabledDatabases = allDatabases.filter((d) => d.enabled === 1);
const disabledDatabases = allDatabases.filter((d) => d.enabled === 0);
// Check how many have compiled caches
let cachedCount = 0;
for (const dbInstance of enabledDatabases) {
const cache = getCache(dbInstance.id);
if (cache?.isBuilt()) {
cachedCount++;
}
}
const total = allDatabases.length;
const enabled = enabledDatabases.length;
const disabled = disabledDatabases.length;
let status: ComponentStatus = 'healthy';
let message: string | undefined;
if (total === 0) {
status = 'healthy';
message = 'No databases configured';
} else if (enabled === 0) {
status = 'unhealthy';
message = 'All databases are disabled';
} else if (disabled > 0) {
status = 'degraded';
message = `${disabled} database(s) disabled due to errors`;
} else if (cachedCount < enabled) {
status = 'degraded';
message = `${enabled - cachedCount} database(s) not cached`;
}
return {
status,
total,
enabled,
cached: cachedCount,
disabled,
message
};
} catch (error) {
return {
status: 'unhealthy',
total: 0,
enabled: 0,
cached: 0,
disabled: 0,
message: error instanceof Error ? error.message : 'Failed to check databases'
};
}
}
function checkJobs(): HealthResponse['components']['jobs'] {
try {
const jobs = jobsQueries.getAll();
const lastRun: Record<string, string | null> = {};
for (const job of jobs) {
lastRun[job.name] = job.last_run_at ?? null;
}
// Check if sync_arr job is stale (hasn't run in 5+ minutes when it should run every minute)
const syncArrJob = jobs.find((j) => j.name === 'sync_arr');
let status: ComponentStatus = 'healthy';
let message: string | undefined;
if (syncArrJob?.enabled && syncArrJob.last_run_at) {
const lastRunTime = new Date(syncArrJob.last_run_at).getTime();
const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
if (lastRunTime < fiveMinutesAgo) {
status = 'degraded';
message = 'sync_arr job is stale';
}
}
return {
status,
lastRun,
message
};
} catch (error) {
return {
status: 'unhealthy',
message: error instanceof Error ? error.message : 'Failed to check jobs'
};
}
}
async function checkBackups(): Promise<HealthResponse['components']['backups']> {
try {
const settings = backupSettingsQueries.get();
const enabled = settings?.enabled === 1;
const retentionDays = settings?.retention_days ?? 30;
if (!enabled) {
return {
status: 'healthy',
enabled: false,
message: 'Backups disabled'
};
}
// Check backup directory
const backupPath = config.paths.backups;
let count = 0;
let totalSizeBytes = 0;
let lastBackup: string | null = null;
let lastBackupTime: number | null = null;
try {
for await (const entry of Deno.readDir(backupPath)) {
if (entry.isFile && entry.name.startsWith('backup-') && entry.name.endsWith('.tar.gz')) {
count++;
const stat = await Deno.stat(`${backupPath}/${entry.name}`);
totalSizeBytes += stat.size;
if (!lastBackupTime || stat.mtime!.getTime() > lastBackupTime) {
lastBackupTime = stat.mtime!.getTime();
lastBackup = stat.mtime!.toISOString();
}
}
}
} catch {
// Directory might not exist yet
}
let status: ComponentStatus = 'healthy';
let message: string | undefined;
if (count === 0) {
status = 'degraded';
message = 'No backups exist';
} else if (lastBackupTime) {
// Check if last backup is older than 2x retention period
const twoDaysAgo = Date.now() - 2 * 24 * 60 * 60 * 1000;
if (lastBackupTime < twoDaysAgo) {
status = 'degraded';
message = 'Last backup is older than 2 days';
}
}
return {
status,
enabled,
lastBackup,
count,
totalSizeBytes,
retentionDays,
message
};
} catch (error) {
return {
status: 'unhealthy',
enabled: false,
message: error instanceof Error ? error.message : 'Failed to check backups'
};
}
}
async function checkLogs(): Promise<HealthResponse['components']['logs']> {
try {
const logPath = config.paths.logs;
let totalSizeBytes = 0;
let fileCount = 0;
let oldestLog: string | null = null;
let newestLog: string | null = null;
const logDateRegex = /^(\d{4}-\d{2}-\d{2})\.log$/;
try {
for await (const entry of Deno.readDir(logPath)) {
if (entry.isFile) {
const match = entry.name.match(logDateRegex);
if (match) {
fileCount++;
const stat = await Deno.stat(`${logPath}/${entry.name}`);
totalSizeBytes += stat.size;
const dateStr = match[1];
if (!oldestLog || dateStr < oldestLog) {
oldestLog = dateStr;
}
if (!newestLog || dateStr > newestLog) {
newestLog = dateStr;
}
}
}
}
} catch {
// Directory might not exist yet
}
let status: ComponentStatus = 'healthy';
let message: string | undefined;
if (totalSizeBytes > LOG_SIZE_CRITICAL_BYTES) {
status = 'degraded';
message = `Log directory is very large (${Math.round(totalSizeBytes / 1024 / 1024)}MB)`;
} else if (totalSizeBytes > LOG_SIZE_WARN_BYTES) {
status = 'degraded';
message = `Log directory is getting large (${Math.round(totalSizeBytes / 1024 / 1024)}MB)`;
}
return {
status,
totalSizeBytes,
fileCount,
oldestLog,
newestLog,
message
};
} catch (error) {
return {
status: 'unhealthy',
message: error instanceof Error ? error.message : 'Failed to check logs'
};
}
}

View File

@@ -0,0 +1,16 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from '@sveltejs/kit';
import { parse } from '@std/yaml';
// Cache the parsed spec to avoid re-reading on every request
let cachedSpec: unknown = null;
export const GET: RequestHandler = async () => {
if (!cachedSpec) {
// Read and parse the OpenAPI spec
const yamlContent = await Deno.readTextFile('docs/api/v1/openapi.yaml');
cachedSpec = parse(yamlContent);
}
return json(cachedSpec);
};

View File

@@ -12,6 +12,7 @@ const config = {
}),
outDir: 'dist/.svelte-kit',
alias: {
$api: './src/lib/api',
$config: './src/lib/server/utils/config/config.ts',
$logger: './src/lib/server/utils/logger',
$shared: './src/lib/shared',

View File

@@ -11,6 +11,7 @@
"strict": true,
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM"],
"types": ["deno"],
"allowImportingTsExtensions": true,
"noEmit": true
}