diff --git a/bun.lock b/bun.lock index 0f6d2906a..1475b36da 100644 --- a/bun.lock +++ b/bun.lock @@ -20,7 +20,7 @@ "@radix-ui/react-separator": "^1.1.3", "@radix-ui/react-slider": "^1.2.4", "@radix-ui/react-slot": "^1.2.0", - "@radix-ui/react-tabs": "^1.1.4", + "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-toast": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.0", "@radix-ui/themes": "^3.2.1", @@ -54,7 +54,7 @@ "geist": "^1.3.1", "immer": "^10.1.1", "lucide-react": "^0.525.0", - "motion": "^12.0.6", + "motion": "^12.18.1", "nanoid": "^5.0.9", "next": "15.3.0-canary.23", "next-logger": "^5.0.1", @@ -77,6 +77,8 @@ "rehype-katex": "^7.0.1", "remark-math": "^6.0.0", "remark-mermaid": "^0.2.0", + "semver": "^7.7.2", + "serialize-error": "^12.0.0", "shiki": "3.2.1", "swr": "^2.3.4", "tailwind-merge": "^2.6.0", @@ -102,6 +104,7 @@ "@types/pg": "^8.11.11", "@types/react": "^19.0.8", "@types/react-dom": "19.0.3", + "@types/semver": "^7.7.0", "@vitest/coverage-v8": "^3.0.7", "@vitest/ui": "3.0.7", "autoprefixer": "^10.4.20", @@ -169,17 +172,17 @@ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - "@babel/helpers": ["@babel/helpers@7.28.2", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.2" } }, "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw=="], + "@babel/helpers": ["@babel/helpers@7.27.6", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" } }, "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug=="], "@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], - "@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="], + "@babel/runtime": ["@babel/runtime@7.27.6", "", {}, "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q=="], "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], "@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="], - "@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], + "@babel/types": ["@babel/types@7.28.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ=="], "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], @@ -309,7 +312,7 @@ "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], - "@eslint/js": ["@eslint/js@9.32.0", "", {}, "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg=="], + "@eslint/js": ["@eslint/js@9.31.0", "", {}, "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw=="], "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], @@ -419,7 +422,7 @@ "@next/env": ["@next/env@15.3.0-canary.23", "", {}, "sha512-WaS/4IYliYQPw9ylCDkJevzUkgwKGj3lI4eznEWr80i5xwVE77cczrmEEfwzceNeQySxaxYIQdjkV019+S319g=="], - "@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.4.4", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-1FDsyN//ai3Jd97SEd7scw5h1yLdzDACGOPRofr2GD3sEFsBylEEoL0MHSerd4n2dq9Zm/mFMqi4+NRMOreOKA=="], + "@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.4.3", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-wYYbP29uZlm9lqD1C6HDgW9WNNt6AlTogYKYpDyATs0QrKYIv/rPueoIDRH6qttXGCe3zNrb7hxfQx4w8OSkLA=="], "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.0-canary.23", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Elplw67rEj5MZLxH4AG21ruV9eH9j9dhvdZHXYVcf6V/McJSgYH0w6mE5haFXhDVUibtj5l0eFOPtFWNmAV2ZQ=="], @@ -661,51 +664,51 @@ "@redocly/config": ["@redocly/config@0.22.2", "", {}, "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ=="], - "@redocly/openapi-core": ["@redocly/openapi-core@1.34.5", "", { "dependencies": { "@redocly/ajv": "^8.11.2", "@redocly/config": "^0.22.0", "colorette": "^1.2.0", "https-proxy-agent": "^7.0.5", "js-levenshtein": "^1.1.6", "js-yaml": "^4.1.0", "minimatch": "^5.0.1", "pluralize": "^8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA=="], + "@redocly/openapi-core": ["@redocly/openapi-core@1.34.3", "", { "dependencies": { "@redocly/ajv": "^8.11.2", "@redocly/config": "^0.22.0", "colorette": "^1.2.0", "https-proxy-agent": "^7.0.5", "js-levenshtein": "^1.1.6", "js-yaml": "^4.1.0", "minimatch": "^5.0.1", "pluralize": "^8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-3arRdUp1fNx55itnjKiUhO6t4Mf91TsrTIYINDNLAZPS0TPd5YpiXRctwjel0qqWoOOhjA34cZ3m4dksLDFUYg=="], "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@28.0.1", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA=="], "@rollup/pluginutils": ["@rollup/pluginutils@5.2.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.46.1", "", { "os": "android", "cpu": "arm" }, "sha512-oENme6QxtLCqjChRUUo3S6X8hjCXnWmJWnedD7VbGML5GUtaOtAyx+fEEXnBXVf0CBZApMQU0Idwi0FmyxzQhw=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.45.1", "", { "os": "android", "cpu": "arm" }, "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.46.1", "", { "os": "android", "cpu": "arm64" }, "sha512-OikvNT3qYTl9+4qQ9Bpn6+XHM+ogtFadRLuT2EXiFQMiNkXFLQfNVppi5o28wvYdHL2s3fM0D/MZJ8UkNFZWsw=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.45.1", "", { "os": "android", "cpu": "arm64" }, "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.46.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EFYNNGij2WllnzljQDQnlFTXzSJw87cpAs4TVBAWLdkvic5Uh5tISrIL6NRcxoh/b2EFBG/TK8hgRrGx94zD4A=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.45.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.46.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZaNH06O1KeTug9WI2+GRBE5Ujt9kZw4a1+OIwnBHal92I8PxSsl5KpsrPvthRynkhMck4XPdvY0z26Cym/b7oA=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.45.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.46.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n4SLVebZP8uUlJ2r04+g2U/xFeiQlw09Me5UFqny8HGbARl503LNH5CqFTb5U5jNxTouhRjai6qPT0CR5c/Iig=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.45.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.46.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-8vu9c02F16heTqpvo3yeiu7Vi1REDEC/yES/dIfq3tSXe6mLndiwvYr3AAvd1tMNUqE9yeGYa5w7PRbI5QUV+w=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.45.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.46.1", "", { "os": "linux", "cpu": "arm" }, "sha512-K4ncpWl7sQuyp6rWiGUvb6Q18ba8mzM0rjWJ5JgYKlIXAau1db7hZnR0ldJvqKWWJDxqzSLwGUhA4jp+KqgDtQ=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.45.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.46.1", "", { "os": "linux", "cpu": "arm" }, "sha512-YykPnXsjUjmXE6j6k2QBBGAn1YsJUix7pYaPLK3RVE0bQL2jfdbfykPxfF8AgBlqtYbfEnYHmLXNa6QETjdOjQ=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.45.1", "", { "os": "linux", "cpu": "arm" }, "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.46.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-kKvqBGbZ8i9pCGW3a1FH3HNIVg49dXXTsChGFsHGXQaVJPLA4f/O+XmTxfklhccxdF5FefUn2hvkoGJH0ScWOA=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.45.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.46.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-zzX5nTw1N1plmqC9RGC9vZHFuiM7ZP7oSWQGqpbmfjK7p947D518cVK1/MQudsBdcD84t6k70WNczJOct6+hdg=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.45.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog=="], - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.46.1", "", { "os": "linux", "cpu": "none" }, "sha512-O8CwgSBo6ewPpktFfSDgB6SJN9XDcPSvuwxfejiddbIC/hn9Tg6Ai0f0eYDf3XvB/+PIWzOQL+7+TZoB8p9Yuw=="], + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.45.1", "", { "os": "linux", "cpu": "none" }, "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.46.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-JnCfFVEKeq6G3h3z8e60kAp8Rd7QVnWCtPm7cxx+5OtP80g/3nmPtfdCXbVl063e3KsRnGSKDHUQMydmzc/wBA=="], + "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.45.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.46.1", "", { "os": "linux", "cpu": "none" }, "sha512-dVxuDqS237eQXkbYzQQfdf/njgeNw6LZuVyEdUaWwRpKHhsLI+y4H/NJV8xJGU19vnOJCVwaBFgr936FHOnJsQ=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.45.1", "", { "os": "linux", "cpu": "none" }, "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.46.1", "", { "os": "linux", "cpu": "none" }, "sha512-CvvgNl2hrZrTR9jXK1ye0Go0HQRT6ohQdDfWR47/KFKiLd5oN5T14jRdUVGF4tnsN8y9oSfMOqH6RuHh+ck8+w=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.45.1", "", { "os": "linux", "cpu": "none" }, "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.46.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-x7ANt2VOg2565oGHJ6rIuuAon+A8sfe1IeUx25IKqi49OjSr/K3awoNqr9gCwGEJo9OuXlOn+H2p1VJKx1psxA=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.45.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.46.1", "", { "os": "linux", "cpu": "x64" }, "sha512-9OADZYryz/7E8/qt0vnaHQgmia2Y0wrjSSn1V/uL+zw/i7NUhxbX4cHXdEQ7dnJgzYDS81d8+tf6nbIdRFZQoQ=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.45.1", "", { "os": "linux", "cpu": "x64" }, "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.46.1", "", { "os": "linux", "cpu": "x64" }, "sha512-NuvSCbXEKY+NGWHyivzbjSVJi68Xfq1VnIvGmsuXs6TCtveeoDRKutI5vf2ntmNnVq64Q4zInet0UDQ+yMB6tA=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.45.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.46.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mWz+6FSRb82xuUMMV1X3NGiaPFqbLN9aIueHleTZCc46cJvwTlvIh7reQLk4p97dv0nddyewBhwzryBHH7wtPw=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.45.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.46.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-7Thzy9TMXDw9AU4f4vsLNBxh7/VOKuXi73VH3d/kHGr0tZ3x/ewgL9uC7ojUKmH1/zvmZe2tLapYcZllk3SO8Q=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.45.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.46.1", "", { "os": "win32", "cpu": "x64" }, "sha512-7GVB4luhFmGUNXXJhH2jJwZCFB3pIOixv2E3s17GQHBFUOQaISlt7aGcQgqvCaDSxTZJUzlK/QJ1FN8S94MrzQ=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.45.1", "", { "os": "win32", "cpu": "x64" }, "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA=="], "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], @@ -713,17 +716,17 @@ "@rvf/set-get": ["@rvf/set-get@7.0.1", "", {}, "sha512-GkTSn9K1GrTYoTUqlUs36k6nJnzjQaFBTTEIqUYmzBcsGsoJM8xG7EAx2WLHWAA4QzFjcwWUSHQ3vM3Fbw50Tg=="], - "@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@9.42.0", "", { "dependencies": { "@sentry/core": "9.42.0" } }, "sha512-kHDPrLSlb9kMKKUNWVUwMbUjZN3o4aBUux9hRTf2HeDA4Uo8O7Ln4XAC7tMCJ+cB016Z2RnnqH3mLdZV7J72/w=="], + "@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@9.40.0", "", { "dependencies": { "@sentry/core": "9.40.0" } }, "sha512-Ajvz6jN+EEMKrOHcUv2+HlhbRUh69uXhhRoBjJw8sc61uqA2vv3QWyBSmTRoHdTnLGboT5bKEhHIkzVXb+YgEw=="], - "@sentry-internal/feedback": ["@sentry-internal/feedback@9.42.0", "", { "dependencies": { "@sentry/core": "9.42.0" } }, "sha512-7WisZVBKnsr+19CFReFnMHe/Lgd9xqn5CBJfBdRng4hyYSiw988Zdr5xwp2wh1ESM0fxqxy6kSe1NPztIbbiVw=="], + "@sentry-internal/feedback": ["@sentry-internal/feedback@9.40.0", "", { "dependencies": { "@sentry/core": "9.40.0" } }, "sha512-39UbLdGWGvSJ7bAzRnkv91cBdd6fLbdkLVVvqE2ZUfegm7+rH1mRPglmEhw4VE4mQfKZM1zWr/xus2+XPqJcYw=="], - "@sentry-internal/replay": ["@sentry-internal/replay@9.42.0", "", { "dependencies": { "@sentry-internal/browser-utils": "9.42.0", "@sentry/core": "9.42.0" } }, "sha512-teKxrVeT8JOYs9Hd4t0jI0X9NP2Ky6iVgTItN07mUD6yOS9se2ZXzmNzXevoqICX6WsnhHDeWY7krvmJ5QCVEg=="], + "@sentry-internal/replay": ["@sentry-internal/replay@9.40.0", "", { "dependencies": { "@sentry-internal/browser-utils": "9.40.0", "@sentry/core": "9.40.0" } }, "sha512-WrmCvqbLJQC45IFRVN3k0J5pU5NkdX0e9o6XxjcmDiATKk00RHnW4yajnCJ8J1cPR4918yqiJHPX5xpG08BZNA=="], - "@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@9.42.0", "", { "dependencies": { "@sentry-internal/replay": "9.42.0", "@sentry/core": "9.42.0" } }, "sha512-rvP2zjfR9x57u8fVFetkwXnZSXazJRLTFDbirFplggkCKeGNTDJmLBsejUNOkwGiXzcui0fuFEQElu2nF97nxw=="], + "@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@9.40.0", "", { "dependencies": { "@sentry-internal/replay": "9.40.0", "@sentry/core": "9.40.0" } }, "sha512-GLoJ4R4Uipd7Vb+0LzSJA2qCyN1J6YalQIoDuOJTfYyykHvKltds5D8a/5S3Q6d8PcL/nxTn93fynauGEZt2Ow=="], "@sentry/babel-plugin-component-annotate": ["@sentry/babel-plugin-component-annotate@3.6.1", "", {}, "sha512-zmvUa4RpzDG3LQJFpGCE8lniz8Rk1Wa6ZvvK+yEH+snZeaHHRbSnAQBMR607GOClP+euGHNO2YtaY4UAdNTYbg=="], - "@sentry/browser": ["@sentry/browser@9.42.0", "", { "dependencies": { "@sentry-internal/browser-utils": "9.42.0", "@sentry-internal/feedback": "9.42.0", "@sentry-internal/replay": "9.42.0", "@sentry-internal/replay-canvas": "9.42.0", "@sentry/core": "9.42.0" } }, "sha512-85RgFSMDS24JD3nSqA4LpDlVGTxVGwYeqCwI6pRM0CH9pz6G+0OESRhTDccj+rv+kr8vcvWl/LUklJkoswH4kw=="], + "@sentry/browser": ["@sentry/browser@9.40.0", "", { "dependencies": { "@sentry-internal/browser-utils": "9.40.0", "@sentry-internal/feedback": "9.40.0", "@sentry-internal/replay": "9.40.0", "@sentry-internal/replay-canvas": "9.40.0", "@sentry/core": "9.40.0" } }, "sha512-qz/1Go817vcsbcIwgrz4/T34vi3oQ4UIqikosuaCTI9wjZvK0HyW3QmLvTbAnsE7G7h6+UZsVkpO5R16IQvQhQ=="], "@sentry/bundler-plugin-core": ["@sentry/bundler-plugin-core@3.6.1", "", { "dependencies": { "@babel/core": "^7.18.5", "@sentry/babel-plugin-component-annotate": "3.6.1", "@sentry/cli": "^2.49.0", "dotenv": "^16.3.1", "find-up": "^5.0.0", "glob": "^9.3.2", "magic-string": "0.30.8", "unplugin": "1.0.1" } }, "sha512-/ubWjPwgLep84sUPzHfKL2Ns9mK9aQrEX4aBFztru7ygiJidKJTxYGtvjh4dL2M1aZ0WRQYp+7PF6+VKwdZXcQ=="], @@ -745,19 +748,19 @@ "@sentry/cli-win32-x64": ["@sentry/cli-win32-x64@2.50.2", "", { "os": "win32", "cpu": "x64" }, "sha512-tE27pu1sRRub1Jpmemykv3QHddBcyUk39Fsvv+n4NDpQyMgsyVPcboxBZyby44F0jkpI/q3bUH2tfCB1TYDNLg=="], - "@sentry/core": ["@sentry/core@9.42.0", "", {}, "sha512-AsfB2eklY09GGsCLC2r0pvh/h3tgr9Co3CB7XisEfzhoQH9RaEb0XeIVLyfo+503ktdlPTjH24j4Zpts4y0Jmg=="], + "@sentry/core": ["@sentry/core@9.40.0", "", {}, "sha512-cZkuz6BDna6VXSqvlWnrRsaDx4QBKq1PcfQrqhVz8ljs0M7Gcl+Mtj8dCzUxx12fkYM62hQXG72DEGNlAQpH/Q=="], - "@sentry/nextjs": ["@sentry/nextjs@9.42.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@rollup/plugin-commonjs": "28.0.1", "@sentry-internal/browser-utils": "9.42.0", "@sentry/core": "9.42.0", "@sentry/node": "9.42.0", "@sentry/opentelemetry": "9.42.0", "@sentry/react": "9.42.0", "@sentry/vercel-edge": "9.42.0", "@sentry/webpack-plugin": "^3.5.0", "chalk": "3.0.0", "resolve": "1.22.8", "rollup": "^4.35.0", "stacktrace-parser": "^0.1.10" }, "peerDependencies": { "next": "^13.2.0 || ^14.0 || ^15.0.0-rc.0" } }, "sha512-hnjvh330LQlYLTFnJjiCu2VHwqLDycPv9P1fJlhl4aYbX0wmGh6yKLwuImpyU7zI3olZg6GvgMz8LXkFr13m1A=="], + "@sentry/nextjs": ["@sentry/nextjs@9.40.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@rollup/plugin-commonjs": "28.0.1", "@sentry-internal/browser-utils": "9.40.0", "@sentry/core": "9.40.0", "@sentry/node": "9.40.0", "@sentry/opentelemetry": "9.40.0", "@sentry/react": "9.40.0", "@sentry/vercel-edge": "9.40.0", "@sentry/webpack-plugin": "^3.5.0", "chalk": "3.0.0", "resolve": "1.22.8", "rollup": "^4.35.0", "stacktrace-parser": "^0.1.10" }, "peerDependencies": { "next": "^13.2.0 || ^14.0 || ^15.0.0-rc.0" } }, "sha512-e/0uoqjzWM1U3NXysNNUa9DuO23Nr9CxQmU07R3XYeVh0QyhTYGovahFVcXFHR254cFuD5xsdQuUCQen+eEa0g=="], - "@sentry/node": ["@sentry/node@9.42.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1", "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "^0.57.2", "@opentelemetry/instrumentation-amqplib": "^0.46.1", "@opentelemetry/instrumentation-connect": "0.43.1", "@opentelemetry/instrumentation-dataloader": "0.16.1", "@opentelemetry/instrumentation-express": "0.47.1", "@opentelemetry/instrumentation-fs": "0.19.1", "@opentelemetry/instrumentation-generic-pool": "0.43.1", "@opentelemetry/instrumentation-graphql": "0.47.1", "@opentelemetry/instrumentation-hapi": "0.45.2", "@opentelemetry/instrumentation-http": "0.57.2", "@opentelemetry/instrumentation-ioredis": "0.47.1", "@opentelemetry/instrumentation-kafkajs": "0.7.1", "@opentelemetry/instrumentation-knex": "0.44.1", "@opentelemetry/instrumentation-koa": "0.47.1", "@opentelemetry/instrumentation-lru-memoizer": "0.44.1", "@opentelemetry/instrumentation-mongodb": "0.52.0", "@opentelemetry/instrumentation-mongoose": "0.46.1", "@opentelemetry/instrumentation-mysql": "0.45.1", "@opentelemetry/instrumentation-mysql2": "0.45.2", "@opentelemetry/instrumentation-pg": "0.51.1", "@opentelemetry/instrumentation-redis-4": "0.46.1", "@opentelemetry/instrumentation-tedious": "0.18.1", "@opentelemetry/instrumentation-undici": "0.10.1", "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", "@prisma/instrumentation": "6.11.1", "@sentry/core": "9.42.0", "@sentry/node-core": "9.42.0", "@sentry/opentelemetry": "9.42.0", "import-in-the-middle": "^1.14.2", "minimatch": "^9.0.0" } }, "sha512-SrfSTy570zk1ucRy5qSZ94eXj7E26ZAJ1jS7mJtUFLu2fwJt39qtbqfDncXneBJcKzLvXE6WSLVlH/WfwQ5lKg=="], + "@sentry/node": ["@sentry/node@9.40.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1", "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "^0.57.2", "@opentelemetry/instrumentation-amqplib": "^0.46.1", "@opentelemetry/instrumentation-connect": "0.43.1", "@opentelemetry/instrumentation-dataloader": "0.16.1", "@opentelemetry/instrumentation-express": "0.47.1", "@opentelemetry/instrumentation-fs": "0.19.1", "@opentelemetry/instrumentation-generic-pool": "0.43.1", "@opentelemetry/instrumentation-graphql": "0.47.1", "@opentelemetry/instrumentation-hapi": "0.45.2", "@opentelemetry/instrumentation-http": "0.57.2", "@opentelemetry/instrumentation-ioredis": "0.47.1", "@opentelemetry/instrumentation-kafkajs": "0.7.1", "@opentelemetry/instrumentation-knex": "0.44.1", "@opentelemetry/instrumentation-koa": "0.47.1", "@opentelemetry/instrumentation-lru-memoizer": "0.44.1", "@opentelemetry/instrumentation-mongodb": "0.52.0", "@opentelemetry/instrumentation-mongoose": "0.46.1", "@opentelemetry/instrumentation-mysql": "0.45.1", "@opentelemetry/instrumentation-mysql2": "0.45.2", "@opentelemetry/instrumentation-pg": "0.51.1", "@opentelemetry/instrumentation-redis-4": "0.46.1", "@opentelemetry/instrumentation-tedious": "0.18.1", "@opentelemetry/instrumentation-undici": "0.10.1", "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", "@prisma/instrumentation": "6.11.1", "@sentry/core": "9.40.0", "@sentry/node-core": "9.40.0", "@sentry/opentelemetry": "9.40.0", "import-in-the-middle": "^1.14.2", "minimatch": "^9.0.0" } }, "sha512-8bVWChXzGH4QmbVw+H/yiJ6zxqPDhnx11fEAP+vpL1UBm1cAV67CoB4eS7OqQdPC8gF/BQb2sqF0TvY/12NPpA=="], - "@sentry/node-core": ["@sentry/node-core@9.42.0", "", { "dependencies": { "@sentry/core": "9.42.0", "@sentry/opentelemetry": "9.42.0", "import-in-the-middle": "^1.14.2" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/resources": "^1.30.1 || ^2.0.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" } }, "sha512-j0zLLatut3tY+KdHqAn1t2lih+RnR2sDUJagq+swZZFgja0nsWybm3kzPN4n2aRB7yLvjU40n8oj8vi2qBK41g=="], + "@sentry/node-core": ["@sentry/node-core@9.40.0", "", { "dependencies": { "@sentry/core": "9.40.0", "@sentry/opentelemetry": "9.40.0", "import-in-the-middle": "^1.14.2" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/resources": "^1.30.1 || ^2.0.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" } }, "sha512-97JONDa8NxItX0Cz5WQPMd1gQjzodt38qQ0OzZNFvYg2Cpvxob8rxwsNA08Liu7B97rlvsvqMt+Wbgw8SAMfgQ=="], - "@sentry/opentelemetry": ["@sentry/opentelemetry@9.42.0", "", { "dependencies": { "@sentry/core": "9.42.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" } }, "sha512-RdF2Pps9XH+oQpb/yBzG4+RyrQc5eJ55zi+kzY1cG5asPxqKfgBrniy9Q2szy3YJpvN73T//aPrasXuCTgWohg=="], + "@sentry/opentelemetry": ["@sentry/opentelemetry@9.40.0", "", { "dependencies": { "@sentry/core": "9.40.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" } }, "sha512-POQ/ZFmBbi15z3EO9gmTExpxCfW0Ug+WooA8QZPJaizo24gcF5AMOgwuGFwT2YLw/2HdPWjPUPujNNGdCWM6hw=="], - "@sentry/react": ["@sentry/react@9.42.0", "", { "dependencies": { "@sentry/browser": "9.42.0", "@sentry/core": "9.42.0", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-U/KTQrtVMAfeuY77jrVldRIEsEK9dRKbqTmKR9Ajd9BAOQlW9RBklvcRyGJ0AHRWt29TZPKLTcZ8uuy9P9/1Ng=="], + "@sentry/react": ["@sentry/react@9.40.0", "", { "dependencies": { "@sentry/browser": "9.40.0", "@sentry/core": "9.40.0", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-y00d33qozmQAKroQ4Kk2jxhznprPBOb55SL4LOpNPRHGEomxZCUeM3geltczrf14JsGowCr5+xlT+cZQ2XcNlA=="], - "@sentry/vercel-edge": ["@sentry/vercel-edge@9.42.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/resources": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", "@sentry/core": "9.42.0", "@sentry/opentelemetry": "9.42.0" } }, "sha512-HD9yH8ItlnM3bhn4DAmI8unqemI4ws/7UmuL/q/S5kdYrnOIkwIi3+EcFa1qBXuXcLx5+w7lsRlbGeIvfWDaYg=="], + "@sentry/vercel-edge": ["@sentry/vercel-edge@9.40.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/resources": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", "@sentry/core": "9.40.0", "@sentry/opentelemetry": "9.40.0" } }, "sha512-oPoJdv9SsTV98c7ix6Kz4Ma+xZZLZiDTGK2Q1cs/K+YT5vOt7ynopw+e42Kctbjc6OFny8IlNNUb0vGNGL0cFg=="], "@sentry/webpack-plugin": ["@sentry/webpack-plugin@3.6.1", "", { "dependencies": { "@sentry/bundler-plugin-core": "3.6.1", "unplugin": "1.0.1", "uuid": "^9.0.0" }, "peerDependencies": { "webpack": ">=4.40.0" } }, "sha512-F2yqwbdxfCENMN5u4ih4WfOtGjW56/92DBC0bU6un7Ns/l2qd+wRONIvrF+58rl/VkCFfMlUtZTVoKGRyMRmHA=="], @@ -799,7 +802,7 @@ "@supabase/storage-js": ["@supabase/storage-js@2.7.1", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA=="], - "@supabase/supabase-js": ["@supabase/supabase-js@2.52.1", "", { "dependencies": { "@supabase/auth-js": "2.71.1", "@supabase/functions-js": "2.4.5", "@supabase/node-fetch": "2.6.15", "@supabase/postgrest-js": "1.19.4", "@supabase/realtime-js": "2.11.15", "@supabase/storage-js": "2.7.1" } }, "sha512-IxYljprgl381j4SuFrW4JimjTb59WJ98DqxhMvEOJjpGJWuZ7kwttIWn7E4NBnvkYwZ948zJkJ7dSI6B0oO0Xw=="], + "@supabase/supabase-js": ["@supabase/supabase-js@2.52.0", "", { "dependencies": { "@supabase/auth-js": "2.71.1", "@supabase/functions-js": "2.4.5", "@supabase/node-fetch": "2.6.15", "@supabase/postgrest-js": "1.19.4", "@supabase/realtime-js": "2.11.15", "@supabase/storage-js": "2.7.1" } }, "sha512-jbs3CV1f2+ge7sgBeEduboT9v/uGjF22v0yWi/5/XFn5tbM8MfWRccsMtsDwAwu24XK8H6wt2LJDiNnZLtx/bg=="], "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], @@ -847,9 +850,9 @@ "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.12", "", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="], - "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], + "@testing-library/dom": ["@testing-library/dom@10.4.0", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "pretty-format": "^27.0.2" } }, "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ=="], - "@testing-library/jest-dom": ["@testing-library/jest-dom@6.6.4", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "lodash": "^4.17.21", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-xDXgLjVunjHqczScfkCJ9iyjdNOVHvvCdqHSSxwM9L0l/wHkTRum67SDc020uAlCoqktJplgO2AAQeLP1wgqDQ=="], + "@testing-library/jest-dom": ["@testing-library/jest-dom@6.6.3", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "lodash": "^4.17.21", "redent": "^3.0.0" } }, "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA=="], "@testing-library/react": ["@testing-library/react@16.3.0", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw=="], @@ -993,6 +996,8 @@ "@types/request": ["@types/request@2.48.12", "", { "dependencies": { "@types/caseless": "*", "@types/node": "*", "@types/tough-cookie": "*", "form-data": "^2.5.0" } }, "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw=="], + "@types/semver": ["@types/semver@7.7.0", "", {}, "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA=="], + "@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="], "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], @@ -1263,7 +1268,7 @@ "chai": ["chai@5.2.1", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A=="], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], "change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="], @@ -1505,7 +1510,7 @@ "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], - "electron-to-chromium": ["electron-to-chromium@1.5.191", "", {}, "sha512-xcwe9ELcuxYLUFqZZxL19Z6HVKcvNkIwhbHUz7L3us6u12yR+7uY89dSl570f/IqNthx8dAw3tojG7i4Ni4tDA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.190", "", {}, "sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw=="], "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], @@ -1557,9 +1562,9 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.32.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.32.0", "@eslint/plugin-kit": "^0.3.4", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg=="], + "eslint": ["eslint@9.31.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.31.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ=="], - "eslint-config-next": ["eslint-config-next@15.4.4", "", { "dependencies": { "@next/eslint-plugin-next": "15.4.4", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-sK/lWLUVF5om18O5w76Jt3F8uzu/LP5mVa6TprCMWkjWHUmByq80iHGHcdH7k1dLiJlj+DRIWf98d5piwRsSuA=="], + "eslint-config-next": ["eslint-config-next@15.4.3", "", { "dependencies": { "@next/eslint-plugin-next": "15.4.3", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-blytVMTpdqqlLBvYOvwT51m5eqRHNofKR/pfBSeeHiQMSY33kCph31hAK3DiAsL/RamVJRQzHwTRbbNr+7c/sw=="], "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], @@ -1679,7 +1684,7 @@ "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], - "framer-motion": ["framer-motion@12.23.10", "", { "dependencies": { "motion-dom": "^12.23.9", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-ziXHr+C91FhgdSV65YA9SNbLy7uIDQ0pq7pEWlMP6Bh9UJAHFUNUvKWzE41g1B7YuvgJtUUNLgNmZyHd/YQ2gA=="], + "framer-motion": ["framer-motion@12.23.7", "", { "dependencies": { "motion-dom": "^12.23.7", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Qs+zNG9D/3c9C0riom1iXVVOOOaY3T32LIofgbQJz9APY/CUE5v6G41WkcZl2lVhaAgQDQcNq94f8qzLf3rTZA=="], "fs-extra": ["fs-extra@4.0.3", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg=="], @@ -1687,11 +1692,11 @@ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "fumadocs-core": ["fumadocs-core@15.6.6", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.6.1", "@orama/orama": "^3.1.11", "@shikijs/rehype": "^3.8.1", "@shikijs/transformers": "^3.8.1", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "image-size": "^2.0.2", "negotiator": "^1.0.0", "npm-to-yarn": "^3.0.1", "react-remove-scroll": "^2.7.1", "remark": "^15.0.0", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^3.8.1", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@mixedbread/sdk": "^0.19.0", "@oramacloud/client": "1.x.x || 2.x.x", "@types/react": "*", "algoliasearch": "5.x.x", "next": "14.x.x || 15.x.x", "react": "18.x.x || 19.x.x", "react-dom": "18.x.x || 19.x.x" }, "optionalPeers": ["@mixedbread/sdk", "@oramacloud/client", "@types/react", "algoliasearch", "next", "react", "react-dom"] }, "sha512-90sUbejUDevfDHykXXudw+3xTqYjuSZU1evhJiRBiZ0Oy0xQX4p5zPO48b5dhuVp44osvOH0ZKfHsVdkor6kZQ=="], + "fumadocs-core": ["fumadocs-core@15.6.5", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.6.1", "@orama/orama": "^3.1.11", "@shikijs/rehype": "^3.8.1", "@shikijs/transformers": "^3.8.1", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "image-size": "^2.0.2", "negotiator": "^1.0.0", "npm-to-yarn": "^3.0.1", "react-remove-scroll": "^2.7.1", "remark": "^15.0.0", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^3.8.1", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@oramacloud/client": "1.x.x || 2.x.x", "@types/react": "*", "algoliasearch": "5.x.x", "next": "14.x.x || 15.x.x", "react": "18.x.x || 19.x.x", "react-dom": "18.x.x || 19.x.x" }, "optionalPeers": ["@oramacloud/client", "@types/react", "algoliasearch", "next", "react", "react-dom"] }, "sha512-n+IXfJs+nQMpH2vC4g5ipfUhfZD+ML8tVUUW+Nsc5SddpVbxlYytP9PSJw3kdyfookiyZDhcpH5Jz8/G6pqXcg=="], - "fumadocs-mdx": ["fumadocs-mdx@11.7.1", "", { "dependencies": { "@mdx-js/mdx": "^3.1.0", "@standard-schema/spec": "^1.0.0", "chokidar": "^4.0.3", "esbuild": "^0.25.8", "estree-util-value-to-estree": "^3.4.0", "js-yaml": "^4.1.0", "lru-cache": "^11.1.0", "picocolors": "^1.1.1", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.14", "unist-util-visit": "^5.0.0", "zod": "^4.0.10" }, "peerDependencies": { "@fumadocs/mdx-remote": "^1.4.0", "fumadocs-core": "^14.0.0 || ^15.0.0", "next": "^15.3.0", "react": "*", "vite": "6.x.x || 7.x.x" }, "optionalPeers": ["@fumadocs/mdx-remote", "next", "react", "vite"], "bin": { "fumadocs-mdx": "bin.js" } }, "sha512-zY2s3OP0XsNhayp1ac3Qz/xSZLdfjFE3zCCt+LDlwAfRwlpP8WdwfUNsPzZSnnXYigLl0oEQNuL+SF4ZgXctfQ=="], + "fumadocs-mdx": ["fumadocs-mdx@11.7.0", "", { "dependencies": { "@mdx-js/mdx": "^3.1.0", "@standard-schema/spec": "^1.0.0", "chokidar": "^4.0.3", "esbuild": "^0.25.8", "estree-util-value-to-estree": "^3.4.0", "js-yaml": "^4.1.0", "lru-cache": "^11.1.0", "picocolors": "^1.1.1", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.14", "unist-util-visit": "^5.0.0", "zod": "^4.0.5" }, "peerDependencies": { "@fumadocs/mdx-remote": "^1.4.0", "fumadocs-core": "^14.0.0 || ^15.0.0", "next": "^15.3.0", "react": "*", "vite": "6.x.x || 7.x.x" }, "optionalPeers": ["@fumadocs/mdx-remote", "next", "react", "vite"], "bin": { "fumadocs-mdx": "bin.js" } }, "sha512-Cjel0WZHqKaRDxRK6yQW/bUnMMq3Sy+TL4U3S6A4Htwbc22qoPi/ZRz7kP2i43TEml/AVVpostu4XdjDRcWgbg=="], - "fumadocs-ui": ["fumadocs-ui@15.6.6", "", { "dependencies": { "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.13", "@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-presence": "^1.1.4", "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.12", "class-variance-authority": "^0.7.1", "fumadocs-core": "15.6.6", "lodash.merge": "^4.6.2", "next-themes": "^0.4.6", "postcss-selector-parser": "^7.1.0", "react-medium-image-zoom": "^5.3.0", "scroll-into-view-if-needed": "^3.1.0", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@types/react": "*", "next": "14.x.x || 15.x.x", "react": "18.x.x || 19.x.x", "react-dom": "18.x.x || 19.x.x", "tailwindcss": "^3.4.14 || ^4.0.0" }, "optionalPeers": ["@types/react", "next", "tailwindcss"] }, "sha512-Ft/F8yrea7Z1kcI6NDFxKUwLiE4b0elvMDGfmJ/EZJHTuHfl5niXUUCCfvgkTDcZRYjPJktnMxC2msr78wWrzA=="], + "fumadocs-ui": ["fumadocs-ui@15.6.5", "", { "dependencies": { "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.13", "@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-presence": "^1.1.4", "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.12", "class-variance-authority": "^0.7.1", "fumadocs-core": "15.6.5", "lodash.merge": "^4.6.2", "next-themes": "^0.4.6", "postcss-selector-parser": "^7.1.0", "react-medium-image-zoom": "^5.3.0", "scroll-into-view-if-needed": "^3.1.0", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@types/react": "*", "next": "14.x.x || 15.x.x", "react": "18.x.x || 19.x.x", "react-dom": "18.x.x || 19.x.x", "tailwindcss": "^3.4.14 || ^4.0.0" }, "optionalPeers": ["@types/react", "next", "tailwindcss"] }, "sha512-YrVlHtXXW9Y+bo2lAoCj2ifpLmzRCWnMTY2QYpeHbnpIte3oJyAea3nhF+rpw3/XxE66Wjluzw/6GXOlpzcpvw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -1937,7 +1942,7 @@ "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], - "jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="], + "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], "js-levenshtein": ["js-levenshtein@1.1.6", "", {}, "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="], @@ -2035,7 +2040,7 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - "loupe": ["loupe@3.2.0", "", {}, "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw=="], + "loupe": ["loupe@3.1.4", "", {}, "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg=="], "lru-cache": ["lru-cache@11.1.0", "", {}, "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A=="], @@ -2195,9 +2200,9 @@ "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], - "motion": ["motion@12.23.10", "", { "dependencies": { "framer-motion": "^12.23.10", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-pE3WsRXbRjVQaB2vCmVLt8gYiZdX11KYIoWblc49JnG2dTn6oHobIw+bORL31ZfiYBZD8BYmqkvEZp+HLPBBVw=="], + "motion": ["motion@12.23.7", "", { "dependencies": { "framer-motion": "^12.23.7", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-h6xccd06g+VizejdBGWKgFzOROIHjhLDmzhdphUQ62C693dtP40YSSgj47916hovFXfH4jvYEca4duW7EZ7Iug=="], - "motion-dom": ["motion-dom@12.23.9", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-6Sv++iWS8XMFCgU1qwKj9l4xuC47Hp4+2jvPfyTXkqDg2tTzSgX6nWKD4kNFXk0k7llO59LZTPuJigza4A2K1A=="], + "motion-dom": ["motion-dom@12.23.7", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-AyJR07/YxObtK3NyGLCfebUe0k9UZGhik+2eIPUoKz76cKRRSkMeifmIxfztIvOaKb/Smu9IfVHkmx+mV+iFmQ=="], "motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="], @@ -2365,7 +2370,7 @@ "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], - "posthog-js": ["posthog-js@1.258.2", "", { "dependencies": { "core-js": "^3.38.1", "fflate": "^0.4.8", "preact": "^10.19.3", "web-vitals": "^4.2.4" }, "peerDependencies": { "@rrweb/types": "2.0.0-alpha.17", "rrweb-snapshot": "2.0.0-alpha.17" }, "optionalPeers": ["@rrweb/types", "rrweb-snapshot"] }, "sha512-XBSeiN4HjiYsy3tW5zss8WOJF2JXTQXAYw2wZ+zjqQuzzi7kkLEXjIgsVrBnt5Opwhqn0krZVsb0ZBw34dIiyQ=="], + "posthog-js": ["posthog-js@1.257.2", "", { "dependencies": { "core-js": "^3.38.1", "fflate": "^0.4.8", "preact": "^10.19.3", "web-vitals": "^4.2.4" }, "peerDependencies": { "@rrweb/types": "2.0.0-alpha.17", "rrweb-snapshot": "2.0.0-alpha.17" }, "optionalPeers": ["@rrweb/types", "rrweb-snapshot"] }, "sha512-E+8wI/ahaiUGrmkilOtAB9aTFL+oELwOEsH1eO/2NyXB5WWcSUk6Rm1loixq8/lC4f3oR+Qqp9rHyXTSYbBDRQ=="], "preact": ["preact@10.26.9", "", {}, "sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA=="], @@ -2417,7 +2422,7 @@ "react-error-boundary": ["react-error-boundary@5.0.0", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "react": ">=16.13.1" } }, "sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ=="], - "react-hook-form": ["react-hook-form@7.61.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-2vbXUFDYgqEgM2RcXcAT2PwDW/80QARi+PKmHy5q2KhuKvOlG8iIYgf7eIlIANR5trW9fJbP4r5aub3a4egsew=="], + "react-hook-form": ["react-hook-form@7.60.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A=="], "react-icons": ["react-icons@5.5.0", "", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="], @@ -2517,7 +2522,7 @@ "robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="], - "rollup": ["rollup@4.46.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.46.1", "@rollup/rollup-android-arm64": "4.46.1", "@rollup/rollup-darwin-arm64": "4.46.1", "@rollup/rollup-darwin-x64": "4.46.1", "@rollup/rollup-freebsd-arm64": "4.46.1", "@rollup/rollup-freebsd-x64": "4.46.1", "@rollup/rollup-linux-arm-gnueabihf": "4.46.1", "@rollup/rollup-linux-arm-musleabihf": "4.46.1", "@rollup/rollup-linux-arm64-gnu": "4.46.1", "@rollup/rollup-linux-arm64-musl": "4.46.1", "@rollup/rollup-linux-loongarch64-gnu": "4.46.1", "@rollup/rollup-linux-ppc64-gnu": "4.46.1", "@rollup/rollup-linux-riscv64-gnu": "4.46.1", "@rollup/rollup-linux-riscv64-musl": "4.46.1", "@rollup/rollup-linux-s390x-gnu": "4.46.1", "@rollup/rollup-linux-x64-gnu": "4.46.1", "@rollup/rollup-linux-x64-musl": "4.46.1", "@rollup/rollup-win32-arm64-msvc": "4.46.1", "@rollup/rollup-win32-ia32-msvc": "4.46.1", "@rollup/rollup-win32-x64-msvc": "4.46.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-33xGNBsDJAkzt0PvninskHlWnTIPgDtTwhg0U38CUoNP/7H6wI2Cz6dUeoNPbjdTdsYTGuiFFASuUOWovH0SyQ=="], + "rollup": ["rollup@4.45.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.45.1", "@rollup/rollup-android-arm64": "4.45.1", "@rollup/rollup-darwin-arm64": "4.45.1", "@rollup/rollup-darwin-x64": "4.45.1", "@rollup/rollup-freebsd-arm64": "4.45.1", "@rollup/rollup-freebsd-x64": "4.45.1", "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", "@rollup/rollup-linux-arm-musleabihf": "4.45.1", "@rollup/rollup-linux-arm64-gnu": "4.45.1", "@rollup/rollup-linux-arm64-musl": "4.45.1", "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", "@rollup/rollup-linux-riscv64-gnu": "4.45.1", "@rollup/rollup-linux-riscv64-musl": "4.45.1", "@rollup/rollup-linux-s390x-gnu": "4.45.1", "@rollup/rollup-linux-x64-gnu": "4.45.1", "@rollup/rollup-linux-x64-musl": "4.45.1", "@rollup/rollup-win32-arm64-msvc": "4.45.1", "@rollup/rollup-win32-ia32-msvc": "4.45.1", "@rollup/rollup-win32-x64-msvc": "4.45.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw=="], "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], @@ -2547,6 +2552,8 @@ "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "serialize-error": ["serialize-error@12.0.0", "", { "dependencies": { "type-fest": "^4.31.0" } }, "sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw=="], + "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], "server-cli-only": ["server-cli-only@0.3.2", "", {}, "sha512-t8cH7ZPomACZ+T+yb5s9TjVjjMe62DLgT5VXkN71Ix7nuPQfR6HQMM/XG1k4MesiHiRKw5mpwwZC7A+bzuZRfw=="], @@ -2591,7 +2598,7 @@ "sonic-boom": ["sonic-boom@3.8.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg=="], - "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -2813,11 +2820,11 @@ "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], - "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="], "victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], - "vite": ["vite@7.0.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg=="], + "vite": ["vite@7.0.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw=="], "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], @@ -2887,6 +2894,8 @@ "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], + "yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="], "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], @@ -2925,12 +2934,14 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - "@fumadocs/mdx-remote/zod": ["zod@4.0.10", "", {}, "sha512-3vB+UU3/VmLL2lvwcY/4RV2i9z/YU0DTV/tDuYjrwmx5WeJ7hwy+rGEEx8glHp6Yxw7ibRbKSaIFBgReRPe5KA=="], + "@fumadocs/mdx-remote/zod": ["zod@4.0.5", "", {}, "sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA=="], "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], "@iconify/utils/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], + "@jest/types/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], "@opentelemetry/instrumentation-http/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], @@ -2959,8 +2970,6 @@ "@sentry/cli/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "@sentry/nextjs/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], - "@sentry/nextjs/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], "@sentry/node/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -2975,6 +2984,8 @@ "@shikijs/twoslash/@shikijs/types": ["@shikijs/types@3.2.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-/NTWAk4KE2M8uac0RhOsIhYQf4pdU0OywQuYDGIGAJ6Mjunxl2cGiuLkvu4HLCMn+OTTLRWkjZITp+aYJv60yA=="], + "@tailwindcss/node/jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="], @@ -2989,6 +3000,8 @@ "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], + "@testing-library/dom/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], "@types/jest/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], @@ -3029,6 +3042,8 @@ "e2b/openapi-fetch": ["openapi-fetch@0.9.8", "", { "dependencies": { "openapi-typescript-helpers": "^0.0.8" } }, "sha512-zM6elH0EZStD/gSiNlcPrzXcVQ/pZo3BDvC6CDwRDUt1dDzxlshpmQnpD6cZaJ39THaSmwVCxxRrPKNM1hHrDg=="], + "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], @@ -3053,7 +3068,7 @@ "fumadocs-mdx/tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="], - "fumadocs-mdx/zod": ["zod@4.0.10", "", {}, "sha512-3vB+UU3/VmLL2lvwcY/4RV2i9z/YU0DTV/tDuYjrwmx5WeJ7hwy+rGEEx8glHp6Yxw7ibRbKSaIFBgReRPe5KA=="], + "fumadocs-mdx/zod": ["zod@4.0.5", "", {}, "sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA=="], "fumadocs-ui/tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], @@ -3071,12 +3086,20 @@ "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "jest-diff/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "jest-diff/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + "jest-matcher-utils/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "jest-matcher-utils/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + "jest-message-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "jest-message-util/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + "jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], @@ -3225,6 +3248,8 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@jest/types/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "@redocly/openapi-core/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@sentry/bundler-plugin-core/glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="], @@ -3239,8 +3264,6 @@ "@sentry/cli/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "@sentry/nextjs/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@sentry/node/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@sentry/webpack-plugin/unplugin/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], @@ -3249,6 +3272,8 @@ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="], + "@testing-library/dom/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "@types/jest/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "@typescript-eslint/typescript-estree/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -3277,6 +3302,8 @@ "e2b/openapi-fetch/openapi-typescript-helpers": ["openapi-typescript-helpers@0.0.8", "", {}, "sha512-1eNjQtbfNi5Z/kFhagDIaIRj6qqDzhjNJKz8cmMW0CVdGwT6e1GLbAfgI0d28VTJa1A8jz82jm/4dG8qNoNS8g=="], + "eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "extract-zip/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "fumadocs-core/@shikijs/rehype/@shikijs/types": ["@shikijs/types@3.8.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-5C39Q8/8r1I26suLh+5TPk1DTrbY/kn3IdWA5HdizR0FhlhD05zx5nKCqhzSfDHH3p4S0ZefxWd77DLV+8FhGg=="], @@ -3331,12 +3358,20 @@ "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "jest-diff/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "jest-diff/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "jest-matcher-utils/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "jest-matcher-utils/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "jest-message-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "jest-message-util/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "jest-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "mermaid.cli/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], "mermaid.cli/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], diff --git a/next.config.mjs b/next.config.mjs index d243ba06c..218ce9ab0 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,5 @@ import { withSentryConfig } from '@sentry/nextjs' - /** @type {import('next').NextConfig} */ const config = { eslint: { @@ -76,22 +75,25 @@ const config = { // Campaigns { source: '/start', - destination: '/careers?utm_source=billboard&utm_medium=outdoor&utm_campaign=launch_2025&utm_content=start_ooh', + destination: + '/careers?utm_source=billboard&utm_medium=outdoor&utm_campaign=launch_2025&utm_content=start_ooh', permanent: false, statusCode: 302, }, { source: '/machines', - destination: '/enterprise?utm_source=billboard&utm_medium=outdoor&utm_campaign=launch_2025&utm_content=machines_ooh', + destination: + '/enterprise?utm_source=billboard&utm_medium=outdoor&utm_campaign=launch_2025&utm_content=machines_ooh', permanent: false, statusCode: 302, }, -{ + { source: '/humans', - destination: '/enterprise?utm_source=billboard&utm_medium=outdoor&utm_campaign=launch_2025&utm_content=humans_ooh', + destination: + '/enterprise?utm_source=billboard&utm_medium=outdoor&utm_campaign=launch_2025&utm_content=humans_ooh', permanent: false, statusCode: 302, - } + }, ], skipTrailingSlashRedirect: true, } diff --git a/package.json b/package.json index 411769730..d035364b2 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@radix-ui/react-separator": "^1.1.3", "@radix-ui/react-slider": "^1.2.4", "@radix-ui/react-slot": "^1.2.0", - "@radix-ui/react-tabs": "^1.1.4", + "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-toast": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.0", "@radix-ui/themes": "^3.2.1", @@ -86,7 +86,7 @@ "geist": "^1.3.1", "immer": "^10.1.1", "lucide-react": "^0.525.0", - "motion": "^12.0.6", + "motion": "^12.18.1", "nanoid": "^5.0.9", "next": "15.3.0-canary.23", "next-logger": "^5.0.1", @@ -109,6 +109,8 @@ "rehype-katex": "^7.0.1", "remark-math": "^6.0.0", "remark-mermaid": "^0.2.0", + "semver": "^7.7.2", + "serialize-error": "^12.0.0", "shiki": "3.2.1", "swr": "^2.3.4", "tailwind-merge": "^2.6.0", @@ -135,6 +137,7 @@ "@types/pg": "^8.11.11", "@types/react": "^19.0.8", "@types/react-dom": "19.0.3", + "@types/semver": "^7.7.0", "@vitest/coverage-v8": "^3.0.7", "@vitest/ui": "3.0.7", "autoprefixer": "^10.4.20", diff --git a/src/__test__/development/metrics.test.ts b/src/__test__/development/metrics.test.ts index 9d7099b12..291c711c0 100644 --- a/src/__test__/development/metrics.test.ts +++ b/src/__test__/development/metrics.test.ts @@ -1,3 +1,4 @@ +import { l } from '@/lib/clients/logger' import { Sandbox } from 'e2b' import { describe, expect, it } from 'vitest' @@ -10,40 +11,262 @@ if (!TEST_E2B_DOMAIN || !TEST_E2B_API_KEY) { ) } -const SPAWN_COUNT = 50 // total sandboxes to spawn -const BATCH_SIZE = 5 // how many sandboxes to spawn concurrently +const SPAWN_COUNT = 10 // total sandboxes to spawn +const BATCH_SIZE = 2 // how many sandboxes to spawn concurrently -const SBX_TIMEOUT_MS = 60_000 -const STRESS_TIMEOUT_MS = 60_000 +const SBX_TIMEOUT_MS = 30_000 +const STRESS_TIMEOUT_MS = 30_000 const TEMPLATE = process.env.TEST_METRICS_TEMPLATE ?? 'base' const MEMORY_MB = 1024 // allocate this much memory inside sandbox in MB const CPU_OPS = 100_000_000 // iterations of CPU intensive math +const CHUNK_SIZE_MB = 64 // memory allocation chunk size in MB +const PROGRESS_INTERVAL = 10_000_000 // report progress every N operations + +interface StressTestConfig { + memoryMb: number + cpuOps: number + ioOps: number + chunkSizeMb: number + progressInterval: number + enableMemoryTest: boolean + enableCpuTest: boolean + enableIoTest: boolean +} + +function buildOptimizedStressCode(config: StressTestConfig): string { + const { + memoryMb, + cpuOps, + chunkSizeMb, + progressInterval, + enableMemoryTest, + enableCpuTest, + enableIoTest + } = config -function buildStressCode(memoryMb: number, cpuOps: number): string { return ` -cat > stress.py << 'EOL' -import time, math, random, os, sys +#!/bin/bash +set -e + +# Create test directory structure +mkdir -p /home/user/test/{data,logs,temp} +cd /home/user/test -mem_mb = ${memoryMb} -cpu_ops = ${cpuOps} +echo "STRESS_START $(date -Iseconds)" -start = time.time() +# Create test files for I/O operations +cat > test1.txt << 'EOL' +This is a test file 1 for I/O stress testing +Contains multiple lines of text data +Used for sequential and random access patterns +EOL -chunk = bytearray(mem_mb * 1024 * 1024) +cat > test2.json << 'EOL' +{ + "name": "Test file 2", + "type": "json", + "data": { + "array": [1, 2, 3, 4, 5], + "nested": {"key": "value", "number": 42} + } +} +EOL -for i in range(0, len(chunk), 4096): - chunk[i] = 1 +cat > test3.md << 'EOL' +# Test file 3 +This is a markdown file for stress testing +## Features +- Multi-line content +- Various formatting +- Used for file I/O benchmarks +EOL -total = 0.0 -for _ in range(cpu_ops): - total += math.sin(random.random()) +# Generate comprehensive stress test script +cat > stress.py << 'EOL' +import time, math, random, os, sys, threading, json +from concurrent.futures import ThreadPoolExecutor, as_completed -duration = time.time() - start -print(f"STRESS_DONE duration={duration} total={total}") +class StressTestRunner: + def __init__(self, mem_mb, cpu_ops, chunk_mb, progress_interval): + self.mem_mb = mem_mb + self.cpu_ops = cpu_ops + self.chunk_mb = chunk_mb + self.progress_interval = progress_interval + self.start_time = time.time() + + def log_progress(self, phase, progress, total, extra_info=""): + elapsed = time.time() - self.start_time + percent = (progress / total * 100) if total > 0 else 0 + print(f"STRESS_PROGRESS phase={phase} progress={progress}/{total} percent={percent:.1f} elapsed={elapsed:.2f}s {extra_info}") + + def memory_stress_test(self): + """Chunked memory allocation with pattern testing""" + if not ${enableMemoryTest}: + print("STRESS_SKIP phase=memory reason=disabled") + return + + print("STRESS_PHASE_START phase=memory target_mb=${memoryMb}") + chunks = [] + chunk_size = self.chunk_mb * 1024 * 1024 + total_chunks = self.mem_mb // self.chunk_mb + + try: + # Phase 1: Chunked allocation + for i in range(total_chunks): + chunk = bytearray(chunk_size) + chunks.append(chunk) + self.log_progress("memory_alloc", i + 1, total_chunks, f"allocated_mb={(i+1)*self.chunk_mb}") + + # Phase 2: Memory pattern testing + print("STRESS_PHASE_START phase=memory_patterns") + for i, chunk in enumerate(chunks): + # Sequential write pattern + for j in range(0, len(chunk), 4096): + chunk[j] = (i + j) % 256 + + # Random access pattern + for _ in range(1000): + idx = random.randint(0, len(chunk) - 1) + chunk[idx] = random.randint(0, 255) + + if (i + 1) % 10 == 0: + self.log_progress("memory_patterns", i + 1, len(chunks)) + + print(f"STRESS_PHASE_COMPLETE phase=memory allocated_chunks={len(chunks)} total_mb={len(chunks) * self.chunk_mb}") + + except MemoryError as e: + print(f"STRESS_ERROR phase=memory error=MemoryError chunks_allocated={len(chunks)} details={str(e)}") + + return chunks + + def cpu_stress_test(self): + """Multi-threaded CPU stress with various operation types""" + if not ${enableCpuTest}: + print("STRESS_SKIP phase=cpu reason=disabled") + return 0.0 + + print("STRESS_PHASE_START phase=cpu target_ops=${cpuOps}") + + def worker_thread(thread_id, ops_per_thread): + """Worker function for CPU stress testing""" + local_total = 0.0 + ops_completed = 0 + + for i in range(ops_per_thread): + # Mix of different CPU operations + if i % 4 == 0: + # Floating point operations + local_total += math.sin(random.random()) * math.cos(random.random()) + elif i % 4 == 1: + # Integer operations + local_total += (i * 17 + 23) % 1000 + elif i % 4 == 2: + # String operations + temp_str = f"stress_test_{i}_{random.randint(1000, 9999)}" + local_total += len(temp_str) * hash(temp_str) % 1000 + else: + # List operations + temp_list = [random.randint(1, 100) for _ in range(10)] + local_total += sum(temp_list) % 1000 + + ops_completed += 1 + if ops_completed % self.progress_interval == 0: + self.log_progress(f"cpu_thread_{thread_id}", ops_completed, ops_per_thread) + + return local_total + + # Use multiple threads for CPU stress + num_threads = min(4, os.cpu_count() or 1) + ops_per_thread = self.cpu_ops // num_threads + total_result = 0.0 + + print(f"STRESS_INFO phase=cpu threads={num_threads} ops_per_thread={ops_per_thread}") + + with ThreadPoolExecutor(max_workers=num_threads) as executor: + futures = [executor.submit(worker_thread, i, ops_per_thread) for i in range(num_threads)] + + for future in as_completed(futures): + try: + result = future.result() + total_result += result + except Exception as e: + print(f"STRESS_ERROR phase=cpu_thread error={str(e)}") + + print(f"STRESS_PHASE_COMPLETE phase=cpu threads={num_threads} total_result={total_result}") + return total_result + + def io_stress_test(self): + """File I/O stress testing""" + if not ${enableIoTest}: + print("STRESS_SKIP phase=io reason=disabled") + return + + print("STRESS_PHASE_START phase=io") + + try: + # Create multiple test files + for i in range(10): + filename = f"temp/stress_file_{i}.dat" + with open(filename, 'w') as f: + # Write substantial data + for j in range(1000): + f.write(f"Line {j} in file {i} with timestamp {time.time()}\\n") + + self.log_progress("io_write", i + 1, 10) + + # Read files back + total_bytes = 0 + for i in range(10): + filename = f"temp/stress_file_{i}.dat" + with open(filename, 'r') as f: + content = f.read() + total_bytes += len(content) + + self.log_progress("io_read", i + 1, 10) + + # JSON operations + test_data = {"iteration": i, "data": list(range(100)), "timestamp": time.time()} + for i in range(100): + with open(f"temp/json_test_{i}.json", 'w') as f: + json.dump(test_data, f) + + if (i + 1) % 20 == 0: + self.log_progress("io_json", i + 1, 100) + + print(f"STRESS_PHASE_COMPLETE phase=io total_bytes={total_bytes}") + + except Exception as e: + print(f"STRESS_ERROR phase=io error={str(e)}") + + def run_all_tests(self): + """Run all stress tests in sequence""" + print(f"STRESS_CONFIG mem_mb={self.mem_mb} cpu_ops={self.cpu_ops} chunk_mb={self.chunk_mb}") + + # Run tests + chunks = self.memory_stress_test() + cpu_result = self.cpu_stress_test() + self.io_stress_test() + + # Cleanup + if chunks: + del chunks + + total_duration = time.time() - self.start_time + print(f"STRESS_COMPLETE duration={total_duration:.2f}s cpu_result={cpu_result}") + +# Initialize and run stress test +runner = StressTestRunner(${memoryMb}, ${cpuOps}, ${chunkSizeMb}, ${progressInterval}) +runner.run_all_tests() EOL +echo "STRESS_EXECUTING $(date -Iseconds)" python3 stress.py +echo "STRESS_FINISHED $(date -Iseconds)" + +# Cleanup +rm -rf temp/ +ls -la ` } @@ -53,52 +276,402 @@ describe('E2B Sandbox metrics', () => { { timeout: 60_000 }, async () => { const sandboxes: Sandbox[] = [] + const testId = `metrics-test-${Date.now()}` + + l.info("test:starting_sandboxes", { + testId, + spawnCount: SPAWN_COUNT, + batchSize: BATCH_SIZE, + template: TEMPLATE, + memoryMb: MEMORY_MB, + cpuOps: CPU_OPS, + sbxTimeoutMs: SBX_TIMEOUT_MS, + stressTimeoutMs: STRESS_TIMEOUT_MS + }) const start = Date.now() - const spawnBatch = async (count: number) => { - const batch = await Promise.all( - Array.from({ length: count }).map(() => - Sandbox.create(TEMPLATE, { - domain: TEST_E2B_DOMAIN as string, - apiKey: TEST_E2B_API_KEY as string, - timeoutMs: SBX_TIMEOUT_MS, + const spawnBatch = async (batchNumber: number, count: number) => { + const batchStart = Date.now() + + l.info('test:starting_sandbox_batch', { + testId, + batchNumber, + batchSize: count, + totalSpawned: sandboxes.length, + remaining: SPAWN_COUNT - sandboxes.length + }) + + try { + const batch = await Promise.all( + Array.from({ length: count }).map(async (_, index) => { + const sandboxStart = Date.now() + + try { + const sandbox = await Sandbox.create(TEMPLATE, { + domain: TEST_E2B_DOMAIN as string, + apiKey: TEST_E2B_API_KEY as string, + timeoutMs: SBX_TIMEOUT_MS, + secure: true + }) + + const sandboxDuration = Date.now() - sandboxStart + l.debug("test:sandbox_created", { + testId, + batchNumber, + sandboxIndex: index, + sandboxId: sandbox.sandboxId, + duration: sandboxDuration + }) + + return sandbox + } catch (error) { + const sandboxDuration = Date.now() - sandboxStart + l.error("test:sandbox_creation_failed", error, { + testId, + batchNumber, + sandboxIndex: index, + duration: sandboxDuration, + }) + throw error + } }) ) - ) - sandboxes.push(...batch) - await new Promise((resolve) => setTimeout(resolve, 1_000)) + + sandboxes.push(...batch) + const batchDuration = Date.now() - batchStart + + l.info("test:batch_completed", { + testId, + batchNumber, + batchSize: count, + successCount: batch.length, + batchDuration, + totalSpawned: sandboxes.length, + averageSpawnTime: batchDuration / count + }) + + // Brief pause between batches to prevent overwhelming the system + await new Promise((resolve) => setTimeout(resolve, 1_000)) + + } catch (error) { + const batchDuration = Date.now() - batchStart + l.error("test:batch_creation_failed", error, { + testId, + batchNumber, + batchDuration, + }) + throw error + } } + // Spawn sandboxes in batches + let batchNumber = 0 for (let spawned = 0; spawned < SPAWN_COUNT; spawned += BATCH_SIZE) { const remaining = SPAWN_COUNT - spawned const currentBatchSize = Math.min(remaining, BATCH_SIZE) - await spawnBatch(currentBatchSize) + await spawnBatch(++batchNumber, currentBatchSize) + } + + const spawnDurationMs = Date.now() - start + l.info("test:all_sandboxes_spawned", { + testId, + totalSandboxes: sandboxes.length, + spawnDurationMs, + averageSpawnTime: spawnDurationMs / sandboxes.length, + batchCount: batchNumber + }) + + const stressConfig: StressTestConfig = { + memoryMb: MEMORY_MB, + cpuOps: CPU_OPS, + ioOps: 1000, + chunkSizeMb: CHUNK_SIZE_MB, + progressInterval: PROGRESS_INTERVAL, + enableMemoryTest: true, + enableCpuTest: true, + enableIoTest: true } - const durationMs = Date.now() - start - console.info( - `Spawned ${SPAWN_COUNT} sandbox(es) in ${durationMs}ms (batch size: ${BATCH_SIZE})` - ) + const stressCode = buildOptimizedStressCode(stressConfig) - const stressCode = buildStressCode(MEMORY_MB, CPU_OPS) + l.debug("test:stress_code_generated", { + testId, + stressConfig, + codeLength: stressCode.length + }) const runStressCode = async () => { + const stressStart = Date.now() + const results: Array<{ + sandboxId: string + success: boolean + duration?: number + error?: string + output?: string + exitCode?: number + }> = [] + + l.info("test:starting_stress_test", { + testId, + sandboxCount: sandboxes.length, + stressTimeoutMs: STRESS_TIMEOUT_MS + }) + try { - // Execute stress code inside each sandbox - await Promise.all( - sandboxes.map((sbx) => - sbx.commands.run(stressCode, { - timeoutMs: STRESS_TIMEOUT_MS, + // Execute stress code inside each sandbox with detailed logging + const stressPromises = sandboxes.map(async (sbx, index) => { + const sandboxStressStart = Date.now() + + l.debug("test:starting_stress_test_for_sandbox", { + testId, + sandboxId: sbx.sandboxId, + sandboxIndex: index, + totalSandboxes: sandboxes.length + }) + + try { + // Write stress test script to sandbox with proper path + const scriptPath = "/home/user/stress_test.sh" + + l.debug("test:writing_stress_script", { + testId, + sandboxId: sbx.sandboxId, + sandboxIndex: index, + scriptPath, + codeLength: stressCode.length }) - ) - ) + + await sbx.files.write(scriptPath, stressCode) + + // Verify file was written successfully + const fileCheck = await sbx.commands.run(`ls -la ${scriptPath}`, { user: 'root' }) + if (fileCheck.exitCode !== 0) { + l.warn("test:file_write_verification_failed", { + testId, + sandboxId: sbx.sandboxId, + error: fileCheck.stderr, + stdout: fileCheck.stdout + }) + + results.push({ + sandboxId: sbx.sandboxId, + success: false, + duration: Date.now() - sandboxStressStart, + error: `Failed to write stress test script: ${fileCheck.stderr}` + }) + return null // Continue with other sandboxes + } + + l.debug("test:file_written_successfully", { + testId, + sandboxId: sbx.sandboxId, + fileCheck: fileCheck.stdout + }) + + // Make script executable + const chmodResult = await sbx.commands.run(`chmod +x ${scriptPath}`, { user: 'root' }) + if (chmodResult.exitCode !== 0) { + l.warn("test:chmod_failed", { + testId, + sandboxId: sbx.sandboxId, + error: chmodResult.stderr, + stdout: chmodResult.stdout + }) + + results.push({ + sandboxId: sbx.sandboxId, + success: false, + duration: Date.now() - sandboxStressStart, + error: `Failed to make script executable: ${chmodResult.stderr}` + }) + return null // Continue with other sandboxes + } + + l.debug("test:stress_script_ready", { + testId, + sandboxId: sbx.sandboxId, + sandboxIndex: index, + scriptPath + }) + + // Execute stress test with output capture - handle non-zero exit codes gracefully + let result + try { + result = await sbx.commands.run(scriptPath, { + timeoutMs: STRESS_TIMEOUT_MS, + requestTimeoutMs: STRESS_TIMEOUT_MS, + user: 'root' + }) + } catch (commandError) { + // Handle command execution errors gracefully + const sandboxStressDuration = Date.now() - sandboxStressStart + const errorMessage = commandError instanceof Error ? commandError.message : String(commandError) + + l.warn("test:stress_command_execution_failed", { + testId, + sandboxId: sbx.sandboxId, + sandboxIndex: index, + duration: sandboxStressDuration, + error: errorMessage, + errorType: 'command_execution_error' + }) + + results.push({ + sandboxId: sbx.sandboxId, + success: false, + duration: sandboxStressDuration, + error: `Command execution failed: ${errorMessage}` + }) + return null // Continue with other sandboxes + } + + const sandboxStressDuration = Date.now() - sandboxStressStart + + // Log completion regardless of exit code + l.info("test:stress_test_completed", { + testId, + sandboxId: sbx.sandboxId, + sandboxIndex: index, + duration: sandboxStressDuration, + exitCode: result.exitCode, + outputLength: result.stdout?.length || 0, + errorLength: result.stderr?.length || 0, + success: result.exitCode === 0 + }) + + // Parse stress test output for metrics + const output = (result.stdout || '') + (result.stderr || '') + const progressLines = output.split('\n').filter(line => + line.includes('STRESS_PROGRESS') || + line.includes('STRESS_DONE') || + line.includes('STRESS_ERROR') || + line.includes('STRESS_COMPLETE') + ) + + if (progressLines.length > 0) { + l.debug("test:stress_progress_captured", { + testId, + sandboxId: sbx.sandboxId, + progressLines: progressLines.slice(-5) // Last 5 progress lines + }) + } + + // Log stderr if present but don't fail the test + if (result.stderr && result.stderr.trim()) { + l.warn("test:stress_stderr_output", { + testId, + sandboxId: sbx.sandboxId, + stderr: result.stderr.slice(0, 500) // First 500 chars + }) + } + + results.push({ + sandboxId: sbx.sandboxId, + success: result.exitCode === 0, + duration: sandboxStressDuration, + output: output.slice(-500), // Last 500 chars of output + exitCode: result.exitCode + }) + + return result + + } catch (error) { + const sandboxStressDuration = Date.now() - sandboxStressStart + const errorMessage = error instanceof Error ? error.message : String(error) + + l.warn("test:stress_test_unexpected_error", { + testId, + sandboxId: sbx.sandboxId, + sandboxIndex: index, + duration: sandboxStressDuration, + error: errorMessage, + errorType: 'unexpected_error' + }) + + results.push({ + sandboxId: sbx.sandboxId, + success: false, + duration: sandboxStressDuration, + error: `Unexpected error: ${errorMessage}` + }) + + return null // Continue with other sandboxes - don't throw + } + }) + + // Wait for all stress tests to complete - use allSettled to handle rejections gracefully + const stressResults = await Promise.allSettled(stressPromises) + + // Log any rejected promises + stressResults.forEach((result, index) => { + if (result.status === 'rejected') { + l.warn("test:stress_promise_rejected", { + testId, + sandboxIndex: index, + error: result.reason instanceof Error ? result.reason.message : String(result.reason) + }) + } + }) + + const totalStressDuration = Date.now() - stressStart + const successCount = results.filter(r => r.success).length + const failureCount = results.filter(r => !r.success).length + const averageDuration = results.reduce((sum, r) => sum + (r.duration || 0), 0) / results.length + + l.info("test:stress_execution_completed", { + testId, + totalStressDuration, + successCount, + failureCount, + totalSandboxes: sandboxes.length, + successRate: (successCount / sandboxes.length) * 100, + averageSandboxDuration: averageDuration + }) + + // Log detailed results summary + const errorSummary = results + .filter(r => !r.success) + .map(r => ({ sandboxId: r.sandboxId, error: r.error })) + + if (errorSummary.length > 0) { + l.warn("test:stress_failures_detected", { + testId, + errorSummary: errorSummary.slice(0, 10) // First 10 errors + }) + } + } catch (error) { - console.error(error) + const totalStressDuration = Date.now() - stressStart + l.error("test:stress_execution_failed", error, { + testId, + totalStressDuration, + resultsCount: results.length, + error: error instanceof Error ? error.message : String(error) + }) + throw error } + + return results } - runStressCode() + // Execute stress tests and capture results + const stressResults = await runStressCode() + + // Final test summary + const totalTestDuration = Date.now() - start + l.info("test:metrics_test_completed", { + testId, + totalTestDuration, + spawnDurationMs, + stressDurationMs: totalTestDuration - spawnDurationMs, + totalSandboxes: sandboxes.length, + stressResults: { + total: stressResults.length, + successful: stressResults.filter(r => r.success).length, + failed: stressResults.filter(r => !r.success).length + } + }) expect(sandboxes.length).toBe(SPAWN_COUNT) } diff --git a/src/__test__/setup.ts b/src/__test__/setup.ts index 717a83790..c76d8740e 100644 --- a/src/__test__/setup.ts +++ b/src/__test__/setup.ts @@ -8,21 +8,21 @@ loadEnvConfig(projectDir) // default mocks vi.mock('@/lib/clients/logger', () => ({ l: { - error: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - debug: vi.fn(), + error: console.error, + info: console.info, + warn: console.warn, + debug: console.info, }, logger: { - error: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - debug: vi.fn(), + error: console.error, + info: console.info, + warn: console.warn, + debug: console.info, }, default: { - error: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - debug: vi.fn(), + error: console.error, + info: console.info, + warn: console.warn, + debug: console.info, }, })) diff --git a/src/app/api/sandbox/details/polling/route.ts b/src/app/api/sandbox/details/polling/route.ts new file mode 100644 index 000000000..d8661e062 --- /dev/null +++ b/src/app/api/sandbox/details/polling/route.ts @@ -0,0 +1,29 @@ +'use server' + +import { cookies } from 'next/headers' +import { COOKIE_KEYS } from '@/configs/keys' +import { z } from 'zod' + +const BodySchema = z.object({ interval: z.number() }) + +export async function POST(request: Request) { + try { + const body = BodySchema.parse(await request.json()) + + const cookieStore = await cookies() + cookieStore.set( + COOKIE_KEYS.SANDBOX_INSPECT_POLLING_INTERVAL, + body.interval.toString(), + { + path: '/', + maxAge: 60 * 60 * 24 * 7, // 7 days + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + } + ) + + return Response.json({ interval: body.interval }) + } catch { + return Response.json({ error: 'Invalid request' }, { status: 400 }) + } +} diff --git a/src/app/api/sandbox/details/route.ts b/src/app/api/sandbox/details/route.ts new file mode 100644 index 000000000..7fdfd4876 --- /dev/null +++ b/src/app/api/sandbox/details/route.ts @@ -0,0 +1,72 @@ +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { infra } from '@/lib/clients/api' +import { createClient } from '@/lib/clients/supabase/server' +import { NextRequest, NextResponse } from 'next/server' + +export async function GET(request: NextRequest) { + const supabase = await createClient() + + const { + data: { session }, + } = await supabase.auth.getSession() + + if (!session) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { searchParams } = new URL(request.url) + const sandboxId = searchParams.get('sandboxId') + const teamId = searchParams.get('teamId') + + if (!sandboxId) { + return NextResponse.json( + { error: 'Missing sandboxId parameter' }, + { status: 400 } + ) + } + + if (!teamId) { + return NextResponse.json( + { error: 'Missing teamId parameter' }, + { status: 400 } + ) + } + + try { + const res = await infra.GET('/sandboxes/{sandboxID}', { + params: { + path: { + sandboxID: sandboxId, + }, + }, + headers: { + ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), + }, + cache: 'no-store', + }) + + if (res.error) { + const status = res.response.status + + if (status === 404) { + return NextResponse.json( + { error: 'Sandbox not found' }, + { status: 404 } + ) + } + + return NextResponse.json( + { error: 'Failed to fetch sandbox details' }, + { status: status || 500 } + ) + } + + return NextResponse.json(res.data) + } catch (error) { + console.error('Error fetching sandbox details:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/src/app/api/sandbox/inspect/root-path/route.ts b/src/app/api/sandbox/inspect/root-path/route.ts new file mode 100644 index 000000000..2387d8c02 --- /dev/null +++ b/src/app/api/sandbox/inspect/root-path/route.ts @@ -0,0 +1,32 @@ +'use server' + +import { cookies } from 'next/headers' +import { COOKIE_KEYS } from '@/configs/keys' +import { z } from 'zod' +import { ResponseCookie } from 'next/dist/compiled/@edge-runtime/cookies' + +const BodySchema = z.object({ path: z.string() }) + +const COOKIE_SETTINGS: Partial = { + path: '/', + maxAge: 60 * 60 * 24 * 365, // 1 year + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', +} + +export async function POST(request: Request) { + try { + const body = BodySchema.parse(await request.json()) + + const cookieStore = await cookies() + cookieStore.set( + COOKIE_KEYS.SANDBOX_INSPECT_ROOT_PATH, + body.path, + COOKIE_SETTINGS + ) + + return Response.json({ path: body.path }) + } catch { + return Response.json({ error: 'Invalid request' }, { status: 400 }) + } +} diff --git a/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/inspect/loading.tsx b/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/inspect/loading.tsx new file mode 100644 index 000000000..249f11404 --- /dev/null +++ b/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/inspect/loading.tsx @@ -0,0 +1 @@ +export { default } from '@/features/dashboard/loading-layout' diff --git a/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/inspect/page.tsx b/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/inspect/page.tsx new file mode 100644 index 000000000..c4976c47c --- /dev/null +++ b/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/inspect/page.tsx @@ -0,0 +1,54 @@ +import { COOKIE_KEYS } from '@/configs/keys' +import { SandboxInspectProvider } from '@/features/dashboard/sandbox/inspect/context' +import SandboxInspectFilesystem from '@/features/dashboard/sandbox/inspect/filesystem' +import SandboxInspectViewer from '@/features/dashboard/sandbox/inspect/viewer' +import { cn } from '@/lib/utils' +import { resolveTeamIdInServerComponent } from '@/lib/utils/server' +import { getSandboxRoot } from '@/server/sandboxes/get-sandbox-root' +import ClientOnly from '@/ui/client-only' +import { cookies } from 'next/headers' + +export const dynamic = 'force-dynamic' +export const fetchCache = 'force-no-store' +export const revalidate = 0 + +const DEFAULT_ROOT_PATH = '/home/user' + +export default async function SandboxInspectPage({ + params, +}: { + params: Promise<{ teamIdOrSlug: string; sandboxId: string }> +}) { + const cookieStore = await cookies() + const rootPath = + cookieStore.get(COOKIE_KEYS.SANDBOX_INSPECT_ROOT_PATH)?.value || + DEFAULT_ROOT_PATH + + const { teamIdOrSlug, sandboxId } = await params + + const teamId = await resolveTeamIdInServerComponent(teamIdOrSlug) + + const res = await getSandboxRoot({ + teamId, + sandboxId, + rootPath, + }) + + return ( + + + + + + + ) +} diff --git a/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/layout.tsx b/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/layout.tsx new file mode 100644 index 000000000..bbcf35fd3 --- /dev/null +++ b/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/layout.tsx @@ -0,0 +1,52 @@ +import { SandboxProvider } from '@/features/dashboard/sandbox/context' +import SandboxDetailsHeader from '@/features/dashboard/sandbox/header/header' +import SandboxLayoutClient from '@/features/dashboard/sandbox/layout' +import { resolveTeamIdInServerComponent } from '@/lib/utils/server' +import { getSandboxDetails } from '@/server/sandboxes/get-sandbox-details' + +export const fetchCache = 'force-no-store' + +interface SandboxLayoutProps { + children: React.ReactNode + params: Promise<{ teamIdOrSlug: string; sandboxId: string }> +} + +export default async function SandboxLayout({ + children, + params, +}: SandboxLayoutProps) { + const { teamIdOrSlug, sandboxId } = await params + + const teamId = await resolveTeamIdInServerComponent(teamIdOrSlug) + const res = await getSandboxDetails({ teamId, sandboxId }) + + const exists = res?.serverError !== 'SANDBOX_NOT_FOUND' + + if (!res?.data || res?.serverError) { + console.error( + 'SANDBOX_DETAILS_LAYOUT', + res?.serverError || 'Unknown error', + res?.data + ) + } + + return ( + + + } + > + {children} + + + ) +} diff --git a/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/loading.tsx b/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/loading.tsx new file mode 100644 index 000000000..e207688d9 --- /dev/null +++ b/src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/loading.tsx @@ -0,0 +1,5 @@ +import LoadingLayout from '@/features/dashboard/loading-layout' + +export default function Loading() { + return +} diff --git a/src/app/dashboard/[teamIdOrSlug]/sandboxes/loading.tsx b/src/app/dashboard/[teamIdOrSlug]/sandboxes/loading.tsx new file mode 100644 index 000000000..e207688d9 --- /dev/null +++ b/src/app/dashboard/[teamIdOrSlug]/sandboxes/loading.tsx @@ -0,0 +1,5 @@ +import LoadingLayout from '@/features/dashboard/loading-layout' + +export default function Loading() { + return +} diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index a5ed3566e..ee4a0d06f 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -58,8 +58,8 @@ export default async function DashboardLayout({ user={session!.user} > -
-
+
+
{children}
diff --git a/src/app/fonts.ts b/src/app/fonts.ts index ec3537aa5..d28fa0b92 100644 --- a/src/app/fonts.ts +++ b/src/app/fonts.ts @@ -1,4 +1,4 @@ -import { IBM_Plex_Mono, IBM_Plex_Sans } from 'next/font/google' +import { Fira_Code, IBM_Plex_Mono, IBM_Plex_Sans } from 'next/font/google' export const mono = IBM_Plex_Mono({ subsets: ['latin'], @@ -11,3 +11,9 @@ export const sans = IBM_Plex_Sans({ variable: '--font-ibm-plex-sans', weight: ['200', '300', '400', '500', '600', '700'], }) + +export const firaCode = Fira_Code({ + subsets: ['latin'], + variable: '--font-fira-code', + weight: ['400'], +}) diff --git a/src/configs/cache.ts b/src/configs/cache.ts deleted file mode 100644 index 5e572ece1..000000000 --- a/src/configs/cache.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const CACHE_TTLS = { - USER_TEAM_ACCESS: 60 * 60, // 1 hour - TEAM_SLUG_TO_ID: 60 * 60, // 1 hour - TEAM_ID_TO_SLUG: 60 * 60, // 1 hour -} diff --git a/src/configs/intervals.ts b/src/configs/intervals.ts index 357cfab74..6d255bb51 100644 --- a/src/configs/intervals.ts +++ b/src/configs/intervals.ts @@ -1 +1,3 @@ export const SANDBOXES_METRICS_POLLING_MS = 5_000 + +export const SANDBOXE_DETAILS_LATEST_METRICS_POLLING_MS = 5_000 diff --git a/src/configs/keys.ts b/src/configs/keys.ts index 12c226e1e..cac3db66a 100644 --- a/src/configs/keys.ts +++ b/src/configs/keys.ts @@ -6,6 +6,8 @@ export const COOKIE_KEYS = { SELECTED_TEAM_ID: 'e2b-selected-team-id', SELECTED_TEAM_SLUG: 'e2b-selected-team-slug', SIDEBAR_STATE: 'e2b-sidebar-state', + SANDBOX_INSPECT_ROOT_PATH: 'e2b-sandbox-inspect-root-path', + SANDBOX_INSPECT_POLLING_INTERVAL: 'e2b-sandbox-inspect-polling-interval', } /* diff --git a/src/configs/shiki.ts b/src/configs/shiki.ts index abd461863..3d9be6bf6 100644 --- a/src/configs/shiki.ts +++ b/src/configs/shiki.ts @@ -2,8 +2,8 @@ import { useTheme } from 'next-themes' import { useMemo } from 'react' import { ThemeRegistration } from 'shiki' -import baseThemeDark from '@shikijs/themes/rose-pine' -import baseThemeLight from '@shikijs/themes/rose-pine-dawn' +import baseThemeDark from '@shikijs/themes/vitesse-dark' +import baseThemeLight from '@shikijs/themes/vitesse-light' export const SHIKI_THEME_DARK: ThemeRegistration = { ...baseThemeDark, diff --git a/src/configs/urls.ts b/src/configs/urls.ts index f356e0a73..40f8be7b5 100644 --- a/src/configs/urls.ts +++ b/src/configs/urls.ts @@ -13,6 +13,10 @@ export const PROTECTED_URLS = { TEAMS: '/dashboard/teams', TEAM: (teamIdOrSlug: string) => `/dashboard/${teamIdOrSlug}/team`, SANDBOXES: (teamIdOrSlug: string) => `/dashboard/${teamIdOrSlug}/sandboxes`, + SANDBOX: (teamIdOrSlug: string, sandboxId: string) => + `/dashboard/${teamIdOrSlug}/sandboxes/${sandboxId}`, + SANDBOX_INSPECT: (teamIdOrSlug: string, sandboxId: string) => + `/dashboard/${teamIdOrSlug}/sandboxes/${sandboxId}/inspect`, TEMPLATES: (teamIdOrSlug: string) => `/dashboard/${teamIdOrSlug}/templates`, USAGE: (teamIdOrSlug: string) => `/dashboard/${teamIdOrSlug}/usage`, BILLING: (teamIdOrSlug: string) => `/dashboard/${teamIdOrSlug}/billing`, @@ -21,6 +25,12 @@ export const PROTECTED_URLS = { RESET_PASSWORD: '/dashboard/account', } +export const HELP_URLS = { + BUILD_TEMPLATE: + 'https://e2b.dev/docs/sandbox-template#4-build-your-sandbox-template', + START_COMMAND: 'https://e2b.dev/docs/sandbox-template/start-cmd', +} + export const BASE_URL = process.env.VERCEL_ENV ? process.env.VERCEL_ENV === 'production' ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` diff --git a/src/configs/versioning.ts b/src/configs/versioning.ts new file mode 100644 index 000000000..fa7ce6332 --- /dev/null +++ b/src/configs/versioning.ts @@ -0,0 +1 @@ +export const SANDBOX_INSPECT_MINIMUM_ENVD_VERSION = '0.2.7' diff --git a/src/features/dashboard/common/resource-usage.tsx b/src/features/dashboard/common/resource-usage.tsx new file mode 100644 index 000000000..40e845413 --- /dev/null +++ b/src/features/dashboard/common/resource-usage.tsx @@ -0,0 +1,84 @@ +import React from 'react' +import { cn } from '@/lib/utils' + +export interface ResourceUsageProps { + type: 'cpu' | 'mem' + metrics?: number | null + total?: number | null + /** Display mode: 'usage' shows metrics/total, 'simple' shows only total */ + mode?: 'usage' | 'simple' + classNames?: { + wrapper?: string + dot?: string + } +} + +const ResourceUsage: React.FC = ({ + type, + metrics, + total, + mode = 'usage', + classNames, +}) => { + const isCpu = type === 'cpu' + const unit = isCpu ? 'Core' : 'MB' + const hasMetrics = metrics !== null && metrics !== undefined + + if (mode === 'simple') { + const displayTotal = total ? total.toLocaleString() : 'n/a' + return ( +

+ {displayTotal} {unit} + {isCpu && total && total > 1 ? 's' : ''} +

+ ) + } + + const percentage = isCpu + ? (metrics ?? 0) + : metrics && total + ? (metrics / total) * 100 + : 0 + const roundedPercentage = Math.round(percentage) + + const textClassName = cn( + roundedPercentage >= (isCpu ? 90 : 95) + ? 'text-error' + : roundedPercentage >= 70 + ? 'text-warning' + : 'text-fg' + ) + + const displayValue = hasMetrics ? metrics.toLocaleString() : 'n/a' + const totalValue = total ? total.toLocaleString() : '-' + + return ( + + {hasMetrics ? ( + <> + {roundedPercentage}% + · + {!isCpu && ( + <> + {displayValue} / + + )} + + ) : ( + <> + n/a + · + + )} + {totalValue} {unit} + {isCpu && total && total > 1 ? 's' : ''} + + ) +} + +export default ResourceUsage diff --git a/src/features/dashboard/sandbox/context.tsx b/src/features/dashboard/sandbox/context.tsx index 9cf82e17e..5f2be9d60 100644 --- a/src/features/dashboard/sandbox/context.tsx +++ b/src/features/dashboard/sandbox/context.tsx @@ -1,10 +1,25 @@ 'use client' -import React, { createContext, useContext, ReactNode } from 'react' +import { MetricsResponse } from '@/app/api/teams/[teamId]/sandboxes/metrics/types' +import { SANDBOXE_DETAILS_LATEST_METRICS_POLLING_MS } from '@/configs/intervals' import { SandboxInfo } from '@/types/api' +import { ClientSandboxMetric } from '@/types/sandboxes.types' +import { + createContext, + ReactNode, + useContext, + useEffect, + useState, +} from 'react' +import useSWR from 'swr' interface SandboxContextValue { - sandboxInfo: SandboxInfo + sandboxInfo?: SandboxInfo + lastMetrics?: ClientSandboxMetric + isRunning: boolean + + isSandboxInfoLoading: boolean + refetchSandboxInfo: () => void } const SandboxContext = createContext(null) @@ -19,17 +34,126 @@ export function useSandboxContext() { interface SandboxProviderProps { children: ReactNode - sandboxInfo: SandboxInfo + serverSandboxInfo?: SandboxInfo + teamId: string + isRunning: boolean } export function SandboxProvider({ children, - sandboxInfo, + serverSandboxInfo, + teamId, + isRunning, }: SandboxProviderProps) { + const [isRunningState, setIsRunningState] = useState(isRunning) + const [lastFallbackData, setLastFallbackData] = useState(serverSandboxInfo) + + const { + data: sandboxInfoData, + mutate: refetchSandboxInfo, + isLoading: isSandboxInfoLoading, + isValidating: isSandboxInfoValidating, + } = useSWR( + !serverSandboxInfo?.sandboxID + ? null + : [`/api/sandbox/details`, serverSandboxInfo?.sandboxID], + async ([url]) => { + if (!serverSandboxInfo?.sandboxID) return + + const origin = document.location.origin + + const requestUrl = new URL(url, origin) + + requestUrl.searchParams.set('teamId', teamId) + requestUrl.searchParams.set('sandboxId', serverSandboxInfo.sandboxID) + + const response = await fetch(requestUrl.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + cache: 'no-store', + }) + + if (!response.ok) { + const status = response.status + + if (status === 404) { + setIsRunningState(false) + return + } + + if (!isRunningState) { + setIsRunningState(true) + } + + throw new Error(`Failed to fetch sandbox info: ${status}`) + } + + return (await response.json()) as SandboxInfo + }, + { + fallbackData: lastFallbackData, + revalidateOnFocus: false, + revalidateOnReconnect: false, + revalidateIfStale: false, + revalidateOnMount: false, + } + ) + + const { data: metricsData } = useSWR( + !serverSandboxInfo?.sandboxID + ? null + : [ + `/api/teams/${teamId}/sandboxes/metrics`, + serverSandboxInfo?.sandboxID, + ], + async ([url]) => { + if (!serverSandboxInfo?.sandboxID || !isRunning) return null + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ sandboxIds: [serverSandboxInfo.sandboxID] }), + cache: 'no-store', + }) + + if (!response.ok) { + const { error } = await response.json() + + throw new Error(error || 'Failed to fetch metrics') + } + + const data = (await response.json()) as MetricsResponse + + return data.metrics[serverSandboxInfo.sandboxID] + }, + { + refreshInterval: SANDBOXE_DETAILS_LATEST_METRICS_POLLING_MS, + errorRetryInterval: 1000, + errorRetryCount: 3, + revalidateIfStale: true, + revalidateOnFocus: true, + revalidateOnReconnect: true, + } + ) + + useEffect(() => { + if (serverSandboxInfo) { + setLastFallbackData(serverSandboxInfo) + } + }, [serverSandboxInfo]) + return ( {children} diff --git a/src/features/dashboard/sandbox/header/header.tsx b/src/features/dashboard/sandbox/header/header.tsx new file mode 100644 index 000000000..9aad1f775 --- /dev/null +++ b/src/features/dashboard/sandbox/header/header.tsx @@ -0,0 +1,127 @@ +import { PROTECTED_URLS } from '@/configs/urls' +import { SandboxInfo } from '@/types/api' +import { ChevronLeftIcon } from 'lucide-react' +import Link from 'next/link' +import RanFor from './ran-for' +import Status from './status' +import RemainingTime from './remaining-time' +import RefreshControl from './refresh' +import TemplateId from './template-id' +import StartedAt from './started-at' +import { cookies } from 'next/headers' +import { COOKIE_KEYS } from '@/configs/keys' +import Metadata from './metadata' +import { ResourceUsageClient } from './resource-usage-client' +import SandboxDetailsTitle from './title' + +interface SandboxDetailsHeaderProps { + teamIdOrSlug: string + state: SandboxInfo['state'] +} + +export default async function SandboxDetailsHeader({ + teamIdOrSlug, + state, +}: SandboxDetailsHeaderProps) { + const initialPollingInterval = (await cookies()).get( + COOKIE_KEYS.SANDBOX_INSPECT_POLLING_INTERVAL + )?.value + + const headerItems = { + state: { + label: 'status', + value: , + }, + templateID: { + label: 'template', + value: , + }, + metadata: { + label: 'metadata', + value: , + }, + remainingTime: { + label: 'timeout in', + value: , + }, + startedAt: { + label: 'created at', + value: , + }, + endAt: { + label: state === 'running' ? 'running for' : 'ran for', + value: , + }, + cpuCount: { + label: 'CPU Usage', + value: ( + + ), + }, + memoryMB: { + label: 'Memory Usage', + value: ( + + ), + }, + } + + return ( +
+
+
+ + + Sandboxes + + +
+ +
+ +
+ {Object.entries(headerItems).map(([key, { label, value }]) => ( + + ))} +
+
+ ) +} + +interface HeaderItemProps { + label: string + value: string | React.ReactNode +} + +function HeaderItem({ label, value }: HeaderItemProps) { + return ( +
+ {label} + {typeof value === 'string' ?

{value}

: value} +
+ ) +} diff --git a/src/features/dashboard/sandbox/header/metadata.tsx b/src/features/dashboard/sandbox/header/metadata.tsx new file mode 100644 index 000000000..5a81767b9 --- /dev/null +++ b/src/features/dashboard/sandbox/header/metadata.tsx @@ -0,0 +1,28 @@ +'use client' + +import { Button } from '@/ui/primitives/button' +import { JsonPopover } from '@/ui/json-popover' +import { Badge } from '@/ui/primitives/badge' +import { useSandboxContext } from '../context' +import { CircleSlash } from 'lucide-react' + +export default function Metadata() { + const { sandboxInfo } = useSandboxContext() + const className = 'h-6' + + if (!sandboxInfo?.metadata) { + return ( + + Empty + + ) + } + + return ( + + + + ) +} diff --git a/src/features/dashboard/sandbox/header/ran-for.tsx b/src/features/dashboard/sandbox/header/ran-for.tsx new file mode 100644 index 000000000..aab354191 --- /dev/null +++ b/src/features/dashboard/sandbox/header/ran-for.tsx @@ -0,0 +1,68 @@ +'use client' + +import { useCallback, useEffect, useMemo, useState } from 'react' +import { useSandboxContext } from '../context' + +export default function RanFor() { + const { sandboxInfo, isRunning } = useSandboxContext() + + const state = sandboxInfo?.state + const startedAt = sandboxInfo?.startedAt + const endAt = sandboxInfo?.endAt + + const startDate = useMemo( + () => (startedAt ? new Date(startedAt) : null), + [startedAt] + ) + const endDate = useMemo(() => (endAt ? new Date(endAt) : null), [endAt]) + + const calcRanFor = useCallback(() => { + if (!startDate) return '-' + + const end = state === 'running' ? new Date() : (endDate ?? new Date()) + const start = startDate + const diffMs = end.getTime() - start.getTime() + if (diffMs < 0) return '-' + + const hours = Math.floor(diffMs / (1000 * 60 * 60)) + const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)) + const seconds = Math.floor((diffMs % (1000 * 60)) / 1000) + + if (hours === 0 && minutes === 0) { + return `${seconds} seconds` + } + + const parts = [] + if (hours > 0) parts.push(`${hours} hours`) + if (minutes > 0) parts.push(`${minutes} minutes`) + return parts.join(' ') + }, [startDate, state, endDate]) + + const [ranFor, setRanFor] = useState(calcRanFor()) + + useEffect(() => { + if (!startDate) return + + let timerId: ReturnType + + const tick = () => { + setRanFor(calcRanFor()) + + if (!isRunning) return + + const diffMs = Date.now() - startDate.getTime() + const nextDelay = diffMs < 60_000 ? 1_000 : 3_000 + timerId = setTimeout(tick, nextDelay) + } + + tick() + + return () => clearTimeout(timerId) + }, [calcRanFor, startDate, isRunning]) + + if (!sandboxInfo) { + return null + } + + return

{ranFor}

+} diff --git a/src/features/dashboard/sandbox/header/refresh.tsx b/src/features/dashboard/sandbox/header/refresh.tsx new file mode 100644 index 000000000..26abf07d2 --- /dev/null +++ b/src/features/dashboard/sandbox/header/refresh.tsx @@ -0,0 +1,64 @@ +'use client' + +import { l } from '@/lib/clients/logger' +import { PollingButton } from '@/ui/polling-button' +import { useCallback, useState } from 'react' +import { serializeError } from 'serialize-error' +import { useSandboxContext } from '../context' + +const pollingIntervals = [ + { value: 0, label: 'Off' }, + { value: 5, label: '5s' }, + { value: 10, label: '10s' }, + { value: 30, label: '30s' }, + { value: 60, label: '1m' }, +] + +type PollingInterval = (typeof pollingIntervals)[number]['value'] + +interface RefreshControlProps { + className?: string + initialPollingInterval?: PollingInterval +} + +export default function RefreshControl({ + className, + initialPollingInterval, +}: RefreshControlProps) { + const [pollingInterval, setPollingInterval] = useState( + initialPollingInterval ?? pollingIntervals[2]!.value + ) + + const { refetchSandboxInfo, isSandboxInfoLoading } = useSandboxContext() + + const handleIntervalChange = useCallback( + async (interval: PollingInterval) => { + setPollingInterval(interval) + try { + await fetch('/api/sandbox/details/polling', { + method: 'POST', + body: JSON.stringify({ interval }), + }) + } catch (error) { + l.error({ + key: 'sandbox_inspect_refresh:save_polling_interval_failed', + message: + error instanceof Error ? error.message : 'Failed to save root path', + error: serializeError(error), + }) + } + }, + [] + ) + + return ( + + ) +} diff --git a/src/features/dashboard/sandbox/header/remaining-time.tsx b/src/features/dashboard/sandbox/header/remaining-time.tsx new file mode 100644 index 000000000..2fb4c2340 --- /dev/null +++ b/src/features/dashboard/sandbox/header/remaining-time.tsx @@ -0,0 +1,93 @@ +'use client' + +import { useCallback, useEffect, useState, useTransition } from 'react' +import { useParams } from 'next/navigation' +import { Button } from '@/ui/primitives/button' +import { RefreshCw, StopCircle } from 'lucide-react' +import { revalidateSandboxDetailsLayout } from '@/server/sandboxes/sandbox-actions' +import { cn } from '@/lib/utils' +import HelpTooltip from '@/ui/help-tooltip' +import { motion } from 'motion/react' +import { useSandboxContext } from '../context' +import { Badge } from '@/ui/primitives/badge' + +export default function RemainingTime() { + const { sandboxInfo, isRunning } = useSandboxContext() + + const endAt = sandboxInfo?.endAt + + const getRemainingSeconds = useCallback(() => { + if (!endAt) return 0 + const endTs = typeof endAt === 'number' ? endAt : new Date(endAt).getTime() + return Math.max(0, Math.floor((endTs - Date.now()) / 1000)) + }, [endAt]) + + const [remaining, setRemaining] = useState(getRemainingSeconds) + + const [isPending, startTransition] = useTransition() + const { teamIdOrSlug, sandboxId } = useParams() + + useEffect(() => { + const id = setInterval(() => { + setRemaining(getRemainingSeconds()) + }, 1000) + + return () => clearInterval(id) + }, [endAt, getRemainingSeconds]) + + const minutes = Math.floor(remaining / 60) + const seconds = remaining % 60 + const formatted = `${minutes.toString().padStart(2, '0')}:${seconds + .toString() + .padStart(2, '0')}` + + const handleRefresh = useCallback(() => { + startTransition(async () => { + await revalidateSandboxDetailsLayout( + teamIdOrSlug as string, + sandboxId as string + ) + }) + }, [teamIdOrSlug, sandboxId]) + + if (!isRunning) { + return ( + + Stopped + + ) + } + + return ( +
+

{formatted}

+ + + + + } + > + The sandbox may have been terminated since last refresh. Refreshing + could make this page inaccessible if the sandbox no longer exists. + + +
+ ) +} diff --git a/src/features/dashboard/sandbox/header/resource-usage-client.tsx b/src/features/dashboard/sandbox/header/resource-usage-client.tsx new file mode 100644 index 000000000..b37e5c6c8 --- /dev/null +++ b/src/features/dashboard/sandbox/header/resource-usage-client.tsx @@ -0,0 +1,46 @@ +'use client' + +import { memo, useMemo } from 'react' +import { useSandboxContext } from '../context' +import type { ResourceUsageProps } from '@/features/dashboard/common/resource-usage' +import ResourceUsage from '@/features/dashboard/common/resource-usage' + +interface ResourceUsageClientProps extends ResourceUsageProps {} + +export const ResourceUsageClient = memo( + function ResourceUsageClient({ ...props }: ResourceUsageClientProps) { + const { lastMetrics, sandboxInfo } = useSandboxContext() + + const metrics = useMemo( + () => + props.type === 'cpu' ? lastMetrics?.cpuUsedPct : lastMetrics?.memUsedMb, + [props.type, lastMetrics] + ) + + const total = useMemo(() => { + if (props.type === 'cpu') { + return sandboxInfo?.cpuCount + } + return sandboxInfo?.memoryMB + }, [props.type, sandboxInfo]) + + return ( + + ) + }, + (prevProps, nextProps) => { + return ( + prevProps.type === nextProps.type && + prevProps.total === nextProps.total && + prevProps.mode === nextProps.mode + ) + } +) diff --git a/src/features/dashboard/sandbox/header/started-at.tsx b/src/features/dashboard/sandbox/header/started-at.tsx new file mode 100644 index 000000000..f33e4f5b8 --- /dev/null +++ b/src/features/dashboard/sandbox/header/started-at.tsx @@ -0,0 +1,47 @@ +'use client' + +import CopyButton from '@/ui/copy-button' +import { useSandboxContext } from '../context' + +export default function StartedAt() { + const { sandboxInfo } = useSandboxContext() + + if (!sandboxInfo) { + return null + } + + const startedAt = sandboxInfo.startedAt + + const date = new Date(startedAt) + const now = new Date() + const isToday = date.toDateString() === now.toDateString() + const isYesterday = + date.toDateString() === + new Date(now.setDate(now.getDate() - 1)).toDateString() + + const prefix = isToday + ? 'Today' + : isYesterday + ? 'Yesterday' + : date.toLocaleDateString() + + const timeStr = date.toLocaleTimeString([], { + hour: 'numeric', + minute: '2-digit', + second: '2-digit', + }) + + return ( +
+

+ {prefix}, {timeStr} +

+ +
+ ) +} diff --git a/src/features/dashboard/sandbox/header/status.tsx b/src/features/dashboard/sandbox/header/status.tsx new file mode 100644 index 000000000..f5d537041 --- /dev/null +++ b/src/features/dashboard/sandbox/header/status.tsx @@ -0,0 +1,19 @@ +'use client' + +import { Badge } from '@/ui/primitives/badge' +import { Circle } from 'lucide-react' +import { useSandboxContext } from '../context' + +export default function Status() { + const { isRunning } = useSandboxContext() + + return ( + + + {isRunning ? 'Running' : 'Stopped'} + + ) +} diff --git a/src/features/dashboard/sandbox/header/template-id.tsx b/src/features/dashboard/sandbox/header/template-id.tsx new file mode 100644 index 000000000..0866b9a60 --- /dev/null +++ b/src/features/dashboard/sandbox/header/template-id.tsx @@ -0,0 +1,26 @@ +'use client' + +import CopyButton from '@/ui/copy-button' +import { Badge } from '@/ui/primitives/badge' +import { useMemo } from 'react' +import { useSandboxContext } from '../context' + +export default function TemplateId() { + const { sandboxInfo } = useSandboxContext() + + const value = useMemo(() => { + return sandboxInfo?.alias || sandboxInfo?.templateID?.toString() || '' + }, [sandboxInfo]) + + return ( + +

{value}

+ +
+ ) +} diff --git a/src/features/dashboard/sandbox/header/title.tsx b/src/features/dashboard/sandbox/header/title.tsx new file mode 100644 index 000000000..2af94294a --- /dev/null +++ b/src/features/dashboard/sandbox/header/title.tsx @@ -0,0 +1,26 @@ +'use client' + +import CopyButton from '@/ui/copy-button' +import { useSandboxContext } from '../context' + +export default function Title() { + const { sandboxInfo } = useSandboxContext() + + if (!sandboxInfo) { + return null + } + + return ( +
+

+ {sandboxInfo.sandboxID} +

+ +
+ ) +} diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index ff61e6024..2553d4a79 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -1,23 +1,24 @@ 'use client' -import React, { +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { AUTH_URLS } from '@/configs/urls' +import { supabase } from '@/lib/clients/supabase/client' +import { getParentPath, normalizePath } from '@/lib/utils/filesystem' +import Sandbox, { EntryInfo } from 'e2b' +import { useRouter } from 'next/navigation' +import { createContext, - useContext, - useRef, ReactNode, - useLayoutEffect, + useCallback, + useContext, + useEffect, useMemo, + useRef, } from 'react' +import { useSandboxContext } from '../context' import { createFilesystemStore, type FilesystemStore } from './filesystem/store' import { FilesystemNode, FilesystemOperations } from './filesystem/types' import { SandboxManager } from './sandbox-manager' -import { getParentPath, normalizePath } from '@/lib/utils/filesystem' -import { useSandboxContext } from '../context' -import Sandbox, { EntryInfo, FileType } from 'e2b' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { supabase } from '@/lib/clients/supabase/client' -import { useRouter } from 'next/navigation' -import { AUTH_URLS } from '@/configs/urls' interface SandboxInspectContextValue { store: FilesystemStore @@ -41,10 +42,9 @@ export function SandboxInspectProvider({ seedEntries, teamId, }: SandboxInspectProviderProps) { - const { sandboxInfo } = useSandboxContext() + const { sandboxInfo, isRunning } = useSandboxContext() const storeRef = useRef(null) const sandboxManagerRef = useRef(null) - const operationsRef = useRef(null) const router = useRouter() @@ -78,7 +78,7 @@ export function SandboxInspectProvider({ { name: rootName, path: normalizedRoot, - type: FileType.DIR, + type: 'dir', isExpanded: true, children: [], }, @@ -93,12 +93,12 @@ export function SandboxInspectProvider({ path: normalizePath(entry.path), } - if (entry.type === FileType.DIR) { + if (entry.type === 'dir') { state.setLoaded(base.path, false) return { ...base, - type: FileType.DIR, + type: 'dir', isExpanded: false, children: [], } @@ -106,118 +106,138 @@ export function SandboxInspectProvider({ return { ...base, - type: FileType.FILE, + type: 'file', } }) state.addNodes(normalizedRoot, seedNodes) } + } + } - const store = storeRef.current - operationsRef.current = { - loadDirectory: async (path: string) => { - await sandboxManagerRef.current?.loadDirectory(path) - }, - selectNode: async (path: string) => { - const node = store.getState().getNode(path) - - if (!node) return - - if (node.type === FileType.FILE && !store.getState().isLoaded(path)) { - await sandboxManagerRef.current?.readFile(path) - } - - store.getState().setSelected(path) - }, - resetSelected: () => { - store.getState().setSelected(undefined) - }, - toggleDirectory: async (path: string) => { - const normalizedPath = normalizePath(path) - const state = store.getState() - const node = state.getNode(normalizedPath) - - if (!node || node.type !== FileType.DIR) return - - const newExpandedState = !node.isExpanded - state.setExpanded(normalizedPath, newExpandedState) - - if (newExpandedState && !state.isLoaded(normalizedPath)) { - await sandboxManagerRef.current?.loadDirectory(normalizedPath) - } - }, - refreshDirectory: async (path: string) => { - await sandboxManagerRef.current?.refreshDirectory(path) - }, - refreshFile: async (path: string) => { + // ---------- filesystem operations exposed via context ---------- + const operations = useMemo( + () => ({ + loadDirectory: async (path: string) => { + if (!isRunning) { + return + } + + await sandboxManagerRef.current?.loadDirectory(path) + }, + selectNode: async (path: string) => { + const node = storeRef.current!.getState().getNode(path) + + if (!node) return + + if ( + isRunning && + node.type === 'file' && + !storeRef.current!.getState().isLoaded(path) + ) { await sandboxManagerRef.current?.readFile(path) - }, - downloadFile: async (path: string) => { - const downloadUrl = - await sandboxManagerRef.current?.getDownloadUrl(path) + } - if (!downloadUrl) return + storeRef.current!.getState().setSelected(path) + }, + resetSelected: () => { + storeRef.current!.setState((state) => { + state.selectedPath = undefined + }) + }, + toggleDirectory: async (path: string) => { + const normalizedPath = normalizePath(path) + const state = storeRef.current!.getState() + const node = state.getNode(normalizedPath) + + if (!node || node.type !== 'dir') return + + const newExpandedState = !node.isExpanded + state.setExpanded(normalizedPath, newExpandedState) + + if (isRunning && newExpandedState && !state.isLoaded(normalizedPath)) { + await sandboxManagerRef.current?.loadDirectory(normalizedPath) + } + }, + refreshDirectory: async (path: string) => { + if (!isRunning) return + + await sandboxManagerRef.current?.refreshDirectory(path) + }, + refreshFile: async (path: string) => { + if (!isRunning) return + + await sandboxManagerRef.current?.readFile(path) + }, + downloadFile: async (path: string) => { + if (!isRunning) return + + const downloadUrl = + await sandboxManagerRef.current?.getDownloadUrl(path) + + if (!downloadUrl) return + + const node = storeRef.current!.getState().getNode(path) + + const a = document.createElement('a') + a.href = downloadUrl + a.download = node?.name || '' + a.target = '_blank' + a.click() + }, + }), + [isRunning, sandboxManagerRef.current, storeRef.current] + ) - const node = store.getState().getNode(path) + const connectSandbox = useCallback(async () => { + if (!storeRef.current || !sandboxInfo) return - const a = document.createElement('a') - a.href = downloadUrl - a.download = node?.name || '' - a.target = '_blank' - a.click() - }, - } + // (re)create the sandbox-manager when sandbox / team / root changes + if (sandboxManagerRef.current) { + sandboxManagerRef.current.stopWatching() } - } - /* - * ---------- watcher (side-effect) initialisation / cleanup ---------- - */ - useLayoutEffect(() => { - const connectSandbox = async () => { - if (!storeRef.current) return + const { data } = await supabase.auth.getSession() - // (re)create the sandbox-manager when sandbox / team / root changes - if (sandboxManagerRef.current) { - sandboxManagerRef.current.stopWatching() - } + if (!data || !data.session) { + router.replace(AUTH_URLS.SIGN_IN) + return + } - const { data } = await supabase.auth.getSession() + const sandbox = await Sandbox.connect(sandboxInfo.sandboxID, { + domain: process.env.NEXT_PUBLIC_E2B_DOMAIN, + headers: { + ...SUPABASE_AUTH_HEADERS(data.session?.access_token, teamId), + }, + }) + + sandboxManagerRef.current = new SandboxManager( + storeRef.current, + sandbox, + rootPath, + sandboxInfo.envdAccessToken !== undefined + ) + }, [sandboxInfo?.sandboxID, teamId, rootPath, router]) - if (!data || !data.session) { - router.replace(AUTH_URLS.SIGN_IN) - return + // handle sandbox connection / disconnection + useEffect(() => { + if (isRunning) { + if (!sandboxManagerRef.current) { + connectSandbox() } - - const sandbox = await Sandbox.connect(sandboxInfo.sandboxID, { - domain: process.env.NEXT_PUBLIC_E2B_DOMAIN, - headers: { - ...SUPABASE_AUTH_HEADERS(data.session?.access_token, teamId), - }, - }) - - sandboxManagerRef.current = new SandboxManager( - storeRef.current, - sandbox, - rootPath, - sandboxInfo.envdAccessToken !== undefined - ) + return } - connectSandbox() - - return () => { - sandboxManagerRef.current?.stopWatching() - } - }, [sandboxInfo.sandboxID, teamId, rootPath, router]) + sandboxManagerRef.current?.stopWatching() + }, [isRunning, connectSandbox]) - if (!storeRef.current || !operationsRef.current) { + if (!storeRef.current || !sandboxInfo) { return null // should never happen, but satisfies type-checker } const contextValue: SandboxInspectContextValue = { store: storeRef.current, - operations: operationsRef.current, + operations, } return ( diff --git a/src/features/dashboard/sandbox/inspect/dir.tsx b/src/features/dashboard/sandbox/inspect/dir.tsx new file mode 100644 index 000000000..60abf37a0 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/dir.tsx @@ -0,0 +1,97 @@ +import { cn } from '@/lib/utils' +import { DataTableRow } from '@/ui/data-table' +import { AlertCircle, ChevronRight } from 'lucide-react' +import { AnimatePresence, motion } from 'motion/react' +import SandboxInspectEmptyNode from './empty' +import { FilesystemNode } from './filesystem/types' +import { useDirectory } from './hooks/use-directory' +import SandboxInspectNode from './node' +import NodeLabel from './node-label' + +interface SandboxInspectDirProps { + dir: FilesystemNode & { + type: 'dir' + } +} + +export default function SandboxInspectDir({ dir }: SandboxInspectDirProps) { + const { + hasError, + error, + isExpanded, + toggle, + isLoading, + isLoaded, + hasChildren, + children, + } = useDirectory(dir.path) + + return ( + <> + { + if (e.key === 'Enter' || e.key === ' ') { + toggle() + } + }} + className={cn( + 'group hover:bg-bg-200 focus:ring-ring focus:bg-bg-200 h-7 min-h-7 cursor-pointer gap-1 truncate transition-none select-none even:bg-transparent focus:outline-none' + )} + data-slot="inspect-dir" + > + + + + + {hasError && ( + + + {error} + + )} + + + + {isExpanded && isLoaded && ( + + {hasChildren ? ( + children.map((child) => ( + + )) + ) : ( + + )} + + )} + + + ) +} diff --git a/src/features/dashboard/sandbox/inspect/empty.tsx b/src/features/dashboard/sandbox/inspect/empty.tsx new file mode 100644 index 000000000..bc0e7755b --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/empty.tsx @@ -0,0 +1,10 @@ +import { DataTableRow } from '@/ui/data-table' +import NodeLabel from './node-label' + +export default function SandboxInspectEmptyNode() { + return ( + + + + ) +} diff --git a/src/features/dashboard/sandbox/inspect/file.tsx b/src/features/dashboard/sandbox/inspect/file.tsx new file mode 100644 index 000000000..e03634069 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/file.tsx @@ -0,0 +1,49 @@ +import { cn } from '@/lib/utils' +import { DataTableRow } from '@/ui/data-table' +import { AlertCircle, FileIcon } from 'lucide-react' +import { FilesystemNode } from './filesystem/types' +import { useFile } from './hooks/use-file' +import NodeLabel from './node-label' + +interface SandboxInspectFileProps { + file: FilesystemNode & { + type: 'file' + } +} + +export default function SandboxInspectFile({ file }: SandboxInspectFileProps) { + const { isSelected, isLoading, hasError, error, toggle } = useFile(file.path) + + return ( + { + if (e.key === 'Enter' || e.key === ' ') { + toggle() + } + }} + > + + + {hasError && ( + + + {error} + + )} + + ) +} diff --git a/src/features/dashboard/sandbox/inspect/filesystem-header.tsx b/src/features/dashboard/sandbox/inspect/filesystem-header.tsx new file mode 100644 index 000000000..44f0d5947 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/filesystem-header.tsx @@ -0,0 +1,23 @@ +'use client' + +import { cn } from '@/lib/utils' +import RootPathInput from './root-path-input' + +interface SandboxInspectHeaderProps { + className?: string + rootPath: string +} + +export default function SandboxInspectHeader({ + className, + rootPath, +}: SandboxInspectHeaderProps) { + return ( +
+
+ {'$'} + +
+
+ ) +} diff --git a/src/features/dashboard/sandbox/inspect/filesystem.tsx b/src/features/dashboard/sandbox/inspect/filesystem.tsx new file mode 100644 index 000000000..72480d7f6 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/filesystem.tsx @@ -0,0 +1,44 @@ +'use client' + +import SandboxInspectFrame from './frame' +import { useRootChildren } from './hooks/use-node' +import SandboxInspectNode from './node' +import { ScrollArea } from '@/ui/primitives/scroll-area' +import SandboxInspectFilesystemHeader from '@/features/dashboard/sandbox/inspect/filesystem-header' +import SandboxInspectNotFound from './not-found' +import { StoppedBanner } from './stopped-banner' + +interface SandboxInspectFilesystemProps { + rootPath: string +} + +export default function SandboxInspectFilesystem({ + rootPath, +}: SandboxInspectFilesystemProps) { + const children = useRootChildren() + + return ( +
+ + } + > +
+ + {children.length > 0 ? ( + children.map((child) => ( + + )) + ) : ( + + )} + +
+
+ + ) +} diff --git a/src/features/dashboard/sandbox/inspect/filesystem/store.ts b/src/features/dashboard/sandbox/inspect/filesystem/store.ts index 2f3719578..5aa2656bf 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/store.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/store.ts @@ -1,16 +1,15 @@ 'use client' -import { create } from 'zustand' -import { immer } from 'zustand/middleware/immer' -import { enableMapSet } from 'immer' import { - normalizePath, + getBasename, getParentPath, isChildPath, - getBasename, + normalizePath, } from '@/lib/utils/filesystem' +import { enableMapSet } from 'immer' +import { create } from 'zustand' +import { immer } from 'zustand/middleware/immer' import { FilesystemNode } from './types' -import { FileType } from 'e2b' enableMapSet() @@ -48,6 +47,8 @@ export interface FilesystemState { errorPaths: Map sortingDirection: 'asc' | 'desc' fileContents: Map + lastUpdated: Date | null + watcherError: string | null } // mutations/actions that modify state @@ -63,6 +64,8 @@ export interface FilesystemMutations { setFileContent: (path: string, updates: FileContentState) => void resetFileContent: (path: string) => void reset: () => void + setLastUpdated: (lastUpdated: Date | null) => void + setWatcherError: (error: string | null) => void } // computed/derived values @@ -96,8 +99,8 @@ function compareFilesystemNodes( ): number { if (!nodeA || !nodeB) return 0 - if (nodeA.type === FileType.DIR && nodeB.type === FileType.FILE) return -1 - if (nodeA.type === FileType.FILE && nodeB.type === FileType.DIR) return 1 + if (nodeA.type === 'dir' && nodeB.type === 'file') return -1 + if (nodeA.type === 'file' && nodeB.type === 'dir') return 1 const cmp = nodeA.name.localeCompare(nodeB.name, undefined, { sensitivity: 'base', @@ -118,6 +121,8 @@ export const createFilesystemStore = (rootPath: string) => errorPaths: new Map(), sortingDirection: 'asc' as 'asc' | 'desc', fileContents: new Map(), + lastUpdated: new Date(), + watcherError: null, addNodes: (parentPath: string, nodes: FilesystemNode[]) => { const normalizedParentPath = normalizePath(parentPath) @@ -131,14 +136,14 @@ export const createFilesystemStore = (rootPath: string) => parentNode = { name: parentName, path: normalizedParentPath, - type: FileType.DIR, + type: 'dir', isExpanded: false, children: [], } state.nodes.set(normalizedParentPath, parentNode) } - if (parentNode.type === FileType.FILE) { + if (parentNode.type === 'file') { throw new Error('Parent node is a file') } @@ -181,7 +186,7 @@ export const createFilesystemStore = (rootPath: string) => const parentPath = getParentPath(normalizedPath) const parentNode = state.nodes.get(parentPath) - if (parentNode && parentNode.type === FileType.DIR) { + if (parentNode && parentNode.type === 'dir') { parentNode.children = parentNode.children.filter( (childPath: string) => childPath !== normalizedPath ) @@ -225,7 +230,7 @@ export const createFilesystemStore = (rootPath: string) => if (!node) return - if (node?.type === FileType.FILE) { + if (node?.type === 'file') { console.error('Cannot expand file', node) return } @@ -306,7 +311,7 @@ export const createFilesystemStore = (rootPath: string) => const state = get() const node = state.nodes.get(normalizedPath) - if (!node || node.type === FileType.FILE) return [] + if (!node || node.type === 'file') return [] const cached = childrenCache.get(normalizedPath) if (cached && cached.ref === node.children) { @@ -330,7 +335,7 @@ export const createFilesystemStore = (rootPath: string) => const normalizedPath = normalizePath(path) const node = get().nodes.get(normalizedPath) - if (!node || node.type === FileType.FILE) return false + if (!node || node.type === 'file') return false return !!node.isExpanded }, @@ -353,7 +358,7 @@ export const createFilesystemStore = (rootPath: string) => const normalizedPath = normalizePath(path) const node = get().nodes.get(normalizedPath) - if (!node || node.type === FileType.FILE) return false + if (!node || node.type === 'file') return false return node.children.length > 0 }, @@ -362,6 +367,18 @@ export const createFilesystemStore = (rootPath: string) => const normalizedPath = normalizePath(path) return get().fileContents.get(normalizedPath) }, + + setLastUpdated: (lastUpdated) => { + set((state: FilesystemState) => { + state.lastUpdated = lastUpdated + }) + }, + + setWatcherError: (error) => { + set((state: FilesystemState) => { + state.watcherError = error + }) + }, })) ) diff --git a/src/features/dashboard/sandbox/inspect/filesystem/types.ts b/src/features/dashboard/sandbox/inspect/filesystem/types.ts index c55b20c6e..2ee34f7ab 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/types.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/types.ts @@ -1,7 +1,5 @@ -import { FileType } from 'e2b' - interface FilesystemDir { - type: FileType.DIR + type: 'dir' name: string path: string children: string[] // paths of children @@ -9,7 +7,7 @@ interface FilesystemDir { } interface FilesystemFile { - type: FileType.FILE + type: 'file' name: string path: string } diff --git a/src/features/dashboard/sandbox/inspect/frame.tsx b/src/features/dashboard/sandbox/inspect/frame.tsx new file mode 100644 index 000000000..fba126a14 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/frame.tsx @@ -0,0 +1,38 @@ +'use client' + +import { cn } from '@/lib/utils' +import React from 'react' +import { motion } from 'framer-motion' + +type SandboxInspectFrameProps = React.ComponentProps & { + header: React.ReactNode + classNames?: { + frame?: string + header?: string + } +} + +export default function SandboxInspectFrame({ + className, + classNames, + children, + header, + ...props +}: SandboxInspectFrameProps) { + return ( + +
+ {header} +
+ {children as React.ReactNode} +
+ ) +} diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx b/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx index 8d82bdcc9..ba460be56 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx +++ b/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx @@ -1,10 +1,9 @@ 'use client' import { useMemo } from 'react' -import { useSandboxInspectContext } from '../context' import { useStore } from 'zustand' +import { useSandboxInspectContext } from '../context' import { useFilesystemNode, useSelectedPath } from './use-node' -import { FileType } from 'e2b' /** * Hook for accessing file state (loading, error) diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-watcher.ts b/src/features/dashboard/sandbox/inspect/hooks/use-watcher.ts new file mode 100644 index 000000000..d9915a557 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/hooks/use-watcher.ts @@ -0,0 +1,14 @@ +import { useSandboxInspectContext } from '../context' +import { useStore } from 'zustand' + +export function useLastUpdated() { + const { store } = useSandboxInspectContext() + + return useStore(store, (state) => state.lastUpdated) +} + +export function useWatcherError() { + const { store } = useSandboxInspectContext() + + return useStore(store, (state) => state.watcherError) +} \ No newline at end of file diff --git a/src/features/dashboard/sandbox/inspect/incompatible.tsx b/src/features/dashboard/sandbox/inspect/incompatible.tsx new file mode 100644 index 000000000..e4cfd8999 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/incompatible.tsx @@ -0,0 +1,138 @@ +'use client' + +import { AlertTriangle, ArrowUpRight, ChevronLeft } from 'lucide-react' +import { motion } from 'motion/react' +import { Button } from '@/ui/primitives/button' +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/ui/primitives/card' +import { HELP_URLS, PROTECTED_URLS } from '@/configs/urls' +import Link from 'next/link' +import { CodeBlock } from '@/ui/code-block' +import { Badge } from '@/ui/primitives/badge' +import { AsciiBackgroundPattern } from '@/ui/patterns' + +interface SandboxInspectIncompatibleProps { + templateNameOrId?: string + teamIdOrSlug: string +} + +export default function SandboxInspectIncompatible({ + templateNameOrId, + teamIdOrSlug, +}: SandboxInspectIncompatibleProps) { + const codeClassNames = 'mx-0.5 h-5.5 rounded-none align-middle' + + return ( +
+
+ + +
+ + + +
+ + Incompatible template +
+ + This sandbox used a template that is incompatible with the + filesystem inspector. To use the inspector in any new sandbox you + launch,{' '} + rebuild the template. + +
+ + {templateNameOrId && ( +
    +
  1. +

    + Navigate to your template's folder +

    + + {`cd path/to/your/template`} + +
    + The folder should contain an{' '} + + e2b.toml + {' '} + file. +
    +
  2. + +
  3. +

    Rebuild the template

    +
    + Use{' '} + + e2b template build + {' '} + along with custom{' '} + + start commands + {' '} + and any other arguments to rebuild. For example: + + -c "start.sh" + +
    +
  4. + +
  5. +

    + New sandboxes have filesystem inspector +

    +
    + Any new sandbox you launch will have filesystem inspector + enabled.{' '} + This won't affect already started sandboxes. +
    +
  6. +
+ )} +
+ + + + +
+
+
+ ) +} diff --git a/src/features/dashboard/sandbox/inspect/node-label.tsx b/src/features/dashboard/sandbox/inspect/node-label.tsx new file mode 100644 index 000000000..c92062ac3 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/node-label.tsx @@ -0,0 +1,40 @@ +import { CSSProperties } from 'react' +import { cn } from '@/lib/utils' + +interface NodeLabelProps { + name: string + isActive?: boolean + isLoading?: boolean + className?: string +} + +export default function NodeLabel({ + name, + isActive = false, + isLoading = false, + className, +}: NodeLabelProps) { + return ( + + {name} + + ) +} diff --git a/src/features/dashboard/sandbox/inspect/node.tsx b/src/features/dashboard/sandbox/inspect/node.tsx new file mode 100644 index 000000000..c733eac93 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/node.tsx @@ -0,0 +1,18 @@ +import SandboxInspectDir from './dir' +import { useFilesystemNode } from './hooks/use-node' +import SandboxInspectFile from './file' + +interface SandboxInspectDirProps { + path: string +} + +export default function SandboxInspectNode({ path }: SandboxInspectDirProps) { + const node = useFilesystemNode(path)! + + switch (node.type) { + case 'dir': + return + case 'file': + return + } +} diff --git a/src/features/dashboard/sandbox/inspect/not-found.tsx b/src/features/dashboard/sandbox/inspect/not-found.tsx new file mode 100644 index 000000000..9232c0130 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/not-found.tsx @@ -0,0 +1,144 @@ +'use client' + +import { PROTECTED_URLS } from '@/configs/urls' +import { l } from '@/lib/clients/logger' +import { cn } from '@/lib/utils' +import { AsciiBackgroundPattern } from '@/ui/patterns' +import { Button } from '@/ui/primitives/button' +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from '@/ui/primitives/card' +import { ArrowLeft, ArrowUp, Home, RefreshCw } from 'lucide-react' +import { useParams, useRouter } from 'next/navigation' +import { useCallback, useEffect, useState, useTransition } from 'react' +import { serializeError } from 'serialize-error' +import { useSandboxContext } from '../context' + +export default function SandboxInspectNotFound() { + const router = useRouter() + const { isRunning } = useSandboxContext() + + const { teamIdOrSlug } = useParams() + + const [pendingPath, setPendingPath] = useState(undefined) + const [isPending, startTransition] = useTransition() + const [isResetPending, resetTransition] = useTransition() + + const save = async (newPath: string) => { + try { + await fetch('/api/sandbox/inspect/root-path', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path: newPath }), + }) + } catch (error) { + l.error({ + key: 'sandbox_inspect_not_found:save_root_path_failed', + message: + error instanceof Error ? error.message : 'Failed to save root path', + error: serializeError(error), + }) + } + } + + const setRootPath = useCallback( + (newPath: string) => { + setPendingPath(newPath) + startTransition(async () => { + await save(newPath) + router.refresh() + }) + }, + [router, startTransition] + ) + + useEffect(() => { + if (!isPending) { + setPendingPath(undefined) + } + }, [isPending]) + + return ( + <> +
+ + +
+ +
+ + + + {isRunning ? 'Empty Directory' : 'Not Connected'} + + + +

+ {isRunning + ? 'This directory appears to be empty or does not exist. You can reset to the default state, navigate to root, or refresh to try again.' + : 'It seems like the sandbox is not connected anymore. We cannot access the filesystem at this time.'} +

+
+ + {isRunning ? ( + <> +
+ + +
+ + + ) : ( + + )} +
+
+
+ + ) +} diff --git a/src/features/dashboard/sandbox/inspect/root-path-input.tsx b/src/features/dashboard/sandbox/inspect/root-path-input.tsx new file mode 100644 index 000000000..c3b849f14 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/root-path-input.tsx @@ -0,0 +1,83 @@ +'use client' + +import { l } from '@/lib/clients/logger' +import { cn } from '@/lib/utils' +import { Loader } from '@/ui/loader' +import { Button } from '@/ui/primitives/button' +import { Input } from '@/ui/primitives/input' +import { ArrowRight } from 'lucide-react' +import { useRouter } from 'next/navigation' +import { useEffect, useState, useTransition } from 'react' +import { serializeError } from 'serialize-error' + +interface RootPathInputProps { + className?: string + initialValue: string +} + +export default function RootPathInput({ + className, + initialValue, +}: RootPathInputProps) { + const [value, setValue] = useState(initialValue) + const [isPending, startTransition] = useTransition() + const router = useRouter() + + const save = async (newPath: string) => { + try { + await fetch('/api/sandbox/inspect/root-path', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path: newPath }), + }) + } catch (error) { + l.error({ + key: 'sandbox_inspect_root_path_input:save_root_path_failed', + message: + error instanceof Error ? error.message : 'Failed to save root path', + error: serializeError(error), + }) + } + } + + const handleSubmit = (newPath: string) => { + if (!newPath) return + startTransition(async () => { + await save(newPath) + router.refresh() + }) + } + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + const isDirty = value !== initialValue + + return ( +
{ + e.preventDefault() + handleSubmit(value) + }} + className={cn('relative flex h-full items-center gap-2', className)} + > + setValue(e.target.value)} + disabled={isPending} + className="border-none pl-0 focus:!border-none" + placeholder="/home/user" + /> + + +
+ ) +} diff --git a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts index f26697289..1aa5364a0 100644 --- a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts +++ b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts @@ -1,19 +1,23 @@ import { - FileType, - type Sandbox, - type FilesystemEvent, - type WatchHandle, + determineFileContentState, + getParentPath, + joinPath, + normalizePath, +} from '@/lib/utils/filesystem' +import { type EntryInfo, + type FilesystemEvent, FilesystemEventType, + type Sandbox, + type WatchHandle, } from 'e2b' import type { FilesystemStore } from './filesystem/store' import { FilesystemNode } from './filesystem/types' -import { normalizePath, joinPath, getParentPath } from '@/lib/utils/filesystem' -import { determineFileContentState } from '@/lib/utils/filesystem' export const HANDLED_ERRORS = { 'signal timed out': 'The operation timed out. Please try again later.', - 'user aborted a request': 'The request was cancelled. Try downloading the file.', + 'user aborted a request': + 'The request was cancelled. Try downloading the file.', } as const export class SandboxManager { @@ -26,7 +30,6 @@ export class SandboxManager { private static readonly LOAD_DEBOUNCE_MS = 250 private static readonly READ_DEBOUNCE_MS = 250 - private loadTimers: Map> = new Map() private pendingLoads: Map< string, @@ -47,7 +50,12 @@ export class SandboxManager { } > = new Map() - constructor(store: FilesystemStore, sandbox: Sandbox, rootPath: string, isSandboxSecure: boolean) { + constructor( + store: FilesystemStore, + sandbox: Sandbox, + rootPath: string, + isSandboxSecure: boolean + ) { this.store = store this.sandbox = sandbox this.rootPath = normalizePath(rootPath) @@ -64,9 +72,25 @@ export class SandboxManager { this.watchHandle = await this.sandbox.files.watchDir( this.rootPath, (event) => this.handleFilesystemEvent(event), - { recursive: true, timeoutMs: 0 } + { + recursive: true, + user: 'root', + timeoutMs: 0, + requestTimeoutMs: 0, + onExit: (error) => { + console.warn(`Watcher exited on ${this.rootPath}:`, error) + }, + } ) } catch (error) { + this.store + .getState() + .setWatcherError( + 'Failed to establish live filesystem updates: ' + + (error instanceof Error + ? error.message + : 'Please try again later. If the problem persists, contact support.') + ) console.error(`Failed to start root watcher on ${this.rootPath}:`, error) throw error } @@ -124,7 +148,7 @@ export class SandboxManager { state.removeNode(removedPath) - if (node?.type === FileType.FILE) { + if (node?.type === 'file') { state.resetFileContent(removedPath) } } @@ -134,7 +158,7 @@ export class SandboxManager { const node = this.store.getState().getNode(normalizedPath) - if (node?.type === FileType.FILE) { + if (node?.type === 'file') { return } @@ -181,25 +205,24 @@ export class SandboxManager { const state = this.store.getState() const node = state.getNode(normalizedPath) - if ( - !node || - node.type !== FileType.DIR || - state.loadingPaths.has(normalizedPath) - ) + if (!node || node.type !== 'dir' || state.loadingPaths.has(normalizedPath)) return state.setLoading(normalizedPath, true) state.setError(normalizedPath) // clear any previous errors try { - const entries = await this.sandbox.files.list(normalizedPath) + const entries = await this.sandbox.files.list(normalizedPath, { + user: 'root', + requestTimeoutMs: 20_000, + }) const nodes: FilesystemNode[] = entries.map((entry: EntryInfo) => { - if (entry.type === FileType.DIR) { + if (entry.type === 'dir') { return { name: entry.name, path: entry.path, - type: FileType.DIR, + type: 'dir', isExpanded: false, isSelected: false, children: [], @@ -208,7 +231,7 @@ export class SandboxManager { return { name: entry.name, path: entry.path, - type: FileType.FILE, + type: 'file', isSelected: false, } } @@ -241,7 +264,7 @@ export class SandboxManager { const state = this.store.getState() const node = state.getNode(normalizedPath) - if (!node || node.type !== FileType.DIR) return + if (!node || node.type !== 'dir') return await this.loadDirectory(normalizedPath) } @@ -251,7 +274,7 @@ export class SandboxManager { const state = this.store.getState() const node = state.getNode(normalizedPath) - if (!node || node.type !== FileType.FILE) return + if (!node || node.type !== 'file') return let pending = this.pendingReads.get(normalizedPath) if (!pending) { @@ -294,7 +317,7 @@ export class SandboxManager { const state = this.store.getState() const node = state.getNode(normalizedPath) - if (!node || node.type !== FileType.FILE) return + if (!node || node.type !== 'file') return try { state.setLoading(normalizedPath, true) @@ -302,12 +325,13 @@ export class SandboxManager { const blob = await this.sandbox.files.read(normalizedPath, { format: 'blob', requestTimeoutMs: 30_000, + user: 'root', }) const contentState = await determineFileContentState(blob) state.setFileContent(normalizedPath, contentState) - } catch (err) { + } catch (err) { const errorMessage = SandboxManager.pipeError(err, 'Failed to read file') console.error(`Failed to read file ${normalizedPath}:`, err) @@ -325,7 +349,7 @@ export class SandboxManager { const state = this.store.getState() const node = state.getNode(normalizedPath) - if (!node || node.type !== FileType.FILE) { + if (!node || node.type !== 'file') { console.error( `Failed to get download URL for file. Invalid node: ${node} ${normalizedPath}` ) diff --git a/src/features/dashboard/sandbox/inspect/stopped-banner.tsx b/src/features/dashboard/sandbox/inspect/stopped-banner.tsx new file mode 100644 index 000000000..14de88153 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/stopped-banner.tsx @@ -0,0 +1,66 @@ +'use client' + +import { cn } from '@/lib/utils' +import { + CardDescription, + CardHeader, + CardTitle, + cardVariants, +} from '@/ui/primitives/card' +import { AnimatePresence, motion } from 'framer-motion' +import { AlertTriangle } from 'lucide-react' +import { useMemo } from 'react' +import { useSandboxContext } from '../context' +import { useLastUpdated, useWatcherError } from './hooks/use-watcher' + +interface StoppedBannerProps { + rootNodeCount: number +} + +export function StoppedBanner({ rootNodeCount }: StoppedBannerProps) { + const { isRunning } = useSandboxContext() + const lastUpdated = useLastUpdated() + const watcherError = useWatcherError() + + const show = useMemo( + () => (!!watcherError || !isRunning) && rootNodeCount > 0, + [isRunning, rootNodeCount, watcherError] + ) + + const showWatcherError = watcherError && isRunning && rootNodeCount > 0 + + return ( + + {show && ( + + + + + {showWatcherError + ? 'Live filesystem updates disabled' + : 'Sandbox Stopped'} + + + {showWatcherError + ? watcherError + : 'Filesystem data is stale and is kept locally on your device.'} + + {' '} + Last updated: {lastUpdated?.toLocaleTimeString()} + + + + + )} + + ) +} diff --git a/src/features/dashboard/sandbox/inspect/viewer-header.tsx b/src/features/dashboard/sandbox/inspect/viewer-header.tsx new file mode 100644 index 000000000..34f2ec51f --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/viewer-header.tsx @@ -0,0 +1,67 @@ +import { Download, FileIcon, RefreshCcw, X } from 'lucide-react' +import { Button } from '@/ui/primitives/button' +import { motion } from 'motion/react' +import CopyButton from '@/ui/copy-button' +import { FileContentState } from './filesystem/store' + +interface SandboxInspectViewerHeaderProps { + name: string + fileContentState?: FileContentState + isLoading: boolean + onRefresh: () => void + onClose: () => void + onDownload: () => void +} + +export default function SandboxInspectViewerHeader({ + name, + fileContentState, + isLoading, + onRefresh, + onClose, + onDownload, +}: SandboxInspectViewerHeaderProps) { + return ( +
+ + {name} + + {fileContentState?.type === 'text' && ( + + )} + + + + + + +
+ ) +} diff --git a/src/features/dashboard/sandbox/inspect/viewer.tsx b/src/features/dashboard/sandbox/inspect/viewer.tsx new file mode 100644 index 000000000..cda48a679 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/viewer.tsx @@ -0,0 +1,192 @@ +'use client' + +import { useContent } from './hooks/use-content' +import { useShikiTheme } from '@/configs/shiki' +import ShikiHighlighter, { Language } from 'react-shiki' +import { useErrorPaths, useSelectedPath } from './hooks/use-node' +import SandboxInspectFrame from './frame' +import SandboxInspectViewerHeader from './viewer-header' +import { ScrollArea, ScrollBar } from '@/ui/primitives/scroll-area' +import { useFile } from './hooks/use-file' +import { Drawer, DrawerContent } from '@/ui/primitives/drawer' +import { useIsMobile } from '@/lib/hooks/use-mobile' +import { useEffect, useState } from 'react' +import { Button } from '@/ui/primitives/button' +import { Download } from 'lucide-react' +import { AnimatePresence } from 'framer-motion' +import { cn } from '@/lib/utils' + +export default function SandboxInspectViewer() { + const path = useSelectedPath() + const isMobile = useIsMobile() + const errorPaths = useErrorPaths() + + const [open, setOpen] = useState(false) + + useEffect(() => { + if (path && !errorPaths.has(path)) { + setOpen(true) + } + }, [path, errorPaths]) + + if (isMobile) { + return ( + + + + {path && } + + + + ) + } + + return ( + + {path && } + + ) +} + +function SandboxInspectViewerContent({ path }: { path: string }) { + const { name, isLoading, refresh, toggle, download } = useFile(path) + const { state } = useContent(path) + const shikiTheme = useShikiTheme() + + if (state === undefined || !name) { + return null + } + + return ( + + } + > + {state.type === 'text' ? ( + + ) : state.type === 'image' ? ( + + ) : ( + + )} + + ) +} + +// ----------------- Content components ----------------- + +interface TextContentProps { + name: string + content: string + shikiTheme: Parameters[0]['theme'] + onDownload: () => void +} + +function TextContent({ + name, + content, + shikiTheme, + onDownload, +}: TextContentProps) { + const hasDot = name.includes('.') + let language: Language = name.split('.').pop() as Language + + if (!hasDot || (name.startsWith('.') && language)) { + language = 'text' + } + + if (content.length === 0) { + return ( +
+ This file is empty. + +
+ ) + } + + return ( +
+ + + {content} + + + +
+ ) +} + +interface ImageContentProps { + name: string + dataUri: string +} + +function ImageContent({ name, dataUri }: ImageContentProps) { + return ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {name} +
+ ) +} + +interface UnreadableContent { + onDownload: () => void +} + +function UnreadableContent({ onDownload }: UnreadableContent) { + return ( +
+ This file is not readable. + +
+ ) +} diff --git a/src/features/dashboard/sandbox/layout.tsx b/src/features/dashboard/sandbox/layout.tsx new file mode 100644 index 000000000..1018af481 --- /dev/null +++ b/src/features/dashboard/sandbox/layout.tsx @@ -0,0 +1,65 @@ +'use client' + +import { useSandboxContext } from './context' +import { SidebarTrigger } from '@/ui/primitives/sidebar' +import { Suspense } from 'react' +import { ThemeSwitcher } from '@/ui/theme-switcher' +import { DashboardSurveyPopover } from '../navbar/dashboard-survey-popover' +import SandboxDetailsTabs from './tabs' +import { isVersionCompatible } from '@/lib/utils/version' +import { SANDBOX_INSPECT_MINIMUM_ENVD_VERSION } from '@/configs/versioning' +import { notFound } from 'next/navigation' + +interface SandboxLayoutProps { + children: React.ReactNode + header: React.ReactNode + teamIdOrSlug: string +} + +export default function SandboxLayout({ + teamIdOrSlug, + children, + header, +}: SandboxLayoutProps) { + const { sandboxInfo } = useSandboxContext() + + const isEnvdVersionIncompatibleForInspect = Boolean( + sandboxInfo?.envdVersion && + isVersionCompatible( + sandboxInfo.envdVersion, + SANDBOX_INSPECT_MINIMUM_ENVD_VERSION + ) + ) + + if (!sandboxInfo) { + throw notFound() + } + + return ( +
+
+
+ + +

Sandbox

+ + + + + {process.env.NEXT_PUBLIC_POSTHOG_KEY && } +
+
+ {header} + + {children} + +
+ ) +} diff --git a/src/features/dashboard/sandbox/tabs.tsx b/src/features/dashboard/sandbox/tabs.tsx new file mode 100644 index 000000000..eb635d6ce --- /dev/null +++ b/src/features/dashboard/sandbox/tabs.tsx @@ -0,0 +1,57 @@ +'use client' + +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/ui/primitives/tabs' +import { usePathname } from 'next/navigation' +import { ReactNode } from 'react' +import SandboxInspectIncompatible from './inspect/incompatible' +import { cn } from '@/lib/utils' + +interface SandboxDetailsTabsProps { + tabs: string[] + children: ReactNode + isEnvdVersionIncompatibleForInspect: boolean + templateNameOrId: string + teamIdOrSlug: string +} + +export default function SandboxDetailsTabs({ + tabs, + children, + isEnvdVersionIncompatibleForInspect, + templateNameOrId, + teamIdOrSlug, +}: SandboxDetailsTabsProps) { + const pathname = usePathname() + const tab = pathname.split('/').pop() || tabs[0] + + const showInspectTab = + tab === 'inspect' && isEnvdVersionIncompatibleForInspect + + return ( + + + {tabs.map((tab) => ( + + {tab.charAt(0).toUpperCase() + tab.slice(1)} + + ))} + + {tabs.map((tab) => ( + + {showInspectTab ? ( + children + ) : ( + + )} + + ))} + + ) +} diff --git a/src/features/dashboard/sandboxes/header.tsx b/src/features/dashboard/sandboxes/header.tsx index 416ac5e1a..65300beaf 100644 --- a/src/features/dashboard/sandboxes/header.tsx +++ b/src/features/dashboard/sandboxes/header.tsx @@ -4,7 +4,10 @@ import { Badge } from '@/ui/primitives/badge' import { Circle, ListFilter } from 'lucide-react' import { useRouter } from 'next/navigation' import { useTransition } from 'react' -import { useSandboxTableStore } from './stores/table-store' +import { + sandboxesPollingIntervals, + useSandboxTableStore, +} from './stores/table-store' import { SandboxesTable } from './table-config' import SandboxesTableFilters from './table-filters' import { SearchInput } from './table-search' @@ -47,6 +50,7 @@ export function SandboxesHeader({
void // Page actions - setPollingInterval: (interval: PollingInterval) => void + setPollingInterval: (interval: SandboxesPollingInterval) => void } type Store = SandboxTableState & SandboxTableActions const initialState: SandboxTableState = { // Page state - pollingInterval: 60, // 1 minute + pollingInterval: sandboxesPollingIntervals[3]!.value, // Table state sorting: [], diff --git a/src/features/dashboard/sandboxes/table-cells.tsx b/src/features/dashboard/sandboxes/table-cells.tsx index b08894f68..05f5bfde6 100644 --- a/src/features/dashboard/sandboxes/table-cells.tsx +++ b/src/features/dashboard/sandboxes/table-cells.tsx @@ -2,7 +2,6 @@ import { PROTECTED_URLS } from '@/configs/urls' import { useServerContext } from '@/features/dashboard/server-context' -import { cn } from '@/lib/utils' import { Template } from '@/types/api' import { JsonPopover } from '@/ui/json-popover' import { Button } from '@/ui/primitives/button' @@ -10,6 +9,7 @@ import { CellContext } from '@tanstack/react-table' import { ArrowUpRight } from 'lucide-react' import { useRouter } from 'next/navigation' import { useMemo } from 'react' +import ResourceUsage from '../common/resource-usage' import { useTemplateTableStore } from '../templates/stores/table-store' import { useSandboxMetricsStore } from './stores/metrics-store' import { SandboxWithMetrics } from './table-config' @@ -27,41 +27,12 @@ export function CpuUsageCell({ (s) => s.metrics?.[row.original.sandboxID] ) - const percentage = metrics?.cpuUsedPct ?? 0 - const cpuCount = row.original.cpuCount - - const hasMetrics = metrics !== null && metrics !== undefined - - const textClassName = useMemo( - () => - cn( - percentage >= 90 - ? 'text-error' - : percentage >= 70 - ? 'text-warning' - : 'text-fg' - ), - [percentage] - ) - return ( - - {hasMetrics ? ( - <> - {percentage}% - · - - ) : ( - <> - n/a - · - - )} - {cpuCount ?? '-'} Core - {cpuCount && cpuCount > 1 ? 's' : ''} - + ) } @@ -72,55 +43,12 @@ export function RamUsageCell({ (s) => s.metrics?.[row.original.sandboxID] ) - const percentage = useMemo(() => { - if (metrics?.memUsedMb && metrics.memTotalMb) { - return Number(((metrics.memUsedMb / metrics.memTotalMb) * 100).toFixed(2)) - } - return 0 - }, [metrics]) - - const hasMetrics = metrics !== null && metrics !== undefined - - const totalRamMB = useMemo( - () => row.original.memoryMB.toLocaleString(), - [row.original.memoryMB] - ) - - const usedRamMB = useMemo( - () => (hasMetrics ? metrics.memUsedMb.toLocaleString() : 'n/a'), - [hasMetrics, metrics] - ) - - const textClassName = useMemo( - () => - cn( - percentage >= 95 - ? 'text-error' - : percentage >= 70 - ? 'text-warning' - : 'text-fg' - ), - [percentage] - ) - return ( - - {hasMetrics ? ( - <> - {percentage}% - · - {usedRamMB} / - - ) : ( - <> - n/a - · - - )} - {totalRamMB} MB - + ) } @@ -170,6 +98,10 @@ export function MetadataCell({ const value = getValue() as string const json = useMemo(() => JSON.parse(value), [value]) + if (value.trim() === '{}') { + return n/a + } + return ( - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ) }) diff --git a/src/features/dashboard/sidebar/content.tsx b/src/features/dashboard/sidebar/content.tsx index 8aa31f419..031cdc849 100644 --- a/src/features/dashboard/sidebar/content.tsx +++ b/src/features/dashboard/sidebar/content.tsx @@ -45,7 +45,23 @@ export default function DashboardSidebarContent() { ) const isActive = (href: string) => { - return href === pathname + if (!pathname) return false + + if (pathname === href) return true + + // split into segments for prefix comparison + const hrefSegments = href.split('/').filter(Boolean) + const pathSegments = pathname.split('/').filter(Boolean) + + if (pathSegments.length < hrefSegments.length) return false + + for (let i = 0; i < hrefSegments.length; i++) { + if (hrefSegments[i] !== pathSegments[i]) { + return false + } + } + + return true } return ( @@ -68,7 +84,7 @@ export default function DashboardSidebarContent() { asChild tooltip={item.label} > - + = version2 + } +} + +/** + * Check if a version meets the minimum required version + * @param currentVersion - The current version to check + * @param minimumVersion - The minimum required version + * @returns true if currentVersion meets or exceeds minimumVersion + */ +export function isVersionCompatible( + currentVersion: string, + minimumVersion: string +): boolean { + return isVersionGreaterOrEqual(currentVersion, minimumVersion) +} diff --git a/src/server/auth/auth-actions.ts b/src/server/auth/auth-actions.ts index bdbe6517f..ad67636b4 100644 --- a/src/server/auth/auth-actions.ts +++ b/src/server/auth/auth-actions.ts @@ -112,8 +112,8 @@ export const signUpAction = actionClient emailRedirectTo: `${origin}${AUTH_URLS.CALLBACK}${returnTo ? `?returnTo=${encodeURIComponent(returnTo)}` : ''}`, data: validationResult?.data ? { - email_validation: validationResult?.data, - } + email_validation: validationResult?.data, + } : undefined, }, }) @@ -210,6 +210,6 @@ export async function signOutAction(returnTo?: string) { throw redirect( AUTH_URLS.SIGN_IN + - (returnTo ? `?returnTo=${encodeURIComponent(returnTo)}` : '') + (returnTo ? `?returnTo=${encodeURIComponent(returnTo)}` : '') ) } diff --git a/src/server/sandboxes/get-sandbox-details.ts b/src/server/sandboxes/get-sandbox-details.ts index b2c4a264b..62e0827db 100644 --- a/src/server/sandboxes/get-sandbox-details.ts +++ b/src/server/sandboxes/get-sandbox-details.ts @@ -7,7 +7,7 @@ import { z } from 'zod' export const GetSandboxDetailsSchema = z.object({ teamId: z.string().uuid(), - sandboxId: z.string().uuid(), + sandboxId: z.string(), }) export const getSandboxDetails = authActionClient @@ -26,6 +26,7 @@ export const getSandboxDetails = authActionClient headers: { ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), }, + cache: 'no-store', }) if (res.error) { @@ -39,9 +40,7 @@ export const getSandboxDetails = authActionClient }) if (status === 404) { - return returnServerError( - 'Sandbox not found. Please check the sandbox ID and try again.' - ) + return returnServerError('SANDBOX_NOT_FOUND') } return handleDefaultInfraError(status) diff --git a/src/server/sandboxes/get-sandbox-root.ts b/src/server/sandboxes/get-sandbox-root.ts index deaf52ede..18331006e 100644 --- a/src/server/sandboxes/get-sandbox-root.ts +++ b/src/server/sandboxes/get-sandbox-root.ts @@ -1,9 +1,9 @@ -import { z } from 'zod' -import { authActionClient } from '@/lib/clients/action' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { returnServerError } from '@/lib/utils/action' -import Sandbox from 'e2b' +import { authActionClient } from '@/lib/clients/action' import { l } from '@/lib/clients/logger' +import { returnServerError } from '@/lib/utils/action' +import Sandbox, { NotFoundError } from 'e2b' +import { z } from 'zod' export const GetSandboxRootSchema = z.object({ teamId: z.string().uuid(), @@ -20,17 +20,28 @@ export const getSandboxRoot = authActionClient const headers = SUPABASE_AUTH_HEADERS(session.access_token, teamId) + let sandbox: Sandbox | null = null + try { - const sandbox = await Sandbox.connect(sandboxId, { + sandbox = await Sandbox.connect(sandboxId, { domain: process.env.NEXT_PUBLIC_E2B_DOMAIN, headers, }) return { - entries: await sandbox.files.list(rootPath), + entries: await sandbox.files.list(rootPath, { + user: 'root', + requestTimeoutMs: 20_000, + }), } } catch (err) { + if (err instanceof NotFoundError && sandbox) { + l.warn('get_sandbox_root:not_found', err) + return returnServerError('ROOT_PATH_NOT_FOUND') + } + l.error('get_sandbox_root:unexpected_error', err) + return returnServerError('Failed to list root directory.') } }) diff --git a/src/server/sandboxes/get-team-sandboxes.ts b/src/server/sandboxes/get-team-sandboxes.ts index 30bc15267..1cde78391 100644 --- a/src/server/sandboxes/get-team-sandboxes.ts +++ b/src/server/sandboxes/get-team-sandboxes.ts @@ -61,8 +61,6 @@ export const getTeamSandboxes = authActionClient return handleDefaultInfraError(status) } - console.log('sandboxesRes.data', sandboxesRes.data) - return { sandboxes: sandboxesRes.data, } diff --git a/src/server/sandboxes/sandbox-actions.ts b/src/server/sandboxes/sandbox-actions.ts new file mode 100644 index 000000000..ed99df699 --- /dev/null +++ b/src/server/sandboxes/sandbox-actions.ts @@ -0,0 +1,10 @@ +'use server' + +import { revalidatePath } from 'next/cache' + +export async function revalidateSandboxDetailsLayout( + teamIdOrSlug: string, + sandboxId: string +) { + revalidatePath(`/dashboard/${teamIdOrSlug}/sandboxes/${sandboxId}`, 'layout') +} diff --git a/src/server/team/team-actions.ts b/src/server/team/team-actions.ts index 10492f178..cea196b40 100644 --- a/src/server/team/team-actions.ts +++ b/src/server/team/team-actions.ts @@ -1,21 +1,21 @@ 'use server' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { KV_KEYS } from '@/configs/keys' -import { authActionClient } from '@/lib/clients/action' -import { l } from '@/lib/clients/logger' -import { deleteFile, getFiles, uploadFile } from '@/lib/clients/storage' import { supabaseAdmin } from '@/lib/clients/supabase/admin' -import { returnServerError } from '@/lib/utils/action' import { checkUserTeamAuthorization } from '@/lib/utils/server' -import { CreateTeamSchema, UpdateTeamNameSchema } from '@/server/team/types' -import { CreateTeamsResponse } from '@/types/billing' +import { z } from 'zod' import { kv } from '@vercel/kv' -import { returnValidationErrors } from 'next-safe-action' +import { KV_KEYS } from '@/configs/keys' import { revalidatePath } from 'next/cache' -import { z } from 'zod' +import { uploadFile, deleteFile, getFiles } from '@/lib/clients/storage' +import { authActionClient } from '@/lib/clients/action' +import { returnServerError } from '@/lib/utils/action' import { zfd } from 'zod-form-data' import { getTeam } from './get-team' +import { CreateTeamSchema, UpdateTeamNameSchema } from '@/server/team/types' +import { CreateTeamsResponse } from '@/types/billing' +import { returnValidationErrors } from 'next-safe-action' +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { l } from '@/lib/clients/logger' export const updateTeamNameAction = authActionClient .schema(UpdateTeamNameSchema) @@ -301,7 +301,7 @@ export const uploadTeamProfilePictureAction = authActionClient await deleteFile(filePath) } } catch (cleanupError) { - l.warn('Error during profile picture cleanup:', cleanupError) + l.warn('upload_team_profile_picture:cleanup_error', cleanupError) } })() diff --git a/src/styles/theme.css b/src/styles/theme.css index 19421307b..879a4bea3 100644 --- a/src/styles/theme.css +++ b/src/styles/theme.css @@ -121,6 +121,7 @@ /* Fonts */ --font-sans: 'IBM Plex Sans', sans-serif; --font-mono: 'IBM Plex Mono', monospace; + --font-fira-code: 'Fira Code', monospace; /* Animations */ @keyframes accordion-down { @@ -177,6 +178,15 @@ } } + @keyframes shiny-text { + 0% { + background-position: calc(-100% - var(--shiny-width)) 0; + } + 100% { + background-position: calc(100% + var(--shiny-width)) 0; + } + } + --animate-accordion-down: accordion-down 0.2s ease-out; --animate-accordion-up: accordion-up 0.2s ease-out; --animate-shimmer: shimmer 1s ease-in-out infinite; @@ -184,6 +194,7 @@ --animate-wave: wave 2s linear linear infinite; --animate-fade-slide-in: fade-slide-in-from-bottom 0.1s cubic-bezier(0.16, 1, 0.3, 1); + --animate-shiny-text: shiny-text 0.5s linear infinite; } @utility container { diff --git a/src/types/api.d.ts b/src/types/api.d.ts index a2cbb8fd5..870b57bcb 100644 --- a/src/types/api.d.ts +++ b/src/types/api.d.ts @@ -29,6 +29,8 @@ type CreatedTeamAPIKey = InfraComponents['schemas']['CreatedTeamAPIKey'] type TeamAPIKey = InfraComponents['schemas']['TeamAPIKey'] +type SandboxInfo = InfraComponents['schemas']['SandboxDetail'] + export type { CreatedAccessToken, CreatedTeamAPIKey, @@ -42,4 +44,5 @@ export type { TeamAPIKey, TeamUser, Template, + IdentifierMaskingDetails, } diff --git a/src/types/dashboard.types.ts b/src/types/dashboard.types.ts index 28d245c07..b3e952725 100644 --- a/src/types/dashboard.types.ts +++ b/src/types/dashboard.types.ts @@ -6,5 +6,3 @@ export type ClientTeam = Database['public']['Tables']['teams']['Row'] & { // e.g. "max.mustermann@gmail.com" -> "Max.mustermann's Team" transformed_default_name?: string } - -export type PollingInterval = 0 | 15 | 30 | 60 // seconds diff --git a/src/ui/code-block.tsx b/src/ui/code-block.tsx new file mode 100644 index 000000000..d19837f1c --- /dev/null +++ b/src/ui/code-block.tsx @@ -0,0 +1,165 @@ +'use client' +import { Check, Copy } from 'lucide-react' +import { + type ButtonHTMLAttributes, + type HTMLAttributes, + type ReactNode, + forwardRef, + useCallback, + useRef, +} from 'react' + +import type { ScrollAreaViewportProps } from '@radix-ui/react-scroll-area' +import { cn } from '@/lib/utils' +import { ScrollArea, ScrollBar, ScrollViewport } from './primitives/scroll-area' +import { buttonVariants } from './primitives/button' +import { useClipboard } from '@/lib/hooks/use-clipboard' +import { useShikiTheme } from '@/configs/shiki' +import ShikiHighlighter from 'react-shiki' +import CopyButton from './copy-button' + +export type CodeBlockProps = HTMLAttributes & { + /** + * Title of the code block + */ + title?: string + + /** + * Language for syntax highlighting + */ + lang?: string + + /** + * Icon of code block + * + * When passed as a string, it assumes the value is the HTML of icon + */ + icon?: ReactNode + + /** + * Allow to copy code with copy button + * + * @defaultValue true + */ + allowCopy?: boolean + + /** + * Keep original background color generated by Shiki or Rehype Code + * + * @defaultValue false + */ + keepBackground?: boolean + + viewportProps?: ScrollAreaViewportProps +} + +export const Pre = forwardRef>( + ({ className, ...props }, ref) => { + return ( +
+        {props.children}
+      
+ ) + } +) + +Pre.displayName = 'Pre' + +export const CodeBlock = forwardRef( + ( + { + title, + lang = 'bash', + allowCopy = true, + keepBackground = false, + icon, + viewportProps, + children, + ...props + }, + ref + ) => { + const [isCopied, copy] = useClipboard() + const areaRef = useRef(null) + const shikiTheme = useShikiTheme() + + const onCopy = useCallback(() => { + const textContent = typeof children === 'string' ? children : '' + copy(textContent) + }, [copy, children]) + + return ( +
+ {title ? ( +
+ {icon ? ( +
+ {typeof icon !== 'string' ? icon : null} +
+ ) : null} +
+ {title} +
+ {allowCopy ? ( + + ) : null} +
+ ) : ( + allowCopy && ( + + ) + )} + + + + {typeof children === 'string' ? children : ''} + + + + + +
+ ) + } +) + +CodeBlock.displayName = 'CodeBlock' diff --git a/src/ui/copy-button.tsx b/src/ui/copy-button.tsx index fc178fa99..4175ed1a6 100644 --- a/src/ui/copy-button.tsx +++ b/src/ui/copy-button.tsx @@ -1,3 +1,5 @@ +'use client' + import { useClipboard } from '@/lib/hooks/use-clipboard' import { Button, ButtonProps } from '@/ui/primitives/button' import { CheckIcon, CopyIcon } from 'lucide-react' diff --git a/src/ui/docs-code-block.tsx b/src/ui/docs-code-block.tsx index 0f5ea1184..795df2611 100644 --- a/src/ui/docs-code-block.tsx +++ b/src/ui/docs-code-block.tsx @@ -85,72 +85,63 @@ export const CodeBlock = forwardRef( }, [copy]) return ( - -
- {title ? ( -
- {icon ? ( -
- {typeof icon !== 'string' ? icon : null} -
- ) : null} -
- {title} -
- {allowCopy ? ( - - ) : null} -
- ) : ( - allowCopy && ( + {title ? ( +
+ {icon ? ( +
+ {typeof icon !== 'string' ? icon : null} +
+ ) : null} +
+ {title} +
+ {allowCopy ? ( - ) - )} - - - {props.children} - - - - -
- + ) : null} +
+ ) : ( + allowCopy && ( + + ) + )} + + + {props.children} + + + + + ) } ) diff --git a/src/ui/error-indicator.tsx b/src/ui/error-indicator.tsx index 44a76687c..e3f73c605 100644 --- a/src/ui/error-indicator.tsx +++ b/src/ui/error-indicator.tsx @@ -19,6 +19,7 @@ interface ErrorIndicatorProps { description?: string message?: string className?: string + children?: React.ReactNode } export function ErrorIndicator({ @@ -26,6 +27,7 @@ export function ErrorIndicator({ description = 'Something went wrong!', message, className, + children, }: ErrorIndicatorProps) { const router = useRouter() const [isPending, startTransition] = useTransition() @@ -43,6 +45,11 @@ export function ErrorIndicator({

{message}

)} + {children && ( + + {children} + + )} diff --git a/src/ui/patterns.tsx b/src/ui/patterns.tsx new file mode 100644 index 000000000..a05814d82 --- /dev/null +++ b/src/ui/patterns.tsx @@ -0,0 +1,220 @@ +import { cn } from '@/lib/utils' + +interface AsciiBackgroundPatternProps { + className?: string +} + +export const AsciiBackgroundPattern = ({ + className, +}: AsciiBackgroundPatternProps) => { + return ( +

+ ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ......................................................................................................................................................................................................./ + ......................................................................................................................................................................................................// + ...................................................................................................................................................................................................../// + ....................................................................................................................................................................................................//// + ..................................................................................................................................................................................................////// + ................................................................................................................................................................................................./////// + ................................................................................................................................................................................................//////// + ...............................................................................................................................................................................................///////// + ..............................................................................................................................................................................................////////// + ............................................................................................................................................................................................//////////// + ...........................................................................................................................................................................................///////////// + ..........................................................................................................................................................................................////////////// + ........................................................................................................................................................................................./////////////// + ........................................................................................................................................................................................//////////////// + .......................................................................................................................................................................................///////////////// + ...................................................................................................................................................................................../////////////////// + ....................................................................................................................................................................................//////////////////// + ...................................................................................................................................................................................///////////////////// + ..................................................................................................................................................................................////////////////////// + ................................................................................................................................................................................./////////////////////// + ...............................................................................................................................................................................///////////////////////// + ..............................................................................................................................................................................////////////////////////// + ............................................................................................................................................................................./////////////////////////// + ............................................................................................................................................................................//////////////////////////// + ...........................................................................................................................................................................///////////////////////////// + ..........................................................................................................................................................................////////////////////////////// + ........................................................................................................................................................................//////////////////////////////// + .......................................................................................................................................................................///////////////////////////////// + ......................................................................................................................................................................////////////////////////////////// + ...................................................................................................................................................................../////////////////////////////////// + ....................................................................................................................................................................//////////////////////////////////// + ..................................................................................................................................................................////////////////////////////////////// + ................................................................................................................................................................./////////////////////////////////////// + ................................................................................................................................................................///////////////////////////////////////. + ...............................................................................................................................................................///////////////////////////////////////.. + ..............................................................................................................................................................//////////////////////////////////////.... + ............................................................................................................................................................///////////////////////////////////////..... + ...........................................................................................................................................................///////////////////////////////////////...... + ..........................................................................................................................................................///////////////////////////////////////....... + .........................................................................................................................................................///////////////////////////////////////........ + ........................................................................................................................................................///////////////////////////////////////......... + .......................................................................................................................................................//////////////////////////////////////........... + .....................................................................................................................................................///////////////////////////////////////............ + ....................................................................................................................................................///////////////////////////////////////............. + ...................................................................................................................................................///////////////////////////////////////.............. + ..................................................................................................................................................///////////////////////////////////////............... + .................................................................................................................................................//////////////////////////////////////................. + ...............................................................................................................................................///////////////////////////////////////.................. + ..............................................................................................................................................///////////////////////////////////////................... + .............................................................................................................................................///////////////////////////////////////.................... + ............................................................................................................................................///////////////////////////////////////..................... + ...........................................................................................................................................//////////////////////////////////////....................... + .........................................................................................................................................///////////////////////////////////////........................ + ........................................................................................................................................///////////////////////////////////////......................... + .......................................................................................................................................///////////////////////////////////////.......................... + ......................................................................................................................................///////////////////////////////////////........................... + .....................................................................................................................................//////////////////////////////////////............................. + ....................................................................................................................................//////////////////////////////////////.............................. + ..................................................................................................................................///////////////////////////////////////............................... + .................................................................................................................................///////////////////////////////////////................................ + ................................................................................................................................///////////////////////////////////////................................. + ...............................................................................................................................///////////////////////////////////////.................................. + ..............................................................................................................................//////////////////////////////////////.................................... + ............................................................................................................................///////////////////////////////////////..................................... + ...........................................................................................................................///////////////////////////////////////...................................... + ..........................................................................................................................///////////////////////////////////////....................................... + .........................................................................................................................///////////////////////////////////////.......................///////////////// + ........................................................................................................................///////////////////////////////////////........................///////////////// + ......................................................................................................................///////////////////////////////////////..........................///////////////// + .....................................................................................................................///////////////////////////////////////...........................///////////////// + ....................................................................................................................///////////////////////////////////////............................///////////////// + ...................................................................................................................///////////////////////////////////////.............................///////////////// + ..................................................................................................................///////////////////////////////////////..............................///////////////// + .................................................................................................................//////////////////////////////////////................................///////////////// + ...............................................................................................................///////////////////////////////////////.................................///////////////// + ..............................................................................................................///////////////////////////////////////..................................///////////////// + .............................................................................................................///////////////////////////////////////...................................///////////////// + ............................................................................................................///////////////////////////////////////....................................///////////////// + ...........................................................................................................//////////////////////////////////////......................................///////////////// + .........................................................................................................///////////////////////////////////////.......................................///////////////// + ........................................................................................................///////////////////////////////////////........................................///////////////// + .......................................................................................................///////////////////////////////////////.........................................///////////////// + ......................................................................................................///////////////////////////////////////..........................................///////////////// + .....................................................................................................//////////////////////////////////////............................................///////////////// + ...................................................................................................///////////////////////////////////////.............................................///////////////// + ..................................................................................................///////////////////////////////////////..............................................///////////////// + .................................................................................................///////////////////////////////////////...............................................///////////////// + ................................................................................................///////////////////////////////////////................................................///////////////// + ...............................................................................................///////////////////////////////////////.................................................///////////////// + ..............................................................................................//////////////////////////////////////...................................................///////////////// + ............................................................................................///////////////////////////////////////....................................................///////////////// + ...........................................................................................///////////////////////////////////////.....................................................///////////////// + ..........................................................................................///////////////////////////////////////......................................................///////////////// + .........................................................................................///////////////////////////////////////.......................................................///////////////// + ........................................................................................//////////////////////////////////////.........................................................///////////////// + ......................................................................................///////////////////////////////////////..........................................................///////////////// + .....................................................................................///////////////////////////////////////...........................................................///////////////// + ....................................................................................///////////////////////////////////////............................................................///////////////// + ...................................................................................///////////////////////////////////////.............................................................///////////////// + ..................................................................................///////////////////////////////////////..............................................................///////////////// + ................................................................................///////////////////////////////////////................................................................///////////////// + ...............................................................................///////////////////////////////////////.................................................................///////////////// + ..............................................................................///////////////////////////////////////..................................................................///////////////// + .............................................................................///////////////////////////////////////...................................................................///////////////// + ............................................................................///////////////////////////////////////....................................................................///////////////// + ...........................................................................//////////////////////////////////////......................................................................///////////////// + .........................................................................///////////////////////////////////////.......................................................................///////////////// + ........................................................................///////////////////////////////////////........................................................................///////////////// + .......................................................................///////////////////////////////////////.......................................................................................... + ......................................................................///////////////////////////////////////........................................................................................... + .....................................................................//////////////////////////////////////............................................................................................. + ...................................................................///////////////////////////////////////.............................................................................................. + ..................................................................///////////////////////////////////////............................................................................................... + .................................................................///////////////////////////////////////................................................................................................ + ................................................................///////////////////////////////////////................................................................................................. + ...............................................................//////////////////////////////////////................................................................................................... + ..............................................................//////////////////////////////////////...................................................................................///////////////// + ............................................................///////////////////////////////////////....................................................................................///////////////// + ...........................................................///////////////////////////////////////.....................................................................................///////////////// + ..........................................................///////////////////////////////////////......................................................................................///////////////// + .........................................................///////////////////////////////////////.......................................................................................///////////////// + .......................................................///////////////////////////////////////.........................................................................................///////////////// + ......................................................///////////////////////////////////////..........................................................................................///////////////// + .....................................................///////////////////////////////////////...........................................................................................///////////////// + ....................................................///////////////////////////////////////............................................................................................///////////////// + ...................................................///////////////////////////////////////.............................................................................................///////////////// + ..................................................//////////////////////////////////////...............................................................................................///////////////// + ................................................///////////////////////////////////////................................................................................................///////////////// + ...............................................///////////////////////////////////////.................................................................................................///////////////// + ..............................................///////////////////////////////////////..................................................................................................///////////////// + .............................................///////////////////////////////////////...................................................................................................///////////////// + ............................................//////////////////////////////////////.....................................................................................................///////////////// + ..........................................///////////////////////////////////////......................................................................................................///////////////// + .........................................///////////////////////////////////////........................................................................................................................ + ........................................///////////////////////////////////////......................................................................................................................... + .......................................///////////////////////////////////////.......................................................................................................................... + ......................................///////////////////////////////////////........................................................................................................................... + .....................................//////////////////////////////////////............................................................................................................................. + ...................................///////////////////////////////////////.............................................................................................................................. + ..................................///////////////////////////////////////............................................................................................................................... + .................................///////////////////////////////////////................................................................................................................................ + ................................//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ...............................///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ............................./////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ............................//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ...........................///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ..........................////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ........................./////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + .......................///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ......................////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ...................../////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ....................//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ...................///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ..................////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ................//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ...............///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ..............////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ............./////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ + ........................................................................................................................................................................................................ +

+ ) +} diff --git a/src/ui/polling-button.tsx b/src/ui/polling-button.tsx index c2c0c4d6c..20a2b7de5 100644 --- a/src/ui/polling-button.tsx +++ b/src/ui/polling-button.tsx @@ -1,4 +1,4 @@ -import { PollingInterval } from '@/types/dashboard.types' +import { cn } from '@/lib/utils' import { Button } from '@/ui/primitives/button' import { Select, @@ -8,50 +8,89 @@ import { } from '@/ui/primitives/select' import { RefreshCw } from 'lucide-react' import { useEffect, useState } from 'react' +import useSWR from 'swr' import { Separator } from './primitives/separator' -interface PollingButtonProps { +type PollingIntervals = Array<{ value: number; label: string }> + +type PollingInterval = PollingIntervals[number]['value'] + +export interface PollingButtonProps { pollingInterval: PollingInterval onIntervalChange: (interval: PollingInterval) => void isPolling?: boolean - onRefresh: () => void + onRefresh: () => Promise | void + className?: string + intervals: PollingIntervals } -const intervals = [ - { value: 0, label: 'Off' }, - { value: 15, label: '15s' }, - { value: 30, label: '30s' }, - { value: 60, label: '1m' }, -] - export function PollingButton({ pollingInterval, onIntervalChange, isPolling, onRefresh, + className, + intervals, }: PollingButtonProps) { const [remainingTime, setRemainingTime] = useState(pollingInterval) + const [isTabVisible, setIsTabVisible] = useState( + typeof document === 'undefined' ? true : !document.hidden + ) + useEffect(() => { setRemainingTime(pollingInterval) }, [pollingInterval]) useEffect(() => { - if (pollingInterval === 0) return + const handleVisibilityChange = () => { + const visible = !document.hidden + setIsTabVisible(visible) - const timer = setInterval(() => { setRemainingTime((prev) => { - if (prev <= 1) { - onRefresh() - return pollingInterval - } - const newTime = prev - 1 - return newTime as PollingInterval + if (!visible) return 0 as PollingIntervals[number]['value'] + return prev === 0 ? pollingInterval : prev }) + } + + // It is safe to access `document` here because this component only runs on the client + document.addEventListener('visibilitychange', handleVisibilityChange) + return () => + document.removeEventListener('visibilitychange', handleVisibilityChange) + }, []) + + const [lastRefreshTs, setLastRefreshTs] = useState(Date.now()) + + const { isValidating, mutate } = useSWR( + pollingInterval === 0 ? null : ['polling-button', pollingInterval], + async () => { + await onRefresh() + setLastRefreshTs(Date.now()) + return null + }, + { + refreshInterval: pollingInterval * 1000, + refreshWhenHidden: false, + revalidateOnFocus: true, + } + ) + + const effectiveIsPolling = isPolling ?? isValidating + + useEffect(() => { + if (pollingInterval === 0 || !isTabVisible) return + + const timer = setInterval(() => { + const elapsed = Math.floor((Date.now() - lastRefreshTs) / 1000) + const next = Math.max( + 0, + pollingInterval - elapsed + ) as PollingIntervals[number]['value'] + setRemainingTime(next) }, 1000) return () => clearInterval(timer) - }, [pollingInterval, onRefresh]) + }, [pollingInterval, lastRefreshTs, isTabVisible]) const formatTime = (seconds: number) => { if (seconds >= 60) { @@ -61,25 +100,31 @@ export function PollingButton({ } const handleIntervalChange = (value: string) => { - const newInterval = Number(value) as PollingInterval + const newInterval = Number(value) as PollingIntervals[number]['value'] onIntervalChange(newInterval) setRemainingTime(newInterval) // Reset timer when interval changes + setLastRefreshTs(Date.now()) + mutate() } return ( -
+
diff --git a/src/ui/primitives/badge.tsx b/src/ui/primitives/badge.tsx index 76401ba5e..02ca7754e 100644 --- a/src/ui/primitives/badge.tsx +++ b/src/ui/primitives/badge.tsx @@ -16,6 +16,12 @@ const badgeVariants = cva( accent: 'bg-accent/15 text-accent', 'contrast-1': 'bg-contrast-1/20 text-contrast-1', 'contrast-2': 'bg-contrast-2/20 text-contrast-2', + outline: 'border border-border-200 bg-bg-200', + }, + size: { + default: 'px-2 py-1 text-xs', + sm: 'px-1 py-0.5 text-xs', + lg: 'px-3 py-1.5 text-sm', }, defaultVariants: { variant: 'default', diff --git a/src/ui/primitives/button.tsx b/src/ui/primitives/button.tsx index e1d0f4827..cee910acd 100644 --- a/src/ui/primitives/button.tsx +++ b/src/ui/primitives/button.tsx @@ -6,7 +6,7 @@ import { Loader } from '../loader' const buttonVariants = cva( [ - 'inline-flex items-center cursor-pointer gap-2 rounded-sm justify-center whitespace-nowrap', + 'inline-flex items-center cursor-pointer rounded-sm justify-center whitespace-nowrap', 'font-mono uppercase tracking-wider text-sm', 'transition-colors duration-150', 'focus-visible:outline-none ', @@ -40,8 +40,13 @@ const buttonVariants = cva( 'hover:bg-error/20 focus:bg-error/20', 'active:translate-y-[1px] active:shadow-none', ].join(' '), + warning: [ + 'bg-warning/10 text-warning', + 'hover:bg-warning/20 focus:bg-warning/20', + 'active:translate-y-[1px] active:shadow-none', + ].join(' '), outline: [ - 'border border-border bg-transparent', + 'border border-border-100 bg-transparent', 'hover:bg-bg-300/80 focus:bg-bg-300/80', 'active:translate-y-[1px] active:shadow-none', ].join(' '), @@ -53,13 +58,13 @@ const buttonVariants = cva( ].join(' '), }, size: { - default: 'h-8 px-3', - sm: 'h-7 px-2', - lg: 'h-10 px-4', - icon: 'h-8 w-8', - iconSm: 'h-7 w-7', - iconLg: 'h-10 w-10 text-xl', - slate: 'h-auto px-0 py-0', + default: 'h-8 px-3 gap-2', + sm: 'h-7 px-2 gap-1', + lg: 'h-10 px-4 gap-2', + icon: 'h-8 w-8 gap-2', + iconSm: 'h-7 w-7 gap-1', + iconLg: 'h-10 w-10 text-xl gap-2', + slate: 'h-auto px-0 py-0 gap-1', }, }, defaultVariants: { diff --git a/src/ui/primitives/drawer.tsx b/src/ui/primitives/drawer.tsx index 611ff4df4..ca8bc815b 100644 --- a/src/ui/primitives/drawer.tsx +++ b/src/ui/primitives/drawer.tsx @@ -5,104 +5,134 @@ import { Drawer as DrawerPrimitive } from 'vaul' import { cn } from '@/lib/utils' -const Drawer = ({ - shouldScaleBackground = true, +function Drawer({ ...props -}: React.ComponentProps) => ( - -) -Drawer.displayName = 'Drawer' - -const DrawerTrigger = DrawerPrimitive.Trigger +}: React.ComponentProps) { + return +} -const DrawerPortal = DrawerPrimitive.Portal +function DrawerTrigger({ + ...props +}: React.ComponentProps) { + return +} -const DrawerClose = DrawerPrimitive.Close +function DrawerPortal({ + ...props +}: React.ComponentProps) { + return +} -const DrawerOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName +function DrawerClose({ + ...props +}: React.ComponentProps) { + return +} -const DrawerContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - ) { + return ( + -
- {children} - - -)) -DrawerContent.displayName = 'DrawerContent' + /> + ) +} -const DrawerHeader = ({ +function DrawerContent({ className, + children, ...props -}: React.HTMLAttributes) => ( -
-) -DrawerHeader.displayName = 'DrawerHeader' +}: React.ComponentProps) { + return ( + + + +
+ {children} + + + ) +} + +function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} -const DrawerFooter = ({ +function DrawerTitle({ className, ...props -}: React.HTMLAttributes) => ( -
-) -DrawerFooter.displayName = 'DrawerFooter' - -const DrawerTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DrawerTitle.displayName = DrawerPrimitive.Title.displayName +}: React.ComponentProps) { + return ( + + ) +} -const DrawerDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DrawerDescription.displayName = DrawerPrimitive.Description.displayName +function DrawerDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} export { Drawer, diff --git a/src/ui/primitives/select.tsx b/src/ui/primitives/select.tsx index 0323e93c4..4a72f943f 100644 --- a/src/ui/primitives/select.tsx +++ b/src/ui/primitives/select.tsx @@ -43,7 +43,7 @@ const SelectTrigger = React.forwardRef< ref={ref} className={cn( 'flex h-10 w-full items-center justify-between rounded-sm', - 'bg-bg border border-dashed px-3 py-2 text-sm', + 'bg-bg cursor-pointer border border-dashed px-3 py-2 text-sm', 'transition-colors outline-none', 'focus:bg-bg-100 active:translate-y-[1px]', 'disabled:cursor-not-allowed disabled:opacity-50', diff --git a/src/ui/primitives/sidebar.tsx b/src/ui/primitives/sidebar.tsx index 8818e9516..2222c95b8 100644 --- a/src/ui/primitives/sidebar.tsx +++ b/src/ui/primitives/sidebar.tsx @@ -327,7 +327,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) {
({}) function Tabs({ className, + defaultValue, + value, + onValueChange, ...props }: React.ComponentProps) { + const [stateValue, setStateValue] = React.useState(defaultValue ?? value) + + React.useEffect(() => { + if (!stateValue) return + + onValueChange?.(stateValue) + }, [stateValue, onValueChange]) + return ( - + + + ) } @@ -26,7 +46,7 @@ function TabsList({ ) { + const { value } = React.useContext(TabsContext) + const isSelected = value === props.value + return ( + > + {children} + {isSelected && ( + + )} + ) } @@ -57,10 +95,7 @@ function TabsContent({ return ( ) diff --git a/src/ui/primitives/tooltip.tsx b/src/ui/primitives/tooltip.tsx index 6e1eb01a0..e63acf174 100644 --- a/src/ui/primitives/tooltip.tsx +++ b/src/ui/primitives/tooltip.tsx @@ -4,29 +4,59 @@ import * as TooltipPrimitive from '@radix-ui/react-tooltip' import * as React from 'react' import { cn } from '@/lib/utils' -import { cardVariants } from '@/ui/primitives/card' +import { cardVariants } from './card' -const TooltipProvider = TooltipPrimitive.Provider +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps) { + return ( + + ) +} -const Tooltip = TooltipPrimitive.Root +function Tooltip({ + ...props +}: React.ComponentProps) { + return ( + + + + ) +} -const TooltipTrigger = TooltipPrimitive.Trigger +function TooltipTrigger({ + ...props +}: React.ComponentProps) { + return +} -const TooltipContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, sideOffset = 4, ...props }, ref) => ( - -)) -TooltipContent.displayName = TooltipPrimitive.Content.displayName +function TooltipContent({ + className, + sideOffset = 4, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + ) +} export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }