diff --git a/bun.lock b/bun.lock index 267f7f4..00016cd 100644 --- a/bun.lock +++ b/bun.lock @@ -4,11 +4,6 @@ "workspaces": { "": { "name": "dimensionlab-website", - "dependencies": { - "@sinclair/typebox": "^0.34.49", - "ajv": "^8.20.0", - "ajv-formats": "^3.0.1", - }, "devDependencies": { "@sveltejs/adapter-node": "^5.5.4", "@sveltejs/kit": "^2.65.2", @@ -18,7 +13,6 @@ "svelte-check": "^4.6.0", "typescript": "^6.0.3", "vite": "^8.0.16", - "vitest": "^4.1.9", }, }, }, @@ -135,8 +129,6 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.0", "", { "os": "win32", "cpu": "x64" }, "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA=="], - "@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="], - "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.10", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA=="], @@ -151,12 +143,8 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], - "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], - "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], - "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], - "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], @@ -165,42 +153,18 @@ "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], - "@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], - - "@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="], - - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.9", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A=="], - - "@vitest/runner": ["@vitest/runner@4.1.9", "", { "dependencies": { "@vitest/utils": "4.1.9", "pathe": "^2.0.3" } }, "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg=="], - - "@vitest/snapshot": ["@vitest/snapshot@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA=="], - - "@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], - - "@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], - "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], - "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - - "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - "aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="], - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], - "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], @@ -211,20 +175,12 @@ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], - "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], "esrap": ["esrap@2.2.11", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-gPdx+I+BjYEinNMQaBXFjbaJVyoPMU4ZODg5mE+M4DqVG9VusAVHHjcBX+zqyITlI0DIARwDMMzZwAWj36dRoQ=="], "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -239,8 +195,6 @@ "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -281,8 +235,6 @@ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], @@ -291,8 +243,6 @@ "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], "rolldown": ["rolldown@1.0.3", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.3", "@rolldown/binding-darwin-arm64": "1.0.3", "@rolldown/binding-darwin-x64": "1.0.3", "@rolldown/binding-freebsd-x64": "1.0.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", "@rolldown/binding-linux-arm64-gnu": "1.0.3", "@rolldown/binding-linux-arm64-musl": "1.0.3", "@rolldown/binding-linux-ppc64-gnu": "1.0.3", "@rolldown/binding-linux-s390x-gnu": "1.0.3", "@rolldown/binding-linux-x64-gnu": "1.0.3", "@rolldown/binding-linux-x64-musl": "1.0.3", "@rolldown/binding-openharmony-arm64": "1.0.3", "@rolldown/binding-wasm32-wasi": "1.0.3", "@rolldown/binding-win32-arm64-msvc": "1.0.3", "@rolldown/binding-win32-x64-msvc": "1.0.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g=="], @@ -303,30 +253,18 @@ "set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - - "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], "svelte": ["svelte@5.56.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.11", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-w7JvrM5IFl5cmfbY0TLik9o7mjRUJmRMhOR51tBPu708Gr/MjbGs7VnJnr/B0CaXeI4vtnOh7RKxDr0cwhMdDA=="], "svelte-check": ["svelte-check@4.6.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "0.1.1", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-KhVnDFDSid57mmZtHz8gfW8AAGylOZ0vPnOIzVmAL+urzwK8sBYXRss953gD8T0OdgAQ11mdWhE6uadmtOz8TQ=="], - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - - "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], - "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], - "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], - "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -339,14 +277,8 @@ "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], - "vitest": ["vitest@4.1.9", "", { "dependencies": { "@vitest/expect": "4.1.9", "@vitest/mocker": "4.1.9", "@vitest/pretty-format": "4.1.9", "@vitest/runner": "4.1.9", "@vitest/snapshot": "4.1.9", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.9", "@vitest/browser-preview": "4.1.9", "@vitest/browser-webdriverio": "4.1.9", "@vitest/coverage-istanbul": "4.1.9", "@vitest/coverage-v8": "4.1.9", "@vitest/ui": "4.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ=="], - - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], "@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="], - - "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], } } diff --git a/package.json b/package.json index 0cdc69e..a52402b 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,7 @@ "dev": "vite --host 0.0.0.0", "build": "vite build", "preview": "vite preview --host 0.0.0.0", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "test": "vitest run" - }, - "dependencies": { - "@sinclair/typebox": "^0.34.49", - "ajv": "^8.20.0", - "ajv-formats": "^3.0.1" + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" }, "devDependencies": { "@sveltejs/adapter-node": "^5.5.4", @@ -23,7 +17,6 @@ "svelte": "^5.56.3", "svelte-check": "^4.6.0", "typescript": "^6.0.3", - "vite": "^8.0.16", - "vitest": "^4.1.9" + "vite": "^8.0.16" } } diff --git a/src/lib/model/fixtures/dimensionlab.ts b/src/lib/model/fixtures/dimensionlab.ts deleted file mode 100644 index be23b02..0000000 --- a/src/lib/model/fixtures/dimensionlab.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { - DASHBOARD_SCHEMA_VERSION, - type DashboardDocument, -} from "../schema"; - -export const dimensionLabDashboardFixture: DashboardDocument = { - schemaVersion: DASHBOARD_SCHEMA_VERSION, - metadata: { - title: "System Overview", - subtitle: "Capacity, noise & top consumers", - description: "Dimension Lab operational dashboard seed data.", - timezone: "Europe/Amsterdam", - refreshIntervalSeconds: 15, - }, - layout: { - density: "dense", - telemetry: [ - "infra-ram", - "gpu-host-ram", - "network-ram", - "user-disk-peak", - "system-disk-peak", - "peak-cpu-busy", - "top-cpu-container", - "top-ram-container", - "gpu-3060-load", - "gpu-3060-vram", - "gpu-3060-temp", - "gpu-3060-fan", - "gpu-3090-load", - "gpu-3090-vram", - "gpu-3090-temp", - "gpu-3090-fan", - ], - serviceGroups: ["essentials", "monitoring", "ai-automation", "systems", "runtime-health"], - statusStrips: ["footer-status"], - modules: ["weather"], - }, - telemetry: [ - metric("infra-ram", "Infra RAM", "percent", 19, "linux-infra", "ok"), - metric("gpu-host-ram", "GPU Host RAM", "percent", 17, "linux", "ok"), - metric("network-ram", "Network RAM", "percent", 5, "network-core", "ok"), - metric("user-disk-peak", "User Disk Peak", "percent", 29, "linux /home", "ok"), - metric("system-disk-peak", "System Disk Peak", "percent", 49, "linux /boot", "ok"), - metric("peak-cpu-busy", "Peak CPU Busy", "percent", 3, "linux-infra", "ok"), - metric("top-cpu-container", "Top CPU Container", "percent", 7.9, "container placeholder", "ok"), - metric("top-ram-container", "Top RAM Container", "bytes", 5.2 * 1024 ** 3, "container placeholder", "danger"), - metric("gpu-3060-load", "3060 GPU Load", "percent", 0, "RTX 3060", "ok"), - metric("gpu-3060-vram", "3060 VRAM", "percent", 0, "RTX 3060", "ok"), - metric("gpu-3060-temp", "3060 Temp", "temperature", 54, "RTX 3060", "ok"), - metric("gpu-3060-fan", "3060 Fan Spin", "percent", 0, "RTX 3060", "ok"), - metric("gpu-3090-load", "3090 GPU Load", "percent", 0, "RTX 3090", "ok"), - metric("gpu-3090-vram", "3090 VRAM", "percent", 74, "RTX 3090", "warning"), - metric("gpu-3090-temp", "3090 Temp", "temperature", 50, "RTX 3090", "ok"), - metric("gpu-3090-fan", "3090 Fan Spin", "percent", 0, "RTX 3090", "ok"), - ], - serviceGroups: [ - group("essentials", "Essentials", [ - service("vaultwarden", "Vaultwarden", "Password manager", "simple-icons:vaultwarden", "https://vault.dimensionlab.net"), - service("forgejo", "Forgejo", "Git repositories", "simple-icons:forgejo", "https://git.dimensionlab.net"), - service("wiki", "Wiki", "Internal documentation", "simple-icons:wikidotjs", "https://wiki.dimensionlab.net"), - service("aws-start", "AWS Start", "AWS access portal", "simple-icons:amazonaws", "https://dimensionlab.awsapps.com/start"), - ]), - group("monitoring", "Monitoring", [ - service("grafana", "Grafana", "Capacity and noise dashboard", "simple-icons:grafana", "https://grafana.dimensionlab.net"), - service("uptime-kuma", "Uptime Kuma", "Service uptime checks", "simple-icons:uptimekuma", "https://uptime.dimensionlab.net"), - service("prometheus", "Prometheus", "Metrics database", "simple-icons:prometheus", "https://prometheus.dimensionlab.net"), - service("backrest", "Backrest", "Restic backup manager", "mdi:backup-restore", "https://backups.dimensionlab.net"), - ]), - group("ai-automation", "AI & Automation", [ - service("n8n", "n8n", "Workflow automation", "simple-icons:n8n", "https://workflows.dimensionlab.net"), - service("open-webui", "Open WebUI", "Chat and model interface", "simple-icons:openwebui", "https://webui.dimensionlab.net"), - service("comfyui", "ComfyUI", "Image generation workflows", "mdi:image-edit-outline", "https://comfy.dimensionlab.net"), - service("models", "Models", "Local model management", "mdi:brain", "https://models.dimensionlab.net"), - ]), - group("systems", "Systems", [ - service("adminer", "Adminer", "PostgreSQL database browser", "simple-icons:adminer", "https://db.dimensionlab.net"), - service("assistant", "Assistant", "Personal AI agent gateway", "mdi:robot-outline", "https://assistant.dimensionlab.net"), - service("suna", "Suna", "AI command center", "mdi:account-hard-hat-outline", "https://suna.dimensionlab.net"), - service("cockpit-infra", "Cockpit Infra", "linux-infra server console", "simple-icons:cockpit", "https://infra-cockpit.dimensionlab.net"), - ]), - group("runtime-health", "Runtime Health", [ - service("forgejo-ssh-relay", "Forgejo SSH Relay", "Public Git SSH relay", "simple-icons:forgejo", "https://uptime.dimensionlab.net/status/dimensionlab"), - service("postgresql", "PostgreSQL", "Shared application database", "simple-icons:postgresql"), - service("ollama-api", "Ollama API", "Local model API", "simple-icons:ollama"), - service("node-exporter", "Node Exporter", "Host metrics exporter", "simple-icons:prometheus"), - ]), - ], - statusStrips: [ - { - id: "footer-status", - items: [ - { id: "system-status", label: "System Status", value: "All systems operational", severity: "ok" }, - { id: "last-sync", label: "Last Sync", value: "2 minutes ago", severity: "stale" }, - { id: "uptime", label: "Uptime", value: "28d 14h 32m", severity: "ok" }, - { id: "load-avg", label: "Load Avg", value: "0.47 0.53 0.59", severity: "neutral" }, - ], - }, - ], - modules: [ - { - id: "weather", - kind: "weather", - title: "Amsterdam", - value: "28.3 C", - detail: "Clear", - icon: "mdi:weather-sunny", - severity: "ok", - datasource: { type: "placeholder", reason: "weather adapter pending" }, - }, - ], -}; - -type NumericValueKind = "percent" | "bytes" | "temperature" | "latency" | "number"; -type Severity = "neutral" | "ok" | "warning" | "danger" | "stale" | "unavailable"; - -function metric( - id: string, - label: string, - kind: NumericValueKind, - value: number, - detail: string, - severity: Severity, -) { - return { - id, - label, - value: { kind, value }, - detail, - severity, - datasource: { type: "placeholder" as const, reason: "live datasource pending" }, - sparkline: [10, 18, 15, 28, 24, 32], - }; -} - -function service( - id: string, - label: string, - description: string, - icon: string, - href?: string, -) { - const entry = { - id, - label, - description, - icon, - severity: "ok" as const, - detail: "ready", - datasource: { type: "placeholder" as const, reason: "service health adapter pending" }, - }; - - return href ? { ...entry, link: { href, external: true } } : entry; -} - -function group(id: string, title: string, services: ReturnType[]) { - return { - id, - title, - layout: "list" as const, - services, - }; -} diff --git a/src/lib/model/fixtures/generic.ts b/src/lib/model/fixtures/generic.ts deleted file mode 100644 index ed4fa16..0000000 --- a/src/lib/model/fixtures/generic.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - DASHBOARD_SCHEMA_VERSION, - type DashboardDocument, -} from "../schema"; - -export const genericDashboardFixture: DashboardDocument = { - schemaVersion: DASHBOARD_SCHEMA_VERSION, - metadata: { - title: "Operations Console", - subtitle: "Generic environment", - description: "Portable fixture for component and renderer tests.", - timezone: "UTC", - refreshIntervalSeconds: 30, - }, - layout: { - density: "dense", - telemetry: ["service-uptime", "queue-depth"], - serviceGroups: ["core-services"], - statusStrips: ["runtime"], - modules: ["ambient"], - }, - telemetry: [ - { - id: "service-uptime", - label: "Service Uptime", - value: { kind: "percent", value: 99.9, precision: 1 }, - detail: "last 30 days", - severity: "ok", - datasource: { type: "static", label: "fixture" }, - sparkline: [99.7, 99.8, 99.9, 99.9], - }, - { - id: "queue-depth", - label: "Queue Depth", - value: { kind: "number", value: 18 }, - detail: "pending jobs", - severity: "warning", - thresholds: { warning: 15, danger: 50 }, - datasource: { type: "placeholder", reason: "adapter pending" }, - sparkline: [6, 11, 13, 18], - }, - ], - serviceGroups: [ - { - id: "core-services", - title: "Core Services", - layout: "list", - services: [ - { - id: "identity", - label: "Identity", - description: "Authentication and profile service", - icon: "mdi:account-key-outline", - severity: "ok", - detail: "ready", - datasource: { type: "static", label: "fixture" }, - }, - { - id: "scheduler", - label: "Scheduler", - description: "Background task coordinator", - icon: "mdi:calendar-clock", - severity: "warning", - detail: "delayed", - datasource: { type: "placeholder", reason: "health adapter pending" }, - }, - ], - }, - ], - statusStrips: [ - { - id: "runtime", - items: [ - { id: "status", label: "System Status", value: "Degraded", severity: "warning" }, - { id: "sync", label: "Last Sync", value: "2 minutes ago", severity: "stale" }, - ], - }, - ], - modules: [ - { - id: "ambient", - kind: "summary", - title: "Environment", - value: "Nominal", - detail: "static fixture", - icon: "mdi:radar", - severity: "ok", - datasource: { type: "static", label: "fixture" }, - }, - ], -}; diff --git a/src/lib/model/fixtures/index.ts b/src/lib/model/fixtures/index.ts deleted file mode 100644 index a04278d..0000000 --- a/src/lib/model/fixtures/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { dimensionLabDashboardFixture } from "./dimensionlab"; -export { genericDashboardFixture } from "./generic"; diff --git a/src/lib/model/index.ts b/src/lib/model/index.ts deleted file mode 100644 index 1a028cc..0000000 --- a/src/lib/model/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -export { - DASHBOARD_SCHEMA_VERSION, - DashboardDocumentSchema, - dashboardDocumentJsonSchema, - type DashboardDocument, - type DashboardModule, - type DatasourceReference, - type MetricValue, - type ServiceEntry, - type ServiceGroup, - type Severity, - type StatusItem, - type StatusStrip, - type TelemetryCard, -} from "./schema"; -export { - assertDashboardDocument, - formatValidationErrors, - isDashboardDocument, - validateDashboardDocument, - type DashboardValidationFailure, - type DashboardValidationResult, - type DashboardValidationSuccess, -} from "./validation"; diff --git a/src/lib/model/schema.test.ts b/src/lib/model/schema.test.ts deleted file mode 100644 index 7eeed74..0000000 --- a/src/lib/model/schema.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - DASHBOARD_SCHEMA_VERSION, - dashboardDocumentJsonSchema, - validateDashboardDocument, -} from "."; -import { - dimensionLabDashboardFixture, - genericDashboardFixture, -} from "./fixtures"; - -describe("dashboard model validation", () => { - it("accepts the generic dashboard fixture", () => { - const result = validateDashboardDocument(genericDashboardFixture); - - expect(result.valid).toBe(true); - if (result.valid) { - expect(result.data.schemaVersion).toBe(DASHBOARD_SCHEMA_VERSION); - } - }); - - it("accepts the Dimension Lab dashboard fixture", () => { - const result = validateDashboardDocument(dimensionLabDashboardFixture); - - expect(result.valid).toBe(true); - }); - - it("rejects documents with an unsupported schema version", () => { - const invalid = { - ...genericDashboardFixture, - schemaVersion: "dashboard.v0", - }; - - const result = validateDashboardDocument(invalid); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.errors.join(" ")).toContain("must be equal to constant"); - expect(result.details.length).toBeGreaterThan(0); - } - }); - - it("returns actionable field paths for invalid documents", () => { - const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); - delete invalid.metadata.title; - invalid.telemetry[0].severity = "fine"; - - const result = validateDashboardDocument(invalid); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.errors.some((error) => error.includes("/metadata"))).toBe(true); - expect(result.errors.some((error) => error.includes("/telemetry/0/severity"))).toBe(true); - } - }); - - it("includes offending additional property names in errors", () => { - const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); - invalid.metadata.unexpected = true; - - const result = validateDashboardDocument(invalid); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.errors.join(" ")).toContain("unexpected"); - } - }); - - it("rejects non-finite numbers that cannot roundtrip through JSON", () => { - const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); - invalid.telemetry[0].value.value = Number.NaN; - - const result = validateDashboardDocument(invalid); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.errors.join(" ")).toContain("finite JSON number"); - } - }); - - it("rejects dangling layout references", () => { - const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); - invalid.layout.telemetry.push("missing-card"); - - const result = validateDashboardDocument(invalid); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.errors.join(" ")).toContain("missing-card"); - expect(result.errors.join(" ")).toContain("must reference an existing item"); - } - }); - - it("rejects duplicate IDs within collections", () => { - const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); - invalid.telemetry[1].id = invalid.telemetry[0].id; - - const result = validateDashboardDocument(invalid); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.errors.join(" ")).toContain("must be unique"); - expect(result.errors.join(" ")).toContain(invalid.telemetry[0].id); - } - }); - - it("rejects duplicate status strip item IDs", () => { - const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); - invalid.statusStrips[0].items.push({ - ...invalid.statusStrips[0].items[0], - }); - - const result = validateDashboardDocument(invalid); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.errors.join(" ")).toContain("must be unique"); - expect(result.errors.join(" ")).toContain(invalid.statusStrips[0].items[0].id); - } - }); - - it("rejects string values for numeric metric kinds", () => { - const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); - invalid.telemetry[0].value.value = "not a percent"; - - const result = validateDashboardDocument(invalid); - - expect(result.valid).toBe(false); - }); - - it("rejects percent values outside 0 to 100", () => { - const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); - invalid.telemetry[0].value.value = 150; - - const result = validateDashboardDocument(invalid); - - expect(result.valid).toBe(false); - }); - - it("rejects negative values for nonnegative metric kinds", () => { - const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); - invalid.telemetry[0].value = { kind: "latency", value: -20 }; - - const result = validateDashboardDocument(invalid); - - expect(result.valid).toBe(false); - }); - - it("rejects contradictory warning and danger thresholds", () => { - const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); - invalid.telemetry[0].thresholds = { warning: 90, danger: 80 }; - - const result = validateDashboardDocument(invalid); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.errors.join(" ")).toContain("warning threshold"); - } - }); - - it("rejects percent thresholds above 100", () => { - const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); - invalid.telemetry[0].thresholds = { warning: 99, danger: 999 }; - - const result = validateDashboardDocument(invalid); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.errors.join(" ")).toContain("percent thresholds"); - } - }); - - it("rejects thresholds on text metric values", () => { - const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); - invalid.telemetry[0].value = { kind: "text", value: "available" }; - invalid.telemetry[0].thresholds = { warning: 10 }; - - const result = validateDashboardDocument(invalid); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.errors.join(" ")).toContain("text metric values"); - } - }); - - it("rejects undefined properties because they are not JSON values", () => { - const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); - invalid.serviceGroups[0].services[0].link = undefined; - - const result = validateDashboardDocument(invalid); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.errors.join(" ")).toContain("must be omitted instead of undefined"); - } - }); - - it("exports JSON Schema for external tool contracts", () => { - expect(dashboardDocumentJsonSchema.$id).toContain("dashboard-document.v1"); - expect(dashboardDocumentJsonSchema.properties).toHaveProperty("schemaVersion"); - expect(dashboardDocumentJsonSchema.properties).toHaveProperty("telemetry"); - expect(dashboardDocumentJsonSchema.properties).toHaveProperty("serviceGroups"); - }); -}); diff --git a/src/lib/model/schema.ts b/src/lib/model/schema.ts deleted file mode 100644 index e0215a4..0000000 --- a/src/lib/model/schema.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { Type, type Static } from "@sinclair/typebox"; - -export const DASHBOARD_SCHEMA_VERSION = "dashboard.v1" as const; - -const IdentifierSchema = Type.String({ - minLength: 1, - pattern: "^[a-z0-9][a-z0-9-_.:]*$", -}); - -const SeveritySchema = Type.Union([ - Type.Literal("neutral"), - Type.Literal("ok"), - Type.Literal("warning"), - Type.Literal("danger"), - Type.Literal("stale"), - Type.Literal("unavailable"), -]); - -const IconReferenceSchema = Type.String({ minLength: 1 }); - -const LinkSchema = Type.Object( - { - href: Type.String({ format: "uri" }), - label: Type.Optional(Type.String({ minLength: 1 })), - external: Type.Optional(Type.Boolean()), - }, - { additionalProperties: false }, -); - -const StaticDatasourceSchema = Type.Object( - { - type: Type.Literal("static"), - label: Type.Optional(Type.String({ minLength: 1 })), - updatedAt: Type.Optional(Type.String({ format: "date-time" })), - }, - { additionalProperties: false }, -); - -const PlaceholderDatasourceSchema = Type.Object( - { - type: Type.Literal("placeholder"), - reason: Type.String({ minLength: 1 }), - }, - { additionalProperties: false }, -); - -const ExternalDatasourceSchema = Type.Object( - { - type: Type.Literal("external"), - adapter: Type.Union([ - Type.Literal("prometheus"), - Type.Literal("http-status"), - Type.Literal("weather"), - Type.Literal("custom"), - ]), - reference: Type.String({ minLength: 1 }), - }, - { additionalProperties: false }, -); - -const DatasourceReferenceSchema = Type.Union([ - StaticDatasourceSchema, - PlaceholderDatasourceSchema, - ExternalDatasourceSchema, -]); - -const PercentMetricValueSchema = Type.Object( - { - kind: Type.Literal("percent"), - value: Type.Number({ minimum: 0, maximum: 100 }), - unit: Type.Optional(Type.String({ minLength: 1 })), - precision: Type.Optional(Type.Integer({ minimum: 0, maximum: 4 })), - }, - { additionalProperties: false }, -); - -const NonNegativeMetricValueSchema = Type.Object( - { - kind: Type.Union([ - Type.Literal("bytes"), - Type.Literal("temperature"), - Type.Literal("latency"), - Type.Literal("number"), - ]), - value: Type.Number({ minimum: 0 }), - unit: Type.Optional(Type.String({ minLength: 1 })), - precision: Type.Optional(Type.Integer({ minimum: 0, maximum: 4 })), - }, - { additionalProperties: false }, -); - -const TextMetricValueSchema = Type.Object( - { - kind: Type.Literal("text"), - value: Type.String(), - unit: Type.Optional(Type.String({ minLength: 1 })), - }, - { additionalProperties: false }, -); - -const MetricValueSchema = Type.Union([ - PercentMetricValueSchema, - NonNegativeMetricValueSchema, - TextMetricValueSchema, -]); - -const ThresholdSchema = Type.Object( - { - warning: Type.Optional(Type.Number({ minimum: 0 })), - danger: Type.Optional(Type.Number({ minimum: 0 })), - }, - { additionalProperties: false, minProperties: 1 }, -); - -export const TelemetryCardSchema = Type.Object( - { - id: IdentifierSchema, - label: Type.String({ minLength: 1 }), - description: Type.Optional(Type.String()), - icon: Type.Optional(IconReferenceSchema), - value: MetricValueSchema, - detail: Type.Optional(Type.String()), - severity: SeveritySchema, - thresholds: Type.Optional(ThresholdSchema), - datasource: Type.Optional(DatasourceReferenceSchema), - sparkline: Type.Optional(Type.Array(Type.Number())), - }, - { additionalProperties: false }, -); - -export const ServiceEntrySchema = Type.Object( - { - id: IdentifierSchema, - label: Type.String({ minLength: 1 }), - description: Type.String(), - icon: Type.Optional(IconReferenceSchema), - link: Type.Optional(LinkSchema), - severity: SeveritySchema, - detail: Type.Optional(Type.String()), - datasource: Type.Optional(DatasourceReferenceSchema), - }, - { additionalProperties: false }, -); - -export const ServiceGroupSchema = Type.Object( - { - id: IdentifierSchema, - title: Type.String({ minLength: 1 }), - layout: Type.Optional(Type.Union([Type.Literal("list"), Type.Literal("grid")])), - services: Type.Array(ServiceEntrySchema), - }, - { additionalProperties: false }, -); - -export const StatusItemSchema = Type.Object( - { - id: IdentifierSchema, - label: Type.String({ minLength: 1 }), - value: Type.String(), - severity: Type.Optional(SeveritySchema), - link: Type.Optional(LinkSchema), - }, - { additionalProperties: false }, -); - -export const StatusStripSchema = Type.Object( - { - id: IdentifierSchema, - title: Type.Optional(Type.String({ minLength: 1 })), - items: Type.Array(StatusItemSchema), - }, - { additionalProperties: false }, -); - -export const DashboardModuleSchema = Type.Object( - { - id: IdentifierSchema, - kind: Type.Union([ - Type.Literal("summary"), - Type.Literal("weather"), - Type.Literal("custom"), - ]), - title: Type.Optional(Type.String({ minLength: 1 })), - label: Type.Optional(Type.String()), - value: Type.Optional(Type.String()), - detail: Type.Optional(Type.String()), - icon: Type.Optional(IconReferenceSchema), - severity: Type.Optional(SeveritySchema), - datasource: Type.Optional(DatasourceReferenceSchema), - }, - { additionalProperties: false }, -); - -const LayoutSchema = Type.Object( - { - density: Type.Optional(Type.Union([Type.Literal("compact"), Type.Literal("dense")])), - telemetry: Type.Array(IdentifierSchema), - serviceGroups: Type.Array(IdentifierSchema), - statusStrips: Type.Array(IdentifierSchema), - modules: Type.Optional(Type.Array(IdentifierSchema)), - }, - { additionalProperties: false }, -); - -const MetadataSchema = Type.Object( - { - title: Type.String({ minLength: 1 }), - subtitle: Type.Optional(Type.String()), - description: Type.Optional(Type.String()), - timezone: Type.Optional(Type.String({ minLength: 1 })), - refreshIntervalSeconds: Type.Optional(Type.Integer({ minimum: 5 })), - }, - { additionalProperties: false }, -); - -export const DashboardDocumentSchema = Type.Object( - { - schemaVersion: Type.Literal(DASHBOARD_SCHEMA_VERSION), - metadata: MetadataSchema, - layout: LayoutSchema, - telemetry: Type.Array(TelemetryCardSchema), - serviceGroups: Type.Array(ServiceGroupSchema), - statusStrips: Type.Array(StatusStripSchema), - modules: Type.Optional(Type.Array(DashboardModuleSchema)), - migration: Type.Optional( - Type.Object( - { - previousVersion: Type.Optional(Type.String({ minLength: 1 })), - notes: Type.Optional(Type.String()), - }, - { additionalProperties: false }, - ), - ), - }, - { - $id: "https://dimensionlab.net/schemas/dashboard-document.v1.json", - additionalProperties: false, - }, -); - -export type Severity = Static; -export type DatasourceReference = Static; -export type MetricValue = Static; -export type TelemetryCard = Static; -export type ServiceEntry = Static; -export type ServiceGroup = Static; -export type StatusItem = Static; -export type StatusStrip = Static; -export type DashboardModule = Static; -export type DashboardDocument = Static; - -export const dashboardDocumentJsonSchema = DashboardDocumentSchema; diff --git a/src/lib/model/validation.ts b/src/lib/model/validation.ts deleted file mode 100644 index f70b0cd..0000000 --- a/src/lib/model/validation.ts +++ /dev/null @@ -1,322 +0,0 @@ -import Ajv, { type ErrorObject } from "ajv"; -import addFormats from "ajv-formats"; -import { - DashboardDocumentSchema, - type DashboardDocument, -} from "./schema"; - -type DashboardValidationIssue = ErrorObject | SemanticValidationIssue; - -interface SemanticValidationIssue { - instancePath: string; - schemaPath: string; - keyword: "semantic"; - params: Record; - message: string; -} - -export interface DashboardValidationFailure { - valid: false; - errors: string[]; - details: DashboardValidationIssue[]; -} - -export interface DashboardValidationSuccess { - valid: true; - data: DashboardDocument; -} - -export type DashboardValidationResult = - | DashboardValidationFailure - | DashboardValidationSuccess; - -const ajv = addFormats( - new Ajv({ - allErrors: true, - strict: false, - strictNumbers: true, - }), -); - -const validateDashboard = ajv.compile(DashboardDocumentSchema); - -export function formatValidationErrors( - errors: DashboardValidationIssue[] = [], -): string[] { - return errors.map((error) => { - const path = error.instancePath || "/"; - const suffix = formatErrorParams(error); - const message = error.message || "is invalid"; - return `${path} ${message}${suffix}`; - }); -} - -export function validateDashboardDocument( - value: unknown, -): DashboardValidationResult { - const finiteNumberIssues: SemanticValidationIssue[] = []; - const undefinedIssues: SemanticValidationIssue[] = []; - collectFiniteNumberIssues(value, "", finiteNumberIssues); - collectUndefinedIssues(value, "", undefinedIssues); - - if (validateDashboard(value)) { - const semanticIssues = [ - ...finiteNumberIssues, - ...undefinedIssues, - ...validateSemanticRules(value), - ]; - if (semanticIssues.length === 0) { - return { valid: true, data: value }; - } - - return { - valid: false, - errors: formatValidationErrors(semanticIssues), - details: semanticIssues, - }; - } - - const details = [ - ...(validateDashboard.errors || []), - ...finiteNumberIssues, - ...undefinedIssues, - ]; - return { - valid: false, - errors: formatValidationErrors(details), - details, - }; -} - -export function assertDashboardDocument( - value: unknown, -): asserts value is DashboardDocument { - const result = validateDashboardDocument(value); - if (!result.valid) { - throw new Error(`Invalid dashboard document: ${result.errors.join("; ")}`); - } -} - -export function isDashboardDocument(value: unknown): value is DashboardDocument { - return validateDashboardDocument(value).valid; -} - -function formatErrorParams(error: DashboardValidationIssue): string { - if (error.keyword === "additionalProperties") { - const additionalProperty = error.params.additionalProperty; - return typeof additionalProperty === "string" - ? `: ${additionalProperty}` - : ""; - } - - if (error.keyword === "semantic") { - const id = error.params.id; - const ref = error.params.ref; - if (typeof id === "string") return `: ${id}`; - if (typeof ref === "string") return `: ${ref}`; - } - - return ""; -} - -function validateSemanticRules(document: DashboardDocument): SemanticValidationIssue[] { - const issues: SemanticValidationIssue[] = []; - - collectDuplicateIdIssues("telemetry", document.telemetry, issues); - collectDuplicateIdIssues("serviceGroups", document.serviceGroups, issues); - collectDuplicateIdIssues("statusStrips", document.statusStrips, issues); - collectDuplicateIdIssues("modules", document.modules || [], issues); - - document.serviceGroups.forEach((group, groupIndex) => { - collectDuplicateIdIssues( - `serviceGroups/${groupIndex}/services`, - group.services, - issues, - ); - }); - - document.statusStrips.forEach((strip, stripIndex) => { - collectDuplicateIdIssues( - `statusStrips/${stripIndex}/items`, - strip.items, - issues, - ); - }); - - collectMissingReferenceIssues( - "layout/telemetry", - document.layout.telemetry, - new Set(document.telemetry.map((item) => item.id)), - issues, - ); - collectMissingReferenceIssues( - "layout/serviceGroups", - document.layout.serviceGroups, - new Set(document.serviceGroups.map((item) => item.id)), - issues, - ); - collectMissingReferenceIssues( - "layout/statusStrips", - document.layout.statusStrips, - new Set(document.statusStrips.map((item) => item.id)), - issues, - ); - collectMissingReferenceIssues( - "layout/modules", - document.layout.modules || [], - new Set((document.modules || []).map((item) => item.id)), - issues, - ); - - document.telemetry.forEach((card, index) => { - if (card.value.kind === "text" && card.thresholds !== undefined) { - issues.push( - semanticIssue( - `/telemetry/${index}/thresholds`, - "must not be set for text metric values", - { id: card.id }, - ), - ); - } - - if ( - card.value.kind === "percent" && - ((card.thresholds?.warning !== undefined && card.thresholds.warning > 100) || - (card.thresholds?.danger !== undefined && card.thresholds.danger > 100)) - ) { - issues.push( - semanticIssue( - `/telemetry/${index}/thresholds`, - "percent thresholds must be between 0 and 100", - { id: card.id }, - ), - ); - } - - const warning = card.thresholds?.warning; - const danger = card.thresholds?.danger; - if (warning !== undefined && danger !== undefined && warning > danger) { - issues.push( - semanticIssue( - `/telemetry/${index}/thresholds`, - "warning threshold must be less than or equal to danger threshold", - { id: card.id }, - ), - ); - } - }); - - return issues; -} - -function collectUndefinedIssues( - value: unknown, - path: string, - issues: SemanticValidationIssue[], -) { - if (value === undefined) { - issues.push( - semanticIssue(path || "/", "must be omitted instead of undefined", {}), - ); - return; - } - - if (Array.isArray(value)) { - value.forEach((item, index) => { - collectUndefinedIssues(item, `${path}/${index}`, issues); - }); - return; - } - - if (value && typeof value === "object") { - Object.entries(value).forEach(([key, item]) => { - collectUndefinedIssues(item, `${path}/${escapeJsonPointer(key)}`, issues); - }); - } -} - -function collectFiniteNumberIssues( - value: unknown, - path: string, - issues: SemanticValidationIssue[], -) { - if (typeof value === "number") { - if (!Number.isFinite(value)) { - issues.push( - semanticIssue(path || "/", "must be a finite JSON number", {}), - ); - } - return; - } - - if (Array.isArray(value)) { - value.forEach((item, index) => { - collectFiniteNumberIssues(item, `${path}/${index}`, issues); - }); - return; - } - - if (value && typeof value === "object") { - Object.entries(value).forEach(([key, item]) => { - collectFiniteNumberIssues(item, `${path}/${escapeJsonPointer(key)}`, issues); - }); - } -} - -function collectDuplicateIdIssues( - collectionPath: string, - items: Array<{ id: string }>, - issues: SemanticValidationIssue[], -) { - const seen = new Set(); - items.forEach((item, index) => { - if (seen.has(item.id)) { - issues.push( - semanticIssue( - `/${collectionPath}/${index}/id`, - "must be unique within its collection", - { id: item.id }, - ), - ); - return; - } - seen.add(item.id); - }); -} - -function collectMissingReferenceIssues( - layoutPath: string, - refs: string[], - validIds: Set, - issues: SemanticValidationIssue[], -) { - refs.forEach((ref, index) => { - if (!validIds.has(ref)) { - issues.push( - semanticIssue( - `/${layoutPath}/${index}`, - "must reference an existing item", - { ref }, - ), - ); - } - }); -} - -function semanticIssue( - instancePath: string, - message: string, - params: Record, -): SemanticValidationIssue { - return { - instancePath, - schemaPath: "", - keyword: "semantic", - params, - message, - }; -} - -function escapeJsonPointer(value: string): string { - return value.replaceAll("~", "~0").replaceAll("/", "~1"); -}