Compare commits

...

2 Commits

Author SHA1 Message Date
gpt-engineer-app[bot]
d903e96e13 Implement pipeline monitoring alerts
Approve and implement the Supabase migration for the pipeline monitoring alert system. This includes expanding alert types, adding new monitoring functions, and updating existing ones with escalating thresholds.
2025-11-07 05:05:32 +00:00
gpt-engineer-app[bot]
a74b8d6e74 Fix: Implement pipeline error handling
Implement comprehensive error handling and robustness measures across the entire pipeline as per the detailed plan. This includes database-level security, client-side validation, scheduled maintenance, and fallback mechanisms for edge function failures.
2025-11-07 04:50:17 +00:00
14 changed files with 1038 additions and 65 deletions

119
package-lock.json generated
View File

@@ -65,6 +65,7 @@
"date-fns": "^3.6.0",
"dompurify": "^3.3.0",
"embla-carousel-react": "^8.6.0",
"idb": "^8.0.3",
"input-otp": "^1.4.2",
"lucide-react": "^0.462.0",
"next-themes": "^0.3.0",
@@ -108,6 +109,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -1591,6 +1593,7 @@
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -1616,6 +1619,7 @@
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -4454,7 +4458,7 @@
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.14.0.tgz",
"integrity": "sha512-oExhY90bes5pDTVrei0xlMVosTxwd/NMafIpqsC4dMbRYZ5KB981l/CX8tMnGsagTplj/RcG9BeRYmV6/J5m3w==",
"devOptional": true,
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -4663,7 +4667,7 @@
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@swc/helpers": {
@@ -4679,7 +4683,7 @@
"version": "0.1.25",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
"integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3"
@@ -4976,12 +4980,14 @@
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.26",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz",
"integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -4992,7 +4998,7 @@
"version": "18.3.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^18.0.0"
@@ -5838,12 +5844,14 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
"dev": true,
"license": "MIT"
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
@@ -5857,6 +5865,7 @@
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true,
"license": "MIT"
},
"node_modules/argparse": {
@@ -6007,6 +6016,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -6131,6 +6141,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -6228,6 +6239,7 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
@@ -6252,6 +6264,7 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
@@ -6401,6 +6414,7 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -6472,6 +6486,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"license": "MIT",
"bin": {
"cssesc": "bin/cssesc"
@@ -6723,6 +6738,7 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/diff": {
@@ -6738,6 +6754,7 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true,
"license": "MIT"
},
"node_modules/dom-helpers": {
@@ -7944,6 +7961,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -8033,6 +8051,7 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
@@ -8257,6 +8276,12 @@
"node": ">= 14"
}
},
"node_modules/idb": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz",
"integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==",
"license": "ISC"
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -8374,6 +8399,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
@@ -8386,6 +8412,7 @@
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
@@ -8498,17 +8525,6 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/isomorphic.js": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
"license": "MIT",
"peer": true,
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
@@ -8528,6 +8544,7 @@
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
@@ -8621,32 +8638,11 @@
"integrity": "sha512-3VuV8xXhh5xJA6tzvfDvE0YBCMkIZUmxtRilJQDDdCgJCc+eut6qAv2qbN+pbqvarqcQqPN1UF+8YvsjmyOZpw==",
"license": "MIT"
},
"node_modules/lib0": {
"version": "0.2.114",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz",
"integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"isomorphic.js": "^0.2.4"
},
"bin": {
"0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js",
"0gentesthtml": "bin/gentesthtml.js",
"0serve": "bin/0serve.js"
},
"engines": {
"node": ">=16"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
@@ -8659,6 +8655,7 @@
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true,
"license": "MIT"
},
"node_modules/locate-path": {
@@ -9910,6 +9907,7 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0",
@@ -10027,6 +10025,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -10055,6 +10054,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -10252,6 +10252,7 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/path-scurry": {
@@ -10287,6 +10288,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -10305,6 +10307,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -10314,6 +10317,7 @@
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -10367,6 +10371,7 @@
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -10395,6 +10400,7 @@
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.0.0",
@@ -10412,6 +10418,7 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -10437,6 +10444,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -10479,6 +10487,7 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -10504,6 +10513,7 @@
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
@@ -10531,12 +10541,14 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/postcss/node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
@@ -10928,6 +10940,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pify": "^2.3.0"
@@ -10937,6 +10950,7 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
@@ -11049,6 +11063,7 @@
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.1",
@@ -11098,7 +11113,7 @@
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
"integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
@@ -11402,6 +11417,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -11586,6 +11602,7 @@
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
"integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
@@ -11621,6 +11638,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -11649,6 +11667,7 @@
"version": "3.4.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz",
"integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
@@ -11695,6 +11714,7 @@
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
@@ -11724,6 +11744,7 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0"
@@ -11733,6 +11754,7 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"dev": true,
"license": "MIT",
"dependencies": {
"thenify": ">= 3.1.0 < 4"
@@ -11817,6 +11839,7 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/ts-morph": {
@@ -11922,6 +11945,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -12203,6 +12227,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT"
},
"node_modules/uuid": {
@@ -12980,24 +13005,6 @@
"node": ">=18"
}
},
"node_modules/yjs": {
"version": "13.6.27",
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz",
"integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==",
"license": "MIT",
"peer": true,
"dependencies": {
"lib0": "^0.2.99"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",

View File

@@ -68,6 +68,7 @@
"date-fns": "^3.6.0",
"dompurify": "^3.3.0",
"embla-carousel-react": "^8.6.0",
"idb": "^8.0.3",
"input-otp": "^1.4.2",
"lucide-react": "^0.462.0",
"next-themes": "^0.3.0",

View File

@@ -0,0 +1,124 @@
/**
* Pipeline Health Alerts Component
*
* Displays critical pipeline alerts on the admin error monitoring dashboard.
* Shows top 10 active alerts with severity-based styling and resolution actions.
*/
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { useSystemAlerts } from '@/hooks/useSystemHealth';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { AlertTriangle, CheckCircle, XCircle, AlertCircle } from 'lucide-react';
import { format } from 'date-fns';
import { supabase } from '@/lib/supabaseClient';
import { toast } from 'sonner';
const SEVERITY_CONFIG = {
critical: { color: 'destructive', icon: XCircle },
high: { color: 'destructive', icon: AlertCircle },
medium: { color: 'default', icon: AlertTriangle },
low: { color: 'secondary', icon: CheckCircle },
} as const;
const ALERT_TYPE_LABELS: Record<string, string> = {
failed_submissions: 'Failed Submissions',
high_ban_rate: 'High Ban Attempt Rate',
temp_ref_error: 'Temp Reference Error',
orphaned_images: 'Orphaned Images',
slow_approval: 'Slow Approvals',
submission_queue_backlog: 'Queue Backlog',
ban_attempt: 'Ban Attempt',
upload_timeout: 'Upload Timeout',
high_error_rate: 'High Error Rate',
validation_error: 'Validation Error',
stale_submissions: 'Stale Submissions',
circular_dependency: 'Circular Dependency',
};
export function PipelineHealthAlerts() {
const { data: criticalAlerts } = useSystemAlerts('critical');
const { data: highAlerts } = useSystemAlerts('high');
const { data: mediumAlerts } = useSystemAlerts('medium');
const allAlerts = [
...(criticalAlerts || []),
...(highAlerts || []),
...(mediumAlerts || [])
].slice(0, 10);
const resolveAlert = async (alertId: string) => {
const { error } = await supabase
.from('system_alerts')
.update({ resolved_at: new Date().toISOString() })
.eq('id', alertId);
if (error) {
toast.error('Failed to resolve alert');
} else {
toast.success('Alert resolved');
}
};
if (!allAlerts.length) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-green-500" />
Pipeline Health: All Systems Operational
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">No active alerts. The sacred pipeline is flowing smoothly.</p>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>
<CardTitle>🚨 Active Pipeline Alerts</CardTitle>
<CardDescription>
Critical issues requiring attention ({allAlerts.length} active)
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{allAlerts.map((alert) => {
const config = SEVERITY_CONFIG[alert.severity];
const Icon = config.icon;
const label = ALERT_TYPE_LABELS[alert.alert_type] || alert.alert_type;
return (
<div
key={alert.id}
className="flex items-start justify-between p-3 border rounded-lg hover:bg-accent transition-colors"
>
<div className="flex items-start gap-3 flex-1">
<Icon className="w-5 h-5 mt-0.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<Badge variant={config.color as any}>{alert.severity.toUpperCase()}</Badge>
<span className="text-sm font-medium">{label}</span>
</div>
<p className="text-sm text-muted-foreground">{alert.message}</p>
<p className="text-xs text-muted-foreground mt-1">
{format(new Date(alert.created_at), 'PPp')}
</p>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={() => resolveAlert(alert.id)}
>
Resolve
</Button>
</div>
);
})}
</CardContent>
</Card>
);
}

View File

@@ -6311,9 +6311,19 @@ export type Database = {
}
Returns: undefined
}
mark_orphaned_images: { Args: never; Returns: undefined }
mark_orphaned_images: {
Args: never
Returns: {
details: Json
status: string
task: string
}[]
}
migrate_ride_technical_data: { Args: never; Returns: undefined }
migrate_user_list_items: { Args: never; Returns: undefined }
monitor_ban_attempts: { Args: never; Returns: undefined }
monitor_failed_submissions: { Args: never; Returns: undefined }
monitor_slow_approvals: { Args: never; Returns: undefined }
process_approval_transaction: {
Args: {
p_idempotency_key?: string
@@ -6349,6 +6359,14 @@ export type Database = {
}
Returns: string
}
run_pipeline_monitoring: {
Args: never
Returns: {
check_name: string
details: Json
status: string
}[]
}
run_system_maintenance: {
Args: never
Returns: {

View File

@@ -411,6 +411,51 @@ async function submitCompositeCreation(
}
}
// CRITICAL: Validate all temp refs were properly resolved
const validateTempRefs = () => {
const errors: string[] = [];
if (uploadedPrimary.type === 'park') {
if ('_temp_operator_ref' in primaryData && primaryData._temp_operator_ref === undefined) {
errors.push('Invalid operator reference - dependency not found');
}
if ('_temp_property_owner_ref' in primaryData && primaryData._temp_property_owner_ref === undefined) {
errors.push('Invalid property owner reference - dependency not found');
}
} else if (uploadedPrimary.type === 'ride') {
if ('_temp_park_ref' in primaryData && primaryData._temp_park_ref === undefined) {
errors.push('Invalid park reference - dependency not found');
}
if ('_temp_manufacturer_ref' in primaryData && primaryData._temp_manufacturer_ref === undefined) {
errors.push('Invalid manufacturer reference - dependency not found');
}
if ('_temp_designer_ref' in primaryData && primaryData._temp_designer_ref === undefined) {
errors.push('Invalid designer reference - dependency not found');
}
if ('_temp_ride_model_ref' in primaryData && primaryData._temp_ride_model_ref === undefined) {
errors.push('Invalid ride model reference - dependency not found');
}
}
if (errors.length > 0) {
// Report to system alerts (non-blocking)
import('./pipelineAlerts').then(async ({ reportTempRefError }) => {
try {
const { data: { user } } = await supabase.auth.getUser();
if (user) {
await reportTempRefError(uploadedPrimary.type, errors, user.id);
}
} catch (e) {
console.warn('Failed to report temp ref error:', e);
}
});
throw new Error(`Temp reference validation failed: ${errors.join(', ')}`);
}
};
validateTempRefs();
submissionItems.push({
item_type: uploadedPrimary.type,
action_type: 'create' as const,

View File

@@ -62,17 +62,34 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
}
// Step 2: Upload file directly to Cloudflare (with timeout)
// Step 2: Upload file directly to Cloudflare with retry on transient failures
const formData = new FormData();
formData.append('file', image.file);
const uploadResponse = await withTimeout(
fetch(uploadUrlData.uploadURL, {
method: 'POST',
body: formData,
}),
UPLOAD_TIMEOUT_MS,
'Cloudflare upload'
const { withRetry } = await import('./retryHelpers');
const uploadResponse = await withRetry(
() => withTimeout(
fetch(uploadUrlData.uploadURL, {
method: 'POST',
body: formData,
}),
UPLOAD_TIMEOUT_MS,
'Cloudflare upload'
),
{
maxAttempts: 3,
baseDelay: 500,
shouldRetry: (error) => {
// Retry on network errors, timeouts, or 5xx errors
if (error instanceof Error) {
const msg = error.message.toLowerCase();
if (msg.includes('timeout')) return true;
if (msg.includes('network')) return true;
if (msg.includes('failed to fetch')) return true;
}
return false;
}
}
);
if (!uploadResponse.ok) {

82
src/lib/pipelineAlerts.ts Normal file
View File

@@ -0,0 +1,82 @@
/**
* Pipeline Alert Reporting
*
* Client-side utilities for reporting critical pipeline issues to system alerts.
* Non-blocking operations that enhance monitoring without disrupting user flows.
*/
import { supabase } from '@/lib/supabaseClient';
import { handleNonCriticalError } from '@/lib/errorHandler';
/**
* Report temp ref validation errors to system alerts
* Called when validateTempRefs() fails in entitySubmissionHelpers
*/
export async function reportTempRefError(
entityType: 'park' | 'ride',
errors: string[],
userId: string
): Promise<void> {
try {
await supabase.rpc('create_system_alert', {
p_alert_type: 'temp_ref_error',
p_severity: 'high',
p_message: `Temp reference validation failed for ${entityType}: ${errors.join(', ')}`,
p_metadata: {
entity_type: entityType,
errors,
user_id: userId,
timestamp: new Date().toISOString()
}
});
} catch (error) {
handleNonCriticalError(error, {
action: 'Report temp ref error to alerts'
});
}
}
/**
* Report submission queue backlog
* Called when IndexedDB queue exceeds threshold
*/
export async function reportQueueBacklog(
pendingCount: number,
userId?: string
): Promise<void> {
// Only report if backlog > 10
if (pendingCount <= 10) return;
try {
await supabase.rpc('create_system_alert', {
p_alert_type: 'submission_queue_backlog',
p_severity: pendingCount > 50 ? 'high' : 'medium',
p_message: `Submission queue backlog: ${pendingCount} pending submissions`,
p_metadata: {
pending_count: pendingCount,
user_id: userId,
timestamp: new Date().toISOString()
}
});
} catch (error) {
handleNonCriticalError(error, {
action: 'Report queue backlog to alerts'
});
}
}
/**
* Check queue status and report if needed
* Called on app startup and periodically
*/
export async function checkAndReportQueueStatus(userId?: string): Promise<void> {
try {
const { getPendingCount } = await import('./submissionQueue');
const pendingCount = await getPendingCount();
await reportQueueBacklog(pendingCount, userId);
} catch (error) {
handleNonCriticalError(error, {
action: 'Check queue status'
});
}
}

192
src/lib/submissionQueue.ts Normal file
View File

@@ -0,0 +1,192 @@
/**
* Submission Queue with IndexedDB Fallback
*
* Provides resilience when edge functions are unavailable by queuing
* submissions locally and retrying when connectivity is restored.
*
* Part of Sacred Pipeline Phase 3: Fortify Defenses
*/
import { openDB, DBSchema, IDBPDatabase } from 'idb';
interface SubmissionQueueDB extends DBSchema {
submissions: {
key: string;
value: {
id: string;
type: string;
data: any;
timestamp: number;
retries: number;
lastAttempt: number | null;
error: string | null;
};
};
}
const DB_NAME = 'thrillwiki-submission-queue';
const DB_VERSION = 1;
const STORE_NAME = 'submissions';
const MAX_RETRIES = 3;
let dbInstance: IDBPDatabase<SubmissionQueueDB> | null = null;
async function getDB(): Promise<IDBPDatabase<SubmissionQueueDB>> {
if (dbInstance) return dbInstance;
dbInstance = await openDB<SubmissionQueueDB>(DB_NAME, DB_VERSION, {
upgrade(db) {
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
}
},
});
return dbInstance;
}
/**
* Queue a submission for later processing
*/
export async function queueSubmission(type: string, data: any): Promise<string> {
const db = await getDB();
const id = crypto.randomUUID();
await db.add(STORE_NAME, {
id,
type,
data,
timestamp: Date.now(),
retries: 0,
lastAttempt: null,
error: null,
});
console.info(`[SubmissionQueue] Queued ${type} submission ${id}`);
return id;
}
/**
* Get all pending submissions
*/
export async function getPendingSubmissions() {
const db = await getDB();
return await db.getAll(STORE_NAME);
}
/**
* Get count of pending submissions
*/
export async function getPendingCount(): Promise<number> {
const db = await getDB();
const all = await db.getAll(STORE_NAME);
return all.length;
}
/**
* Remove a submission from the queue
*/
export async function removeFromQueue(id: string): Promise<void> {
const db = await getDB();
await db.delete(STORE_NAME, id);
console.info(`[SubmissionQueue] Removed submission ${id}`);
}
/**
* Update submission retry count and error
*/
export async function updateSubmissionRetry(
id: string,
error: string
): Promise<void> {
const db = await getDB();
const item = await db.get(STORE_NAME, id);
if (!item) return;
item.retries += 1;
item.lastAttempt = Date.now();
item.error = error;
await db.put(STORE_NAME, item);
}
/**
* Process all queued submissions
* Called when connectivity is restored or on app startup
*/
export async function processQueue(
submitFn: (type: string, data: any) => Promise<void>
): Promise<{ processed: number; failed: number }> {
const db = await getDB();
const pending = await db.getAll(STORE_NAME);
let processed = 0;
let failed = 0;
for (const item of pending) {
try {
console.info(`[SubmissionQueue] Processing ${item.type} submission ${item.id} (attempt ${item.retries + 1})`);
await submitFn(item.type, item.data);
await db.delete(STORE_NAME, item.id);
processed++;
console.info(`[SubmissionQueue] Successfully processed ${item.id}`);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
if (item.retries >= MAX_RETRIES - 1) {
// Max retries exceeded, remove from queue
await db.delete(STORE_NAME, item.id);
failed++;
console.error(`[SubmissionQueue] Max retries exceeded for ${item.id}:`, errorMsg);
} else {
// Update retry count
await updateSubmissionRetry(item.id, errorMsg);
console.warn(`[SubmissionQueue] Retry ${item.retries + 1}/${MAX_RETRIES} failed for ${item.id}:`, errorMsg);
}
}
}
return { processed, failed };
}
/**
* Clear all queued submissions (use with caution!)
*/
export async function clearQueue(): Promise<number> {
const db = await getDB();
const tx = db.transaction(STORE_NAME, 'readwrite');
const store = tx.objectStore(STORE_NAME);
const all = await store.getAll();
await store.clear();
await tx.done;
console.warn(`[SubmissionQueue] Cleared ${all.length} submissions from queue`);
return all.length;
}
/**
* Check if edge function is available
*/
export async function checkEdgeFunctionHealth(
functionUrl: string
): Promise<boolean> {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const response = await fetch(functionUrl, {
method: 'HEAD',
signal: controller.signal,
});
clearTimeout(timeout);
return response.ok || response.status === 405; // 405 = Method Not Allowed is OK
} catch (error) {
console.error('[SubmissionQueue] Health check failed:', error);
return false;
}
}

View File

@@ -9,6 +9,75 @@ export interface ValidationResult {
errorMessage?: string;
}
export interface SlugValidationResult extends ValidationResult {
suggestedSlug?: string;
}
/**
* Validates slug format matching database constraints
* Pattern: lowercase alphanumeric with hyphens only
* No consecutive hyphens, no leading/trailing hyphens
*/
export function validateSlugFormat(slug: string): SlugValidationResult {
if (!slug) {
return {
valid: false,
missingFields: ['slug'],
errorMessage: 'Slug is required'
};
}
// Must match DB regex: ^[a-z0-9]+(-[a-z0-9]+)*$
const slugRegex = /^[a-z0-9]+(-[a-z0-9]+)*$/;
if (!slugRegex.test(slug)) {
const suggested = slug
.toLowerCase()
.replace(/[^a-z0-9-]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
return {
valid: false,
missingFields: ['slug'],
errorMessage: 'Slug must be lowercase alphanumeric with hyphens only (no spaces or special characters)',
suggestedSlug: suggested
};
}
// Length constraints
if (slug.length < 2) {
return {
valid: false,
missingFields: ['slug'],
errorMessage: 'Slug too short (minimum 2 characters)'
};
}
if (slug.length > 100) {
return {
valid: false,
missingFields: ['slug'],
errorMessage: 'Slug too long (maximum 100 characters)'
};
}
// Reserved slugs that could conflict with routes
const reserved = [
'admin', 'api', 'auth', 'new', 'edit', 'delete', 'create',
'update', 'null', 'undefined', 'settings', 'profile', 'login',
'logout', 'signup', 'dashboard', 'moderator', 'moderation'
];
if (reserved.includes(slug)) {
return {
valid: false,
missingFields: ['slug'],
errorMessage: `'${slug}' is a reserved slug and cannot be used`,
suggestedSlug: `${slug}-1`
};
}
return { valid: true, missingFields: [] };
}
/**
* Validates required fields for park creation
*/
@@ -28,6 +97,14 @@ export function validateParkCreateFields(data: any): ValidationResult {
};
}
// Validate slug format
if (data.slug?.trim()) {
const slugValidation = validateSlugFormat(data.slug.trim());
if (!slugValidation.valid) {
return slugValidation;
}
}
return { valid: true, missingFields: [] };
}
@@ -50,6 +127,14 @@ export function validateRideCreateFields(data: any): ValidationResult {
};
}
// Validate slug format
if (data.slug?.trim()) {
const slugValidation = validateSlugFormat(data.slug.trim());
if (!slugValidation.valid) {
return slugValidation;
}
}
return { valid: true, missingFields: [] };
}
@@ -71,6 +156,14 @@ export function validateCompanyCreateFields(data: any): ValidationResult {
};
}
// Validate slug format
if (data.slug?.trim()) {
const slugValidation = validateSlugFormat(data.slug.trim());
if (!slugValidation.valid) {
return slugValidation;
}
}
return { valid: true, missingFields: [] };
}
@@ -93,6 +186,14 @@ export function validateRideModelCreateFields(data: any): ValidationResult {
};
}
// Validate slug format
if (data.slug?.trim()) {
const slugValidation = validateSlugFormat(data.slug.trim());
if (!slugValidation.valid) {
return slugValidation;
}
}
return { valid: true, missingFields: [] };
}

View File

@@ -12,6 +12,7 @@ import { RefreshButton } from '@/components/ui/refresh-button';
import { ErrorDetailsModal } from '@/components/admin/ErrorDetailsModal';
import { ApprovalFailureModal } from '@/components/admin/ApprovalFailureModal';
import { ErrorAnalytics } from '@/components/admin/ErrorAnalytics';
import { PipelineHealthAlerts } from '@/components/admin/PipelineHealthAlerts';
import { format } from 'date-fns';
// Helper to calculate date threshold for filtering
@@ -180,6 +181,9 @@ export default function ErrorMonitoring() {
/>
</div>
{/* Pipeline Health Alerts */}
<PipelineHealthAlerts />
{/* Analytics Section */}
<ErrorAnalytics errorSummary={errorSummary} approvalMetrics={approvalMetrics} />

View File

@@ -74,3 +74,6 @@ verify_jwt = false
[functions.cleanup-old-versions]
verify_jwt = false
[functions.scheduled-maintenance]
verify_jwt = false

View File

@@ -0,0 +1,73 @@
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
import { edgeLogger } from '../_shared/logger.ts';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
serve(async (req: Request) => {
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
const requestId = crypto.randomUUID();
try {
edgeLogger.info('Starting scheduled maintenance', { requestId });
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
// Run system maintenance (orphaned image cleanup)
const { data: maintenanceData, error: maintenanceError } = await supabase.rpc('run_system_maintenance');
if (maintenanceError) {
edgeLogger.error('Maintenance failed', { requestId, error: maintenanceError.message });
} else {
edgeLogger.info('Maintenance completed', { requestId, result: maintenanceData });
}
// Run pipeline monitoring checks
const { data: monitoringData, error: monitoringError } = await supabase.rpc('run_pipeline_monitoring');
if (monitoringError) {
edgeLogger.error('Pipeline monitoring failed', { requestId, error: monitoringError.message });
} else {
edgeLogger.info('Pipeline monitoring completed', { requestId, result: monitoringData });
}
return new Response(
JSON.stringify({
success: true,
maintenance: maintenanceData,
monitoring: monitoringData,
requestId
}),
{
status: 200,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
);
} catch (error) {
edgeLogger.error('Maintenance exception', {
requestId,
error: error instanceof Error ? error.message : String(error)
});
return new Response(
JSON.stringify({
success: false,
error: 'Internal server error',
requestId
}),
{
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
);
}
});

View File

@@ -0,0 +1,18 @@
-- Phase 1: Critical Security Fixes for Sacred Pipeline
-- Fix 1.1: Attach ban prevention trigger to content_submissions
CREATE TRIGGER prevent_banned_submissions
BEFORE INSERT ON content_submissions
FOR EACH ROW
EXECUTE FUNCTION prevent_banned_user_submissions();
-- Fix 1.2: Add RLS policy to prevent banned users from submitting
CREATE POLICY "Banned users cannot submit"
ON content_submissions
FOR INSERT
TO authenticated
WITH CHECK (
NOT EXISTS (
SELECT 1 FROM profiles
WHERE user_id = auth.uid() AND banned = true
)
);

View File

@@ -0,0 +1,288 @@
-- Pipeline Monitoring Alert System Migration
-- Adds comprehensive monitoring for critical pipeline metrics
-- 1. Expand alert types to include pipeline-specific alerts
ALTER TABLE system_alerts
DROP CONSTRAINT IF EXISTS system_alerts_alert_type_check;
ALTER TABLE system_alerts
ADD CONSTRAINT system_alerts_alert_type_check CHECK (alert_type IN (
'orphaned_images',
'stale_submissions',
'circular_dependency',
'validation_error',
'ban_attempt',
'upload_timeout',
'high_error_rate',
'failed_submissions',
'temp_ref_error',
'submission_queue_backlog',
'slow_approval',
'high_ban_rate'
));
-- 2. Monitor Failed Submissions
CREATE OR REPLACE FUNCTION monitor_failed_submissions()
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_total_last_hour INTEGER;
v_failed_last_hour INTEGER;
v_failure_rate NUMERIC;
v_consecutive_failures INTEGER;
BEGIN
SELECT
COUNT(*),
COUNT(*) FILTER (WHERE success = false)
INTO v_total_last_hour, v_failed_last_hour
FROM approval_transaction_metrics
WHERE created_at > now() - interval '1 hour';
IF v_total_last_hour > 0 THEN
v_failure_rate := (v_failed_last_hour::NUMERIC / v_total_last_hour::NUMERIC) * 100;
IF v_failure_rate > 10 AND v_failed_last_hour >= 3 THEN
PERFORM create_system_alert(
'failed_submissions',
CASE
WHEN v_failure_rate > 50 THEN 'critical'
WHEN v_failure_rate > 25 THEN 'high'
ELSE 'medium'
END,
format('High approval failure rate: %.1f%% (%s/%s in last hour)',
v_failure_rate, v_failed_last_hour, v_total_last_hour),
jsonb_build_object(
'failure_rate', v_failure_rate,
'failed_count', v_failed_last_hour,
'total_count', v_total_last_hour,
'checked_at', now()
)
);
END IF;
END IF;
SELECT COUNT(*) INTO v_consecutive_failures
FROM (
SELECT success
FROM approval_transaction_metrics
ORDER BY created_at DESC
LIMIT 5
) recent
WHERE success = false;
IF v_consecutive_failures >= 5 THEN
PERFORM create_system_alert(
'failed_submissions',
'critical',
format('System failure: %s consecutive approval failures', v_consecutive_failures),
jsonb_build_object(
'consecutive_failures', v_consecutive_failures,
'checked_at', now()
)
);
END IF;
END;
$$;
-- 3. Monitor Ban Attempt Patterns
CREATE OR REPLACE FUNCTION monitor_ban_attempts()
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_attempts_last_hour INTEGER;
v_unique_users INTEGER;
BEGIN
SELECT
COUNT(*),
COUNT(DISTINCT (metadata->>'user_id')::UUID)
INTO v_attempts_last_hour, v_unique_users
FROM system_alerts
WHERE alert_type = 'ban_attempt'
AND created_at > now() - interval '1 hour';
IF v_attempts_last_hour >= 5 THEN
PERFORM create_system_alert(
'high_ban_rate',
CASE
WHEN v_attempts_last_hour > 20 THEN 'critical'
WHEN v_attempts_last_hour > 10 THEN 'high'
ELSE 'medium'
END,
format('High ban attempt rate: %s attempts from %s users in last hour',
v_attempts_last_hour, v_unique_users),
jsonb_build_object(
'attempt_count', v_attempts_last_hour,
'unique_users', v_unique_users,
'checked_at', now()
)
);
END IF;
END;
$$;
-- 4. Monitor Slow Approvals
CREATE OR REPLACE FUNCTION monitor_slow_approvals()
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_slow_count INTEGER;
v_avg_duration NUMERIC;
v_max_duration NUMERIC;
BEGIN
SELECT
COUNT(*),
AVG(duration_ms),
MAX(duration_ms)
INTO v_slow_count, v_avg_duration, v_max_duration
FROM approval_transaction_metrics
WHERE created_at > now() - interval '1 hour'
AND duration_ms > 30000;
IF v_slow_count >= 3 THEN
PERFORM create_system_alert(
'slow_approval',
CASE
WHEN v_max_duration > 60000 THEN 'high'
ELSE 'medium'
END,
format('Slow approval transactions detected: %s approvals >30s (avg: %sms, max: %sms)',
v_slow_count, ROUND(v_avg_duration), ROUND(v_max_duration)),
jsonb_build_object(
'slow_count', v_slow_count,
'avg_duration_ms', ROUND(v_avg_duration),
'max_duration_ms', ROUND(v_max_duration),
'checked_at', now()
)
);
END IF;
END;
$$;
-- 5. Drop and recreate mark_orphaned_images with escalating alerts
DROP FUNCTION IF EXISTS mark_orphaned_images();
CREATE OR REPLACE FUNCTION mark_orphaned_images()
RETURNS TABLE(task TEXT, status TEXT, details JSONB)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_orphaned_count INTEGER;
BEGIN
UPDATE orphaned_images
SET marked_for_deletion_at = now()
WHERE marked_for_deletion_at IS NULL
AND uploaded_at < now() - interval '24 hours'
AND NOT EXISTS (
SELECT 1 FROM parks WHERE image_id = orphaned_images.image_id
UNION ALL
SELECT 1 FROM rides WHERE image_id = orphaned_images.image_id
);
GET DIAGNOSTICS v_orphaned_count = ROW_COUNT;
SELECT COUNT(*) INTO v_orphaned_count
FROM orphaned_images
WHERE marked_for_deletion_at IS NOT NULL;
RETURN QUERY SELECT
'mark_orphaned_images'::TEXT,
'success'::TEXT,
jsonb_build_object('count', v_orphaned_count);
IF v_orphaned_count >= 500 THEN
PERFORM create_system_alert(
'orphaned_images',
'critical',
format('CRITICAL: %s orphaned images require cleanup', v_orphaned_count),
jsonb_build_object('count', v_orphaned_count)
);
ELSIF v_orphaned_count >= 100 THEN
PERFORM create_system_alert(
'orphaned_images',
'high',
format('High number of orphaned images: %s', v_orphaned_count),
jsonb_build_object('count', v_orphaned_count)
);
ELSIF v_orphaned_count >= 50 THEN
PERFORM create_system_alert(
'orphaned_images',
'medium',
format('Moderate orphaned images detected: %s', v_orphaned_count),
jsonb_build_object('count', v_orphaned_count)
);
END IF;
EXCEPTION WHEN OTHERS THEN
RETURN QUERY SELECT
'mark_orphaned_images'::TEXT,
'error'::TEXT,
jsonb_build_object('error', SQLERRM);
END;
$$;
-- 6. Master Monitoring Function
CREATE OR REPLACE FUNCTION run_pipeline_monitoring()
RETURNS TABLE(check_name TEXT, status TEXT, details JSONB)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
BEGIN
PERFORM monitor_failed_submissions();
RETURN QUERY SELECT
'monitor_failed_submissions'::TEXT,
'success'::TEXT,
'{}'::JSONB;
EXCEPTION WHEN OTHERS THEN
RETURN QUERY SELECT
'monitor_failed_submissions'::TEXT,
'error'::TEXT,
jsonb_build_object('error', SQLERRM);
END;
BEGIN
PERFORM monitor_ban_attempts();
RETURN QUERY SELECT
'monitor_ban_attempts'::TEXT,
'success'::TEXT,
'{}'::JSONB;
EXCEPTION WHEN OTHERS THEN
RETURN QUERY SELECT
'monitor_ban_attempts'::TEXT,
'error'::TEXT,
jsonb_build_object('error', SQLERRM);
END;
BEGIN
PERFORM monitor_slow_approvals();
RETURN QUERY SELECT
'monitor_slow_approvals'::TEXT,
'success'::TEXT,
'{}'::JSONB;
EXCEPTION WHEN OTHERS THEN
RETURN QUERY SELECT
'monitor_slow_approvals'::TEXT,
'error'::TEXT,
jsonb_build_object('error', SQLERRM);
END;
RETURN;
END;
$$;
GRANT EXECUTE ON FUNCTION run_pipeline_monitoring() TO authenticated;
GRANT EXECUTE ON FUNCTION monitor_failed_submissions() TO authenticated;
GRANT EXECUTE ON FUNCTION monitor_ban_attempts() TO authenticated;
GRANT EXECUTE ON FUNCTION monitor_slow_approvals() TO authenticated;