feat: add mantain config

This commit is contained in:
2026-08-29 20:23:51 +07:00
parent 1cb94f7f05
commit d63a46cac6
31 changed files with 2552 additions and 89 deletions
+271 -27
View File
@@ -20,13 +20,17 @@
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^8.0.1",
"@cloudflare/vite-plugin": "^1.54.1", "@cloudflare/vite-plugin": "^1.54.1",
"@cloudflare/vitest-plugin": "^1.1.1", "@cloudflare/vitest-plugin": "^1.1.1",
"@rolldown/plugin-babel": "^0.2.3",
"@tailwindcss/vite": "^4.3.3", "@tailwindcss/vite": "^4.3.3",
"@types/babel__core": "^7.20.5",
"@types/node": "^26.4.0", "@types/node": "^26.4.0",
"@types/react": "^19.2.18", "@types/react": "^19.2.18",
"@types/react-dom": "^19.2.5", "@types/react-dom": "^19.2.5",
"@vitejs/plugin-react": "^6.1.0", "@vitejs/plugin-react": "^6.1.0",
"babel-plugin-react-compiler": "^1.0.0",
"drizzle-kit": "^0.31.10", "drizzle-kit": "^0.31.10",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^17.4.1", "lint-staged": "^17.4.1",
@@ -45,21 +49,21 @@
"workerd", "workerd",
], ],
"packages": { "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-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-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=="], "@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-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=="], "@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/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=="], "@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/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=="], "@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/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=="], "@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=="], "@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=="], "@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/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=="], "@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/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/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=="], "@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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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-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=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "@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=="], "@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/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=="], "@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=="], "@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=="], "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/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/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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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/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=="], "@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=="], "@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=="], "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=="], "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=="], "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=="], "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=="],
} }
} }
+22
View File
@@ -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;
+810
View File
@@ -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": {}
}
}
+7
View File
@@ -36,6 +36,13 @@
"when": 1787997596170, "when": 1787997596170,
"tag": "0004_smiling_jack_flag", "tag": "0004_smiling_jack_flag",
"breakpoints": true "breakpoints": true
},
{
"idx": 5,
"version": "6",
"when": 1788007515959,
"tag": "0005_pink_trish_tilby",
"breakpoints": true
} }
] ]
} }
+4
View File
@@ -19,13 +19,17 @@
"prepare": "husky" "prepare": "husky"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^8.0.1",
"@cloudflare/vite-plugin": "^1.54.1", "@cloudflare/vite-plugin": "^1.54.1",
"@cloudflare/vitest-plugin": "^1.1.1", "@cloudflare/vitest-plugin": "^1.1.1",
"@rolldown/plugin-babel": "^0.2.3",
"@tailwindcss/vite": "^4.3.3", "@tailwindcss/vite": "^4.3.3",
"@types/babel__core": "^7.20.5",
"@types/node": "^26.4.0", "@types/node": "^26.4.0",
"@types/react": "^19.2.18", "@types/react": "^19.2.18",
"@types/react-dom": "^19.2.5", "@types/react-dom": "^19.2.5",
"@vitejs/plugin-react": "^6.1.0", "@vitejs/plugin-react": "^6.1.0",
"babel-plugin-react-compiler": "^1.0.0",
"drizzle-kit": "^0.31.10", "drizzle-kit": "^0.31.10",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^17.4.1", "lint-staged": "^17.4.1",
+16 -7
View File
@@ -1,11 +1,20 @@
import { TooltipProvider } from '@/components/ui/tooltip'; import { lazy, Suspense } from 'react';
import { RequireAuth } from './components/RequireAuth'; import { RequireAuth } from './components/RequireAuth';
import { usePathname } from './lib/router'; import { usePathname } from './lib/router';
import { DashboardPage } from './pages/DashboardPage';
import { LoginPage } from './pages/LoginPage'; const DashboardPage = lazy(() => import('./pages/DashboardPage').then((module) => ({ default: module.DashboardPage })));
import { MonitorDetailPage } from './pages/MonitorDetailPage'; const LoginPage = lazy(() => import('./pages/LoginPage').then((module) => ({ default: module.LoginPage })));
import { SettingsPage } from './pages/SettingsPage'; const MonitorDetailPage = lazy(() => import('./pages/MonitorDetailPage').then((module) => ({ default: module.MonitorDetailPage })));
import { StatusPage } from './pages/StatusPage'; 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() { function App() {
const pathname = usePathname(); const pathname = usePathname();
@@ -24,7 +33,7 @@ function App() {
content = <RequireAuth>{page}</RequireAuth>; content = <RequireAuth>{page}</RequireAuth>;
} }
return <TooltipProvider>{content}</TooltipProvider>; return <Suspense fallback={<PageFallback />}>{content}</Suspense>;
} }
export default App; export default App;
+39
View File
@@ -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}`);
}
+2 -1
View File
@@ -1,6 +1,6 @@
import { getJson } from './http'; 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 PublicOverallStatus = 'operational' | 'degraded' | 'down';
export type PublicService = { export type PublicService = {
@@ -8,6 +8,7 @@ export type PublicService = {
name: string; name: string;
status: PublicServiceStatus; status: PublicServiceStatus;
message: string | null; message: string | null;
maintenance: { name: string; endsAt: string } | null;
lastCheckedAt: string | null; lastCheckedAt: string | null;
uptime90d: number | null; uptime90d: number | null;
history: Array<{ history: Array<{
+29 -27
View File
@@ -1,7 +1,7 @@
import { Activity, LogOut, Menu, Settings, Zap, type LucideIcon } from 'lucide-react'; import { Activity, LogOut, Menu, Settings, Zap, type LucideIcon } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; 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 { navigate, usePathname } from '../lib/router';
import { useLogoutMutation } from '../queries/auth'; import { useLogoutMutation } from '../queries/auth';
@@ -51,33 +51,35 @@ export function AppHeader({ context }: { context?: string }) {
<span>upwatch</span> <span>upwatch</span>
</Button> </Button>
<nav className="nav-actions" aria-label="Primary navigation"> <TooltipProvider>
{context && <span className="header-context">{context}</span>} <nav className="nav-actions" aria-label="Primary navigation">
{items.map((item) => { {context && <span className="header-context">{context}</span>}
const ItemIcon = item.icon; {items.map((item) => {
const isCurrent = item.href === pathname; const ItemIcon = item.icon;
const isCurrent = item.href === pathname;
return ( return (
<Tooltip key={item.label}> <Tooltip key={item.label}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="app-nav-icon" className="app-nav-icon"
type="button" type="button"
aria-label={item.label} aria-label={item.label}
aria-current={isCurrent ? 'page' : undefined} aria-current={isCurrent ? 'page' : undefined}
disabled={item.disabled} disabled={item.disabled}
onClick={item.onClick} onClick={item.onClick}
> >
<ItemIcon aria-hidden="true" /> <ItemIcon aria-hidden="true" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent sideOffset={6}>{item.label}</TooltipContent> <TooltipContent sideOffset={6}>{item.label}</TooltipContent>
</Tooltip> </Tooltip>
); );
})} })}
</nav> </nav>
</TooltipProvider>
<div className="app-nav-mobile-trigger"> <div className="app-nav-mobile-trigger">
<Sheet> <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>
</>
);
}
+18 -3
View File
@@ -1,5 +1,5 @@
import { type FormEvent, useState } from 'react'; 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 { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { Empty, EmptyTitle } from '@/components/ui/empty'; import { Empty, EmptyTitle } from '@/components/ui/empty';
@@ -7,6 +7,7 @@ import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import type { AiSettings, NotificationSettings } from '../api/settings'; import type { AiSettings, NotificationSettings } from '../api/settings';
import { AppHeader } from '../components/AppHeader'; import { AppHeader } from '../components/AppHeader';
import { MaintenanceWindowsPanel } from '../components/settings/MaintenanceWindowsPanel';
import { navigate } from '../lib/router'; import { navigate } from '../lib/router';
import { import {
useAiSettingsQuery, useAiSettingsQuery,
@@ -158,8 +159,8 @@ export function SettingsPage() {
</Button> </Button>
<section className="settings-heading"> <section className="settings-heading">
<p className="overline">Integrations</p> <p className="overline">Integrations</p>
<h1>Notifications &amp; AI</h1> <h1>Notifications, AI &amp; maintenance</h1>
<p>Configure incident alerts and visitor-friendly status updates from one place.</p> <p>Configure incident alerts, visitor-friendly updates, and planned downtime from one place.</p>
</section> </section>
<Card asChild> <Card asChild>
<section className="settings-card"> <section className="settings-card">
@@ -207,6 +208,20 @@ export function SettingsPage() {
)} )}
</section> </section>
</Card> </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"> <section className="payload-preview">
<p className="overline">Payload preview</p> <p className="overline">Payload preview</p>
<pre>{`{ <pre>{`{
+43 -2
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'; 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 { Badge, type BadgeVariant } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Empty, EmptyContent, EmptyDescription, EmptyMedia, EmptyTitle } from '@/components/ui/empty'; 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' }, up: { label: 'Operational', className: 'online' },
down: { label: 'Down', className: 'offline' }, down: { label: 'Down', className: 'offline' },
unknown: { label: 'Awaiting data', className: 'checking' }, 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) { function errorMessage(error: unknown) {
return error instanceof Error ? error.message : 'Unknown request error'; return error instanceof Error ? error.message : 'Unknown request error';
} }
@@ -55,6 +58,7 @@ export function StatusPage() {
const [now, setNow] = useState(Date.now); const [now, setNow] = useState(Date.now);
const status = statusQuery.data; const status = statusQuery.data;
const activeIncidents = status?.services.filter((service) => service.message) ?? []; const activeIncidents = status?.services.filter((service) => service.message) ?? [];
const maintenanceServices = status?.services.filter((service) => service.maintenance) ?? [];
useEffect(() => { useEffect(() => {
const timer = window.setInterval(() => setNow(Date.now()), 5_000); 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> <time dateTime={new Date(status.updatedAt).toISOString()}>{relativeUpdate(status.updatedAt, now)}</time>
</section> </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 && ( {activeIncidents.length > 0 && (
<section className="active-incidents" aria-labelledby="active-incidents-title" aria-live="polite"> <section className="active-incidents" aria-labelledby="active-incidents-title" aria-live="polite">
<header className="active-incidents-header"> <header className="active-incidents-header">
@@ -184,7 +225,7 @@ export function StatusPage() {
</div> </div>
{status.services.length === 0 ? ( {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"> <EmptyMedia variant="icon">
<Database /> <Database />
</EmptyMedia> </EmptyMedia>
+39
View File
@@ -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 });
}
+378
View File
@@ -1393,6 +1393,232 @@ button {
color: #16754f; color: #16754f;
background: #f2fbf7; 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 { .payload-preview {
margin-top: 18px; margin-top: 18px;
padding: 24px 26px; padding: 24px 26px;
@@ -1540,6 +1766,126 @@ button {
box-shadow: 0 14px 40px rgb(117 38 38 / 0.06); box-shadow: 0 14px 40px rgb(117 38 38 / 0.06);
animation: enter 500ms 45ms cubic-bezier(0.16, 1, 0.3, 1) both; 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 { .active-incidents-header {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -2168,6 +2514,20 @@ button {
.settings-form { .settings-form {
padding: 20px; 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 { .settings-actions {
align-items: stretch; align-items: stretch;
flex-direction: column-reverse; flex-direction: column-reverse;
@@ -2196,6 +2556,24 @@ button {
gap: 14px; gap: 14px;
padding: 18px 16px; 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 { .active-incidents-count {
margin-left: 54px; margin-left: 54px;
} }
+1
View File
@@ -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)]', 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]', offline: 'text-[#ae3d3d] [&>i]:bg-[#d05a5a]',
checking: 'text-[#8b7722] [&>i]:animate-[blink_1.1s_ease-in-out_infinite] [&>i]:bg-[#d7bd53]', checking: 'text-[#8b7722] [&>i]:animate-[blink_1.1s_ease-in-out_infinite] [&>i]:bg-[#d7bd53]',
maintenance: 'text-[#3f6fb8] [&>i]:bg-[#5a8fd0]',
}, },
}, },
defaultVariants: { defaultVariants: {
+1 -1
View File
@@ -109,7 +109,7 @@ export async function buildIncidentContext(db: Database, monitor: Monitor, resul
const recent = await db const recent = await db
.select({ ok: checks.ok, latencyMs: checks.latencyMs, checkedAt: checks.checkedAt }) .select({ ok: checks.ok, latencyMs: checks.latencyMs, checkedAt: checks.checkedAt })
.from(checks) .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)) .orderBy(desc(checks.checkedAt))
.limit(RECENT_LIMIT); .limit(RECENT_LIMIT);
+5 -2
View File
@@ -7,7 +7,7 @@ export type IncidentTransition = 'opened' | 'resolved' | null;
type BatchStatement = Parameters<Database['batch']>[0][number]; 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[] = [ const statements: BatchStatement[] = [
db.insert(checks).values({ db.insert(checks).values({
monitorId: monitor.id, monitorId: monitor.id,
@@ -16,11 +16,12 @@ export function buildResultStatements(db: Database, monitor: Monitor, result: Ch
latencyMs: result.latencyMs, latencyMs: result.latencyMs,
error: result.error, error: result.error,
checkedAt, checkedAt,
maintenance,
}), }),
db db
.update(monitors) .update(monitors)
.set({ .set({
lastOk: result.ok, ...(maintenance ? {} : { lastOk: result.ok }),
lastStatusCode: result.statusCode, lastStatusCode: result.statusCode,
lastLatencyMs: result.latencyMs, lastLatencyMs: result.latencyMs,
lastError: result.error, lastError: result.error,
@@ -30,6 +31,8 @@ export function buildResultStatements(db: Database, monitor: Monitor, result: Ch
.where(eq(monitors.id, monitor.id)), .where(eq(monitors.id, monitor.id)),
]; ];
if (maintenance) return { statements, transition: null };
let transition: IncidentTransition = null; let transition: IncidentTransition = null;
if (monitor.lastOk !== false && !result.ok) { if (monitor.lastOk !== false && !result.ok) {
statements.push( statements.push(
+3 -1
View File
@@ -2,6 +2,7 @@ import { and, eq, sql } from 'drizzle-orm';
import { generateIncidentMessage } from '../ai/incident-message'; import { generateIncidentMessage } from '../ai/incident-message';
import { getDb } from '../db/client'; import { getDb } from '../db/client';
import { monitors } from '../db/schema'; import { monitors } from '../db/schema';
import { loadActiveMaintenance } from '../maintenance/windows';
import { sendIncidentAlert } from '../notifications/webhook'; import { sendIncidentAlert } from '../notifications/webhook';
import { buildResultStatements } from './persist-result'; import { buildResultStatements } from './persist-result';
import { runCheck } from './run-check'; import { runCheck } from './run-check';
@@ -32,6 +33,7 @@ export async function runDueChecks(env: Env, ctx?: Pick<ExecutionContext, 'waitU
.limit(MAX_MONITORS_PER_RUN); .limit(MAX_MONITORS_PER_RUN);
if (due.length === 0) return { checked: 0, up: 0, down: 0 }; if (due.length === 0) return { checked: 0, up: 0, down: 0 };
const activeMaintenance = await loadActiveMaintenance(db, new Date());
const completed: Array<{ const completed: Array<{
monitor: (typeof due)[number]; monitor: (typeof due)[number];
@@ -55,7 +57,7 @@ export async function runDueChecks(env: Env, ctx?: Pick<ExecutionContext, 'waitU
monitor, monitor,
result, result,
checkedAt, checkedAt,
...buildResultStatements(db, monitor, result, checkedAt), ...buildResultStatements(db, monitor, result, checkedAt, activeMaintenance.has(monitor.id)),
})); }));
const statements = persisted.flatMap((item) => item.statements); const statements = persisted.flatMap((item) => item.statements);
+33 -1
View File
@@ -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', { export const adminCredentials = sqliteTable('admin_credentials', {
id: integer('id').primaryKey(), 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)], (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( export const checks = sqliteTable(
'checks', 'checks',
{ {
@@ -81,6 +112,7 @@ export const checks = sqliteTable(
latencyMs: integer('latency_ms'), latencyMs: integer('latency_ms'),
error: text('error'), error: text('error'),
checkedAt: integer('checked_at', { mode: 'timestamp_ms' }).notNull(), 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)], (table) => [index('checks_monitor_id_checked_at_idx').on(table.monitorId, table.checkedAt)],
); );
+2
View File
@@ -2,6 +2,7 @@ import { Hono } from 'hono';
import { csrf } from 'hono/csrf'; import { csrf } from 'hono/csrf';
import { runDueChecks } from './checks/run-due-checks'; import { runDueChecks } from './checks/run-due-checks';
import authRoutes from './routes/auth'; import authRoutes from './routes/auth';
import maintenanceRoutes from './routes/maintenance';
import monitorRoutes from './routes/monitors'; import monitorRoutes from './routes/monitors';
import settingsRoutes from './routes/settings'; import settingsRoutes from './routes/settings';
import statusRoutes from './routes/status'; import statusRoutes from './routes/status';
@@ -25,6 +26,7 @@ app.get('/api/health', async (context) => {
}); });
app.route('/', authRoutes); app.route('/', authRoutes);
app.route('/api/maintenance', maintenanceRoutes);
app.route('/api/monitors', monitorRoutes); app.route('/api/monitors', monitorRoutes);
app.route('/api/settings', settingsRoutes); app.route('/api/settings', settingsRoutes);
app.route('/api/status', statusRoutes); app.route('/api/status', statusRoutes);
+63
View File
@@ -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;
}
+222
View File
@@ -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;
+12 -6
View File
@@ -4,8 +4,9 @@ import { generateIncidentMessage } from '../ai/incident-message';
import { buildResultStatements } from '../checks/persist-result'; import { buildResultStatements } from '../checks/persist-result';
import { runCheck } from '../checks/run-check'; import { runCheck } from '../checks/run-check';
import { getDb } from '../db/client'; 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 { requireAuth, type AuthVariables } from '../lib/require-auth';
import { loadActiveMaintenance } from '../maintenance/windows';
import { isSafeRemoteUrl } from '../lib/safe-url'; import { isSafeRemoteUrl } from '../lib/safe-url';
import { sendIncidentAlert } from '../notifications/webhook'; 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); return typeof value === 'object' && value !== null && !Array.isArray(value);
} }
function parseInteger( export function parseInteger(
value: unknown, value: unknown,
name: string, name: string,
minimum: number, minimum: number,
@@ -381,7 +382,7 @@ monitorRoutes.get('/:id/stats', async (context) => {
avgLatencyMs: sql<number | null>`round(avg(${checks.latencyMs}))`, avgLatencyMs: sql<number | null>`round(avg(${checks.latencyMs}))`,
}) })
.from(checks) .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({ : await db.select({
totalChecks: sql<number>`coalesce(sum(total_checks), 0)`, totalChecks: sql<number>`coalesce(sum(total_checks), 0)`,
upChecks: sql<number>`coalesce(sum(up_checks), 0)`, upChecks: sql<number>`coalesce(sum(up_checks), 0)`,
@@ -393,7 +394,7 @@ monitorRoutes.get('/:id/stats', async (context) => {
union all union all
select count(*), coalesce(sum(case when ok = 1 then 1 else 0 end), 0), round(avg(latency_ms)) select count(*), coalesce(sum(case when ok = 1 then 1 else 0 end), 0), round(avg(latency_ms))
from checks 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 const [incidentAggregate] = await db
.select({ count: sql<number>`count(*)` }) .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); 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); 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 }); return context.json({ ok: true });
}); });
@@ -526,7 +531,8 @@ monitorRoutes.post('/:id/check', async (context) => {
const result = await runCheck(monitor); const result = await runCheck(monitor);
const checkedAt = new Date(); 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]); await db.batch(statements as [(typeof statements)[number], ...typeof statements]);
if (transition) { if (transition) {
await sendIncidentAlert(context.env, { monitor, kind: transition, result, at: checkedAt }); await sendIncidentAlert(context.env, { monitor, kind: transition, result, at: checkedAt });
+13 -6
View File
@@ -3,12 +3,13 @@ import { Hono } from 'hono';
import { deterministicIncidentMessage } from '../ai/fallback-message'; import { deterministicIncidentMessage } from '../ai/fallback-message';
import { getDb } from '../db/client'; import { getDb } from '../db/client';
import { checks, incidents, monitorDailyStats, monitors } from '../db/schema'; import { checks, incidents, monitorDailyStats, monitors } from '../db/schema';
import { loadActiveMaintenance, type ActiveMaintenance } from '../maintenance/windows';
import { resolveFavicon } from './monitors'; import { resolveFavicon } from './monitors';
const DAY_MS = 24 * 60 * 60 * 1000; const DAY_MS = 24 * 60 * 60 * 1000;
const FAVICON_CACHE_SECONDS = 86_400; const FAVICON_CACHE_SECONDS = 86_400;
type ServiceStatus = 'up' | 'down' | 'unknown'; type ServiceStatus = 'up' | 'down' | 'unknown' | 'maintenance';
type OverallStatus = 'operational' | 'degraded' | 'down'; type OverallStatus = 'operational' | 'degraded' | 'down';
type HistoryEntry = { type HistoryEntry = {
@@ -53,7 +54,7 @@ function overallStatus(statuses: ServiceStatus[]): OverallStatus {
let checked = 0; let checked = 0;
let down = 0; let down = 0;
for (const status of statuses) { for (const status of statuses) {
if (status === 'unknown') continue; if (status === 'unknown' || status === 'maintenance') continue;
checked += 1; checked += 1;
if (status === 'down') down += 1; if (status === 'down') down += 1;
} }
@@ -84,9 +85,10 @@ statusRoutes.get('/', async (context) => {
let historicalRows: DailyAggregate[] = []; let historicalRows: DailyAggregate[] = [];
let todayRows: DailyAggregate[] = []; let todayRows: DailyAggregate[] = [];
let openIncidentRows: OpenIncident[] = []; let openIncidentRows: OpenIncident[] = [];
let activeMaintenance = new Map<number, ActiveMaintenance>();
if (monitorIds.length > 0) { if (monitorIds.length > 0) {
[historicalRows, todayRows, openIncidentRows] = await Promise.all([ [historicalRows, todayRows, openIncidentRows, activeMaintenance] = await Promise.all([
db db
.select({ .select({
monitorId: monitorDailyStats.monitorId, 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)`, upChecks: sql<number>`coalesce(sum(case when ${checks.ok} = 1 then 1 else 0 end), 0)`,
}) })
.from(checks) .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), .groupBy(checks.monitorId),
db db
.select({ .select({
@@ -121,6 +123,7 @@ statusRoutes.get('/', async (context) => {
}) })
.from(incidents) .from(incidents)
.where(and(inArray(incidents.monitorId, monitorIds), isNull(incidents.resolvedAt))), .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 openIncident = openIncidentsByMonitor.get(monitor.id);
const maintenance = activeMaintenance.get(monitor.id);
return { return {
id: monitor.id, id: monitor.id,
name: monitor.name, name: monitor.name,
status: serviceStatus(monitor.lastOk), status: maintenance ? ('maintenance' as const) : serviceStatus(monitor.lastOk),
message: 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, lastCheckedAt: monitor.lastCheckedAt?.toISOString() ?? null,
uptime90d: roundUptime(upChecks, totalChecks), uptime90d: roundUptime(upChecks, totalChecks),
history, history,
+2 -2
View File
@@ -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 { getDb } from '../db/client';
import { checks, monitorDailyStats } from '../db/schema'; 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})`, maxLatencyMs: sql<number | null>`max(${checks.latencyMs})`,
}) })
.from(checks) .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); .groupBy(checks.monitorId);
if (rows.length > 0) { if (rows.length > 0) {
+2
View File
@@ -4,6 +4,8 @@ import { runDueChecks } from '../src/worker/checks/run-due-checks';
async function clearMonitoringTables() { async function clearMonitoringTables() {
await env.DB.batch([ 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 checks'),
env.DB.prepare('DELETE FROM incidents'), env.DB.prepare('DELETE FROM incidents'),
env.DB.prepare('DELETE FROM monitor_daily_stats'), env.DB.prepare('DELETE FROM monitor_daily_stats'),
+110
View File
@@ -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);
});
});
+5
View File
@@ -9,6 +9,8 @@ describe('daily monitor rollups', () => {
}); });
beforeEach(async () => { beforeEach(async () => {
await env.DB.batch([ 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 monitor_daily_stats'),
env.DB.prepare('DELETE FROM checks'), env.DB.prepare('DELETE FROM checks'),
env.DB.prepare('DELETE FROM incidents'), env.DB.prepare('DELETE FROM incidents'),
@@ -38,6 +40,9 @@ describe('daily monitor rollups', () => {
id, id,
Date.parse('2026-08-27T22:00:00Z'), 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 }); expect(await runDailyRollup(env, now)).toEqual({ day: '2026-08-27', monitors: 1 });
+26 -1
View File
@@ -9,8 +9,9 @@ type PublicStatusResponse = {
services: Array<{ services: Array<{
id: number; id: number;
name: string; name: string;
status: 'up' | 'down' | 'unknown'; status: 'up' | 'down' | 'unknown' | 'maintenance';
message: string | null; message: string | null;
maintenance: { name: string; endsAt: string } | null;
lastCheckedAt: string | null; lastCheckedAt: string | null;
uptime90d: number | null; uptime90d: number | null;
history: Array<{ day: number; uptimePct: number | null }>; history: Array<{ day: number; uptimePct: number | null }>;
@@ -19,6 +20,8 @@ type PublicStatusResponse = {
async function resetDatabase() { async function resetDatabase() {
await env.DB.batch([ 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 checks'),
env.DB.prepare('DELETE FROM incidents'), env.DB.prepare('DELETE FROM incidents'),
env.DB.prepare('DELETE FROM monitor_daily_stats'), env.DB.prepare('DELETE FROM monitor_daily_stats'),
@@ -128,6 +131,28 @@ describe('public status API', () => {
expect(body.overall).toBe('down'); 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 () => { it("combines historical daily rollups and today's checks into 90-day uptime", async () => {
const id = await insertMonitor({ name: 'Aggregated', lastOk: true }); const id = await insertMonitor({ name: 'Aggregated', lastOk: true });
const now = new Date(); const now = new Date();
+3 -2
View File
@@ -1,11 +1,12 @@
import { cloudflare } from '@cloudflare/vite-plugin'; import { cloudflare } from '@cloudflare/vite-plugin';
import babel from '@rolldown/plugin-babel';
import tailwindcss from '@tailwindcss/vite'; import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react'; import react, { reactCompilerPreset } from '@vitejs/plugin-react';
import path from 'node:path'; import path from 'node:path';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
export default defineConfig({ export default defineConfig({
plugins: [react(), tailwindcss(), cloudflare()], plugins: [react(), babel({ presets: [reactCompilerPreset()] }), tailwindcss(), cloudflare()],
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(import.meta.dirname, './src'), '@': path.resolve(import.meta.dirname, './src'),