mirror of
https://github.com/Nezumi-2711/uptime-monitoring.git
synced 2026-09-22 05:41:59 +00:00
feat: add mantain config
This commit is contained in:
@@ -20,13 +20,17 @@
|
||||
"tw-animate-css": "^1.4.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^8.0.1",
|
||||
"@cloudflare/vite-plugin": "^1.54.1",
|
||||
"@cloudflare/vitest-plugin": "^1.1.1",
|
||||
"@rolldown/plugin-babel": "^0.2.3",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@types/node": "^26.4.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.5",
|
||||
"@vitejs/plugin-react": "^6.1.0",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^17.4.1",
|
||||
@@ -45,21 +49,21 @@
|
||||
"workerd",
|
||||
],
|
||||
"packages": {
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
"@babel/code-frame": ["@babel/code-frame@8.0.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^8.0.0", "js-tokens": "^10.0.0" } }, "sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
|
||||
"@babel/compat-data": ["@babel/compat-data@8.0.0", "", {}, "sha512-DOjnob/cXOUgDOozCDeq/aK2p5y8dUIVdf6tNhEV1HQRd6I8aQ4f4fbtHRVEvb6lP3BGomrKHiS8ICAASSVQSw=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
|
||||
"@babel/core": ["@babel/core@8.0.1", "", { "dependencies": { "@babel/code-frame": "^8.0.0", "@babel/generator": "^8.0.0", "@babel/helper-compilation-targets": "^8.0.0", "@babel/helpers": "^8.0.0", "@babel/parser": "^8.0.0", "@babel/template": "^8.0.0", "@babel/traverse": "^8.0.0", "@babel/types": "^8.0.0", "@types/gensync": "^1.0.5", "convert-source-map": "^2.0.0", "empathic": "^2.0.1", "gensync": "^1.0.0-beta.2", "import-meta-resolve": "^4.2.0", "json5": "^2.2.3", "obug": "^2.1.1", "semver": "^7.7.3" } }, "sha512-5FgxM4dLQpMJHSiVATk8foW263dVHQHBVpXYiimNECVWG01f4nFyEbQixeT6Mwvg7TayREJ2gpKl3o2RoMdnqw=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
|
||||
"@babel/generator": ["@babel/generator@8.0.0", "", { "dependencies": { "@babel/parser": "^8.0.0", "@babel/types": "^8.0.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "@types/jsesc": "^2.5.0", "jsesc": "^3.0.2" } }, "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g=="],
|
||||
|
||||
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@8.0.0", "", { "dependencies": { "@babel/compat-data": "^8.0.0", "@babel/helper-validator-option": "^8.0.0", "browserslist": "^4.24.0", "lru-cache": "^11.0.0", "semver": "^7.7.3" } }, "sha512-JwculLABZvyPvyLBpwU/E/IbH2uM3mnxNtIJpxnIfb24y1PrdVxK5Dqjle4DpgqpGRnwgC7G8IkzPdSXZrO1Ew=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
|
||||
"@babel/helper-globals": ["@babel/helper-globals@8.0.0", "", {}, "sha512-lLozHOM6sWWlxNo8CYqHy4MBZeTvHXNgVPBfPOGsjPKUzHC2Az9QwB6gxdQmpwHl6GlQtbGgS+lj5887guDiLw=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="],
|
||||
|
||||
@@ -75,15 +79,15 @@
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0", "", {}, "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.4", "", {}, "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@8.0.0", "", {}, "sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
|
||||
"@babel/helpers": ["@babel/helpers@8.0.0", "", { "dependencies": { "@babel/template": "^8.0.0", "@babel/types": "^8.0.0" } }, "sha512-wfbi91pM3py96oIiJEz7qIpyXDytgr9zQC1HEWwlGNVRAEmItuU/0a41ZUKu1sJGyhhOIpc4t5vk4PYzt8wpsg=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||
"@babel/parser": ["@babel/parser@8.0.4", "", { "dependencies": { "@babel/types": "^8.0.4" }, "bin": "./bin/babel-parser.js" }, "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g=="],
|
||||
|
||||
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="],
|
||||
|
||||
@@ -95,11 +99,11 @@
|
||||
|
||||
"@babel/preset-typescript": ["@babel/preset-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
"@babel/template": ["@babel/template@8.0.0", "", { "dependencies": { "@babel/code-frame": "^8.0.0", "@babel/parser": "^8.0.0", "@babel/types": "^8.0.0" } }, "sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
|
||||
"@babel/traverse": ["@babel/traverse@8.0.4", "", { "dependencies": { "@babel/code-frame": "^8.0.0", "@babel/generator": "^8.0.0", "@babel/helper-globals": "^8.0.0", "@babel/parser": "^8.0.4", "@babel/template": "^8.0.0", "@babel/types": "^8.0.4", "obug": "^2.1.1" } }, "sha512-bZnmqzGG8UZneG1lLxBoWIH0G6Gr1D846Yu4/3XnY6FhCndMR49u26nTY08u/dAxWmLWF9vGQOuC+84FfIUoeg=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
"@babel/types": ["@babel/types@8.0.4", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0", "@babel/helper-validator-identifier": "^8.0.4" } }, "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g=="],
|
||||
|
||||
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="],
|
||||
|
||||
@@ -257,7 +261,7 @@
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.30.0", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA=="],
|
||||
|
||||
@@ -465,6 +469,8 @@
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ=="],
|
||||
|
||||
"@rolldown/plugin-babel": ["@rolldown/plugin-babel@0.2.3", "", { "dependencies": { "picomatch": "^4.0.4" }, "peerDependencies": { "@babel/core": "^7.29.0 || ^8.0.0-rc.1", "@babel/plugin-transform-runtime": "^7.29.0 || ^8.0.0-rc.1", "@babel/runtime": "^7.27.0 || ^8.0.0-rc.1", "rolldown": "^1.0.0-rc.5", "vite": "^8.0.0" }, "optionalPeers": ["@babel/plugin-transform-runtime", "@babel/runtime", "vite"] }, "sha512-+zEk16yGlz1F9STiRr6uG9hmIXb6nprjLczV/htGptYuLoCuxb+itZ03RKCEeOhBpDDd1NU7qF6x1VLMUp62bw=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||
|
||||
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
||||
@@ -515,6 +521,14 @@
|
||||
|
||||
"@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
|
||||
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
|
||||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||
|
||||
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||
@@ -539,6 +553,10 @@
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/gensync": ["@types/gensync@1.0.5", "", {}, "sha512-MbsRCT7mTikHwKZ0X+LVUTLRrZZRLipTuXEO9qOYO+zmjMVk81axyClMROf6uoPD9MRVu46bx8zoR0Ad9q3NAg=="],
|
||||
|
||||
"@types/jsesc": ["@types/jsesc@2.5.1", "", {}, "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw=="],
|
||||
|
||||
"@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="],
|
||||
@@ -585,6 +603,8 @@
|
||||
|
||||
"atomically": ["atomically@1.7.0", "", {}, "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w=="],
|
||||
|
||||
"babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
|
||||
|
||||
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.11.20", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw=="],
|
||||
@@ -715,6 +735,8 @@
|
||||
|
||||
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"empathic": ["empathic@2.0.1", "", {}, "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],
|
||||
@@ -835,6 +857,8 @@
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||
@@ -879,7 +903,7 @@
|
||||
|
||||
"jose": ["jose@6.2.10", "", {}, "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
"js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="],
|
||||
|
||||
@@ -929,7 +953,7 @@
|
||||
|
||||
"log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
"lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="],
|
||||
|
||||
"lucide-react": ["lucide-react@1.34.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-vnjGJNI7Htk5+oWW8gXGuaLgwgAb0T6/iZbBrp9JCfRFwdNWZ0YTm3eyxjOLgwN6r8iyAf3UA70zNmBRBNv7yg=="],
|
||||
|
||||
@@ -1093,7 +1117,7 @@
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
"semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||
|
||||
@@ -1253,7 +1277,35 @@
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
||||
|
||||
"@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
"@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
|
||||
|
||||
"@babel/helper-optimise-call-expression/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@babel/preset-typescript/@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
|
||||
|
||||
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||
|
||||
"@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||
|
||||
@@ -1263,10 +1315,6 @@
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||
|
||||
"@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@jridgewell/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@tailwindcss/node/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=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
|
||||
@@ -1281,14 +1329,26 @@
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@types/babel__core/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||
|
||||
"@types/babel__core/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@types/babel__generator/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@types/babel__template/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||
|
||||
"@types/babel__template/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@types/babel__traverse/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"babel-plugin-react-compiler/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
|
||||
|
||||
"conf/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
|
||||
|
||||
"conf/json-schema-typed": ["json-schema-typed@7.0.3", "", {}, "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A=="],
|
||||
|
||||
"conf/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="],
|
||||
@@ -1311,15 +1371,19 @@
|
||||
|
||||
"onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
"parse-json/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||
|
||||
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||
|
||||
"router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
|
||||
|
||||
"shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
"shadcn/@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
|
||||
|
||||
"sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
"shadcn/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||
|
||||
"shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
|
||||
|
||||
@@ -1327,6 +1391,92 @@
|
||||
|
||||
"wsl-utils/powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
|
||||
|
||||
"@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/traverse/@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/traverse/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/helper-module-transforms/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
"@babel/helper-module-transforms/@babel/traverse/@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
|
||||
|
||||
"@babel/helper-module-transforms/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
|
||||
|
||||
"@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||
|
||||
"@babel/helper-module-transforms/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
|
||||
"@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@babel/helper-optimise-call-expression/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@babel/helper-optimise-call-expression/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse/@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||
|
||||
"@dotenvx/dotenvx/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
|
||||
@@ -1407,6 +1557,26 @@
|
||||
|
||||
"@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
|
||||
"@types/babel__core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@types/babel__core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@types/babel__generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@types/babel__generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@types/babel__template/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@types/babel__template/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@types/babel__traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@types/babel__traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"babel-plugin-react-compiler/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"babel-plugin-react-compiler/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||
@@ -1462,5 +1632,79 @@
|
||||
"drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"parse-json/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"parse-json/@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"shadcn/@babel/core/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
"shadcn/@babel/core/@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
|
||||
|
||||
"shadcn/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
|
||||
|
||||
"shadcn/@babel/core/@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
|
||||
|
||||
"shadcn/@babel/core/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
|
||||
"shadcn/@babel/core/@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
|
||||
|
||||
"shadcn/@babel/core/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"shadcn/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"shadcn/@babel/parser/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/traverse/@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"@babel/helper-module-transforms/@babel/traverse/@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"@babel/helper-module-transforms/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse/@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"shadcn/@babel/core/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"shadcn/@babel/core/@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"shadcn/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
|
||||
|
||||
"shadcn/@babel/core/@babel/helper-compilation-targets/@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
|
||||
|
||||
"shadcn/@babel/core/@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"shadcn/@babel/core/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
|
||||
|
||||
"shadcn/@babel/core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"shadcn/@babel/core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"shadcn/@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"shadcn/@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
CREATE TABLE `maintenance_window_monitors` (
|
||||
`window_id` integer NOT NULL,
|
||||
`monitor_id` integer NOT NULL,
|
||||
PRIMARY KEY(`window_id`, `monitor_id`),
|
||||
FOREIGN KEY (`window_id`) REFERENCES `maintenance_windows`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`monitor_id`) REFERENCES `monitors`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `maintenance_window_monitors_monitor_id_idx` ON `maintenance_window_monitors` (`monitor_id`);--> statement-breakpoint
|
||||
CREATE TABLE `maintenance_windows` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`start_minute` integer NOT NULL,
|
||||
`duration_minutes` integer NOT NULL,
|
||||
`timezone` text DEFAULT 'UTC' NOT NULL,
|
||||
`enabled` integer DEFAULT true NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `maintenance_windows_enabled_idx` ON `maintenance_windows` (`enabled`);--> statement-breakpoint
|
||||
ALTER TABLE `checks` ADD `maintenance` integer DEFAULT false NOT NULL;
|
||||
@@ -0,0 +1,810 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "bf7ffa06-45b1-43d1-ba9a-193681f05bf2",
|
||||
"prevId": "5c9f981a-cccd-4832-806c-4b156078d34d",
|
||||
"tables": {
|
||||
"admin_credentials": {
|
||||
"name": "admin_credentials",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"ai_settings": {
|
||||
"name": "ai_settings",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"base_url": {
|
||||
"name": "base_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"api_key": {
|
||||
"name": "api_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"checks": {
|
||||
"name": "checks",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"monitor_id": {
|
||||
"name": "monitor_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ok": {
|
||||
"name": "ok",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status_code": {
|
||||
"name": "status_code",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"latency_ms": {
|
||||
"name": "latency_ms",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"error": {
|
||||
"name": "error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"checked_at": {
|
||||
"name": "checked_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"maintenance": {
|
||||
"name": "maintenance",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"checks_monitor_id_checked_at_idx": {
|
||||
"name": "checks_monitor_id_checked_at_idx",
|
||||
"columns": [
|
||||
"monitor_id",
|
||||
"checked_at"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"checks_monitor_id_monitors_id_fk": {
|
||||
"name": "checks_monitor_id_monitors_id_fk",
|
||||
"tableFrom": "checks",
|
||||
"tableTo": "monitors",
|
||||
"columnsFrom": [
|
||||
"monitor_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"incidents": {
|
||||
"name": "incidents",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"monitor_id": {
|
||||
"name": "monitor_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"started_at": {
|
||||
"name": "started_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"resolved_at": {
|
||||
"name": "resolved_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"start_status_code": {
|
||||
"name": "start_status_code",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"start_error": {
|
||||
"name": "start_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ai_message": {
|
||||
"name": "ai_message",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"duration_ms": {
|
||||
"name": "duration_ms",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"incidents_monitor_id_started_at_idx": {
|
||||
"name": "incidents_monitor_id_started_at_idx",
|
||||
"columns": [
|
||||
"monitor_id",
|
||||
"started_at"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"incidents_monitor_id_monitors_id_fk": {
|
||||
"name": "incidents_monitor_id_monitors_id_fk",
|
||||
"tableFrom": "incidents",
|
||||
"tableTo": "monitors",
|
||||
"columnsFrom": [
|
||||
"monitor_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"login_attempts": {
|
||||
"name": "login_attempts",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"attempted_at": {
|
||||
"name": "attempted_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"login_attempts_ip_attempted_at_idx": {
|
||||
"name": "login_attempts_ip_attempted_at_idx",
|
||||
"columns": [
|
||||
"ip_address",
|
||||
"attempted_at"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"maintenance_window_monitors": {
|
||||
"name": "maintenance_window_monitors",
|
||||
"columns": {
|
||||
"window_id": {
|
||||
"name": "window_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"monitor_id": {
|
||||
"name": "monitor_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"maintenance_window_monitors_monitor_id_idx": {
|
||||
"name": "maintenance_window_monitors_monitor_id_idx",
|
||||
"columns": [
|
||||
"monitor_id"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"maintenance_window_monitors_window_id_maintenance_windows_id_fk": {
|
||||
"name": "maintenance_window_monitors_window_id_maintenance_windows_id_fk",
|
||||
"tableFrom": "maintenance_window_monitors",
|
||||
"tableTo": "maintenance_windows",
|
||||
"columnsFrom": [
|
||||
"window_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"maintenance_window_monitors_monitor_id_monitors_id_fk": {
|
||||
"name": "maintenance_window_monitors_monitor_id_monitors_id_fk",
|
||||
"tableFrom": "maintenance_window_monitors",
|
||||
"tableTo": "monitors",
|
||||
"columnsFrom": [
|
||||
"monitor_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"maintenance_window_monitors_window_id_monitor_id_pk": {
|
||||
"columns": [
|
||||
"window_id",
|
||||
"monitor_id"
|
||||
],
|
||||
"name": "maintenance_window_monitors_window_id_monitor_id_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"maintenance_windows": {
|
||||
"name": "maintenance_windows",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"start_minute": {
|
||||
"name": "start_minute",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"duration_minutes": {
|
||||
"name": "duration_minutes",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"timezone": {
|
||||
"name": "timezone",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'UTC'"
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"maintenance_windows_enabled_idx": {
|
||||
"name": "maintenance_windows_enabled_idx",
|
||||
"columns": [
|
||||
"enabled"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"monitor_daily_stats": {
|
||||
"name": "monitor_daily_stats",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"monitor_id": {
|
||||
"name": "monitor_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"day": {
|
||||
"name": "day",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"total_checks": {
|
||||
"name": "total_checks",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"up_checks": {
|
||||
"name": "up_checks",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"avg_latency_ms": {
|
||||
"name": "avg_latency_ms",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"min_latency_ms": {
|
||||
"name": "min_latency_ms",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"max_latency_ms": {
|
||||
"name": "max_latency_ms",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"monitor_daily_stats_monitor_id_day_uidx": {
|
||||
"name": "monitor_daily_stats_monitor_id_day_uidx",
|
||||
"columns": [
|
||||
"monitor_id",
|
||||
"day"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"monitor_daily_stats_monitor_id_monitors_id_fk": {
|
||||
"name": "monitor_daily_stats_monitor_id_monitors_id_fk",
|
||||
"tableFrom": "monitor_daily_stats",
|
||||
"tableTo": "monitors",
|
||||
"columnsFrom": [
|
||||
"monitor_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"monitors": {
|
||||
"name": "monitors",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"method": {
|
||||
"name": "method",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'GET'"
|
||||
},
|
||||
"expected_status": {
|
||||
"name": "expected_status",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 200
|
||||
},
|
||||
"interval_seconds": {
|
||||
"name": "interval_seconds",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 300
|
||||
},
|
||||
"timeout_ms": {
|
||||
"name": "timeout_ms",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 10000
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"alerts_enabled": {
|
||||
"name": "alerts_enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"last_ok": {
|
||||
"name": "last_ok",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_status_code": {
|
||||
"name": "last_status_code",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_latency_ms": {
|
||||
"name": "last_latency_ms",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_error": {
|
||||
"name": "last_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_checked_at": {
|
||||
"name": "last_checked_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"monitors_enabled_last_checked_at_idx": {
|
||||
"name": "monitors_enabled_last_checked_at_idx",
|
||||
"columns": [
|
||||
"enabled",
|
||||
"last_checked_at"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"notification_settings": {
|
||||
"name": "notification_settings",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"webhook_url": {
|
||||
"name": "webhook_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"webhook_enabled": {
|
||||
"name": "webhook_enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"sessions": {
|
||||
"name": "sessions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"sessions_expires_at_idx": {
|
||||
"name": "sessions_expires_at_idx",
|
||||
"columns": [
|
||||
"expires_at"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,13 @@
|
||||
"when": 1787997596170,
|
||||
"tag": "0004_smiling_jack_flag",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "6",
|
||||
"when": 1788007515959,
|
||||
"tag": "0005_pink_trish_tilby",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -19,13 +19,17 @@
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^8.0.1",
|
||||
"@cloudflare/vite-plugin": "^1.54.1",
|
||||
"@cloudflare/vitest-plugin": "^1.1.1",
|
||||
"@rolldown/plugin-babel": "^0.2.3",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@types/node": "^26.4.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.5",
|
||||
"@vitejs/plugin-react": "^6.1.0",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^17.4.1",
|
||||
|
||||
+16
-7
@@ -1,11 +1,20 @@
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { RequireAuth } from './components/RequireAuth';
|
||||
import { usePathname } from './lib/router';
|
||||
import { DashboardPage } from './pages/DashboardPage';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { MonitorDetailPage } from './pages/MonitorDetailPage';
|
||||
import { SettingsPage } from './pages/SettingsPage';
|
||||
import { StatusPage } from './pages/StatusPage';
|
||||
|
||||
const DashboardPage = lazy(() => import('./pages/DashboardPage').then((module) => ({ default: module.DashboardPage })));
|
||||
const LoginPage = lazy(() => import('./pages/LoginPage').then((module) => ({ default: module.LoginPage })));
|
||||
const MonitorDetailPage = lazy(() => import('./pages/MonitorDetailPage').then((module) => ({ default: module.MonitorDetailPage })));
|
||||
const SettingsPage = lazy(() => import('./pages/SettingsPage').then((module) => ({ default: module.SettingsPage })));
|
||||
const StatusPage = lazy(() => import('./pages/StatusPage').then((module) => ({ default: module.StatusPage })));
|
||||
|
||||
function PageFallback() {
|
||||
return (
|
||||
<main className="full-page-loading" aria-busy="true" aria-label="Loading page">
|
||||
<p>Loading…</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const pathname = usePathname();
|
||||
@@ -24,7 +33,7 @@ function App() {
|
||||
content = <RequireAuth>{page}</RequireAuth>;
|
||||
}
|
||||
|
||||
return <TooltipProvider>{content}</TooltipProvider>;
|
||||
return <Suspense fallback={<PageFallback />}>{content}</Suspense>;
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { deleteJson, getJson, patchJson, postJson } from './http';
|
||||
|
||||
export type MaintenanceWindow = {
|
||||
id: number;
|
||||
name: string;
|
||||
startMinute: number;
|
||||
durationMinutes: number;
|
||||
timezone: string;
|
||||
enabled: boolean;
|
||||
monitorIds: number[];
|
||||
active: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type MaintenanceWindowInput = {
|
||||
name: string;
|
||||
startMinute: number;
|
||||
durationMinutes: number;
|
||||
timezone: string;
|
||||
enabled: boolean;
|
||||
monitorIds: number[];
|
||||
};
|
||||
|
||||
export function getMaintenanceWindows(signal?: AbortSignal) {
|
||||
return getJson<{ windows: MaintenanceWindow[] }>('/api/maintenance', { signal, credentials: 'same-origin' });
|
||||
}
|
||||
|
||||
export function createMaintenanceWindow(input: MaintenanceWindowInput) {
|
||||
return postJson<{ window: MaintenanceWindow }>('/api/maintenance', input);
|
||||
}
|
||||
|
||||
export function updateMaintenanceWindow(id: number, input: Partial<MaintenanceWindowInput>) {
|
||||
return patchJson<{ window: MaintenanceWindow }>(`/api/maintenance/${id}`, input);
|
||||
}
|
||||
|
||||
export function deleteMaintenanceWindow(id: number) {
|
||||
return deleteJson<{ ok: true }>(`/api/maintenance/${id}`);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getJson } from './http';
|
||||
|
||||
export type PublicServiceStatus = 'up' | 'down' | 'unknown';
|
||||
export type PublicServiceStatus = 'up' | 'down' | 'unknown' | 'maintenance';
|
||||
export type PublicOverallStatus = 'operational' | 'degraded' | 'down';
|
||||
|
||||
export type PublicService = {
|
||||
@@ -8,6 +8,7 @@ export type PublicService = {
|
||||
name: string;
|
||||
status: PublicServiceStatus;
|
||||
message: string | null;
|
||||
maintenance: { name: string; endsAt: string } | null;
|
||||
lastCheckedAt: string | null;
|
||||
uptime90d: number | null;
|
||||
history: Array<{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Activity, LogOut, Menu, Settings, Zap, type LucideIcon } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { navigate, usePathname } from '../lib/router';
|
||||
import { useLogoutMutation } from '../queries/auth';
|
||||
|
||||
@@ -51,6 +51,7 @@ export function AppHeader({ context }: { context?: string }) {
|
||||
<span>upwatch</span>
|
||||
</Button>
|
||||
|
||||
<TooltipProvider>
|
||||
<nav className="nav-actions" aria-label="Primary navigation">
|
||||
{context && <span className="header-context">{context}</span>}
|
||||
{items.map((item) => {
|
||||
@@ -78,6 +79,7 @@ export function AppHeader({ context }: { context?: string }) {
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</TooltipProvider>
|
||||
|
||||
<div className="app-nav-mobile-trigger">
|
||||
<Sheet>
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
import { type FormEvent, useMemo, useState } from 'react';
|
||||
import { Check, ChevronDown } from 'lucide-react';
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import type { MaintenanceWindow, MaintenanceWindowInput } from '../../api/maintenance';
|
||||
import { useCreateMaintenanceWindowMutation, useUpdateMaintenanceWindowMutation } from '../../queries/maintenance';
|
||||
import { useMonitorsQuery } from '../../queries/monitors';
|
||||
|
||||
export function minutesToTime(minutes: number) {
|
||||
const normalized = ((minutes % 1440) + 1440) % 1440;
|
||||
return `${String(Math.floor(normalized / 60)).padStart(2, '0')}:${String(normalized % 60).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function timeToMinutes(value: string) {
|
||||
const [hour, minute] = value.split(':').map(Number);
|
||||
return hour * 60 + minute;
|
||||
}
|
||||
|
||||
function detectedTimezone() {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
||||
}
|
||||
|
||||
function timezoneOptions() {
|
||||
const intl = Intl as typeof Intl & { supportedValuesOf?: (key: 'timeZone') => string[] };
|
||||
if (intl.supportedValuesOf) return [...new Set(['UTC', detectedTimezone(), ...intl.supportedValuesOf('timeZone')])];
|
||||
return [...new Set(['UTC', detectedTimezone()])];
|
||||
}
|
||||
|
||||
type FormState = {
|
||||
name: string;
|
||||
start: string;
|
||||
end: string;
|
||||
timezone: string;
|
||||
enabled: boolean;
|
||||
monitorIds: number[];
|
||||
};
|
||||
|
||||
function initialForm(editing: MaintenanceWindow | null): FormState {
|
||||
if (!editing) {
|
||||
return { name: '', start: '02:00', end: '03:00', timezone: detectedTimezone(), enabled: true, monitorIds: [] };
|
||||
}
|
||||
return {
|
||||
name: editing.name,
|
||||
start: minutesToTime(editing.startMinute),
|
||||
end: minutesToTime(editing.startMinute + editing.durationMinutes),
|
||||
timezone: editing.timezone,
|
||||
enabled: editing.enabled,
|
||||
monitorIds: editing.monitorIds,
|
||||
};
|
||||
}
|
||||
|
||||
export function MaintenanceWindowDialog({ editing, onClose }: { editing: MaintenanceWindow | null; onClose: () => void }) {
|
||||
const createMutation = useCreateMaintenanceWindowMutation();
|
||||
const updateMutation = useUpdateMaintenanceWindowMutation();
|
||||
const monitorsQuery = useMonitorsQuery();
|
||||
const [form, setForm] = useState(() => initialForm(editing));
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
const timezones = useMemo(() => timezoneOptions(), []);
|
||||
const mutation = editing ? updateMutation : createMutation;
|
||||
|
||||
function close() {
|
||||
if (!mutation.isPending) onClose();
|
||||
}
|
||||
|
||||
function submit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const startMinute = timeToMinutes(form.start);
|
||||
const durationMinutes = (timeToMinutes(form.end) - startMinute + 1440) % 1440;
|
||||
if (durationMinutes === 0) {
|
||||
setValidationError('Start and end time must differ');
|
||||
return;
|
||||
}
|
||||
setValidationError(null);
|
||||
const input: MaintenanceWindowInput = {
|
||||
name: form.name,
|
||||
startMinute,
|
||||
durationMinutes,
|
||||
timezone: form.timezone,
|
||||
enabled: form.enabled,
|
||||
monitorIds: form.monitorIds,
|
||||
};
|
||||
if (editing) updateMutation.mutate({ id: editing.id, input }, { onSuccess: onClose });
|
||||
else createMutation.mutate(input, { onSuccess: onClose });
|
||||
}
|
||||
|
||||
function toggleMonitor(id: number, checked: boolean) {
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
monitorIds: checked ? [...current.monitorIds, id] : current.monitorIds.filter((monitorId) => monitorId !== id),
|
||||
}));
|
||||
}
|
||||
|
||||
const monitors = monitorsQuery.data?.monitors ?? [];
|
||||
const selectedMonitors = monitors.filter((monitor) => form.monitorIds.includes(monitor.id));
|
||||
const selectedServicesLabel =
|
||||
selectedMonitors.length === 0
|
||||
? 'Select services'
|
||||
: selectedMonitors.length === 1
|
||||
? selectedMonitors[0].name
|
||||
: `${selectedMonitors[0].name} +${selectedMonitors.length - 1} more`;
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(open) => !open && close()}>
|
||||
<DialogContent
|
||||
className="max-h-[90vh] w-[calc(100%-2rem)] overflow-y-auto sm:max-w-2xl"
|
||||
onEscapeKeyDown={(event) => mutation.isPending && event.preventDefault()}
|
||||
onInteractOutside={(event) => mutation.isPending && event.preventDefault()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<p className="overline">Daily schedule</p>
|
||||
<DialogTitle>{editing ? `Edit ${editing.name}` : 'Add maintenance window'}</DialogTitle>
|
||||
<DialogDescription>Probes continue, but alerts and uptime calculations pause for selected services.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form className="maintenance-form" onSubmit={submit}>
|
||||
<label className="field maintenance-name" htmlFor="maintenance-name">
|
||||
<span>Name</span>
|
||||
<Input
|
||||
id="maintenance-name"
|
||||
value={form.name}
|
||||
onChange={(event) => setForm({ ...form, name: event.target.value })}
|
||||
maxLength={100}
|
||||
placeholder="Nightly database backup"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label className="field" htmlFor="maintenance-start">
|
||||
<span>Start</span>
|
||||
<Input
|
||||
id="maintenance-start"
|
||||
type="time"
|
||||
value={form.start}
|
||||
onChange={(event) => setForm({ ...form, start: event.target.value })}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label className="field" htmlFor="maintenance-end">
|
||||
<span>End</span>
|
||||
<Input
|
||||
id="maintenance-end"
|
||||
type="time"
|
||||
value={form.end}
|
||||
onChange={(event) => setForm({ ...form, end: event.target.value })}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<div className="field maintenance-timezone">
|
||||
<span id="maintenance-timezone-label">Timezone</span>
|
||||
<Select value={form.timezone} onValueChange={(timezone) => setForm({ ...form, timezone })}>
|
||||
<SelectTrigger aria-labelledby="maintenance-timezone-label">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{timezones.map((timezone) => (
|
||||
<SelectItem key={timezone} value={timezone}>
|
||||
{timezone}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<fieldset className="maintenance-services">
|
||||
<legend>Services</legend>
|
||||
<DropdownMenuPrimitive.Root>
|
||||
<DropdownMenuPrimitive.Trigger asChild>
|
||||
<button
|
||||
className="maintenance-service-select"
|
||||
type="button"
|
||||
disabled={monitorsQuery.isPending || monitors.length === 0}
|
||||
aria-label="Select services for this maintenance window"
|
||||
>
|
||||
<span className={selectedMonitors.length === 0 ? 'is-placeholder' : undefined}>
|
||||
{monitorsQuery.isPending
|
||||
? 'Loading services…'
|
||||
: monitors.length === 0
|
||||
? 'No services available'
|
||||
: selectedServicesLabel}
|
||||
</span>
|
||||
<span className="maintenance-service-select-meta">
|
||||
{selectedMonitors.length > 0 && <small>{selectedMonitors.length} selected</small>}
|
||||
<ChevronDown aria-hidden="true" />
|
||||
</span>
|
||||
</button>
|
||||
</DropdownMenuPrimitive.Trigger>
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content className="maintenance-service-menu" sideOffset={5} align="start">
|
||||
{monitors.map((monitor) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
key={monitor.id}
|
||||
className="maintenance-service-option"
|
||||
checked={form.monitorIds.includes(monitor.id)}
|
||||
onCheckedChange={(checked) => toggleMonitor(monitor.id, checked === true)}
|
||||
onSelect={(event) => event.preventDefault()}
|
||||
>
|
||||
<span className="maintenance-service-check" aria-hidden="true">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<span>{monitor.name}</span>
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))}
|
||||
</DropdownMenuPrimitive.Content>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
</DropdownMenuPrimitive.Root>
|
||||
{monitors.length === 0 && !monitorsQuery.isPending && (
|
||||
<small className="maintenance-services-empty">Add a monitor before assigning a maintenance window.</small>
|
||||
)}
|
||||
</fieldset>
|
||||
<div className="settings-toggle maintenance-enabled">
|
||||
<Switch id="maintenance-enabled" checked={form.enabled} onCheckedChange={(enabled) => setForm({ ...form, enabled })} />
|
||||
<label htmlFor="maintenance-enabled">
|
||||
<strong>Enable this window</strong>
|
||||
<small>The schedule repeats every day in the selected timezone.</small>
|
||||
</label>
|
||||
</div>
|
||||
<p className="maintenance-helper">Checks run every five minutes. Add a few minutes of padding before and after the backup.</p>
|
||||
<div className="form-actions compact-actions maintenance-actions">
|
||||
<Button variant="unstyled" className="secondary-button" type="button" onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="unstyled" className="primary-button" type="submit" disabled={mutation.isPending || monitorsQuery.isError}>
|
||||
{mutation.isPending ? 'Saving…' : editing ? 'Save changes' : 'Add window'}
|
||||
</Button>
|
||||
</div>
|
||||
{(validationError || mutation.isError) && (
|
||||
<p className="form-error" role="alert">
|
||||
{validationError ?? mutation.error?.message ?? 'Unable to save maintenance window'}
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { useState } from 'react';
|
||||
import { Pencil, Plus, Trash2 } from 'lucide-react';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Empty, EmptyDescription, EmptyTitle } from '@/components/ui/empty';
|
||||
import type { MaintenanceWindow } from '../../api/maintenance';
|
||||
import { useDeleteMaintenanceWindowMutation, useMaintenanceWindowsQuery } from '../../queries/maintenance';
|
||||
import { useMonitorsQuery } from '../../queries/monitors';
|
||||
import { MaintenanceWindowDialog, minutesToTime } from './MaintenanceWindowDialog';
|
||||
|
||||
export function MaintenanceWindowsPanel() {
|
||||
const windowsQuery = useMaintenanceWindowsQuery();
|
||||
const monitorsQuery = useMonitorsQuery();
|
||||
const deleteMutation = useDeleteMaintenanceWindowMutation();
|
||||
const [dialog, setDialog] = useState<{ open: boolean; editing: MaintenanceWindow | null }>({ open: false, editing: null });
|
||||
const [deleting, setDeleting] = useState<MaintenanceWindow | null>(null);
|
||||
const monitorNames = new Map(monitorsQuery.data?.monitors.map((monitor) => [monitor.id, monitor.name]));
|
||||
|
||||
function confirmDelete() {
|
||||
if (!deleting) return;
|
||||
deleteMutation.mutate(deleting.id, { onSuccess: () => setDeleting(null) });
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="maintenance-panel-header">
|
||||
<p>Define daily quiet periods for backups or planned service work.</p>
|
||||
<Button variant="unstyled" className="secondary-button" type="button" onClick={() => setDialog({ open: true, editing: null })}>
|
||||
<Plus /> Add window
|
||||
</Button>
|
||||
</div>
|
||||
{windowsQuery.isPending ? (
|
||||
<div className="table-empty">Loading maintenance windows…</div>
|
||||
) : windowsQuery.isError ? (
|
||||
<Empty variant="error" className="m-6">
|
||||
<EmptyTitle>Unable to load maintenance windows</EmptyTitle>
|
||||
</Empty>
|
||||
) : windowsQuery.data.windows.length === 0 ? (
|
||||
<Empty className="maintenance-empty">
|
||||
<EmptyTitle>No maintenance windows</EmptyTitle>
|
||||
<EmptyDescription>Add a recurring window to keep planned downtime out of alerts and uptime.</EmptyDescription>
|
||||
</Empty>
|
||||
) : (
|
||||
<div className="maintenance-window-list">
|
||||
{windowsQuery.data.windows.map((window) => (
|
||||
<article className="maintenance-window-row" key={window.id}>
|
||||
<div className="maintenance-window-main">
|
||||
<div className="maintenance-window-title">
|
||||
<strong>{window.name}</strong>
|
||||
{window.active && <Badge variant="maintenance">Active now</Badge>}
|
||||
{!window.enabled && <span className="maintenance-disabled">Disabled</span>}
|
||||
</div>
|
||||
<p>
|
||||
{minutesToTime(window.startMinute)}–{minutesToTime(window.startMinute + window.durationMinutes)}{' '}
|
||||
<span>Daily · {window.timezone}</span>
|
||||
</p>
|
||||
<div className="maintenance-window-services">
|
||||
{window.monitorIds.length ? (
|
||||
window.monitorIds.map((id) => <span key={id}>{monitorNames.get(id) ?? `Service ${id}`}</span>)
|
||||
) : (
|
||||
<em>No services assigned</em>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="maintenance-window-actions">
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className="icon-button"
|
||||
type="button"
|
||||
aria-label={`Edit ${window.name}`}
|
||||
onClick={() => setDialog({ open: true, editing: window })}
|
||||
>
|
||||
<Pencil />
|
||||
</Button>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className="icon-button danger-icon-button"
|
||||
type="button"
|
||||
aria-label={`Delete ${window.name}`}
|
||||
onClick={() => setDeleting(window)}
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{dialog.open && <MaintenanceWindowDialog editing={dialog.editing} onClose={() => setDialog({ open: false, editing: null })} />}
|
||||
<AlertDialog open={deleting !== null} onOpenChange={(open) => !open && !deleteMutation.isPending && setDeleting(null)}>
|
||||
<AlertDialogContent onEscapeKeyDown={(event) => deleteMutation.isPending && event.preventDefault()}>
|
||||
<AlertDialogHeader>
|
||||
<p className="overline">Confirm</p>
|
||||
<AlertDialogTitle>Delete {deleting?.name}?</AlertDialogTitle>
|
||||
<AlertDialogDescription>This removes the recurring schedule. Existing check history is unchanged.</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter className="form-actions compact-actions">
|
||||
<AlertDialogCancel>
|
||||
<Button variant="unstyled" className="secondary-button" type="button" disabled={deleteMutation.isPending}>
|
||||
Cancel
|
||||
</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className="danger-button"
|
||||
type="button"
|
||||
disabled={deleteMutation.isPending}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
confirmDelete();
|
||||
}}
|
||||
>
|
||||
{deleteMutation.isPending ? 'Deleting…' : 'Delete'}
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type FormEvent, useState } from 'react';
|
||||
import { ArrowLeft, BellRing, Send, Sparkles } from 'lucide-react';
|
||||
import { ArrowLeft, BellRing, Send, Sparkles, Wrench } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Empty, EmptyTitle } from '@/components/ui/empty';
|
||||
@@ -7,6 +7,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import type { AiSettings, NotificationSettings } from '../api/settings';
|
||||
import { AppHeader } from '../components/AppHeader';
|
||||
import { MaintenanceWindowsPanel } from '../components/settings/MaintenanceWindowsPanel';
|
||||
import { navigate } from '../lib/router';
|
||||
import {
|
||||
useAiSettingsQuery,
|
||||
@@ -158,8 +159,8 @@ export function SettingsPage() {
|
||||
</Button>
|
||||
<section className="settings-heading">
|
||||
<p className="overline">Integrations</p>
|
||||
<h1>Notifications & AI</h1>
|
||||
<p>Configure incident alerts and visitor-friendly status updates from one place.</p>
|
||||
<h1>Notifications, AI & maintenance</h1>
|
||||
<p>Configure incident alerts, visitor-friendly updates, and planned downtime from one place.</p>
|
||||
</section>
|
||||
<Card asChild>
|
||||
<section className="settings-card">
|
||||
@@ -207,6 +208,20 @@ export function SettingsPage() {
|
||||
)}
|
||||
</section>
|
||||
</Card>
|
||||
<Card asChild>
|
||||
<section className="settings-card maintenance-settings-card">
|
||||
<div className="settings-card-intro">
|
||||
<span>
|
||||
<Wrench />
|
||||
</span>
|
||||
<div>
|
||||
<h2>Maintenance windows</h2>
|
||||
<p>Keep probing during planned work while suppressing alerts and excluding those checks from uptime.</p>
|
||||
</div>
|
||||
</div>
|
||||
<MaintenanceWindowsPanel />
|
||||
</section>
|
||||
</Card>
|
||||
<section className="payload-preview">
|
||||
<p className="overline">Payload preview</p>
|
||||
<pre>{`{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Activity, CircleCheck, Database, RefreshCw, TriangleAlert, Zap } from 'lucide-react';
|
||||
import { Activity, CircleCheck, Database, RefreshCw, TriangleAlert, Wrench, Zap } from 'lucide-react';
|
||||
import { Badge, type BadgeVariant } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Empty, EmptyContent, EmptyDescription, EmptyMedia, EmptyTitle } from '@/components/ui/empty';
|
||||
@@ -29,8 +29,11 @@ const SERVICE_STATUS: Record<PublicServiceStatus, { label: string; className: Ba
|
||||
up: { label: 'Operational', className: 'online' },
|
||||
down: { label: 'Down', className: 'offline' },
|
||||
unknown: { label: 'Awaiting data', className: 'checking' },
|
||||
maintenance: { label: 'Under maintenance', className: 'maintenance' },
|
||||
};
|
||||
|
||||
const maintenanceTime = new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
function errorMessage(error: unknown) {
|
||||
return error instanceof Error ? error.message : 'Unknown request error';
|
||||
}
|
||||
@@ -55,6 +58,7 @@ export function StatusPage() {
|
||||
const [now, setNow] = useState(Date.now);
|
||||
const status = statusQuery.data;
|
||||
const activeIncidents = status?.services.filter((service) => service.message) ?? [];
|
||||
const maintenanceServices = status?.services.filter((service) => service.maintenance) ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => setNow(Date.now()), 5_000);
|
||||
@@ -126,6 +130,43 @@ export function StatusPage() {
|
||||
<time dateTime={new Date(status.updatedAt).toISOString()}>{relativeUpdate(status.updatedAt, now)}</time>
|
||||
</section>
|
||||
|
||||
{maintenanceServices.length > 0 && (
|
||||
<section className="scheduled-maintenance" aria-labelledby="scheduled-maintenance-title" aria-live="polite">
|
||||
<header className="scheduled-maintenance-header">
|
||||
<div className="scheduled-maintenance-heading">
|
||||
<span className="scheduled-maintenance-icon">
|
||||
<Wrench aria-hidden="true" />
|
||||
</span>
|
||||
<div>
|
||||
<p>Scheduled maintenance</p>
|
||||
<h2 id="scheduled-maintenance-title">Planned service work is in progress</h2>
|
||||
</div>
|
||||
</div>
|
||||
<span className="scheduled-maintenance-count">
|
||||
{maintenanceServices.length} {maintenanceServices.length === 1 ? 'service' : 'services'}
|
||||
</span>
|
||||
</header>
|
||||
<div className="scheduled-maintenance-list">
|
||||
{maintenanceServices.map((service) => (
|
||||
<article className="scheduled-maintenance-row" key={service.id}>
|
||||
<div className="scheduled-maintenance-service">
|
||||
<span className="scheduled-maintenance-service-icon">
|
||||
<SiteIcon monitorId={service.id} favicon="public" />
|
||||
</span>
|
||||
<div>
|
||||
<strong>{service.name}</strong>
|
||||
<span>{service.maintenance?.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="scheduled-maintenance-state">
|
||||
<i /> until {maintenanceTime.format(new Date(service.maintenance!.endsAt))}
|
||||
</span>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{activeIncidents.length > 0 && (
|
||||
<section className="active-incidents" aria-labelledby="active-incidents-title" aria-live="polite">
|
||||
<header className="active-incidents-header">
|
||||
@@ -184,7 +225,7 @@ export function StatusPage() {
|
||||
</div>
|
||||
|
||||
{status.services.length === 0 ? (
|
||||
<Empty className="min-h-[280px] place-content-center p-12">
|
||||
<Empty className="min-h-70 place-content-center p-12">
|
||||
<EmptyMedia variant="icon">
|
||||
<Database />
|
||||
</EmptyMedia>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
createMaintenanceWindow,
|
||||
deleteMaintenanceWindow,
|
||||
getMaintenanceWindows,
|
||||
updateMaintenanceWindow,
|
||||
type MaintenanceWindowInput,
|
||||
} from '../api/maintenance';
|
||||
import { queryClient } from '../lib/query-client';
|
||||
|
||||
export const maintenanceKeys = { all: ['maintenance'] as const };
|
||||
|
||||
export function useMaintenanceWindowsQuery() {
|
||||
return useQuery({
|
||||
queryKey: maintenanceKeys.all,
|
||||
queryFn: ({ signal }) => getMaintenanceWindows(signal),
|
||||
refetchInterval: 60_000,
|
||||
refetchIntervalInBackground: false,
|
||||
});
|
||||
}
|
||||
|
||||
function invalidateMaintenance() {
|
||||
return queryClient.invalidateQueries({ queryKey: maintenanceKeys.all });
|
||||
}
|
||||
|
||||
export function useCreateMaintenanceWindowMutation() {
|
||||
return useMutation({ mutationFn: (input: MaintenanceWindowInput) => createMaintenanceWindow(input), onSuccess: invalidateMaintenance });
|
||||
}
|
||||
|
||||
export function useUpdateMaintenanceWindowMutation() {
|
||||
return useMutation({
|
||||
mutationFn: ({ id, input }: { id: number; input: Partial<MaintenanceWindowInput> }) => updateMaintenanceWindow(id, input),
|
||||
onSuccess: invalidateMaintenance,
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteMaintenanceWindowMutation() {
|
||||
return useMutation({ mutationFn: (id: number) => deleteMaintenanceWindow(id), onSuccess: invalidateMaintenance });
|
||||
}
|
||||
@@ -1393,6 +1393,232 @@ button {
|
||||
color: #16754f;
|
||||
background: #f2fbf7;
|
||||
}
|
||||
.maintenance-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 20px 26px;
|
||||
border-bottom: 1px solid #ededed;
|
||||
}
|
||||
.maintenance-panel-header p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.maintenance-panel-header svg,
|
||||
.maintenance-window-actions svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
.maintenance-empty {
|
||||
padding: 44px 24px;
|
||||
}
|
||||
.maintenance-window-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 21px 26px;
|
||||
}
|
||||
.maintenance-window-row + .maintenance-window-row {
|
||||
border-top: 1px solid #ededed;
|
||||
}
|
||||
.maintenance-window-main {
|
||||
min-width: 0;
|
||||
}
|
||||
.maintenance-window-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.maintenance-window-title > strong {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.maintenance-disabled {
|
||||
font-size: 11px;
|
||||
color: var(--faint);
|
||||
}
|
||||
.maintenance-window-main > p {
|
||||
margin: 7px 0 10px;
|
||||
font:
|
||||
500 12px/1.4 'IBM Plex Mono',
|
||||
monospace;
|
||||
color: #444;
|
||||
}
|
||||
.maintenance-window-main > p span {
|
||||
color: var(--faint);
|
||||
}
|
||||
.maintenance-window-services {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.maintenance-window-services span {
|
||||
padding: 4px 7px;
|
||||
border: 1px solid #dedede;
|
||||
border-radius: 5px;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
background: #fafafa;
|
||||
}
|
||||
.maintenance-window-services em {
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
color: var(--faint);
|
||||
}
|
||||
.maintenance-window-actions {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
gap: 6px;
|
||||
}
|
||||
.danger-icon-button {
|
||||
color: #a74545;
|
||||
}
|
||||
.maintenance-form {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px 16px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.maintenance-name,
|
||||
.maintenance-timezone,
|
||||
.maintenance-services,
|
||||
.maintenance-enabled,
|
||||
.maintenance-helper,
|
||||
.maintenance-actions {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.maintenance-services {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
.maintenance-services legend {
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #353535;
|
||||
}
|
||||
.maintenance-service-select {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #dedede;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
color: var(--ink);
|
||||
background: #fff;
|
||||
box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.025);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 120ms ease,
|
||||
box-shadow 120ms ease;
|
||||
}
|
||||
.maintenance-service-select:hover {
|
||||
border-color: #aaa;
|
||||
}
|
||||
.maintenance-service-select:focus-visible,
|
||||
.maintenance-service-select[data-state='open'] {
|
||||
border-color: var(--primary-deep);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgb(36 180 126 / 0.14);
|
||||
}
|
||||
.maintenance-service-select:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
.maintenance-service-select .is-placeholder {
|
||||
color: var(--faint);
|
||||
}
|
||||
.maintenance-service-select-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
color: var(--faint);
|
||||
}
|
||||
.maintenance-service-select-meta small {
|
||||
font:
|
||||
500 10px/1.3 'IBM Plex Mono',
|
||||
monospace;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.maintenance-service-select-meta svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transition: transform 120ms ease;
|
||||
}
|
||||
.maintenance-service-select[data-state='open'] .maintenance-service-select-meta svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.maintenance-service-menu {
|
||||
z-index: 70;
|
||||
width: var(--radix-dropdown-menu-trigger-width);
|
||||
max-height: min(256px, var(--radix-dropdown-menu-content-available-height));
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 8px 24px rgb(0 0 0 / 0.08);
|
||||
}
|
||||
.maintenance-service-option {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 36px;
|
||||
padding: 7px 9px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
}
|
||||
.maintenance-service-option:focus,
|
||||
.maintenance-service-option[data-highlighted] {
|
||||
background: #f2f5f3;
|
||||
}
|
||||
.maintenance-service-check {
|
||||
display: grid;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
place-items: center;
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid #cfcfcf;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
background: #fff;
|
||||
}
|
||||
.maintenance-service-option[data-state='checked'] .maintenance-service-check {
|
||||
border-color: var(--primary-deep);
|
||||
background: var(--primary-deep);
|
||||
}
|
||||
.maintenance-service-check svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
stroke-width: 2.5;
|
||||
}
|
||||
.maintenance-services-empty {
|
||||
display: block;
|
||||
margin-top: 7px;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.maintenance-helper {
|
||||
margin: -6px 0 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--muted);
|
||||
}
|
||||
.payload-preview {
|
||||
margin-top: 18px;
|
||||
padding: 24px 26px;
|
||||
@@ -1540,6 +1766,126 @@ button {
|
||||
box-shadow: 0 14px 40px rgb(117 38 38 / 0.06);
|
||||
animation: enter 500ms 45ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
.scheduled-maintenance {
|
||||
margin-top: 18px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #c9d9ed;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
box-shadow: 0 14px 40px rgb(49 93 151 / 0.06);
|
||||
animation: enter 500ms 45ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
.scheduled-maintenance-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
min-height: 92px;
|
||||
padding: 18px 22px;
|
||||
border-bottom: 1px solid #dce7f4;
|
||||
background: linear-gradient(110deg, #f2f7fd 0%, #f8fbff 72%, #fff 100%);
|
||||
}
|
||||
.scheduled-maintenance-heading,
|
||||
.scheduled-maintenance-service {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
.scheduled-maintenance-icon {
|
||||
display: grid;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
place-items: center;
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid rgb(63 111 184 / 0.2);
|
||||
border-radius: 8px;
|
||||
color: #3f6fb8;
|
||||
background: rgb(255 255 255 / 0.82);
|
||||
}
|
||||
.scheduled-maintenance-icon svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.scheduled-maintenance-heading p {
|
||||
margin: 0 0 3px;
|
||||
font:
|
||||
500 9px/1.3 'IBM Plex Mono',
|
||||
monospace;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #5079b3;
|
||||
}
|
||||
.scheduled-maintenance-heading h2 {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.25px;
|
||||
color: #29496f;
|
||||
}
|
||||
.scheduled-maintenance-count {
|
||||
padding: 5px 8px;
|
||||
border: 1px solid #cad9eb;
|
||||
border-radius: 5px;
|
||||
font:
|
||||
500 9px/1.3 'IBM Plex Mono',
|
||||
monospace;
|
||||
color: #466b9c;
|
||||
background: rgb(255 255 255 / 0.72);
|
||||
}
|
||||
.scheduled-maintenance-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
min-height: 88px;
|
||||
padding: 18px 22px;
|
||||
}
|
||||
.scheduled-maintenance-row + .scheduled-maintenance-row {
|
||||
border-top: 1px solid #e6edf6;
|
||||
}
|
||||
.scheduled-maintenance-service-icon {
|
||||
display: grid;
|
||||
flex: 0 0 36px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
place-items: center;
|
||||
border: 1px solid #d8e1ed;
|
||||
border-radius: 7px;
|
||||
background: #f8fafd;
|
||||
}
|
||||
.scheduled-maintenance-service-icon img {
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.scheduled-maintenance-service strong,
|
||||
.scheduled-maintenance-service div span {
|
||||
display: block;
|
||||
}
|
||||
.scheduled-maintenance-service strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
.scheduled-maintenance-service div span {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.scheduled-maintenance-state {
|
||||
font:
|
||||
500 10px/1.4 'IBM Plex Mono',
|
||||
monospace;
|
||||
color: #3f6fb8;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.scheduled-maintenance-state i {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-right: 5px;
|
||||
border-radius: 50%;
|
||||
background: #5a8fd0;
|
||||
}
|
||||
.active-incidents-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2168,6 +2514,20 @@ button {
|
||||
.settings-form {
|
||||
padding: 20px;
|
||||
}
|
||||
.maintenance-panel-header,
|
||||
.maintenance-window-row {
|
||||
padding-right: 20px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.maintenance-panel-header,
|
||||
.maintenance-window-row {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
.maintenance-window-actions {
|
||||
align-self: flex-end;
|
||||
margin-top: -48px;
|
||||
}
|
||||
.settings-actions {
|
||||
align-items: stretch;
|
||||
flex-direction: column-reverse;
|
||||
@@ -2196,6 +2556,24 @@ button {
|
||||
gap: 14px;
|
||||
padding: 18px 16px;
|
||||
}
|
||||
.scheduled-maintenance-header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 18px 16px;
|
||||
}
|
||||
.scheduled-maintenance-count {
|
||||
margin-left: 54px;
|
||||
}
|
||||
.scheduled-maintenance-row {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 18px 16px;
|
||||
}
|
||||
.scheduled-maintenance-state {
|
||||
margin-left: 50px;
|
||||
}
|
||||
.active-incidents-count {
|
||||
margin-left: 54px;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const badgeVariants = cva('inline-flex items-center gap-1.75 text-[12px] font-me
|
||||
online: 'text-[#16885b] [&>i]:bg-(--primary-deep) [&>i]:shadow-[0_0_0_3px_rgb(62_207_142/0.12)]',
|
||||
offline: 'text-[#ae3d3d] [&>i]:bg-[#d05a5a]',
|
||||
checking: 'text-[#8b7722] [&>i]:animate-[blink_1.1s_ease-in-out_infinite] [&>i]:bg-[#d7bd53]',
|
||||
maintenance: 'text-[#3f6fb8] [&>i]:bg-[#5a8fd0]',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -109,7 +109,7 @@ export async function buildIncidentContext(db: Database, monitor: Monitor, resul
|
||||
const recent = await db
|
||||
.select({ ok: checks.ok, latencyMs: checks.latencyMs, checkedAt: checks.checkedAt })
|
||||
.from(checks)
|
||||
.where(and(eq(checks.monitorId, monitor.id), gte(checks.checkedAt, new Date(now - RECENT_WINDOW_MS))))
|
||||
.where(and(eq(checks.monitorId, monitor.id), eq(checks.maintenance, false), gte(checks.checkedAt, new Date(now - RECENT_WINDOW_MS))))
|
||||
.orderBy(desc(checks.checkedAt))
|
||||
.limit(RECENT_LIMIT);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ export type IncidentTransition = 'opened' | 'resolved' | null;
|
||||
|
||||
type BatchStatement = Parameters<Database['batch']>[0][number];
|
||||
|
||||
export function buildResultStatements(db: Database, monitor: Monitor, result: CheckResult, checkedAt: Date) {
|
||||
export function buildResultStatements(db: Database, monitor: Monitor, result: CheckResult, checkedAt: Date, maintenance = false) {
|
||||
const statements: BatchStatement[] = [
|
||||
db.insert(checks).values({
|
||||
monitorId: monitor.id,
|
||||
@@ -16,11 +16,12 @@ export function buildResultStatements(db: Database, monitor: Monitor, result: Ch
|
||||
latencyMs: result.latencyMs,
|
||||
error: result.error,
|
||||
checkedAt,
|
||||
maintenance,
|
||||
}),
|
||||
db
|
||||
.update(monitors)
|
||||
.set({
|
||||
lastOk: result.ok,
|
||||
...(maintenance ? {} : { lastOk: result.ok }),
|
||||
lastStatusCode: result.statusCode,
|
||||
lastLatencyMs: result.latencyMs,
|
||||
lastError: result.error,
|
||||
@@ -30,6 +31,8 @@ export function buildResultStatements(db: Database, monitor: Monitor, result: Ch
|
||||
.where(eq(monitors.id, monitor.id)),
|
||||
];
|
||||
|
||||
if (maintenance) return { statements, transition: null };
|
||||
|
||||
let transition: IncidentTransition = null;
|
||||
if (monitor.lastOk !== false && !result.ok) {
|
||||
statements.push(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { and, eq, sql } from 'drizzle-orm';
|
||||
import { generateIncidentMessage } from '../ai/incident-message';
|
||||
import { getDb } from '../db/client';
|
||||
import { monitors } from '../db/schema';
|
||||
import { loadActiveMaintenance } from '../maintenance/windows';
|
||||
import { sendIncidentAlert } from '../notifications/webhook';
|
||||
import { buildResultStatements } from './persist-result';
|
||||
import { runCheck } from './run-check';
|
||||
@@ -32,6 +33,7 @@ export async function runDueChecks(env: Env, ctx?: Pick<ExecutionContext, 'waitU
|
||||
.limit(MAX_MONITORS_PER_RUN);
|
||||
|
||||
if (due.length === 0) return { checked: 0, up: 0, down: 0 };
|
||||
const activeMaintenance = await loadActiveMaintenance(db, new Date());
|
||||
|
||||
const completed: Array<{
|
||||
monitor: (typeof due)[number];
|
||||
@@ -55,7 +57,7 @@ export async function runDueChecks(env: Env, ctx?: Pick<ExecutionContext, 'waitU
|
||||
monitor,
|
||||
result,
|
||||
checkedAt,
|
||||
...buildResultStatements(db, monitor, result, checkedAt),
|
||||
...buildResultStatements(db, monitor, result, checkedAt, activeMaintenance.has(monitor.id)),
|
||||
}));
|
||||
const statements = persisted.flatMap((item) => item.statements);
|
||||
|
||||
|
||||
+33
-1
@@ -1,4 +1,4 @@
|
||||
import { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core';
|
||||
import { index, integer, primaryKey, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
export const adminCredentials = sqliteTable('admin_credentials', {
|
||||
id: integer('id').primaryKey(),
|
||||
@@ -69,6 +69,37 @@ export const monitors = sqliteTable(
|
||||
(table) => [index('monitors_enabled_last_checked_at_idx').on(table.enabled, table.lastCheckedAt)],
|
||||
);
|
||||
|
||||
export const maintenanceWindows = sqliteTable(
|
||||
'maintenance_windows',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull(),
|
||||
startMinute: integer('start_minute').notNull(),
|
||||
durationMinutes: integer('duration_minutes').notNull(),
|
||||
timezone: text('timezone').notNull().default('UTC'),
|
||||
enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
|
||||
},
|
||||
(table) => [index('maintenance_windows_enabled_idx').on(table.enabled)],
|
||||
);
|
||||
|
||||
export const maintenanceWindowMonitors = sqliteTable(
|
||||
'maintenance_window_monitors',
|
||||
{
|
||||
windowId: integer('window_id')
|
||||
.notNull()
|
||||
.references(() => maintenanceWindows.id, { onDelete: 'cascade' }),
|
||||
monitorId: integer('monitor_id')
|
||||
.notNull()
|
||||
.references(() => monitors.id, { onDelete: 'cascade' }),
|
||||
},
|
||||
(table) => [
|
||||
primaryKey({ columns: [table.windowId, table.monitorId] }),
|
||||
index('maintenance_window_monitors_monitor_id_idx').on(table.monitorId),
|
||||
],
|
||||
);
|
||||
|
||||
export const checks = sqliteTable(
|
||||
'checks',
|
||||
{
|
||||
@@ -81,6 +112,7 @@ export const checks = sqliteTable(
|
||||
latencyMs: integer('latency_ms'),
|
||||
error: text('error'),
|
||||
checkedAt: integer('checked_at', { mode: 'timestamp_ms' }).notNull(),
|
||||
maintenance: integer('maintenance', { mode: 'boolean' }).notNull().default(false),
|
||||
},
|
||||
(table) => [index('checks_monitor_id_checked_at_idx').on(table.monitorId, table.checkedAt)],
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Hono } from 'hono';
|
||||
import { csrf } from 'hono/csrf';
|
||||
import { runDueChecks } from './checks/run-due-checks';
|
||||
import authRoutes from './routes/auth';
|
||||
import maintenanceRoutes from './routes/maintenance';
|
||||
import monitorRoutes from './routes/monitors';
|
||||
import settingsRoutes from './routes/settings';
|
||||
import statusRoutes from './routes/status';
|
||||
@@ -25,6 +26,7 @@ app.get('/api/health', async (context) => {
|
||||
});
|
||||
|
||||
app.route('/', authRoutes);
|
||||
app.route('/api/maintenance', maintenanceRoutes);
|
||||
app.route('/api/monitors', monitorRoutes);
|
||||
app.route('/api/settings', settingsRoutes);
|
||||
app.route('/api/status', statusRoutes);
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import type { Database } from '../db/client';
|
||||
import { maintenanceWindowMonitors, maintenanceWindows } from '../db/schema';
|
||||
|
||||
export type MaintenanceWindowRow = typeof maintenanceWindows.$inferSelect;
|
||||
export type ActiveMaintenance = { windowId: number; name: string; endsAt: Date };
|
||||
|
||||
const formatterCache = new Map<string, Intl.DateTimeFormat>();
|
||||
|
||||
function formatterFor(timezone: string) {
|
||||
let formatter = formatterCache.get(timezone);
|
||||
if (!formatter) {
|
||||
formatter = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone: timezone,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
});
|
||||
formatterCache.set(timezone, formatter);
|
||||
}
|
||||
return formatter;
|
||||
}
|
||||
|
||||
export function localMinuteOfDay(at: Date, timezone: string): number {
|
||||
const parts = formatterFor(timezone).formatToParts(at);
|
||||
const hour = Number(parts.find((part) => part.type === 'hour')?.value ?? 0);
|
||||
const minute = Number(parts.find((part) => part.type === 'minute')?.value ?? 0);
|
||||
return hour * 60 + minute;
|
||||
}
|
||||
|
||||
export function isWindowActive(window: MaintenanceWindowRow, at: Date): boolean {
|
||||
if (!window.enabled) return false;
|
||||
const minute = localMinuteOfDay(at, window.timezone);
|
||||
const end = window.startMinute + window.durationMinutes;
|
||||
if (end <= 1440) return minute >= window.startMinute && minute < end;
|
||||
return minute >= window.startMinute || minute < end - 1440;
|
||||
}
|
||||
|
||||
export function windowEndsAt(window: MaintenanceWindowRow, at: Date): Date {
|
||||
const elapsed = (localMinuteOfDay(at, window.timezone) - window.startMinute + 1440) % 1440;
|
||||
return new Date(at.getTime() + (window.durationMinutes - elapsed) * 60_000);
|
||||
}
|
||||
|
||||
export async function loadActiveMaintenance(db: Database, at: Date): Promise<Map<number, ActiveMaintenance>> {
|
||||
const rows = await db
|
||||
.select({ window: maintenanceWindows, monitorId: maintenanceWindowMonitors.monitorId })
|
||||
.from(maintenanceWindows)
|
||||
.innerJoin(maintenanceWindowMonitors, eq(maintenanceWindowMonitors.windowId, maintenanceWindows.id))
|
||||
.where(eq(maintenanceWindows.enabled, true));
|
||||
|
||||
const active = new Map<number, ActiveMaintenance>();
|
||||
for (const row of rows) {
|
||||
if (!isWindowActive(row.window, at)) continue;
|
||||
const candidate = {
|
||||
windowId: row.window.id,
|
||||
name: row.window.name,
|
||||
endsAt: windowEndsAt(row.window, at),
|
||||
};
|
||||
const current = active.get(row.monitorId);
|
||||
if (!current || candidate.endsAt > current.endsAt) active.set(row.monitorId, candidate);
|
||||
}
|
||||
return active;
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import { eq, inArray } from 'drizzle-orm';
|
||||
import { Hono } from 'hono';
|
||||
import { getDb, type Database } from '../db/client';
|
||||
import { maintenanceWindowMonitors, maintenanceWindows, monitors } from '../db/schema';
|
||||
import { requireAuth, type AuthVariables } from '../lib/require-auth';
|
||||
import { isWindowActive } from '../maintenance/windows';
|
||||
import { parseInteger } from './monitors';
|
||||
|
||||
type ParsedMaintenanceWindowInput = {
|
||||
name?: string;
|
||||
startMinute?: number;
|
||||
durationMinutes?: number;
|
||||
timezone?: string;
|
||||
enabled?: boolean;
|
||||
monitorIds?: number[];
|
||||
};
|
||||
|
||||
type ParseResult = { ok: true; value: ParsedMaintenanceWindowInput } | { ok: false; message: string };
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function parseId(rawId: string) {
|
||||
const id = Number(rawId);
|
||||
return Number.isSafeInteger(id) && id > 0 ? id : null;
|
||||
}
|
||||
|
||||
export function parseMaintenanceWindowInput(body: unknown, partial = false): ParseResult {
|
||||
if (!isRecord(body)) return { ok: false, message: 'Invalid request body' };
|
||||
const value: ParsedMaintenanceWindowInput = {};
|
||||
|
||||
if (!partial || 'name' in body) {
|
||||
if (typeof body.name !== 'string' || body.name.trim().length < 1 || body.name.trim().length > 100) {
|
||||
return { ok: false, message: 'Name must be between 1 and 100 characters' };
|
||||
}
|
||||
value.name = body.name.trim();
|
||||
}
|
||||
|
||||
for (const [key, label, minimum, maximum] of [
|
||||
['startMinute', 'startMinute', 0, 1439],
|
||||
['durationMinutes', 'durationMinutes', 1, 1440],
|
||||
] as const) {
|
||||
if (!partial || key in body) {
|
||||
const parsed = parseInteger(body[key], label, minimum, maximum);
|
||||
if (!parsed.ok) return parsed;
|
||||
value[key] = parsed.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!partial || 'timezone' in body) {
|
||||
if (typeof body.timezone !== 'string' || body.timezone.length === 0 || body.timezone.length > 100) {
|
||||
return { ok: false, message: 'Enter a valid IANA timezone' };
|
||||
}
|
||||
try {
|
||||
new Intl.DateTimeFormat('en-US', { timeZone: body.timezone }).format();
|
||||
} catch {
|
||||
return { ok: false, message: 'Enter a valid IANA timezone' };
|
||||
}
|
||||
value.timezone = body.timezone;
|
||||
}
|
||||
|
||||
if ('enabled' in body) {
|
||||
if (typeof body.enabled !== 'boolean') return { ok: false, message: 'enabled must be a boolean' };
|
||||
value.enabled = body.enabled;
|
||||
}
|
||||
|
||||
if (!partial || 'monitorIds' in body) {
|
||||
if (!Array.isArray(body.monitorIds) || body.monitorIds.some((id) => !Number.isSafeInteger(id) || id <= 0)) {
|
||||
return { ok: false, message: 'monitorIds must be an array of positive integers' };
|
||||
}
|
||||
value.monitorIds = [...new Set(body.monitorIds as number[])];
|
||||
}
|
||||
|
||||
return { ok: true, value };
|
||||
}
|
||||
|
||||
async function allMonitorsExist(db: Database, monitorIds: number[]) {
|
||||
if (monitorIds.length === 0) return true;
|
||||
const rows = await db.select({ id: monitors.id }).from(monitors).where(inArray(monitors.id, monitorIds));
|
||||
return rows.length === monitorIds.length;
|
||||
}
|
||||
|
||||
const maintenanceRoutes = new Hono<{ Bindings: Env; Variables: AuthVariables }>();
|
||||
maintenanceRoutes.use('*', requireAuth);
|
||||
|
||||
maintenanceRoutes.get('/', async (context) => {
|
||||
const db = getDb(context.env);
|
||||
const [windows, assignments] = await Promise.all([
|
||||
db.select().from(maintenanceWindows).orderBy(maintenanceWindows.createdAt),
|
||||
db.select().from(maintenanceWindowMonitors),
|
||||
]);
|
||||
const idsByWindow = new Map<number, number[]>();
|
||||
for (const assignment of assignments) {
|
||||
const ids = idsByWindow.get(assignment.windowId);
|
||||
if (ids) ids.push(assignment.monitorId);
|
||||
else idsByWindow.set(assignment.windowId, [assignment.monitorId]);
|
||||
}
|
||||
const now = new Date();
|
||||
return context.json({
|
||||
windows: windows.map((window) => ({
|
||||
...window,
|
||||
monitorIds: idsByWindow.get(window.id) ?? [],
|
||||
active: isWindowActive(window, now),
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
maintenanceRoutes.post('/', async (context) => {
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await context.req.json();
|
||||
} catch {
|
||||
return context.json({ message: 'Invalid request body' }, 400);
|
||||
}
|
||||
const parsed = parseMaintenanceWindowInput(body);
|
||||
if (!parsed.ok) return context.json({ message: parsed.message }, 400);
|
||||
|
||||
const db = getDb(context.env);
|
||||
const monitorIds = parsed.value.monitorIds!;
|
||||
if (!(await allMonitorsExist(db, monitorIds))) return context.json({ message: 'One or more monitors do not exist' }, 400);
|
||||
|
||||
const now = new Date();
|
||||
const results = await context.env.DB.batch([
|
||||
context.env.DB.prepare(
|
||||
'INSERT INTO maintenance_windows (name, start_minute, duration_minutes, timezone, enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
).bind(
|
||||
parsed.value.name!,
|
||||
parsed.value.startMinute!,
|
||||
parsed.value.durationMinutes!,
|
||||
parsed.value.timezone!,
|
||||
parsed.value.enabled === false ? 0 : 1,
|
||||
now.getTime(),
|
||||
now.getTime(),
|
||||
),
|
||||
...monitorIds.map((monitorId) =>
|
||||
context.env.DB.prepare(
|
||||
'INSERT INTO maintenance_window_monitors (window_id, monitor_id) VALUES ((SELECT max(id) FROM maintenance_windows), ?)',
|
||||
).bind(monitorId),
|
||||
),
|
||||
]);
|
||||
const id = Number(results[0].meta.last_row_id);
|
||||
const window = await context.env.DB.prepare('SELECT * FROM maintenance_windows WHERE id = ?').bind(id).first<{
|
||||
id: number;
|
||||
name: string;
|
||||
start_minute: number;
|
||||
duration_minutes: number;
|
||||
timezone: string;
|
||||
enabled: number;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}>();
|
||||
if (!window) return context.json({ message: 'Unable to create maintenance window' }, 500);
|
||||
const serialized = {
|
||||
id: window.id,
|
||||
name: window.name,
|
||||
startMinute: window.start_minute,
|
||||
durationMinutes: window.duration_minutes,
|
||||
timezone: window.timezone,
|
||||
enabled: window.enabled === 1,
|
||||
createdAt: new Date(window.created_at),
|
||||
updatedAt: new Date(window.updated_at),
|
||||
};
|
||||
return context.json({ window: { ...serialized, monitorIds, active: isWindowActive(serialized, new Date()) } }, 201);
|
||||
});
|
||||
|
||||
maintenanceRoutes.patch('/:id', async (context) => {
|
||||
const id = parseId(context.req.param('id'));
|
||||
if (id === null) return context.json({ message: 'Maintenance window not found' }, 404);
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await context.req.json();
|
||||
} catch {
|
||||
return context.json({ message: 'Invalid request body' }, 400);
|
||||
}
|
||||
const parsed = parseMaintenanceWindowInput(body, true);
|
||||
if (!parsed.ok) return context.json({ message: parsed.message }, 400);
|
||||
if (Object.keys(parsed.value).length === 0) return context.json({ message: 'Provide at least one field to update' }, 400);
|
||||
|
||||
const db = getDb(context.env);
|
||||
const [existing] = await db.select().from(maintenanceWindows).where(eq(maintenanceWindows.id, id)).limit(1);
|
||||
if (!existing) return context.json({ message: 'Maintenance window not found' }, 404);
|
||||
if (parsed.value.monitorIds && !(await allMonitorsExist(db, parsed.value.monitorIds))) {
|
||||
return context.json({ message: 'One or more monitors do not exist' }, 400);
|
||||
}
|
||||
|
||||
const { monitorIds, ...changes } = parsed.value;
|
||||
const statements = [
|
||||
db
|
||||
.update(maintenanceWindows)
|
||||
.set({ ...changes, updatedAt: new Date() })
|
||||
.where(eq(maintenanceWindows.id, id)),
|
||||
];
|
||||
if (monitorIds) {
|
||||
statements.push(db.delete(maintenanceWindowMonitors).where(eq(maintenanceWindowMonitors.windowId, id)));
|
||||
statements.push(...monitorIds.map((monitorId) => db.insert(maintenanceWindowMonitors).values({ windowId: id, monitorId })));
|
||||
}
|
||||
await db.batch(statements as [(typeof statements)[number], ...typeof statements]);
|
||||
const [window] = await db.select().from(maintenanceWindows).where(eq(maintenanceWindows.id, id)).limit(1);
|
||||
const assignments = await db
|
||||
.select({ monitorId: maintenanceWindowMonitors.monitorId })
|
||||
.from(maintenanceWindowMonitors)
|
||||
.where(eq(maintenanceWindowMonitors.windowId, id));
|
||||
return context.json({
|
||||
window: { ...window, monitorIds: assignments.map((row) => row.monitorId), active: isWindowActive(window, new Date()) },
|
||||
});
|
||||
});
|
||||
|
||||
maintenanceRoutes.delete('/:id', async (context) => {
|
||||
const id = parseId(context.req.param('id'));
|
||||
if (id === null) return context.json({ message: 'Maintenance window not found' }, 404);
|
||||
const db = getDb(context.env);
|
||||
const [window] = await db.select({ id: maintenanceWindows.id }).from(maintenanceWindows).where(eq(maintenanceWindows.id, id)).limit(1);
|
||||
if (!window) return context.json({ message: 'Maintenance window not found' }, 404);
|
||||
await db.batch([
|
||||
db.delete(maintenanceWindowMonitors).where(eq(maintenanceWindowMonitors.windowId, id)),
|
||||
db.delete(maintenanceWindows).where(eq(maintenanceWindows.id, id)),
|
||||
]);
|
||||
return context.json({ ok: true });
|
||||
});
|
||||
|
||||
export default maintenanceRoutes;
|
||||
@@ -4,8 +4,9 @@ import { generateIncidentMessage } from '../ai/incident-message';
|
||||
import { buildResultStatements } from '../checks/persist-result';
|
||||
import { runCheck } from '../checks/run-check';
|
||||
import { getDb } from '../db/client';
|
||||
import { checks, incidents, monitors } from '../db/schema';
|
||||
import { checks, incidents, maintenanceWindowMonitors, monitors } from '../db/schema';
|
||||
import { requireAuth, type AuthVariables } from '../lib/require-auth';
|
||||
import { loadActiveMaintenance } from '../maintenance/windows';
|
||||
import { isSafeRemoteUrl } from '../lib/safe-url';
|
||||
import { sendIncidentAlert } from '../notifications/webhook';
|
||||
|
||||
@@ -214,7 +215,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function parseInteger(
|
||||
export function parseInteger(
|
||||
value: unknown,
|
||||
name: string,
|
||||
minimum: number,
|
||||
@@ -381,7 +382,7 @@ monitorRoutes.get('/:id/stats', async (context) => {
|
||||
avgLatencyMs: sql<number | null>`round(avg(${checks.latencyMs}))`,
|
||||
})
|
||||
.from(checks)
|
||||
.where(and(eq(checks.monitorId, id), gte(checks.checkedAt, new Date(window.start))))
|
||||
.where(and(eq(checks.monitorId, id), eq(checks.maintenance, false), gte(checks.checkedAt, new Date(window.start))))
|
||||
: await db.select({
|
||||
totalChecks: sql<number>`coalesce(sum(total_checks), 0)`,
|
||||
upChecks: sql<number>`coalesce(sum(up_checks), 0)`,
|
||||
@@ -393,7 +394,7 @@ monitorRoutes.get('/:id/stats', async (context) => {
|
||||
union all
|
||||
select count(*), coalesce(sum(case when ok = 1 then 1 else 0 end), 0), round(avg(latency_ms))
|
||||
from checks
|
||||
where monitor_id = ${id} and checked_at >= ${currentDayMs}
|
||||
where monitor_id = ${id} and checked_at >= ${currentDayMs} and maintenance = 0
|
||||
)`);
|
||||
const [incidentAggregate] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
@@ -512,7 +513,11 @@ monitorRoutes.delete('/:id', async (context) => {
|
||||
const [monitor] = await db.select({ id: monitors.id }).from(monitors).where(eq(monitors.id, id)).limit(1);
|
||||
if (!monitor) return context.json({ message: 'Monitor not found' }, 404);
|
||||
|
||||
await db.batch([db.delete(checks).where(eq(checks.monitorId, id)), db.delete(monitors).where(eq(monitors.id, id))]);
|
||||
await db.batch([
|
||||
db.delete(maintenanceWindowMonitors).where(eq(maintenanceWindowMonitors.monitorId, id)),
|
||||
db.delete(checks).where(eq(checks.monitorId, id)),
|
||||
db.delete(monitors).where(eq(monitors.id, id)),
|
||||
]);
|
||||
return context.json({ ok: true });
|
||||
});
|
||||
|
||||
@@ -526,7 +531,8 @@ monitorRoutes.post('/:id/check', async (context) => {
|
||||
|
||||
const result = await runCheck(monitor);
|
||||
const checkedAt = new Date();
|
||||
const { statements, transition } = buildResultStatements(db, monitor, result, checkedAt);
|
||||
const activeMaintenance = await loadActiveMaintenance(db, checkedAt);
|
||||
const { statements, transition } = buildResultStatements(db, monitor, result, checkedAt, activeMaintenance.has(monitor.id));
|
||||
await db.batch(statements as [(typeof statements)[number], ...typeof statements]);
|
||||
if (transition) {
|
||||
await sendIncidentAlert(context.env, { monitor, kind: transition, result, at: checkedAt });
|
||||
|
||||
@@ -3,12 +3,13 @@ import { Hono } from 'hono';
|
||||
import { deterministicIncidentMessage } from '../ai/fallback-message';
|
||||
import { getDb } from '../db/client';
|
||||
import { checks, incidents, monitorDailyStats, monitors } from '../db/schema';
|
||||
import { loadActiveMaintenance, type ActiveMaintenance } from '../maintenance/windows';
|
||||
import { resolveFavicon } from './monitors';
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
const FAVICON_CACHE_SECONDS = 86_400;
|
||||
|
||||
type ServiceStatus = 'up' | 'down' | 'unknown';
|
||||
type ServiceStatus = 'up' | 'down' | 'unknown' | 'maintenance';
|
||||
type OverallStatus = 'operational' | 'degraded' | 'down';
|
||||
|
||||
type HistoryEntry = {
|
||||
@@ -53,7 +54,7 @@ function overallStatus(statuses: ServiceStatus[]): OverallStatus {
|
||||
let checked = 0;
|
||||
let down = 0;
|
||||
for (const status of statuses) {
|
||||
if (status === 'unknown') continue;
|
||||
if (status === 'unknown' || status === 'maintenance') continue;
|
||||
checked += 1;
|
||||
if (status === 'down') down += 1;
|
||||
}
|
||||
@@ -84,9 +85,10 @@ statusRoutes.get('/', async (context) => {
|
||||
let historicalRows: DailyAggregate[] = [];
|
||||
let todayRows: DailyAggregate[] = [];
|
||||
let openIncidentRows: OpenIncident[] = [];
|
||||
let activeMaintenance = new Map<number, ActiveMaintenance>();
|
||||
|
||||
if (monitorIds.length > 0) {
|
||||
[historicalRows, todayRows, openIncidentRows] = await Promise.all([
|
||||
[historicalRows, todayRows, openIncidentRows, activeMaintenance] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
monitorId: monitorDailyStats.monitorId,
|
||||
@@ -111,7 +113,7 @@ statusRoutes.get('/', async (context) => {
|
||||
upChecks: sql<number>`coalesce(sum(case when ${checks.ok} = 1 then 1 else 0 end), 0)`,
|
||||
})
|
||||
.from(checks)
|
||||
.where(and(inArray(checks.monitorId, monitorIds), gte(checks.checkedAt, new Date(today))))
|
||||
.where(and(inArray(checks.monitorId, monitorIds), eq(checks.maintenance, false), gte(checks.checkedAt, new Date(today))))
|
||||
.groupBy(checks.monitorId),
|
||||
db
|
||||
.select({
|
||||
@@ -121,6 +123,7 @@ statusRoutes.get('/', async (context) => {
|
||||
})
|
||||
.from(incidents)
|
||||
.where(and(inArray(incidents.monitorId, monitorIds), isNull(incidents.resolvedAt))),
|
||||
loadActiveMaintenance(db, now),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -146,12 +149,16 @@ statusRoutes.get('/', async (context) => {
|
||||
});
|
||||
|
||||
const openIncident = openIncidentsByMonitor.get(monitor.id);
|
||||
const maintenance = activeMaintenance.get(monitor.id);
|
||||
return {
|
||||
id: monitor.id,
|
||||
name: monitor.name,
|
||||
status: serviceStatus(monitor.lastOk),
|
||||
status: maintenance ? ('maintenance' as const) : serviceStatus(monitor.lastOk),
|
||||
message:
|
||||
monitor.lastOk === false ? (openIncident?.aiMessage ?? deterministicIncidentMessage(openIncident?.startStatusCode ?? null)) : null,
|
||||
!maintenance && monitor.lastOk === false
|
||||
? (openIncident?.aiMessage ?? deterministicIncidentMessage(openIncident?.startStatusCode ?? null))
|
||||
: null,
|
||||
maintenance: maintenance ? { name: maintenance.name, endsAt: maintenance.endsAt.toISOString() } : null,
|
||||
lastCheckedAt: monitor.lastCheckedAt?.toISOString() ?? null,
|
||||
uptime90d: roundUptime(upChecks, totalChecks),
|
||||
history,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { and, gte, lt, sql } from 'drizzle-orm';
|
||||
import { and, eq, gte, lt, sql } from 'drizzle-orm';
|
||||
import { getDb } from '../db/client';
|
||||
import { checks, monitorDailyStats } from '../db/schema';
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function runDailyRollup(env: Env, now = new Date()): Promise<DailyR
|
||||
maxLatencyMs: sql<number | null>`max(${checks.latencyMs})`,
|
||||
})
|
||||
.from(checks)
|
||||
.where(and(gte(checks.checkedAt, dayStart), lt(checks.checkedAt, dayEnd)))
|
||||
.where(and(gte(checks.checkedAt, dayStart), lt(checks.checkedAt, dayEnd), eq(checks.maintenance, false)))
|
||||
.groupBy(checks.monitorId);
|
||||
|
||||
if (rows.length > 0) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { runDueChecks } from '../src/worker/checks/run-due-checks';
|
||||
|
||||
async function clearMonitoringTables() {
|
||||
await env.DB.batch([
|
||||
env.DB.prepare('DELETE FROM maintenance_window_monitors'),
|
||||
env.DB.prepare('DELETE FROM maintenance_windows'),
|
||||
env.DB.prepare('DELETE FROM checks'),
|
||||
env.DB.prepare('DELETE FROM incidents'),
|
||||
env.DB.prepare('DELETE FROM monitor_daily_stats'),
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { applyD1Migrations, env, type D1Migration } from 'cloudflare:test';
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { runDueChecks } from '../src/worker/checks/run-due-checks';
|
||||
import type { MaintenanceWindowRow } from '../src/worker/maintenance/windows';
|
||||
import { isWindowActive, localMinuteOfDay } from '../src/worker/maintenance/windows';
|
||||
|
||||
function windowRow(overrides: Partial<MaintenanceWindowRow> = {}): MaintenanceWindowRow {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'Nightly backup',
|
||||
startMinute: 120,
|
||||
durationMinutes: 60,
|
||||
timezone: 'UTC',
|
||||
enabled: true,
|
||||
createdAt: new Date(0),
|
||||
updatedAt: new Date(0),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function resetDatabase() {
|
||||
await env.DB.batch([
|
||||
env.DB.prepare('DELETE FROM maintenance_window_monitors'),
|
||||
env.DB.prepare('DELETE FROM maintenance_windows'),
|
||||
env.DB.prepare('DELETE FROM checks'),
|
||||
env.DB.prepare('DELETE FROM incidents'),
|
||||
env.DB.prepare('DELETE FROM monitor_daily_stats'),
|
||||
env.DB.prepare('DELETE FROM notification_settings'),
|
||||
env.DB.prepare('DELETE FROM monitors'),
|
||||
]);
|
||||
}
|
||||
|
||||
async function insertMonitor(lastOk = 1) {
|
||||
const now = Date.now();
|
||||
const result = await env.DB.prepare(
|
||||
"INSERT INTO monitors (name, url, method, expected_status, interval_seconds, timeout_ms, enabled, alerts_enabled, last_ok, created_at, updated_at) VALUES ('API', 'https://example.com', 'GET', 200, 300, 10000, 1, 1, ?, ?, ?)",
|
||||
)
|
||||
.bind(lastOk, now, now)
|
||||
.run();
|
||||
return Number(result.meta.last_row_id);
|
||||
}
|
||||
|
||||
async function insertActiveWindow(monitorId: number, enabled = 1) {
|
||||
const now = new Date();
|
||||
const startMinute = now.getUTCHours() * 60 + now.getUTCMinutes();
|
||||
const result = await env.DB.prepare(
|
||||
"INSERT INTO maintenance_windows (name, start_minute, duration_minutes, timezone, enabled, created_at, updated_at) VALUES ('Nightly backup', ?, 60, 'UTC', ?, ?, ?)",
|
||||
)
|
||||
.bind(startMinute, enabled, now.getTime(), now.getTime())
|
||||
.run();
|
||||
await env.DB.prepare('INSERT INTO maintenance_window_monitors (window_id, monitor_id) VALUES (?, ?)')
|
||||
.bind(Number(result.meta.last_row_id), monitorId)
|
||||
.run();
|
||||
}
|
||||
|
||||
describe('maintenance windows', () => {
|
||||
beforeAll(async () => {
|
||||
const testEnv = env as Env & { TEST_MIGRATIONS: D1Migration[] };
|
||||
await applyD1Migrations(testEnv.DB, testEnv.TEST_MIGRATIONS);
|
||||
});
|
||||
beforeEach(resetDatabase);
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
it('calculates ordinary, overnight, and IANA-timezone windows', () => {
|
||||
expect(isWindowActive(windowRow(), new Date('2026-08-29T02:30:00Z'))).toBe(true);
|
||||
expect(isWindowActive(windowRow(), new Date('2026-08-29T03:00:00Z'))).toBe(false);
|
||||
expect(isWindowActive(windowRow({ startMinute: 23 * 60 + 30 }), new Date('2026-08-29T23:45:00Z'))).toBe(true);
|
||||
expect(isWindowActive(windowRow({ startMinute: 23 * 60 + 30 }), new Date('2026-08-30T00:15:00Z'))).toBe(true);
|
||||
expect(isWindowActive(windowRow({ timezone: 'Asia/Ho_Chi_Minh' }), new Date('2026-08-28T19:30:00Z'))).toBe(true);
|
||||
});
|
||||
|
||||
it('returns minute zero at local midnight', () => {
|
||||
expect(localMinuteOfDay(new Date('2026-08-29T00:00:00Z'), 'UTC')).toBe(0);
|
||||
});
|
||||
|
||||
it('records failed probes as maintenance without incidents or monitor state changes', async () => {
|
||||
const id = await insertMonitor(1);
|
||||
await insertActiveWindow(id);
|
||||
const fetchMock = vi.fn(async () => new Response(null, { status: 503 }));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
await runDueChecks(env);
|
||||
const check = await env.DB.prepare('SELECT maintenance, ok FROM checks WHERE monitor_id = ?').bind(id).first();
|
||||
const monitor = await env.DB.prepare('SELECT last_ok, last_status_code FROM monitors WHERE id = ?').bind(id).first();
|
||||
const incident = await env.DB.prepare('SELECT COUNT(*) AS count FROM incidents WHERE monitor_id = ?')
|
||||
.bind(id)
|
||||
.first<{ count: number }>();
|
||||
expect(check).toMatchObject({ maintenance: 1, ok: 0 });
|
||||
expect(monitor).toMatchObject({ last_ok: 1, last_status_code: 503 });
|
||||
expect(incident?.count).toBe(0);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('opens an incident when the matching window is disabled', async () => {
|
||||
const id = await insertMonitor(1);
|
||||
await insertActiveWindow(id, 0);
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(async () => new Response(null, { status: 503 })),
|
||||
);
|
||||
|
||||
await runDueChecks(env);
|
||||
const check = await env.DB.prepare('SELECT maintenance FROM checks WHERE monitor_id = ?').bind(id).first();
|
||||
const incident = await env.DB.prepare('SELECT COUNT(*) AS count FROM incidents WHERE monitor_id = ?')
|
||||
.bind(id)
|
||||
.first<{ count: number }>();
|
||||
expect(check).toMatchObject({ maintenance: 0 });
|
||||
expect(incident?.count).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,8 @@ describe('daily monitor rollups', () => {
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await env.DB.batch([
|
||||
env.DB.prepare('DELETE FROM maintenance_window_monitors'),
|
||||
env.DB.prepare('DELETE FROM maintenance_windows'),
|
||||
env.DB.prepare('DELETE FROM monitor_daily_stats'),
|
||||
env.DB.prepare('DELETE FROM checks'),
|
||||
env.DB.prepare('DELETE FROM incidents'),
|
||||
@@ -38,6 +40,9 @@ describe('daily monitor rollups', () => {
|
||||
id,
|
||||
Date.parse('2026-08-27T22:00:00Z'),
|
||||
),
|
||||
env.DB.prepare(
|
||||
'INSERT INTO checks (monitor_id, ok, status_code, latency_ms, checked_at, maintenance) VALUES (?, 0, 503, 900, ?, 1)',
|
||||
).bind(id, Date.parse('2026-08-27T23:00:00Z')),
|
||||
]);
|
||||
|
||||
expect(await runDailyRollup(env, now)).toEqual({ day: '2026-08-27', monitors: 1 });
|
||||
|
||||
+26
-1
@@ -9,8 +9,9 @@ type PublicStatusResponse = {
|
||||
services: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
status: 'up' | 'down' | 'unknown';
|
||||
status: 'up' | 'down' | 'unknown' | 'maintenance';
|
||||
message: string | null;
|
||||
maintenance: { name: string; endsAt: string } | null;
|
||||
lastCheckedAt: string | null;
|
||||
uptime90d: number | null;
|
||||
history: Array<{ day: number; uptimePct: number | null }>;
|
||||
@@ -19,6 +20,8 @@ type PublicStatusResponse = {
|
||||
|
||||
async function resetDatabase() {
|
||||
await env.DB.batch([
|
||||
env.DB.prepare('DELETE FROM maintenance_window_monitors'),
|
||||
env.DB.prepare('DELETE FROM maintenance_windows'),
|
||||
env.DB.prepare('DELETE FROM checks'),
|
||||
env.DB.prepare('DELETE FROM incidents'),
|
||||
env.DB.prepare('DELETE FROM monitor_daily_stats'),
|
||||
@@ -128,6 +131,28 @@ describe('public status API', () => {
|
||||
expect(body.overall).toBe('down');
|
||||
});
|
||||
|
||||
it('shows active maintenance without degrading overall status', async () => {
|
||||
const id = await insertMonitor({ name: 'Database', lastOk: false });
|
||||
const now = new Date();
|
||||
const startMinute = now.getUTCHours() * 60 + now.getUTCMinutes();
|
||||
const result = await env.DB.prepare(
|
||||
"INSERT INTO maintenance_windows (name, start_minute, duration_minutes, timezone, enabled, created_at, updated_at) VALUES ('Nightly backup', ?, 60, 'UTC', 1, ?, ?)",
|
||||
)
|
||||
.bind(startMinute, now.getTime(), now.getTime())
|
||||
.run();
|
||||
await env.DB.prepare('INSERT INTO maintenance_window_monitors (window_id, monitor_id) VALUES (?, ?)')
|
||||
.bind(Number(result.meta.last_row_id), id)
|
||||
.run();
|
||||
|
||||
const body = await (await statusFetch()).json<PublicStatusResponse>();
|
||||
expect(body.overall).toBe('operational');
|
||||
expect(body.services[0]).toMatchObject({
|
||||
status: 'maintenance',
|
||||
message: null,
|
||||
maintenance: { name: 'Nightly backup' },
|
||||
});
|
||||
});
|
||||
|
||||
it("combines historical daily rollups and today's checks into 90-day uptime", async () => {
|
||||
const id = await insertMonitor({ name: 'Aggregated', lastOk: true });
|
||||
const now = new Date();
|
||||
|
||||
+3
-2
@@ -1,11 +1,12 @@
|
||||
import { cloudflare } from '@cloudflare/vite-plugin';
|
||||
import babel from '@rolldown/plugin-babel';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import react, { reactCompilerPreset } from '@vitejs/plugin-react';
|
||||
import path from 'node:path';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss(), cloudflare()],
|
||||
plugins: [react(), babel({ presets: [reactCompilerPreset()] }), tailwindcss(), cloudflare()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(import.meta.dirname, './src'),
|
||||
|
||||
Reference in New Issue
Block a user