diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b492a6b80..4036ac982 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -45,6 +45,7 @@ jobs: "PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false" "PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}" "PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY }}" + "PUBLIC_CONSOLE_FINGERPRINT_KEY=${{ secrets.PUBLIC_CONSOLE_FINGERPRINT_KEY }}" "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" "SENTRY_RELEASE=${{ github.event.release.tag_name }}" publish-cloud-stage: @@ -87,6 +88,7 @@ jobs: "PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false" "PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}" "PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}" + "PUBLIC_CONSOLE_FINGERPRINT_KEY=${{ secrets.PUBLIC_CONSOLE_FINGERPRINT_KEY_STAGE }}" publish-self-hosted: runs-on: ubuntu-latest steps: @@ -166,4 +168,5 @@ jobs: "PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false" "PUBLIC_CONSOLE_FEATURE_FLAGS=" "PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}" + "PUBLIC_CONSOLE_FINGERPRINT_KEY=${{ secrets.PUBLIC_CONSOLE_FINGERPRINT_KEY_STAGE }}" "PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}" diff --git a/Dockerfile b/Dockerfile index 788db3b62..691652e7c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,7 @@ ARG PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS ARG PUBLIC_APPWRITE_ENDPOINT ARG PUBLIC_GROWTH_ENDPOINT ARG PUBLIC_STRIPE_KEY +ARG PUBLIC_CONSOLE_FINGERPRINT_KEY ARG SENTRY_AUTH_TOKEN ARG SENTRY_RELEASE @@ -33,6 +34,7 @@ ENV PUBLIC_APPWRITE_MULTI_REGION=$PUBLIC_APPWRITE_MULTI_REGION ENV PUBLIC_CONSOLE_EMAIL_VERIFICATION=$PUBLIC_CONSOLE_EMAIL_VERIFICATION ENV PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=$PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS ENV PUBLIC_STRIPE_KEY=$PUBLIC_STRIPE_KEY +ENV PUBLIC_CONSOLE_FINGERPRINT_KEY=$PUBLIC_CONSOLE_FINGERPRINT_KEY ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN ENV SENTRY_RELEASE=$SENTRY_RELEASE ENV NODE_OPTIONS=--max_old_space_size=8192 diff --git a/bun.lock b/bun.lock index 416a42d2f..b3a79ed60 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "@appwrite/console", "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@d6a457f", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@13c8c34", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@df765cc", "@appwrite.io/pink-legacy": "^1.0.3", @@ -89,6 +89,8 @@ }, }, "overrides": { + "immutable": "^5.1.5", + "minimatch": "10.2.3", "vite": "npm:rolldown-vite@latest", }, "packages": { @@ -120,7 +122,7 @@ "@analytics/type-utils": ["@analytics/type-utils@0.6.4", "", {}, "sha512-Ou1gQxFakOWLcPnbFVsrPb8g1wLLUZYYJXDPjHkG07+5mustGs5yqACx42UAu4A6NszNN6Z5gGxhyH45zPWRxw=="], - "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@d6a457f", { "dependencies": { "bignumber.js": "9.0.0", "json-bigint": "1.0.0" } }], + "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@13c8c34", { "dependencies": { "json-bigint": "1.0.0" } }], "@appwrite.io/pink-icons": ["@appwrite.io/pink-icons@0.25.0", "", {}, "sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q=="], @@ -266,7 +268,7 @@ "@melt-ui/svelte": ["@melt-ui/svelte@0.86.6", "", { "dependencies": { "@floating-ui/core": "^1.3.1", "@floating-ui/dom": "^1.4.5", "@internationalized/date": "^3.5.0", "dequal": "^2.0.3", "focus-trap": "^7.5.2", "nanoid": "^5.0.4" }, "peerDependencies": { "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.118" } }, "sha512-Jer+M7DgIwT5IHfTayb4Iw/fkkxWNmC/mqn/nMh9JrbPbkxmyabfLQnhJ+JDn5HK77f84j34lubO3iqFtYAfMg=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -342,9 +344,9 @@ "@opentelemetry/sql-common": ["@opentelemetry/sql-common@0.40.1", "", { "dependencies": { "@opentelemetry/core": "^1.1.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0" } }, "sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg=="], - "@oxc-project/runtime": ["@oxc-project/runtime@0.98.0", "", {}, "sha512-F0ldlBv2orG2YqNL0w77deq9yCaO4zEHbanGnW/jaJxGBR8ImekvZb8x42zAHvdzr8J76psibijvHtXfSjbEIQ=="], + "@oxc-project/runtime": ["@oxc-project/runtime@0.101.0", "", {}, "sha512-t3qpfVZIqSiLQ5Kqt/MC4Ge/WCOGrrcagAdzTcDaggupjiGxUx4nJF2v6wUCXWSzWHn5Ns7XLv13fCJEwCOERQ=="], - "@oxc-project/types": ["@oxc-project/types@0.98.0", "", {}, "sha512-Vzmd6FsqVuz5HQVcRC/hrx7Ujo3WEVeQP7C2UNP5uy1hUY4SQvMB+93jxkI1KRHz9a/6cni3glPOtvteN+zpsw=="], + "@oxc-project/types": ["@oxc-project/types@0.101.0", "", {}, "sha512-nuFhqlUzJX+gVIPPfuE6xurd4lST3mdcWOhyK/rZO0B9XWMKm79SuszIQEnSMmmDhq1DC8WWVYGVd+6F93o1gQ=="], "@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="], @@ -384,35 +386,33 @@ "@prisma/instrumentation": ["@prisma/instrumentation@5.22.0", "", { "dependencies": { "@opentelemetry/api": "^1.8", "@opentelemetry/instrumentation": "^0.49 || ^0.50 || ^0.51 || ^0.52.0 || ^0.53.0", "@opentelemetry/sdk-trace-base": "^1.22" } }, "sha512-LxccF392NN37ISGxIurUljZSh1YWnphO34V5a0+T7FVQG2u9bhAXRTJpgmQ3483woVhkraQZFF7cbRrpbw/F4Q=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.51", "", { "os": "android", "cpu": "arm64" }, "sha512-Ctn8FUXKWWQI9pWC61P1yumS9WjQtelNS9riHwV7oCkknPGaAry4o7eFx2KgoLMnI2BgFJYpW7Im8/zX3BuONg=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.53", "", { "os": "android", "cpu": "arm64" }, "sha512-Ok9V8o7o6YfSdTTYA/uHH30r3YtOxLD6G3wih/U9DO0ucBBFq8WPt/DslU53OgfteLRHITZny9N/qCUxMf9kjQ=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.51", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EL1aRW2Oq15ShUEkBPsDtLMO8GTqfb/ktM/dFaVzXKQiEE96Ss6nexMgfgQrg8dGnNpndFyffVDb5IdSibsu1g=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.53", "", { "os": "darwin", "cpu": "arm64" }, "sha512-yIsKqMz0CtRnVa6x3Pa+mzTihr4Ty+Z6HfPbZ7RVbk1Uxnco4+CUn7Qbm/5SBol1JD/7nvY8rphAgyAi7Lj6Vg=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.51", "", { "os": "darwin", "cpu": "x64" }, "sha512-uGtYKlFen9pMIPvkHPWZVDtmYhMQi5g5Ddsndg1gf3atScKYKYgs5aDP4DhHeTwGXQglhfBG7lEaOIZ4UAIWww=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.53", "", { "os": "darwin", "cpu": "x64" }, "sha512-GTXe+mxsCGUnJOFMhfGWmefP7Q9TpYUseHvhAhr21nCTgdS8jPsvirb0tJwM3lN0/u/cg7bpFNa16fQrjKrCjQ=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.51", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JRoVTQtHYbZj1P07JLiuTuXjiBtIa7ag7/qgKA6CIIXnAcdl4LrOf7nfDuHPJcuRKaP5dzecMgY99itvWfmUFQ=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.53", "", { "os": "freebsd", "cpu": "x64" }, "sha512-9Tmp7bBvKqyDkMcL4e089pH3RsjD3SUungjmqWtyhNOxoQMh0fSmINTyYV8KXtE+JkxYMPWvnEt+/mfpVCkk8w=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.51", "", { "os": "linux", "cpu": "arm" }, "sha512-BKATVnpPZ0TYBW9XfDwyd4kPGgvf964HiotIwUgpMrFOFYWqpZ+9ONNzMV4UFAYC7Hb5C2qgYQk/qj2OnAd4RQ=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.53", "", { "os": "linux", "cpu": "arm" }, "sha512-a1y5fiB0iovuzdbjUxa7+Zcvgv+mTmlGGC4XydVIsyl48eoxgaYkA3l9079hyTyhECsPq+mbr0gVQsFU11OJAQ=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.51", "", { "os": "linux", "cpu": "arm64" }, "sha512-xLd7da5jkfbVsBCm1buIRdWtuXY8+hU3+6ESXY/Tk5X5DPHaifrUblhYDgmA34dQt6WyNC2kfXGgrduPEvDI6Q=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.53", "", { "os": "linux", "cpu": "arm64" }, "sha512-bpIGX+ov9PhJYV+wHNXl9rzq4F0QvILiURn0y0oepbQx+7stmQsKA0DhPGwmhfvF856wq+gbM8L92SAa/CBcLg=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.51", "", { "os": "linux", "cpu": "arm64" }, "sha512-EQFXTgHxxTzv3t5EmjUP/DfxzFYx9sMndfLsYaAY4DWF6KsK1fXGYsiupif6qPTViPC9eVmRm78q0pZU/kuIPg=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.53", "", { "os": "linux", "cpu": "arm64" }, "sha512-bGe5EBB8FVjHBR1mOLOPEFg1Lp3//7geqWkU5NIhxe+yH0W8FVrQ6WRYOap4SUTKdklD/dC4qPLREkMMQ855FA=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.51", "", { "os": "linux", "cpu": "x64" }, "sha512-p5P6Xpa68w3yFaAdSzIZJbj+AfuDnMDqNSeglBXM7UlJT14Q4zwK+rV+8Mhp9MiUb4XFISZtbI/seBprhkQbiQ=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.53", "", { "os": "linux", "cpu": "x64" }, "sha512-qL+63WKVQs1CMvFedlPt0U9PiEKJOAL/bsHMKUDS6Vp2Q+YAv/QLPu8rcvkfIMvQ0FPU2WL0aX4eWwF6e/GAnA=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.51", "", { "os": "linux", "cpu": "x64" }, "sha512-sNVVyLa8HB8wkFipdfz1s6i0YWinwpbMWk5hO5S+XAYH2UH67YzUT13gs6wZTKg2x/3gtgXzYnHyF5wMIqoDAw=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.53", "", { "os": "linux", "cpu": "x64" }, "sha512-VGl9JIGjoJh3H8Mb+7xnVqODajBmrdOOb9lxWXdcmxyI+zjB2sux69br0hZJDTyLJfvBoYm439zPACYbCjGRmw=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-beta.51", "", { "os": "none", "cpu": "arm64" }, "sha512-e/JMTz9Q8+T3g/deEi8DK44sFWZWGKr9AOCW5e8C8SCVWzAXqYXAG7FXBWBNzWEZK0Rcwo9TQHTQ9Q0gXgdCaA=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-beta.53", "", { "os": "none", "cpu": "arm64" }, "sha512-B4iIserJXuSnNzA5xBLFUIjTfhNy7d9sq4FUMQY3GhQWGVhS2RWWzzDnkSU6MUt7/aHUrep0CdQfXUJI9D3W7A=="], - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.51", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.7" }, "cpu": "none" }, "sha512-We3LWqSu6J9s5Y0MK+N7fUiiu37aBGPG3Pc347EoaROuAwkCS2u9xJ5dpIyLW4B49CIbS3KaPmn4kTgPb3EyPw=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.53", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.0" }, "cpu": "none" }, "sha512-BUjAEgpABEJXilGq/BPh7jeU3WAJ5o15c1ZEgHaDWSz3LB881LQZnbNJHmUiM4d1JQWMYYyR1Y490IBHi2FPJg=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.51", "", { "os": "win32", "cpu": "arm64" }, "sha512-fj56buHRuMM+r/cb6ZYfNjNvO/0xeFybI6cTkTROJatdP4fvmQ1NS8D/Lm10FCSDEOkqIz8hK3TGpbAThbPHsA=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.53", "", { "os": "win32", "cpu": "arm64" }, "sha512-s27uU7tpCWSjHBnxyVXHt3rMrQdJq5MHNv3BzsewCIroIw3DJFjMH1dzCPPMUFxnh1r52Nf9IJ/eWp6LDoyGcw=="], - "@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.51", "", { "os": "win32", "cpu": "ia32" }, "sha512-fkqEqaeEx8AySXiDm54b/RdINb3C0VovzJA3osMhZsbn6FoD73H0AOIiaVAtGr6x63hefruVKTX8irAm4Jkt2w=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.53", "", { "os": "win32", "cpu": "x64" }, "sha512-cjWL/USPJ1g0en2htb4ssMjIycc36RvdQAx1WlXnS6DpULswiUTVXPDesTifSKYSyvx24E0YqQkEm0K/M2Z/AA=="], - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.51", "", { "os": "win32", "cpu": "x64" }, "sha512-CWuLG/HMtrVcjKGa0C4GnuxONrku89g0+CsH8nT0SNhOtREXuzwgjIXNJImpE/A/DMf9JF+1Xkrq/YRr+F/rCg=="], - - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.51", "", {}, "sha512-51/8cNXMrqWqX3o8DZidhwz1uYq0BhHDDSfVygAND1Skx5s1TDw3APSSxCMcFFedwgqGcx34gRouwY+m404BBQ=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="], "@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@8.55.0", "", { "dependencies": { "@sentry/core": "8.55.0" } }, "sha512-ROgqtQfpH/82AQIpESPqPQe0UyWywKJsmVIqi3c5Fh+zkds5LUxnssTj3yNd1x+kxaPDVB023jAP+3ibNgeNDw=="], @@ -640,7 +640,7 @@ "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.8.31", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw=="], @@ -650,7 +650,7 @@ "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -704,8 +704,6 @@ "commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], @@ -976,7 +974,7 @@ "ignore": ["ignore@6.0.2", "", {}, "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A=="], - "immutable": ["immutable@5.1.4", "", {}, "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA=="], + "immutable": ["immutable@5.1.5", "", {}, "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -1138,7 +1136,7 @@ "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "minimatch": ["minimatch@10.2.3", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -1290,7 +1288,7 @@ "robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="], - "rolldown": ["rolldown@1.0.0-beta.51", "", { "dependencies": { "@oxc-project/types": "=0.98.0", "@rolldown/pluginutils": "1.0.0-beta.51" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.51", "@rolldown/binding-darwin-arm64": "1.0.0-beta.51", "@rolldown/binding-darwin-x64": "1.0.0-beta.51", "@rolldown/binding-freebsd-x64": "1.0.0-beta.51", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.51", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.51", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.51", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.51", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.51", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.51", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.51", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.51", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.51", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.51" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZRLgPlS91l4JztLYEZnmMcd3Umcla1hkXJgiEiR4HloRJBBoeaX8qogTu5Jfu36rRMVLndzqYv0h+M5gJAkUfg=="], + "rolldown": ["rolldown@1.0.0-beta.53", "", { "dependencies": { "@oxc-project/types": "=0.101.0", "@rolldown/pluginutils": "1.0.0-beta.53" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.53", "@rolldown/binding-darwin-arm64": "1.0.0-beta.53", "@rolldown/binding-darwin-x64": "1.0.0-beta.53", "@rolldown/binding-freebsd-x64": "1.0.0-beta.53", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.53", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.53", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.53", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.53", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.53", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.53", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.53", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.53", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.53" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Qd9c2p0XKZdgT5AYd+KgAMggJ8ZmCs3JnS9PTMWkyUfteKlfmKtxJbWTHkVakxwXs1Ub7jrRYVeFeF7N0sQxyw=="], "rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], @@ -1476,7 +1474,7 @@ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - "vite": ["rolldown-vite@7.2.7", "", { "dependencies": { "@oxc-project/runtime": "0.98.0", "fdir": "^6.5.0", "lightningcss": "^1.30.2", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rolldown": "1.0.0-beta.51", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "esbuild": "^0.25.0", "jiti": ">=1.21.0", "less": "^4.0.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", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-N6a9KgNZ0xgCJ6/Ej2FQ7W8D3fOzDwFw7CLWZ2ubZknVrs9NdNkx25AFEuNbSwQO76VEHp4N7YatsZwp/ST1Gg=="], + "vite": ["rolldown-vite@7.3.1", "", { "dependencies": { "@oxc-project/runtime": "0.101.0", "fdir": "^6.5.0", "lightningcss": "^1.30.2", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rolldown": "1.0.0-beta.53", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.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", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-LYzdNAjRHhF2yA4JUQm/QyARyi216N2rpJ0lJZb8E9FU2y5v6Vk+xq/U4XBOxMefpWixT5H3TslmAHm1rqIq2w=="], "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=="], @@ -1588,8 +1586,6 @@ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.13", "", { "dependencies": { "@ai-sdk/provider": "3.0.7", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HHG72BN4d+OWTcq2NwTxOm/2qvk1duYsnhCDtsbYwn/h/4zeqURu1S0+Cn0nY2Ysq9a9HGKvrYuMn9bgFhR2Og=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -1604,8 +1600,6 @@ "framesync/tslib": ["tslib@2.4.0", "", {}, "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="], - "glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="], - "js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -1646,14 +1640,10 @@ "@sentry/cli/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "ai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], diff --git a/package.json b/package.json index 7acf9d2b6..fbf733cfc 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@d6a457f", + "@appwrite.io/console": "github:appwrite/sdk-for-console#b8d2d50", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@df765cc", "@appwrite.io/pink-legacy": "^1.0.3", @@ -101,6 +101,8 @@ "vitest": "^3.2.4" }, "overrides": { - "vite": "npm:rolldown-vite@latest" + "vite": "npm:rolldown-vite@latest", + "minimatch": "10.2.3", + "immutable": "^5.1.5" } } diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index 89fd94475..083be84b9 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -254,6 +254,7 @@ export enum Submit { ProjectUpdateLabels = 'submit_project_update_labels', ProjectService = 'submit_project_service', ProjectUpdateSMTP = 'submit_project_update_smtp', + ProjectResume = 'submit_project_resume', MemberCreate = 'submit_member_create', MemberDelete = 'submit_member_delete', MembershipUpdate = 'submit_membership_update', diff --git a/src/lib/actions/portal.ts b/src/lib/actions/portal.ts index 05531b47c..9beb0b26f 100644 --- a/src/lib/actions/portal.ts +++ b/src/lib/actions/portal.ts @@ -3,7 +3,7 @@ import type { Action } from 'svelte/action'; export type PortalConfig = string | HTMLElement | undefined; -export const portal: Action = (el, target = 'body') => { +export const portal: Action = (el, target: PortalConfig = 'body') => { let targetEl; async function update(newTarget: PortalConfig) { @@ -17,7 +17,7 @@ export const portal: Action = (el, target = 'body') = if (targetEl === null) { throw new Error(`No element found matching css selector: "${target}"`); } - } else if (target instanceof HTMLElement) { + } else if (typeof target === 'object' && target instanceof HTMLElement) { targetEl = target; } else { throw new TypeError( diff --git a/src/lib/actions/tooltip.ts b/src/lib/actions/tooltip.ts index b4dc14a5f..939292ef2 100644 --- a/src/lib/actions/tooltip.ts +++ b/src/lib/actions/tooltip.ts @@ -1,4 +1,5 @@ import type { Action } from 'svelte/action'; +import type { Instance } from 'tippy.js'; import type { Props as TippyProps } from 'tippy.js'; import tippy from 'tippy.js'; @@ -7,7 +8,8 @@ type Props = TippyProps & { }; export const tooltip: Action> = (node, config) => { - const instance = tippy(node, config); + const tippyInstance = tippy(node, config); + const instance: Instance = Array.isArray(tippyInstance) ? tippyInstance[0] : tippyInstance; if (config.disabled) instance.disable(); return { diff --git a/src/lib/components/archiveProject.svelte b/src/lib/components/archiveProject.svelte deleted file mode 100644 index 8a324b395..000000000 --- a/src/lib/components/archiveProject.svelte +++ /dev/null @@ -1,379 +0,0 @@ - - -{#if projectsToArchive.length > 0} -
- - - {#if isPlanBelowPro} - These projects are archived and require a plan upgrade to restore access. - {:else} - These projects will be archived at the end of your billing cycle. - {/if} - - -
- - {#each projectsToArchive as project} - {@const platforms = filterPlatforms( - project.platforms.map((platform) => getPlatformInfo(platform.type)) - )} - {@const formatted = formatName(project.name)} - - - {project?.platforms?.length ? project?.platforms?.length : 'No'} apps - - {formatted} - -
- - - - handleUnarchiveProject(project)} - >Unarchive project - handleMigrateProject(project)} - >Migrate project -
- -
- handleDeleteProject(project)} - >Delete project -
-
-
-
- - {#each platforms.slice(0, 2) as platform} - {@const icon = getIconForPlatform(platform.icon)} - - - - {/each} - - {#if platforms.length > 2} - - {/if} - - - {#if isCloud && $regionsStore?.regions} - {@const region = findRegion(project)} - {region?.name} - {/if} - -
- {/each} -
- - -
-
-
-{/if} - - - -

Are you sure you want to unarchive {projectToUnarchive?.name}?

-

This will move the project back to your active projects list.

- - - - - - - -
- - - - - The archived project {projectToDelete?.name} will be deleted along with all - of its metadata, stats, and other resources. - This action is irreversible. - - - - - - - - - - - diff --git a/src/lib/components/backupDatabaseAlert.svelte b/src/lib/components/backupDatabaseAlert.svelte index cc7e2f326..3ae79f1b0 100644 --- a/src/lib/components/backupDatabaseAlert.svelte +++ b/src/lib/components/backupDatabaseAlert.svelte @@ -1,7 +1,7 @@ {#if $showPolicyAlert && isCloud && $organization?.$id && page.url.pathname.match(/\/databases\/database-[^/]+$/)} - {@const areBackupsAvailable = $organization?.billingPlanDetails.backupsEnabled} + {@const areBackupsAvailable = $currentPlan?.backupsEnabled} {@const subtitle = !areBackupsAvailable ? 'Upgrade your plan to ensure your data stays safe and backed up' diff --git a/src/lib/components/billing/alerts/projectsLimit.svelte b/src/lib/components/billing/alerts/projectsLimit.svelte deleted file mode 100644 index 493e44510..000000000 --- a/src/lib/components/billing/alerts/projectsLimit.svelte +++ /dev/null @@ -1,53 +0,0 @@ - - - - -{#if organizationId && $currentPlan && $currentPlan.projects > 0 && !hideBillingHeaderRoutes.includes(page.url.pathname)} - - - Choose which projects to keep before {toLocaleDate( - $organization.billingNextInvoiceDate - )} or upgrade to Pro. Projects over the limit will be blocked after this date. - - - - - - -{/if} diff --git a/src/lib/components/billing/alerts/selectProjectCloud.svelte b/src/lib/components/billing/alerts/selectProjectCloud.svelte deleted file mode 100644 index a0084f97a..000000000 --- a/src/lib/components/billing/alerts/selectProjectCloud.svelte +++ /dev/null @@ -1,180 +0,0 @@ - - - - - Choose which {$currentPlan?.projects || 2} projects to keep. Projects over the limit will be - blocked after this date. - - - {#if loading} -
- - - Project Name - Created - - - {#each Array.from({ length: 5 }) as _} - - - - - - - - - {/each} - -
- {:else if projectsLoadingError} - {projectsLoadingError} - {:else} - {#if error} - {error} - {/if} - - - - Project Name - Created - - {#each projects as project} - - {project.name} - {toLocaleDateTime(project.$createdAt)} - - {/each} - - - {#if selectedProjects.length > $currentPlan?.projects} -
- You can only select {$currentPlan?.projects} projects. Please deselect others to continue. -
- {/if} - - {#if selectedProjects.length === $currentPlan?.projects} - {@const difference = projects.length - selectedProjects.length} - {@const messagePrefix = - difference > 1 ? `${difference} projects` : `${difference} project`} - - - {@html formatProjectsToArchive()} - will be archived. - - - {/if} - {/if} - - (showSelectProject = false)} - >Cancel - Save - -
- - diff --git a/src/lib/components/domains/recordTable.svelte b/src/lib/components/domains/recordTable.svelte index 98b9a1480..fdba7438e 100644 --- a/src/lib/components/domains/recordTable.svelte +++ b/src/lib/components/domains/recordTable.svelte @@ -54,8 +54,6 @@ case 'cname': if (service === 'sites') { return $regionalConsoleVariables._APP_DOMAIN_SITES; - } else if (service === 'functions') { - return $regionalConsoleVariables._APP_DOMAIN_FUNCTIONS; } else { return $regionalConsoleVariables._APP_DOMAIN_TARGET_CNAME; } diff --git a/src/lib/components/git/DirectoryItem.svelte b/src/lib/components/git/DirectoryItem.svelte new file mode 100644 index 000000000..baf706601 --- /dev/null +++ b/src/lib/components/git/DirectoryItem.svelte @@ -0,0 +1,247 @@ + + +{#each directories as { title, fileCount, fullPath, thumbnailUrl, thumbnailIcon, thumbnailHtml, children, hasChildren: explicitHasChildren, showThumbnail = true, loading = false }, i} + {@const hasChildren = explicitHasChildren ?? !!children?.length} + {@const __MELTUI_BUILDER_1__ = $group({ id: fullPath })} + {@const __MELTUI_BUILDER_0__ = $item({ + id: fullPath, + hasChildren + })} + +
+ + + {#if children} +
+ +
+ {/if} +
+{/each} + + diff --git a/src/lib/components/git/DirectoryPicker.svelte b/src/lib/components/git/DirectoryPicker.svelte new file mode 100644 index 000000000..d071ab2ee --- /dev/null +++ b/src/lib/components/git/DirectoryPicker.svelte @@ -0,0 +1,132 @@ + + + + +
+ {#if isLoading} +
+ Loading directory data... +
+ {:else} + + {/if} +
+ + diff --git a/src/lib/components/git/connectRepoModal.svelte b/src/lib/components/git/connectRepoModal.svelte index 05cfd5863..faca63d91 100644 --- a/src/lib/components/git/connectRepoModal.svelte +++ b/src/lib/components/git/connectRepoModal.svelte @@ -52,6 +52,12 @@ } }); + $effect(() => { + if ($installation?.$id) { + selectedInstallationId = $installation.$id; + } + }); + async function connectRepo() { try { if (repositoryBehaviour === 'new') { @@ -105,13 +111,29 @@ {product} action="button" {callbackState} - connect={(e) => { + connect={async (e) => { trackEvent(Click.ConnectRepositoryClick, { from: product }); repository.set(e); repositoryName = e.name; selectedRepository = e.id; + if (!selectedInstallationId && $installation?.$id) { + selectedInstallationId = $installation.$id; + } + try { + await connect(selectedInstallationId, e.id); + show = false; + addNotification({ + type: 'success', + message: 'Repository connected successfully' + }); + } catch (error) { + addNotification({ + type: 'error', + message: error?.message ?? 'Failed to connect repository' + }); + } }} /> {/if} diff --git a/src/lib/components/git/repositories.svelte b/src/lib/components/git/repositories.svelte index 105ed8fb1..df2c80050 100644 --- a/src/lib/components/git/repositories.svelte +++ b/src/lib/components/git/repositories.svelte @@ -20,6 +20,7 @@ import { Query, VCSDetectionType, type Models } from '@appwrite.io/console'; import { getFrameworkIcon } from '$lib/stores/sites'; import { connectGitHub } from '$lib/stores/git'; + import { addNotification } from '$lib/stores/notifications'; import { page } from '$app/state'; import Card from '../card.svelte'; import SkeletonRepoList from './skeletonRepoList.svelte'; @@ -274,9 +275,20 @@ variant="secondary" style="flex-shrink: 0;" disabled={!!connectingRepositoryId} - on:click={() => { + on:click={async () => { connectingRepositoryId = repo.id; - connect(repo); + try { + await Promise.resolve(connect(repo)); + } catch (error) { + addNotification({ + type: 'error', + message: + error?.message ?? + 'Failed to connect repository' + }); + } finally { + connectingRepositoryId = null; + } }}> Connect diff --git a/src/lib/components/git/selectRootModal.svelte b/src/lib/components/git/selectRootModal.svelte index fed59657c..a121239cb 100644 --- a/src/lib/components/git/selectRootModal.svelte +++ b/src/lib/components/git/selectRootModal.svelte @@ -6,140 +6,378 @@ import { sdk } from '$lib/stores/sdk'; import { installation, repository } from '$lib/stores/vcs'; import { VCSDetectionType, type Models } from '@appwrite.io/console'; - import { DirectoryPicker } from '@appwrite.io/pink-svelte'; - import { onMount } from 'svelte'; + import DirectoryPicker from '$lib/components/git/DirectoryPicker.svelte'; import { writable } from 'svelte/store'; type Directory = { title: string; fullPath: string; - fileCount: number; - thumbnailUrl: string; + fileCount?: number; + thumbnailUrl?: string; children?: Directory[]; + hasChildren?: boolean; loading?: boolean; }; - export let show = false; - export let rootDir: string; - export let product: 'sites' | 'functions' = 'functions'; - export let branch: string; + let { + show = $bindable(false), + rootDir = $bindable(''), + product = 'functions' as 'sites' | 'functions', + branch + }: { + show?: boolean; + rootDir?: string; + product?: 'sites' | 'functions'; + branch: string; + } = $props(); - let isLoading = true; - let directories: Directory[] = [ + let isLoading = $state(true); + let directories = $state([ { - title: 'Root', - fullPath: './', - fileCount: 0, - thumbnailUrl: 'root', + title: 'Repository (root)', + fullPath: '/', + fileCount: undefined, + thumbnailUrl: $iconPath('empty', 'grayscale'), children: [], + hasChildren: true, loading: false } - ]; - let currentPath: string = './'; - let currentDir: Directory; - export let expanded = writable(['lib-0', 'tree-0']); + ]); + let currentPath = $state('/'); + let expandedPaths = $state([]); + const expandedStore = writable([]); - onMount(async () => { + $effect(() => { + expandedStore.set(expandedPaths); + }); + $effect(() => { + const unsub = expandedStore.subscribe((v) => { + expandedPaths = v; + }); + return unsub; + }); + + let initialized = $state(false); + let initialPath = $state('/'); + const inFlightPaths = new Set(); + const contentsCache = new Map< + string, + { fileCount: number; directories: Array<{ name: string }> } + >(); + const iconCache = new Map(); + + let hasChanges = $derived(currentPath !== initialPath); + + const iconAliases = new Map([ + ['svelte-kit', 'svelte'], + ['sveltekit', 'svelte'], + ['svelte_kit', 'svelte'], + ['sveltejs', 'svelte'], + ['other', 'empty'] + ]); + + function normalizePath(path: string): string { + if (!path || path === './' || path === '/') return '/'; + const trimmed = path.replace(/^\.\//, '').replace(/^\/+/, '').replace(/\/$/, ''); + return `/${trimmed}`; + } + + function toProviderPath(path: string): string { + const normalized = normalizePath(path); + if (normalized === '/') return './'; + return `./${normalized.slice(1)}`; + } + + function resolveIconUrl(rawIconName: string | null | undefined): string | null { + if (!rawIconName) return null; + const normalized = rawIconName.toLowerCase(); + const iconName = iconAliases.get(normalized) ?? normalized; + return $iconPath(iconName, 'color'); + } + + async function detectRuntimeOrFramework(path: string): Promise { try { - const content = await sdk + if (iconCache.has(path)) { + return iconCache.get(path) ?? null; + } + const detection = await sdk .forProject(page.params.region, page.params.project) - .vcs.getRepositoryContents({ + .vcs.createRepositoryDetection({ installationId: $installation.$id, providerRepositoryId: $repository.id, - providerRootDirectory: currentPath, - providerReference: branch + type: + product === 'sites' ? VCSDetectionType.Framework : VCSDetectionType.Runtime, + providerRootDirectory: toProviderPath(path) }); - directories[0].fileCount = content.contents?.length ?? 0; - directories[0].children = content.contents - .filter((e) => e.isDirectory) - .map((dir) => ({ - title: dir.name, - fullPath: currentPath + dir.name, - fileCount: undefined, - thumbnailUrl: dir.name, - loading: false - })); - currentDir = directories[0]; - isLoading = false; - } catch { + + const iconName = + product === 'sites' + ? detection.framework + : (detection as unknown as Models.DetectionRuntime).runtime; + const resolved = resolveIconUrl(iconName); + iconCache.set(path, resolved); + return resolved; + } catch (err) { + iconCache.set(path, null); + return null; + } + } + + async function detectIconsForChildren(parentPath: string) { + const targetDir = getDirByPath(parentPath); + if (!targetDir?.children?.length) return; + + const children = targetDir.children; + const concurrency = 3; + let index = 0; + + async function worker() { + while (index < children.length) { + const current = index; + index += 1; + const child = children[current]; + const icon = await detectRuntimeOrFramework(child.fullPath); + if (icon && icon !== child.thumbnailUrl) { + child.thumbnailUrl = icon; + } + } + } + + await Promise.all(Array.from({ length: Math.min(concurrency, children.length) }, worker)); + } + + async function fetchContents(path: string) { + const cached = contentsCache.get(path); + if (cached) return cached; + + const content = await sdk + .forProject(page.params.region, page.params.project) + .vcs.getRepositoryContents({ + installationId: $installation.$id, + providerRepositoryId: $repository.id, + providerRootDirectory: toProviderPath(path), + providerReference: branch + }); + + const contents = content.contents ?? []; + const fileCount = contents.length; + const directories = contents + .filter((e) => e.isDirectory) + .map((dir) => ({ name: dir.name })); + + const result = { fileCount, directories }; + contentsCache.set(path, result); + return result; + } + + function ensureChildren(path: string, directories: Array<{ name: string }>) { + const targetDir = getDirByPath(path); + if (!targetDir) return; + + if (directories.length === 0) { + targetDir.hasChildren = false; + targetDir.children = []; return; } + + const existingByTitle = new Map( + (targetDir.children ?? []).map((child) => [child.title, child]) + ); + targetDir.children = directories.map((dir) => { + const fullPath = path === '/' ? `/${dir.name}` : `${path}/${dir.name}`; + const existing = existingByTitle.get(dir.name); + if (existing) { + existing.fullPath = fullPath; + existing.hasChildren = true; + existing.loading = existing.loading ?? false; + existing.thumbnailUrl = existing.thumbnailUrl ?? $iconPath('empty', 'grayscale'); + existing.children = existing.children ?? []; + return existing; + } + return { + title: dir.name, + fullPath, + fileCount: undefined, + thumbnailUrl: $iconPath('empty', 'grayscale'), + children: [], + hasChildren: true, + loading: false + }; + }); + } + + async function prefetchPath(path: string) { + const normalized = normalizePath(path); + const segments = normalized.split('/').filter((s) => s !== ''); + const pathsToLoad = ['/']; + let currentPath = '/'; + + for (const segment of segments) { + currentPath = currentPath === '/' ? `/${segment}` : `${currentPath}/${segment}`; + pathsToLoad.push(currentPath); + } + + for (const pathToLoad of pathsToLoad) { + const { fileCount, directories } = await fetchContents(pathToLoad); + const targetDir = getDirByPath(pathToLoad); + if (targetDir) { + targetDir.fileCount = fileCount; + } + ensureChildren(pathToLoad, directories); + } + } + + $effect(() => { + if (!isLoading) return; + + (async () => { + try { + const content = await fetchContents('/'); + + const repoTitle = $repository?.name + ? `${$repository.name} (root)` + : 'Repository (root)'; + + directories[0] = { + ...directories[0], + title: repoTitle, + fileCount: content.fileCount + }; + ensureChildren('/', content.directories); + + const detectedIcon = await detectRuntimeOrFramework('/'); + if (detectedIcon) { + directories[0].thumbnailUrl = detectedIcon; + } + + isLoading = false; + expandedPaths = [...new Set([...expandedPaths, '/'])]; + prefetchPath(rootDir || '/'); + detectIconsForChildren('/'); + } catch (error) { + console.error('Failed to load root directory:', error); + isLoading = false; + } + })(); + }); + + function getDirByPath(path: string): Directory | null { + const segments = path.split('/').filter((s) => s !== ''); + let node: Directory | null = directories[0] ?? null; + for (const seg of segments) { + const next = node?.children?.find((d) => d.title === seg) ?? null; + if (!next) return null; + node = next; + } + return node; + } + + async function loadPath(path: string) { + // skip loading if this directory was done + const targetDir = getDirByPath(path); + if (!targetDir || targetDir.fileCount !== undefined) return; + + if (!targetDir.children) { + targetDir.children = []; + } + + if (inFlightPaths.has(path)) return; + inFlightPaths.add(path); + targetDir.loading = true; + + try { + const { fileCount, directories: contentDirectories } = await fetchContents(path); + + if (contentDirectories.length === 0) { + targetDir.hasChildren = false; + targetDir.children = []; + expandedPaths = [...new Set([...expandedPaths, path])]; + return; + } + + targetDir.fileCount = fileCount; + + // set logo only for the current folder, not for the children + const detectedIcon = await detectRuntimeOrFramework(path); + if (detectedIcon) { + targetDir.thumbnailUrl = detectedIcon; + } + + ensureChildren(path, contentDirectories); + detectIconsForChildren(path); + + expandedPaths = [...new Set([...expandedPaths, path])]; + } catch (error) { + console.error('Failed to load directory:', error); + } finally { + targetDir.loading = false; + inFlightPaths.delete(path); + } + } + + async function expandToPath(path: string) { + const normalized = normalizePath(path); + const segments = normalized.split('/').filter((s) => s !== ''); + + const pathsToExpand = ['/']; + + let currentDir = directories[0]; + let walkPath = '/'; + + for (const segment of segments) { + walkPath = walkPath === '/' ? `/${segment}` : `${walkPath}/${segment}`; + pathsToExpand.push(walkPath); + + if (!currentDir.children) { + currentDir.children = []; + } + + let nextDir = currentDir.children.find((d) => d.title === segment); + if (!nextDir) { + nextDir = { + title: segment, + fullPath: walkPath, + fileCount: undefined, + thumbnailUrl: $iconPath('empty', 'grayscale'), + children: [], + hasChildren: true + }; + currentDir.children = [...currentDir.children, nextDir]; + } + + currentDir = nextDir; + } + + expandedPaths = [...new Set([...expandedPaths, ...pathsToExpand])]; + + // ensure each segment loads in order so deeper children appear + for (const pathToLoad of pathsToExpand) { + // eslint-disable-next-line no-await-in-loop + await loadPath(pathToLoad); + } + + currentPath = normalized; + } + + $effect(() => { + if (show && !initialized && !isLoading) { + initialized = true; + const normalized = normalizePath(rootDir || '/'); + initialPath = normalized; + currentPath = normalized; + expandToPath(normalized); + } }); - async function fetchContents(e: CustomEvent) { - const path = e.detail.fullPath as string; - currentPath = path; - - const pathSegments = path.split('/').filter((segment) => segment !== '.' && segment !== ''); - let traversedDir = directories[0]; // Start at root - - for (const segment of pathSegments) { - const nextDir = traversedDir.children?.find((dir) => dir.title === segment); - if (!nextDir) break; - traversedDir = nextDir; + // reset state when modal closes + $effect(() => { + if (!show && initialized) { + initialized = false; } + }); - currentDir = traversedDir; - - if (!currentDir.fileCount) { - currentDir.loading = true; - directories = [...directories]; - - try { - const content = await sdk - .forProject(page.params.region, page.params.project) - .vcs.getRepositoryContents({ - installationId: $installation.$id, - providerRepositoryId: $repository.id, - providerRootDirectory: path, - providerReference: branch - }); - - const fileCount = content.contents?.length ?? 0; - const contentDirectories = content.contents.filter((e) => e.isDirectory); - - if (contentDirectories.length === 0) { - return; - } - - currentDir.fileCount = fileCount; - currentDir.children = contentDirectories.map((dir) => ({ - title: dir.name, - fullPath: path + '/' + dir.name, - fileCount: undefined, - thumbnailUrl: undefined - })); - const runtime = await sdk - .forProject(page.params.region, page.params.project) - .vcs.createRepositoryDetection({ - installationId: $installation.$id, - providerRepositoryId: $repository.id, - type: - product === 'sites' - ? VCSDetectionType.Framework - : VCSDetectionType.Runtime, - providerRootDirectory: path - }); - if (product === 'sites') { - currentDir.children.forEach((dir) => { - dir.thumbnailUrl = $iconPath(runtime.framework, 'color'); - }); - } else if (product === 'functions') { - currentDir.children.forEach((dir) => { - dir.thumbnailUrl = $iconPath( - (runtime as unknown as Models.DetectionRuntime).runtime, - 'color' - ); - }); - } - directories = [...directories]; - $expanded = [...$expanded, path]; - } catch (error) { - console.error(error); - } finally { - currentDir.loading = false; - } - } + function handleSelect(detail: { fullPath: string }) { + loadPath(detail.fullPath); } function handleSubmit() { @@ -155,13 +393,13 @@ + expanded={expandedStore} + bind:selected={currentPath} + openTo={initialPath} + onSelect={handleSelect} /> - + diff --git a/src/lib/components/git/types.ts b/src/lib/components/git/types.ts new file mode 100644 index 000000000..04fb88738 --- /dev/null +++ b/src/lib/components/git/types.ts @@ -0,0 +1,14 @@ +import type { Icon } from '@appwrite.io/pink-svelte'; + +export type DirectoryEntry = { + title: string; + fullPath: string; + fileCount?: number; + thumbnailUrl?: string; + thumbnailIcon?: typeof Icon; + thumbnailHtml?: string; + children?: DirectoryEntry[]; + hasChildren?: boolean; + showThumbnail?: boolean; + loading?: boolean; +}; diff --git a/src/lib/components/organizationUsageLimits.svelte b/src/lib/components/organizationUsageLimits.svelte index fc97c9b25..39ae9a1ea 100644 --- a/src/lib/components/organizationUsageLimits.svelte +++ b/src/lib/components/organizationUsageLimits.svelte @@ -1,5 +1,5 @@ @@ -220,11 +241,7 @@ {:else} - - {formatNumber(currentUsage.members)} / {formatNumber( - freePlanLimits.members - )} - + N/A {/if} @@ -259,54 +276,90 @@ {#if showSelectProject} - - - Choose which {freePlanLimits.projects} projects to keep. Projects over the limit will be - blocked after your billing cycle ends on {toLocaleDate( - $organization.billingNextInvoiceDate - )}. - + {@const requiredToDelete = currentUsage.projects - allowedProjectsToKeep} + + + The Free plan lets you keep {allowedProjectsToKeep} projects. Select projects you want to + permanently delete. + {#if error} {error} - {/if} - -
- - - Project Name - Created - - {#each projects as project} - - {project.name} - - {toLocaleDateTime(project.$createdAt)} - - - {/each} - -
- {#if selectedProjects.length === allowedProjectsToKeep} - {@const difference = projects.length - selectedProjects.length} - {@const messagePrefix = - difference > 1 ? `${difference} projects` : `${difference} project`} + {:else} - {formatProjectsToArchive()} will be archived + title="The selected projects will be permanently deleted"> + The selected projects and all associated data will be permanently deleted and cannot + be recovered. {/if} + + + Select {requiredToDelete} project{requiredToDelete !== 1 ? 's' : ''} to delete + + + + +
+ + + Project Name + Created + + {#each projects as project} + {@const isRowSelected = selectedProjectsToDelete.includes(project.$id)} + {@const shouldDisable = + !isRowSelected && selectedProjectsToDelete.length >= requiredToDelete} + + {project.name} + + {toLocaleDateTime(project.$createdAt)} + + + {/each} + +
+
+ + {#if selectedProjectsToDelete.length >= requiredToDelete} + + + + {/if} + - +
{/if} @@ -352,4 +405,14 @@ min-width: 96px; } } + + .controlled-selection :global([role='rowheader']) { + pointer-events: none; + + :global([role='cell']) { + opacity: 0.5; + cursor: not-allowed; + color: var(--fgcolor-neutral-secondary); + } + } diff --git a/src/lib/components/promos/shader.svelte b/src/lib/components/promos/shader.svelte index 15ce23040..2f8ee715b 100644 --- a/src/lib/components/promos/shader.svelte +++ b/src/lib/components/promos/shader.svelte @@ -3,10 +3,7 @@ import { Uniform, MeshBasicMaterial, Vector2 } from 'three'; import noise from './threlte/shaders/noise.glsl?raw'; import { onMount } from 'svelte'; - import { useViewport } from '@threlte/extras'; - - const { invalidate } = useThrelte(); - const viewport = useViewport(); + const { invalidate, size } = useThrelte(); let uAspect: Uniform; let uOpacity: Uniform; @@ -14,8 +11,8 @@ let uViewportSize: Uniform; $effect(() => { - const width = $viewport.width; - const height = $viewport.height; + const width = $size.width || 1; + const height = $size.height || 1; if (uAspect) { uAspect.value = width / height; invalidate(); @@ -30,10 +27,13 @@ let material = $state(new MeshBasicMaterial()); onMount(() => { - uAspect = new Uniform($viewport.width / $viewport.height); + const width = $size.width || 1; + const height = $size.height || 1; + + uAspect = new Uniform(width / height); uOpacity = new Uniform(0); uTime = new Uniform(0); - uViewportSize = new Uniform(new Vector2($viewport.width, $viewport.height)); + uViewportSize = new Uniform(new Vector2(width, height)); material.onBeforeCompile = (shader) => { shader.uniforms.uTime = uTime; @@ -104,6 +104,6 @@ - + diff --git a/src/routes/(console)/project-[region]-[project]/sites/create-site/createVariableModal.svelte b/src/lib/components/variables/createVariableModal.svelte similarity index 83% rename from src/routes/(console)/project-[region]-[project]/sites/create-site/createVariableModal.svelte rename to src/lib/components/variables/createVariableModal.svelte index 8dc28a966..06b73ea54 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/create-site/createVariableModal.svelte +++ b/src/lib/components/variables/createVariableModal.svelte @@ -9,12 +9,29 @@ import { page } from '$app/state'; import { IconPlus, IconX } from '@appwrite.io/pink-icons-svelte'; - export let show = false; - export let variables: Partial[]; + export type ProductLabel = 'site' | 'function'; - let newVariables: Partial[] = [{ key: '', value: '' }]; - let secret = false; - let error = ''; + let { + show = $bindable(false), + variables = $bindable(), + productLabel = 'site' + }: { + show: boolean; + variables: Partial[]; + productLabel?: ProductLabel; + } = $props(); + + let newVariables = $state[]>([{ key: '', value: '' }]); + let secret = $state(false); + let error = $state(''); + + $effect(() => { + if (!show) { + newVariables = [{ key: '', value: '' }]; + secret = false; + error = ''; + } + }); function handleVariable() { try { @@ -54,19 +71,17 @@ function removeVariable(index: number) { if (newVariables.length === 1) { - newVariables[0].key = ''; - newVariables[0].value = ''; + newVariables = [{ key: '', value: '' }]; } else { - newVariables.splice(index, 1); - newVariables = [...newVariables]; + newVariables = newVariables.filter((_, i) => i !== index); } } - Set the environment variables or secret that will be passed to your site. Global variables - can be set in project settings removeVariable(i)}> + onclick={() => removeVariable(i)}> diff --git a/src/routes/(console)/project-[region]-[project]/sites/create-site/deleteVariableModal.svelte b/src/lib/components/variables/deleteVariableModal.svelte similarity index 100% rename from src/routes/(console)/project-[region]-[project]/sites/create-site/deleteVariableModal.svelte rename to src/lib/components/variables/deleteVariableModal.svelte diff --git a/src/lib/components/variables/environmentVariables.svelte b/src/lib/components/variables/environmentVariables.svelte new file mode 100644 index 000000000..8b4edb8c3 --- /dev/null +++ b/src/lib/components/variables/environmentVariables.svelte @@ -0,0 +1,271 @@ + + + + + Set up environment variables to securely manage keys and settings for your project. + + + + + + + {#if variables?.length} + + {/if} + + + {#if isLoading && !variables?.length} + + + Key + Value + + + {#each Array(3) as _} + + + + + + + + + + + + {/each} + + {:else if variables?.length} + + {#snippet children(paginatedItems)} + + + Key + Value + + + {#each paginatedItems as variable} + + {variable.key} + + +
+ {#if variable.secret} + + + + This value is secret, you cannot see its + value. + + + {:else} + + {/if} +
+
+ +
+ + { + e.preventDefault(); + toggle(e); + }}> + + + + + + {#if !variable?.secret} + { + toggle(e); + currentVariable = variable; + showUpdate = true; + }}> + Update + + {/if} + {#if !variable?.secret} + { + toggle(e); + currentVariable = variable; + showSecretModal = true; + }}> + Secret + + {/if} + { + toggle(e); + currentVariable = variable; + showDelete = true; + }}> + Delete + + + + +
+
+
+ {/each} +
+ {/snippet} +
+ {:else} + { + showCreate = true; + trackEvent(Click.VariablesCreateClick, { + source: createSource + }); + }}>Create variables to get started + {/if} +
+
+
+ +{#if showEditorModal} + +{/if} + +{#if showSecretModal} + +{/if} + +{#if showImportModal} + +{/if} + +{#if showCreate} + +{/if} +{#if showUpdate} + +{/if} + +{#if showDelete} + +{/if} diff --git a/src/routes/(console)/project-[region]-[project]/sites/create-site/importSiteVariablesModal.svelte b/src/lib/components/variables/importVariablesModal.svelte similarity index 100% rename from src/routes/(console)/project-[region]-[project]/sites/create-site/importSiteVariablesModal.svelte rename to src/lib/components/variables/importVariablesModal.svelte diff --git a/src/lib/components/variables/index.ts b/src/lib/components/variables/index.ts new file mode 100644 index 000000000..f8680034b --- /dev/null +++ b/src/lib/components/variables/index.ts @@ -0,0 +1,7 @@ +export { default as CreateVariableModal, type ProductLabel } from './createVariableModal.svelte'; +export { default as DeleteVariableModal } from './deleteVariableModal.svelte'; +export { default as EnvironmentVariables } from './environmentVariables.svelte'; +export { default as ImportVariablesModal } from './importVariablesModal.svelte'; +export { default as SecretVariableModal } from './secretVariableModal.svelte'; +export { default as UpdateVariableModal } from './updateVariableModal.svelte'; +export { default as VariableEditorModal } from './variableEditorModal.svelte'; diff --git a/src/routes/(console)/project-[region]-[project]/sites/create-site/secretVariableModal.svelte b/src/lib/components/variables/secretVariableModal.svelte similarity index 100% rename from src/routes/(console)/project-[region]-[project]/sites/create-site/secretVariableModal.svelte rename to src/lib/components/variables/secretVariableModal.svelte diff --git a/src/routes/(console)/project-[region]-[project]/sites/create-site/updateVariableModal.svelte b/src/lib/components/variables/updateVariableModal.svelte similarity index 94% rename from src/routes/(console)/project-[region]-[project]/sites/create-site/updateVariableModal.svelte rename to src/lib/components/variables/updateVariableModal.svelte index c678007f9..bf48d8b62 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/create-site/updateVariableModal.svelte +++ b/src/lib/components/variables/updateVariableModal.svelte @@ -11,6 +11,7 @@ export let show = false; export let selectedVar: Partial; export let variables: Partial[]; + export let productLabel = 'site'; let pair = { $id: selectedVar?.$id, @@ -40,7 +41,8 @@ - Update the environment variable for your site. Global variables can be set in project settings[]; + export let docsLink = + 'https://appwrite.io/docs/products/sites/develop#accessing-environment-variables'; const editableVariables = variables.filter((variable) => !variable.secret); const secretVariables = variables.filter((variable) => variable.secret); @@ -122,11 +124,7 @@ {#if secretVariables?.length > 0} {secretVariables.length} secret variables are hidden from the editor. Their values will - remain unchanged. Learn more. + remain unchanged. Learn more. {/if} diff --git a/src/lib/elements/forms/inputNumber.svelte b/src/lib/elements/forms/inputNumber.svelte index 0a465e505..77bb59937 100644 --- a/src/lib/elements/forms/inputNumber.svelte +++ b/src/lib/elements/forms/inputNumber.svelte @@ -19,7 +19,17 @@ export let leadingIcon: ComponentType | undefined = undefined; let error: string; - + // TODO: Remove this once Pink Svelte is fixed + $: if (value !== null && typeof value === 'object' && 'target' in (value as object)) { + const event = value as Event; + const target = event.target as HTMLInputElement; + if (target?.value !== undefined) { + const parsedValue = target.value === '' ? null : Number(target.value); + value = Number.isNaN(parsedValue) ? null : parsedValue; + } else { + value = null; + } + } const handleInvalid = (event: Event & { currentTarget: EventTarget & HTMLInputElement }) => { event.preventDefault(); diff --git a/src/lib/helpers/fingerprint.ts b/src/lib/helpers/fingerprint.ts new file mode 100644 index 000000000..a4d53b13d --- /dev/null +++ b/src/lib/helpers/fingerprint.ts @@ -0,0 +1,225 @@ +import { env } from '$env/dynamic/public'; + +const SECRET = env.PUBLIC_CONSOLE_FINGERPRINT_KEY ?? ''; +const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour + +async function sha256(message: string): Promise { + if (!crypto?.subtle) { + console.warn('crypto.subtle unavailable, fingerprinting disabled'); + return ''; + } + const msgBuffer = new TextEncoder().encode(message); + const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); +} + +async function hmacSha256(message: string, secret: string): Promise { + if (!crypto?.subtle) return ''; + const key = await crypto.subtle.importKey( + 'raw', + new TextEncoder().encode(secret), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(message)); + return Array.from(new Uint8Array(sig)) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); +} + +function getCanvasFingerprint(): string { + try { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) return ''; + + canvas.width = 200; + canvas.height = 50; + + ctx.textBaseline = 'top'; + ctx.font = '14px Arial'; + ctx.fillStyle = '#f60'; + ctx.fillRect(125, 1, 62, 20); + ctx.fillStyle = '#069'; + ctx.fillText('Appwrite Console', 2, 15); + ctx.fillStyle = 'rgba(102, 204, 0, 0.7)'; + ctx.fillText('Appwrite Console', 4, 17); + + return canvas.toDataURL(); + } catch { + return ''; + } +} + +function getWebGLFingerprint(): string { + try { + const canvas = document.createElement('canvas'); + const gl = + canvas.getContext('webgl') || + (canvas.getContext('experimental-webgl') as WebGLRenderingContext | null); + if (!gl) return ''; + + const debugInfo = gl.getExtension('WEBGL_debug_renderer_info'); + if (!debugInfo) return 'webgl-no-debug'; + + const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || ''; + const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || ''; + + return `${vendor}~${renderer}`; + } catch { + return ''; + } +} + +async function getAudioFingerprint(): Promise { + try { + const OfflineCtx = + window.OfflineAudioContext || + (window as unknown as { webkitOfflineAudioContext: typeof OfflineAudioContext }) + .webkitOfflineAudioContext; + if (!OfflineCtx) return ''; + + const sampleRate = 44100; + const length = 4096; + const context = new OfflineCtx(1, length, sampleRate); + + const oscillator = context.createOscillator(); + oscillator.type = 'triangle'; + oscillator.frequency.value = 10000; + + const compressor = context.createDynamicsCompressor(); + compressor.threshold.value = -50; + compressor.knee.value = 40; + compressor.ratio.value = 12; + compressor.attack.value = 0; + compressor.release.value = 0.25; + + oscillator.connect(compressor); + compressor.connect(context.destination); + + oscillator.start(0); + + const buffer = await context.startRendering(); + const samples = buffer.getChannelData(0); + + let sum = 0; + for (let i = 0; i < samples.length; i++) { + sum += Math.abs(samples[i]); + } + + return sum.toString(); + } catch { + return ''; + } +} + +interface StaticSignals { + userAgent: string; + language: string; + languages: string[]; + platform: string; + hardwareConcurrency: number; + deviceMemory: number | undefined; + maxTouchPoints: number; + screenWidth: number; + screenHeight: number; + screenColorDepth: number; + screenPixelDepth: number; + devicePixelRatio: number; + timezoneOffset: number; + timezone: string; + canvas: string; + webgl: string; + audio: string; +} + +interface BrowserSignals extends StaticSignals { + timestamp: number; +} + +interface SignalsCache { + signals: StaticSignals; + collectedAt: number; +} + +let cache: SignalsCache | null = null; +let cachePromise: Promise | null = null; + +async function collectStaticSignals(): Promise { + const [canvasRaw, webgl, audio] = await Promise.all([ + Promise.resolve(getCanvasFingerprint()), + Promise.resolve(getWebGLFingerprint()), + getAudioFingerprint() + ]); + + const canvas = canvasRaw ? await sha256(canvasRaw) : ''; + + return { + userAgent: navigator.userAgent, + language: navigator.language, + languages: [...(navigator.languages || [])], + platform: navigator.platform, + hardwareConcurrency: navigator.hardwareConcurrency || 0, + deviceMemory: (navigator as Navigator & { deviceMemory?: number }).deviceMemory, + maxTouchPoints: navigator.maxTouchPoints || 0, + screenWidth: screen.width, + screenHeight: screen.height, + screenColorDepth: screen.colorDepth, + screenPixelDepth: screen.pixelDepth, + devicePixelRatio: window.devicePixelRatio || 1, + timezoneOffset: new Date().getTimezoneOffset(), + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + canvas, + webgl, + audio + }; +} + +async function getCachedSignals(): Promise { + const now = Date.now(); + + if (cache && now - cache.collectedAt < CACHE_TTL_MS) { + return cache.signals; + } + + if (cachePromise) { + return cachePromise; + } + + cachePromise = collectStaticSignals(); + + try { + const signals = await cachePromise; + cache = { signals, collectedAt: now }; + return signals; + } finally { + cachePromise = null; + } +} + +export async function generateFingerprintToken(): Promise { + const staticSignals = await getCachedSignals(); + + const signals: BrowserSignals = { + ...staticSignals, + timestamp: Math.floor(Date.now() / 1000) + }; + + const payload = JSON.stringify(signals); + const bytes = new TextEncoder().encode(payload); + let binary = ''; + for (const byte of bytes) { + binary += String.fromCharCode(byte); + } + const encoded = btoa(binary); + + if (!SECRET) { + return encoded; + } + + const signature = await hmacSha256(encoded, SECRET); + + return `${encoded}.${signature}`; +} diff --git a/src/lib/helpers/prefs.ts b/src/lib/helpers/prefs.ts index 1a1451a4b..850ae28c0 100644 --- a/src/lib/helpers/prefs.ts +++ b/src/lib/helpers/prefs.ts @@ -1,12 +1,19 @@ export type PrefRow = { key: string; value: string }; -export function normalizePrefs(entries: [string, string][] | PrefRow[]): [string, string][] { +function stringTrim(s: unknown): string { + return String(s ?? '').trim(); +} + +export function normalizePrefs( + entries: [string, unknown][] | PrefRow[] | { key: unknown; value: unknown }[] +): [string, string][] { return entries .map((item): [string, string] => - Array.isArray(item) ? [item[0], item[1]] : [item.key, item.value] + Array.isArray(item) + ? [String(item[0] ?? ''), String(item[1] ?? '')] + : [String(item.key ?? ''), String(item.value ?? '')] ) - - .filter(([k, v]) => k.trim() && v.trim()) + .filter(([k, v]) => stringTrim(k).length > 0 && stringTrim(v).length > 0) .sort(([a], [b]) => a.localeCompare(b)); } @@ -23,5 +30,5 @@ export function isAddDisabled(prefs: PrefRow[] | null): boolean { } export function sanitizePrefs(prefs: PrefRow[]) { - return prefs.filter((p) => p.key.trim() && p.value.trim()); + return prefs.filter((p) => stringTrim(p.key).length > 0 && stringTrim(p.value).length > 0); } diff --git a/src/lib/helpers/variables.ts b/src/lib/helpers/variables.ts new file mode 100644 index 000000000..90219499e --- /dev/null +++ b/src/lib/helpers/variables.ts @@ -0,0 +1,35 @@ +import type { Models } from '@appwrite.io/console'; + +export function normalizeDetectedVariables( + detected: Models.DetectionVariable[] = [] +): Partial[] { + const normalized: Partial[] = []; + for (const variable of detected) { + const key = variable.name?.trim(); + if (!key) { + continue; + } + normalized.push({ + key, + value: variable.value ?? '', + secret: false + }); + } + return normalized; +} + +export function mergeVariables( + existing: Partial[], + detected: Partial[] +) { + const map = new Map(existing.map((variable) => [variable.key, variable])); + detected.forEach((variable) => { + if (!variable.key) { + return; + } + if (!map.has(variable.key)) { + map.set(variable.key, variable); + } + }); + return Array.from(map.values()); +} diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts index baf663114..50e3a3071 100644 --- a/src/lib/stores/billing.ts +++ b/src/lib/stores/billing.ts @@ -31,7 +31,6 @@ import { user } from './user'; import BudgetLimitAlert from '$routes/(console)/organization-[organization]/budgetLimitAlert.svelte'; import TeamReadonlyAlert from '$routes/(console)/organization-[organization]/teamReadonlyAlert.svelte'; -import ProjectsLimit from '$lib/components/billing/alerts/projectsLimit.svelte'; import EnterpriseTrial from '$routes/(console)/organization-[organization]/enterpriseTrial.svelte'; export const roles = [ @@ -368,32 +367,6 @@ export function calculateTrialDay(org: Models.Organization) { return days; } -export async function checkForProjectsLimit(org: Models.Organization, orgProjectCount?: number) { - if (!isCloud) return; - if (!org) return; - - const plan = await sdk.forConsole.organizations.getPlan({ - organizationId: org.$id - }); - if (!plan) return; - - if (!org.projects) return; - if (org.projects.length > 0) return; - - const projectCount = orgProjectCount; - if (projectCount === undefined) return; - - // not unlimited and current exceeds plan limits! - if (plan.projects > 0 && projectCount > plan.projects) { - headerAlert.add({ - id: 'projectsLimitReached', - component: ProjectsLimit, - show: true, - importance: 12 - }); - } -} - export async function checkForUsageLimit(organization: Models.Organization) { if ( organization?.status === teamStatusReadonly && diff --git a/src/lib/stores/feedback.ts b/src/lib/stores/feedback.ts index 8b140c0ca..49b351647 100644 --- a/src/lib/stores/feedback.ts +++ b/src/lib/stores/feedback.ts @@ -105,8 +105,6 @@ function createFeedbackStore() { return feedback; }); }, - // TODO: update growth server to accept `billingPlan`. - // TODO: update growth server to accept `source` key to know the feedback source area. submitFeedback: async ( subject: string, message: string, @@ -122,18 +120,6 @@ function createFeedbackStore() { if (!VARS.GROWTH_ENDPOINT) return; trackEvent(Submit.FeedbackSubmit); - const customFields: Array<{ id: string; value: string | number }> = [ - { id: '47364', value: currentPage } - ]; - - if (value) { - customFields.push({ id: '40655', value }); - } - - if (billingPlan) { - customFields.push({ id: '56109', value: billingPlan }); - } - const response = await fetch(`${VARS.GROWTH_ENDPOINT}/feedback`, { method: 'POST', headers: { @@ -143,9 +129,11 @@ function createFeedbackStore() { subject, message, email, - customFields, firstname: (name || 'Unknown').slice(0, 40), metaFields: { + currentPage, + npsScore: value, + billingPlan, source: get(feedback).source, orgId, projectId, diff --git a/src/lib/stores/migration.ts b/src/lib/stores/migration.ts index 534cddc6a..4d1205a72 100644 --- a/src/lib/stores/migration.ts +++ b/src/lib/stores/migration.ts @@ -1,5 +1,10 @@ import { writable } from 'svelte/store'; -import { Resources } from '@appwrite.io/console'; +import { + AppwriteMigrationResource, + FirebaseMigrationResource, + NHostMigrationResource, + SupabaseMigrationResource +} from '@appwrite.io/console'; import { includesAll } from '$lib/helpers/array'; const initialFormData = { @@ -51,66 +56,47 @@ export const ResourcesFriendly = { row: { singular: 'Row', plural: 'Rows' } }; -// @todo: @itznotabug - check if other resources are correct and work fine! -export const providerResources: Record = { - appwrite: Object.values(Resources), - supabase: [ - Resources.User, - Resources.Database, - Resources.Collection, - Resources.Attribute, - Resources.Index, - Resources.Document, - Resources.Bucket, - Resources.File - ], - nhost: [ - Resources.User, - Resources.Database, - Resources.Collection, - Resources.Attribute, - Resources.Index, - Resources.Document, - Resources.Bucket, - Resources.File - ], - firebase: [ - Resources.User, - Resources.Database, - Resources.Collection, - Resources.Attribute, - Resources.Document, - Resources.Bucket, - Resources.File - ] +export const MigrationResources = AppwriteMigrationResource; + +export type MigrationResource = + | AppwriteMigrationResource + | FirebaseMigrationResource + | SupabaseMigrationResource + | NHostMigrationResource; + +export const providerResources: Record = { + appwrite: Object.values(AppwriteMigrationResource), + supabase: Object.values(SupabaseMigrationResource), + nhost: Object.values(NHostMigrationResource), + firebase: Object.values(FirebaseMigrationResource) }; export const migrationFormToResources = ( formData: MigrationFormData, provider: Provider -): Resources[] => { - const resources: Resources[] = []; - const addResource = (resource: Resources) => { +): MigrationResource[] => { + const resources: MigrationResource[] = []; + const addResource = (resource: MigrationResource) => { if (providerResources[provider].includes(resource)) { resources.push(resource); } }; if (formData.users.root) { - addResource(Resources.User); + addResource(MigrationResources.User); } if (formData.databases.root) { - addResource(Resources.Database); - addResource(Resources.Table); - addResource(Resources.Column); - addResource(Resources.Index); + addResource(MigrationResources.Database); + addResource(MigrationResources.Table); + addResource(MigrationResources.Column); + addResource(MigrationResources.Index); } if (formData.databases.rows) { - addResource(Resources.Row); + addResource(MigrationResources.Row); } if (formData.storage.root) { - addResource(Resources.Bucket); - addResource(Resources.File); + addResource(MigrationResources.Bucket); + addResource(MigrationResources.File); } return resources; @@ -137,18 +123,29 @@ export const isVersionAtLeast = (version: string, atLeast: string) => { return compareVersions(version, atLeast) >= 0; }; -export const resourcesToMigrationForm = (resources: Resources[]): MigrationFormData => { +export const resourcesToMigrationForm = (resources: MigrationResource[]): MigrationFormData => { const formData = { ...initialFormData }; - if (resources.includes(Resources.User)) { + if (resources.includes(MigrationResources.User)) { formData.users.root = true; } - if (resources.includes(Resources.Database)) { + if (resources.includes(MigrationResources.Database)) { formData.databases.root = true; } - if (includesAll(resources, [Resources.Table, Resources.Column, Resources.Row] as Resources[])) { + if ( + includesAll(resources, [ + MigrationResources.Table, + MigrationResources.Column, + MigrationResources.Row + ] as MigrationResource[]) + ) { formData.databases.rows = true; } - if (includesAll(resources, [Resources.Bucket, Resources.File] as Resources[])) { + if ( + includesAll(resources, [ + MigrationResources.Bucket, + MigrationResources.File + ] as MigrationResource[]) + ) { formData.storage.root = true; } diff --git a/src/routes/(authenticated)/git/authorize-contributor/+page.svelte b/src/routes/(authenticated)/git/authorize-contributor/+page.svelte index c8cebcfc4..a23855236 100644 --- a/src/routes/(authenticated)/git/authorize-contributor/+page.svelte +++ b/src/routes/(authenticated)/git/authorize-contributor/+page.svelte @@ -53,5 +53,5 @@ The deployment for pull request #{data.providerPullRequestId} is awaiting approval. When authorized, deployments will be started. - + diff --git a/src/routes/(console)/(migration-wizard)/resource-form.svelte b/src/routes/(console)/(migration-wizard)/resource-form.svelte index 54e3f23ba..acc2efcec 100644 --- a/src/routes/(console)/(migration-wizard)/resource-form.svelte +++ b/src/routes/(console)/(migration-wizard)/resource-form.svelte @@ -7,11 +7,18 @@ createMigrationProviderStore, type MigrationFormData, providerResources, - resourcesToMigrationForm + resourcesToMigrationForm, + MigrationResources } from '$lib/stores/migration'; import { Button } from '$lib/elements/forms'; import { wizard } from '$lib/stores/wizard'; - import { Resources, type Models } from '@appwrite.io/console'; + import { + type Models, + AppwriteMigrationResource, + FirebaseMigrationResource, + NHostMigrationResource, + SupabaseMigrationResource + } from '@appwrite.io/console'; import type { sdk } from '$lib/stores/sdk'; import ImportReport from '$routes/(console)/project-[region]-[project]/settings/migrations/(import)/importReport.svelte'; @@ -46,7 +53,7 @@ switch ($provider.provider) { case 'appwrite': report = await projectSdk.migrations.getAppwriteReport({ - resources: providerResources.appwrite, + resources: providerResources.appwrite as AppwriteMigrationResource[], endpoint: $provider.endpoint, projectID: $provider.projectID, key: $provider.apiKey @@ -54,7 +61,7 @@ break; case 'supabase': report = await projectSdk.migrations.getSupabaseReport({ - resources: providerResources.supabase, + resources: providerResources.supabase as SupabaseMigrationResource[], endpoint: $provider.endpoint, apiKey: $provider.apiKey, databaseHost: $provider.host, @@ -65,13 +72,13 @@ break; case 'firebase': report = await projectSdk.migrations.getFirebaseReport({ - resources: providerResources.firebase, + resources: providerResources.firebase as FirebaseMigrationResource[], serviceAccount: $provider.serviceAccount }); break; case 'nhost': report = await projectSdk.migrations.getNHostReport({ - resources: providerResources.nhost, + resources: providerResources.nhost as NHostMigrationResource[], subdomain: $provider.subdomain, region: $provider.region, adminSecret: $provider.adminSecret, @@ -100,13 +107,19 @@ } if (groupKey === 'storage') { - return resources.includes(Resources.Bucket) && resources.includes(Resources.File); + return ( + resources.includes(MigrationResources.Bucket) && + resources.includes(MigrationResources.File) + ); } - // Map groupKey to Resources enum - const groupToResource: Record = { - users: Resources.User, - databases: Resources.Database + // Map groupKey to MigrationResources enum + const groupToResource: Record< + string, + (typeof MigrationResources)[keyof typeof MigrationResources] + > = { + users: MigrationResources.User, + databases: MigrationResources.Database }; const resource = groupToResource[groupKey]; return resource ? resources.includes(resource) : false; diff --git a/src/routes/(console)/(migration-wizard)/wizard.svelte b/src/routes/(console)/(migration-wizard)/wizard.svelte index 1b63e87c6..13430e898 100644 --- a/src/routes/(console)/(migration-wizard)/wizard.svelte +++ b/src/routes/(console)/(migration-wizard)/wizard.svelte @@ -6,7 +6,7 @@ import ResourceForm from './resource-form.svelte'; import { requestedMigration } from '$routes/store'; import { formData, provider, selectedProject, selectedRegion } from '.'; - import { ID, type Models, Query } from '@appwrite.io/console'; + import { ID, type Models, Query, AppwriteMigrationResource } from '@appwrite.io/console'; import { InputSelect, InputText } from '$lib/elements/forms'; import { Button, @@ -114,7 +114,7 @@ try { await projectSdkInstance.migrations.createAppwriteMigration({ - resources, + resources: resources as AppwriteMigrationResource[], endpoint: $provider.endpoint, projectId: $provider.projectID, apiKey: $provider.apiKey diff --git a/src/routes/(console)/+layout.svelte b/src/routes/(console)/+layout.svelte index 1b0230829..c7a11414c 100644 --- a/src/routes/(console)/+layout.svelte +++ b/src/routes/(console)/+layout.svelte @@ -18,7 +18,6 @@ checkForMarkedForDeletion, checkForMissingPaymentMethod, checkForNewDevUpgradePro, - checkForProjectsLimit, checkForUsageLimit, checkPaymentAuthorizationRequired, paymentExpired, @@ -39,7 +38,7 @@ import { showSupportModal } from './wizard/support/store'; import { activeHeaderAlert, consoleVariables } from './store'; - import { base } from '$app/paths'; + import { base, resolve } from '$app/paths'; import { headerAlert } from '$lib/stores/headerAlert'; import { UsageRates } from '$lib/components/billing'; import { canSeeProjects } from '$lib/stores/roles'; @@ -54,11 +53,8 @@ IconSparkles, IconSwitchHorizontal } from '@appwrite.io/pink-icons-svelte'; - import type { LayoutData } from './$types'; import type { Models } from '@appwrite.io/console'; - export let data: LayoutData; - function kebabToSentenceCase(str: string) { return str .split('-') @@ -75,9 +71,7 @@ $: $registerCommands([ { label: 'Go to Projects', - callback: () => { - goto(base); - }, + callback: () => goto(resolve('/')), keys: ['g', 'p'], group: 'navigation', disabled: @@ -296,9 +290,6 @@ if (currentOrganizationId === org.$id) return; if (isCloud) { currentOrganizationId = org.$id; - const orgProjectCount = - data.currentOrgId === org.$id ? data.allProjectsCount : undefined; - await checkForProjectsLimit(org, orgProjectCount); checkForEnterpriseTrial(org); await checkForUsageLimit(org); checkForMarkedForDeletion(org); diff --git a/src/routes/(console)/apply-credit/+page.svelte b/src/routes/(console)/apply-credit/+page.svelte index e2ad497d5..e4bdd6a83 100644 --- a/src/routes/(console)/apply-credit/+page.svelte +++ b/src/routes/(console)/apply-credit/+page.svelte @@ -316,7 +316,7 @@ {/if} - {#if selectedOrgId && !selectedOrg?.billingPlanDetails.addons.seats.supported} + {#if selectedOrgId && !(selectedOrg?.billingPlanDetails?.addons?.seats?.supported ?? true)} {#if selectedOrgId === newOrgId} region.$id === project.region); } - function isSetToArchive(project: Models.Project): boolean { - if (!isCloud) return false; - if (!project || !project.$id) return false; - return project.status === 'archived'; - } - - const projectsToArchive = $derived( - (data.archivedProjectsPage ?? data.projects.projects).filter( - (project) => project.status === 'archived' - ) - ); - - const activeTotalOverall = $derived( - data?.activeTotalOverall ?? - data?.organization?.projects?.length ?? - data?.projects?.total ?? - 0 - ); + const activeProjectsTotal = $derived(data?.projects.total); function clearSearch() { searchQuery?.clearInput(); @@ -162,11 +141,6 @@ }); - - @@ -196,34 +170,11 @@ {/if} - {#if isCloud && data.currentPlan?.projects && data.currentPlan?.projects > 0 && data.organization.projects.length > 0 && $canWriteProjects && (projectsToArchive.length > 0 || data.projects.total > data.currentPlan.projects)} - {@const difference = projectsToArchive.length} - {@const messagePrefix = - difference !== 1 ? `${difference} projects are` : `${difference} project is`} - - Upgrade your plan to restore archived projects - - - - - {/if} - - {#if isCloud && data.currentPlan?.projects !== 0 && projectsToArchive.length === 0 && !freePlanAlertDismissed} + {#if isCloud && !data.program && data.currentPlan?.projects && activeProjectsTotal <= data.currentPlan.projects && !freePlanAlertDismissed} Your Free plan includes up to 2 projects and limited resources. Upgrade to unlock - more capacity and features. + >Your Free plan includes up to {data.currentPlan?.projects} projects and limited resources. + Upgrade to unlock more capacity and features. - {/if} + + +
+ {#if !currentPlan.requiresPaymentMethod} + + {:else if $organization?.billingPlanDowngrade !== null} + + {:else} + + {/if} +
+ + Your payment is still being processed, check with your payment provider. + +
+ {#if !currentPlan?.usagePerProject} -
- {:else} - - {#if $organization?.billingPlanDowngrade !== null} - - {:else} - - {/if} - {#if !currentPlan?.usagePerProject} - - {/if} - - {/if} + {/if} + {/key} diff --git a/src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte b/src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte index 0d4c8b132..f1fd80d7e 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte @@ -31,6 +31,7 @@ const isTrial = new Date($organization?.billingStartDate).getTime() - today.getTime() > 0 && $organization?.billingTrialDays; /* number of trial days. */ + $: isUpgrading = $organization?.status === 'upgrading'; const extraUsage = currentInvoice ? currentInvoice.amount - currentPlan?.price : 0; @@ -41,9 +42,11 @@ A breakdown of your estimated upcoming payment for the current billing period. Totals displayed exclude accumulated credits and applicable taxes. -

- Due at: {toLocaleDate($organization?.billingNextInvoiceDate)} -

+ +

+ Due at: {toLocaleDate($organization?.billingNextInvoiceDate)} +

+
@@ -165,50 +168,55 @@
- {#if !currentPlan.requiresPaymentMethod} -
- {#if !currentPlan?.usagePerProject} - - {/if} +
+ +
+ {#if !currentPlan.requiresPaymentMethod} + + {:else if $organization?.billingPlanDowngrade !== null} + + {:else} + + {/if} +
+ + Your payment is still being processed, check with your payment provider. + +
+ {#if !currentPlan?.usagePerProject} -
- {:else} -
- {#if $organization?.billingPlanDowngrade !== null} - - {:else} - - {/if} - {#if !currentPlan?.usagePerProject} - - {/if} -
- {/if} + {/if} +
{/if} diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte index 7ad7171c0..bef7c9f9b 100644 --- a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte +++ b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte @@ -46,9 +46,6 @@ let previousPage: string = resolve('/'); let showExitModal = false; let formComponent: Form; - let usageLimitsComponent: - | { validateOrAlert: () => boolean; getSelectedProjects: () => string[] } - | undefined; let isSubmitting = writable(false); let collaborators: string[] = data?.members?.memberships @@ -62,7 +59,7 @@ let feedbackDowngradeReason: string; let feedbackMessage: string; let orgUsage: Models.UsageOrganization; - let allProjects: { projects: Models.Project[] } | undefined; + let allProjects: { projects: Models.Project[] } = { projects: [] }; $: paymentMethods = null; @@ -136,18 +133,15 @@ return paymentMethods; } + function hasExcessProjectsForFreePlan(): boolean { + const freeBasePlan = getBasePlanFromGroup(BillingPlanGroup.Starter); + const freePlanProjectLimit = freeBasePlan?.projects ?? 2; + const currentProjectCount = allProjects.projects.length; + return currentProjectCount > freePlanProjectLimit; + } + async function handleSubmit() { if (isDowngrade) { - // If target plan has a non-zero project limit, ensure selection made - const targetProjectsLimit = selectedPlan?.projects ?? 0; - const shouldShowProjectSelector = - targetProjectsLimit > 0 && allProjects.projects.length > targetProjectsLimit; - - if (shouldShowProjectSelector && usageLimitsComponent?.validateOrAlert) { - const ok = usageLimitsComponent.validateOrAlert(); - if (!ok) return; - } - await downgrade(); } else if (isUpgrade) { await upgrade(); @@ -173,6 +167,18 @@ } async function downgrade() { + if (selectedPlan.group === BillingPlanGroup.Starter && hasExcessProjectsForFreePlan()) { + const freeBasePlan = getBasePlanFromGroup(BillingPlanGroup.Starter); + const freePlanProjectLimit = freeBasePlan?.projects ?? 2; + const currentProjectCount = allProjects?.projects?.length ?? 0; + + addNotification({ + type: 'error', + message: `Please delete ${currentProjectCount - freePlanProjectLimit} project${currentProjectCount - freePlanProjectLimit !== 1 ? 's' : ''} before downgrading` + }); + return; + } + try { // 1) update the plan first await sdk.forConsole.organizations.updatePlan({ @@ -181,22 +187,6 @@ paymentMethodId }); - // 2) If the target plan has a project limit, apply selected projects now - const targetProjectsLimit = selectedPlan?.projects ?? 0; - if (targetProjectsLimit > 0 && usageLimitsComponent) { - const selected = usageLimitsComponent.getSelectedProjects(); - if (selected?.length) { - try { - await sdk.forConsole.organizations.updateProjects({ - organizationId: data.organization.$id, - projects: selected - }); - } catch (projectError) { - console.warn('Project selection failed after plan update:', projectError); - } - } - } - await Promise.all([trackDowngradeFeedback(), invalidate(Dependencies.ORGANIZATION)]); await goto(previousPage); @@ -323,9 +313,19 @@ $: isUpgrade = selectedPlan.order > $currentPlan?.order; $: isDowngrade = selectedPlan.order < $currentPlan?.order; - $: isButtonDisabled = - $organization?.billingPlanId === selectedPlan.$id || - (isDowngrade && selectedPlan.group === BillingPlanGroup.Starter && data.hasFreeOrgs); + + // Check if projects exceed Free plan limit when downgrading + $: isButtonDisabled = (() => { + const freeBasePlan = getBasePlanFromGroup(BillingPlanGroup.Starter); + const freePlanProjectLimit = freeBasePlan?.projects ?? 2; + const hasExcessProjects = allProjects.projects.length > freePlanProjectLimit; + + if ($organization?.billingPlanId === selectedPlan.$id) return true; + if (isDowngrade && selectedPlan.group === BillingPlanGroup.Starter && data.hasFreeOrgs) + return true; + + return isDowngrade && selectedPlan.group === BillingPlanGroup.Starter && hasExcessProjects; + })(); @@ -405,9 +405,8 @@ {/if} {/if} diff --git a/src/routes/(console)/organization-[organization]/domains/+page.svelte b/src/routes/(console)/organization-[organization]/domains/+page.svelte index 477786e8f..650c287b5 100644 --- a/src/routes/(console)/organization-[organization]/domains/+page.svelte +++ b/src/routes/(console)/organization-[organization]/domains/+page.svelte @@ -178,7 +178,7 @@ limit={data.limit} offset={data.offset} total={data.domains.total} /> - {:else if data?.query} + {:else if data?.query || data?.search}
- {!organization?.billingPlanDetails.addons.seats.supported + {!( + organization?.billingPlanDetails?.addons?.seats?.supported ?? + true + ) ? 'Upgrade to add more members' : `You've reached the members limit for the ${ organization?.billingPlanDetails.name diff --git a/src/routes/(console)/organization-[organization]/members/+page.svelte b/src/routes/(console)/organization-[organization]/members/+page.svelte index ca9969cc0..cdd4c9851 100644 --- a/src/routes/(console)/organization-[organization]/members/+page.svelte +++ b/src/routes/(console)/organization-[organization]/members/+page.svelte @@ -6,7 +6,7 @@ import { base } from '$app/paths'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import Upgrade from '$lib/components/roles/upgrade.svelte'; - import { getRoleLabel } from '$lib/stores/billing'; + import { getServiceLimit, readOnly, getRoleLabel } from '$lib/stores/billing'; import { addNotification } from '$lib/stores/notifications'; import { currentPlan, newMemberModal, organization } from '$lib/stores/organization'; import { isOwner } from '$lib/stores/roles'; @@ -14,7 +14,7 @@ import type { Models } from '@appwrite.io/console'; import Delete from '../deleteMember.svelte'; import Edit from './edit.svelte'; - import { isCloud } from '$lib/system'; + import { isCloud, GRACE_PERIOD_OVERRIDE } from '$lib/system'; import { IconDotsHorizontal, IconInfo, @@ -45,8 +45,10 @@ // Calculate if button should be disabled and tooltip should show $: memberCount = data.organizationMembers?.total ?? 0; $: supportsMembers = $organization?.billingPlanDetails?.addons?.seats; - $: isFreeWithMembers = !supportsMembers && memberCount >= 1; - $: isButtonDisabled = isCloud ? isFreeWithMembers : false; + $: limit = getServiceLimit('members', null, $currentPlan) || Infinity; + $: isLimited = limit !== 0 && limit < Infinity; + $: isButtonDisabled = + isCloud && (($readOnly && !GRACE_PERIOD_OVERRIDE) || (isLimited && memberCount >= limit)); const resend = async (member: Models.Membership) => { try { diff --git a/src/routes/(console)/organization-[organization]/settings/BAAModal.svelte b/src/routes/(console)/organization-[organization]/settings/BAAModal.svelte index a6835a9c0..1aaccfef2 100644 --- a/src/routes/(console)/organization-[organization]/settings/BAAModal.svelte +++ b/src/routes/(console)/organization-[organization]/settings/BAAModal.svelte @@ -67,13 +67,11 @@ firstName: ($user?.name ?? '').slice(0, 40), message: `BAA request for ${$organization?.name ?? ''} (${$organization?.$id ?? ''})`, tags: ['cloud'], - customFields: [ - { id: '41612', value: 'BAA' }, - { id: '48493', value: $user?.name ?? '' }, - { id: '48492', value: $organization?.$id ?? '' }, - { id: '48490', value: $user?.$id ?? '' } - ], metaFields: { + category: 'BAA', + userName: $user?.name ?? '', + orgId: $organization?.$id ?? '', + userId: $user?.$id ?? '', employees: employees, country: country, role: role, diff --git a/src/routes/(console)/organization-[organization]/settings/Soc2Modal.svelte b/src/routes/(console)/organization-[organization]/settings/Soc2Modal.svelte index bce0634a3..c4b3b645d 100644 --- a/src/routes/(console)/organization-[organization]/settings/Soc2Modal.svelte +++ b/src/routes/(console)/organization-[organization]/settings/Soc2Modal.svelte @@ -68,13 +68,11 @@ firstName: ($user?.name ?? '').slice(0, 40), message: `SOC-2 request for ${$organization?.name ?? ''} (${$organization?.$id ?? ''})`, tags: ['cloud'], - customFields: [ - { id: '41612', value: 'SOC-2' }, - { id: '48493', value: $user?.name ?? '' }, - { id: '48492', value: $organization?.$id ?? '' }, - { id: '48490', value: $user?.$id ?? '' } - ], metaFields: { + category: 'SOC-2', + userName: $user?.name ?? '', + orgId: $organization?.$id ?? '', + userId: $user?.$id ?? '', employees: employees, country: country, role: role, diff --git a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/totalMembers.svelte b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/totalMembers.svelte index 302bdfcd1..e63977881 100644 --- a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/totalMembers.svelte +++ b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/totalMembers.svelte @@ -12,14 +12,15 @@ $: total = members?.total ?? 0; $: organizationMembersSupported = - !$organization?.billingPlanDetails.addons.seats.supported; /* false on free */ + $organization?.billingPlanDetails?.addons?.seats?.supported ?? + true; /* true on paid plans */ Members The number of members in your organization. - {#if !organizationMembersSupported} + {#if organizationMembersSupported}
diff --git a/src/routes/(console)/project-[region]-[project]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/+layout.svelte index 763287a38..2aac95d82 100644 --- a/src/routes/(console)/project-[region]-[project]/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/+layout.svelte @@ -25,6 +25,11 @@ canWriteSites } from '$lib/stores/roles'; import CsvImportBox from '$lib/components/csvImportBox.svelte'; + import { isCloud } from '$lib/system'; + import PausedProjectModal from './pausedProjectModal.svelte'; + import type { LayoutData } from './$types'; + + export let data: LayoutData; onMount(() => { return realtime.forProject(page.params.region, ['project', 'console'], (response) => { @@ -114,6 +119,10 @@ +{#if isCloud && data.project?.status === 'paused'} + +{/if} +
diff --git a/src/routes/(console)/project-[region]-[project]/+layout.ts b/src/routes/(console)/project-[region]-[project]/+layout.ts index 26683edda..8da51bb51 100644 --- a/src/routes/(console)/project-[region]-[project]/+layout.ts +++ b/src/routes/(console)/project-[region]-[project]/+layout.ts @@ -11,13 +11,25 @@ import { loadAvailableRegions } from '$routes/(console)/regions'; import { type Models, Platform } from '@appwrite.io/console'; import { redirect } from '@sveltejs/kit'; import { resolve } from '$app/paths'; +import { generateFingerprintToken } from '$lib/helpers/fingerprint'; import { normalizeConsoleVariables } from '$lib/helpers/domains'; +import { browser } from '$app/environment'; export const load: LayoutLoad = async ({ params, depends, parent }) => { const { plansInfo, organizations, preferences: prefs } = await parent(); depends(Dependencies.PROJECT); const project = await sdk.forConsole.projects.get({ projectId: params.project }); + if (project.status !== 'active' && project.status !== 'paused') { + // project isn't active, redirect back to organizations page + redirect( + 303, + resolve('/(console)/organization-[organization]', { + organization: project.teamId + }) + ); + } + project.region ??= 'default'; // fast path without a network call! @@ -92,6 +104,22 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => { plansInfo.set(organization.billingPlanId, organizationPlan); } + // Track console access for cloud projects (fire-and-forget, backend has 6-day cooldown). + // Skip if paused — user must explicitly resume via the paused project modal. + if (isCloud && browser && project.status !== 'paused') { + generateFingerprintToken() + .then((fingerprint) => { + sdk.forConsole.client.headers['X-Appwrite-Console-Fingerprint'] = fingerprint; + return sdk.forConsole.projects.updateConsoleAccess({ + projectId: params.project + }); + }) + .catch((e) => console.error('Failed to update console access:', e)) + .finally(() => { + delete sdk.forConsole.client.headers['X-Appwrite-Console-Fingerprint']; + }); + } + return { project, organization, diff --git a/src/routes/(console)/project-[region]-[project]/auth/teams/team-[team]/updatePrefs.svelte b/src/routes/(console)/project-[region]-[project]/auth/teams/team-[team]/updatePrefs.svelte index 3320b31e4..b38435249 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/teams/team-[team]/updatePrefs.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/teams/team-[team]/updatePrefs.svelte @@ -22,9 +22,7 @@ $: if (prefs) { const currentNormalized = normalizePrefs(prefs); - const originalNormalized = normalizePrefs( - Object.entries(($team?.prefs ?? {}) as Record) - ); + const originalNormalized = normalizePrefs(Object.entries($team?.prefs ?? {})); arePrefsDisabled = deepEqual(currentNormalized, originalNormalized); } @@ -33,10 +31,12 @@ let arePrefsDisabled = true; onMount(async () => { - const entries = Object.entries(($team?.prefs ?? {}) as Record); + const entries = Object.entries($team?.prefs ?? {}); prefs = entries.length > 0 - ? entries.map(([key, value]) => createPrefRow(key, value)) + ? entries.map(([key, value]) => + createPrefRow(String(key ?? ''), String(value ?? '')) + ) : [createPrefRow()]; }); diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/email2FATemplate.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/email2FATemplate.svelte index 932bd7e2c..0cea45a33 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/email2FATemplate.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/templates/email2FATemplate.svelte @@ -23,12 +23,12 @@ try { const template = await loadEmailTemplate( project.$id, - EmailTemplateType.Mfachallenge, + EmailTemplateType.MfaChallenge, locale ); emailTemplate.set(template); $baseEmailTemplate = { ...$emailTemplate }; - trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.Mfachallenge }); + trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.MfaChallenge }); } catch (error) { trackError(error, Submit.EmailChangeLocale); addNotification({ diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/emailMagicUrlTemplate.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/emailMagicUrlTemplate.svelte index ac962841c..62c79c9d3 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/emailMagicUrlTemplate.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/templates/emailMagicUrlTemplate.svelte @@ -23,12 +23,12 @@ try { const template = await loadEmailTemplate( project.$id, - EmailTemplateType.Magicsession, + EmailTemplateType.MagicSession, locale ); emailTemplate.set(template); $baseEmailTemplate = { ...$emailTemplate }; - trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.Magicsession }); + trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.MagicSession }); } catch (error) { trackError(error, Submit.EmailChangeLocale); addNotification({ diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/emailOtpSessionTemplate.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/emailOtpSessionTemplate.svelte index 1ec2f24bd..76bb1ef5d 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/emailOtpSessionTemplate.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/templates/emailOtpSessionTemplate.svelte @@ -23,12 +23,12 @@ try { const template = await loadEmailTemplate( project.$id, - EmailTemplateType.Otpsession, + EmailTemplateType.OtpSession, locale ); emailTemplate.set(template); $baseEmailTemplate = { ...$emailTemplate }; - trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.Otpsession }); + trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.OtpSession }); } catch (error) { trackError(error, Submit.EmailChangeLocale); addNotification({ diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/emailSessionAlertTemplate.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/emailSessionAlertTemplate.svelte index ecd1cbb28..e8b8f49de 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/emailSessionAlertTemplate.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/templates/emailSessionAlertTemplate.svelte @@ -23,12 +23,12 @@ try { const template = await loadEmailTemplate( project.$id, - EmailTemplateType.Sessionalert, + EmailTemplateType.SessionAlert, locale ); emailTemplate.set(template); $baseEmailTemplate = { ...$emailTemplate }; - trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.Sessionalert }); + trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.SessionAlert }); } catch (error) { trackError(error, Submit.EmailChangeLocale); addNotification({ diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/store.ts b/src/routes/(console)/project-[region]-[project]/auth/templates/store.ts index d02f9de08..9ae9f838b 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/store.ts +++ b/src/routes/(console)/project-[region]-[project]/auth/templates/store.ts @@ -51,13 +51,13 @@ export const templates = [ component: EmailVerificationTemplate }, { - key: EmailTemplateType.Magicsession, + key: EmailTemplateType.MagicSession, title: 'Magic URL', description: 'Send an email to users that sign in with a magic URL.', component: EmailMagicUrlTemplate }, { - key: EmailTemplateType.Otpsession, + key: EmailTemplateType.OtpSession, title: 'OTP session', description: 'Send an email to users that sign in with a email OTP.', component: EmailOtpSessionTemplate @@ -75,13 +75,13 @@ export const templates = [ component: EmailInviteTemplate }, { - key: EmailTemplateType.Mfachallenge, + key: EmailTemplateType.MfaChallenge, title: '2FA verification', description: 'Send a two-factor authentication email to a user.', component: Email2FaTemplate }, { - key: EmailTemplateType.Sessionalert, + key: EmailTemplateType.SessionAlert, title: 'Session alert', description: 'Send an email to users when a new session is created.', component: EmailSessionAlertTemplate, diff --git a/src/routes/(console)/project-[region]-[project]/auth/user-[user]/updatePrefs.svelte b/src/routes/(console)/project-[region]-[project]/auth/user-[user]/updatePrefs.svelte index c3f6c54aa..932583f94 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/user-[user]/updatePrefs.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/user-[user]/updatePrefs.svelte @@ -34,7 +34,9 @@ const entries = Object.entries($user?.prefs ?? {}); prefs = entries.length > 0 - ? entries.map(([key, value]) => createPrefRow(key, value)) + ? entries.map(([key, value]) => + createPrefRow(String(key ?? ''), String(value ?? '')) + ) : [createPrefRow()]; }); diff --git a/src/routes/(console)/project-[region]-[project]/databases/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/+page.svelte index e4d535952..863de545a 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/+page.svelte @@ -94,7 +94,7 @@ {#if $canWriteDatabases} @@ -108,7 +108,9 @@
- You have reached the maximum number of databases for your plan. +
+ You have reached the maximum number of databases for your plan. +
{/if} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts index 2f5f38b95..252589f9f 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts @@ -75,7 +75,7 @@ export type DatabaseSdkResult = { deleteRecord: (params: { databaseId: string; entityId: string; - recordId?: string; + recordId: string; databaseType?: DatabaseType; }) => Promise; deleteRecords: (params: { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.svelte index 6b5ec9690..9c2b21871 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.svelte @@ -212,6 +212,7 @@ diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte index 1e671a9b2..6b035712f 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte @@ -226,8 +226,11 @@ size="s" secondary class="small-button-dimensions" - disabled={!(hasColumns && hasValidColumns && data.rows.total) || - disableButton} + disabled={!( + hasColumns && + hasValidColumns && + data.rows?.total + ) || disableButton} on:click={() => { trackEvent(Click.DatabaseExportCsv); goto(getTableExportUrl()); @@ -238,13 +241,13 @@ Export CSV - + + + Protect column against data leaks for best privacy compliance. Encrypted + columns cannot be queried. + +
+ + + + Available on Pro plan. Upgrade + to enable encrypted columns. + + + +
+
+ + + Encryption can only be set when creating the column. + + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/longtext.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/longtext.svelte index 8087541af..92580dcf5 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/longtext.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/longtext.svelte @@ -17,7 +17,8 @@ key, required: data.required, xdefault: data.default, - array: data.array + array: data.array, + encrypt: data.encrypt }); } export async function updateLongtext( @@ -42,17 +43,21 @@ + min={data.encrypt ? 150 : 1} + max={16383} + helper={data.encrypt + ? 'Encrypted varchar columns require a minimum size of 150.' + : undefined} /> {#if !editing} @@ -173,3 +185,7 @@ {disabled} bind:array={data.array} bind:required={data.required} /> + + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/createColumn.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/createColumn.svelte index ed9228b03..450cd2851 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/createColumn.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/createColumn.svelte @@ -48,6 +48,7 @@ required: column?.required ?? false, array: column?.array ?? false, default: column?.default ?? null, + encrypt: (column as { encrypt?: boolean })?.encrypt ?? false, ...column } as Partial); @@ -61,7 +62,8 @@ data = { required: false, array: false, - default: null + default: null, + encrypt: false }; /* default to text */ diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte index 30a761050..c77b87ae1 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte @@ -15,7 +15,7 @@ isRelationship, isRelationshipToMany, isSpatialType, - isString + isTextType } from '$database/table-[table]/rows/store'; import { columns, @@ -1101,7 +1101,9 @@ {@const isEmptyArray = formatted === 'Empty'} {@const isDatetimeAttribute = rowColumn.type === 'datetime'} {@const isEncryptedAttribute = - isString(rowColumn) && rowColumn.encrypt} + isTextType(rowColumn) && + 'encrypt' in rowColumn && + rowColumn.encrypt} {#if isDatetimeAttribute} Timestamp diff --git a/src/routes/(console)/project-[region]-[project]/functions/+page.svelte b/src/routes/(console)/project-[region]-[project]/functions/+page.svelte index 0eeb67c8b..a02b38e35 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/+page.svelte @@ -110,7 +110,9 @@
- You have reached the maximum number of functions for your plan. +
+ You have reached the maximum number of functions for your plan. +
diff --git a/src/routes/(console)/project-[region]-[project]/functions/create-function/repository-[repository]/+page.svelte b/src/routes/(console)/project-[region]-[project]/functions/create-function/repository-[repository]/+page.svelte index 9a06f70dd..20dcb484c 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/create-function/repository-[repository]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/create-function/repository-[repository]/+page.svelte @@ -22,6 +22,7 @@ import RepoCard from './repoCard.svelte'; import { getIconFromRuntime } from '$lib/stores/runtimes'; import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store'; + import { normalizeDetectedVariables, mergeVariables } from '$lib/helpers/variables'; export let data; @@ -59,6 +60,12 @@ let detectingRuntime = true; + let prevRootDir = rootDir; + $: if (rootDir !== prevRootDir) { + prevRootDir = rootDir; + detectRuntime(); + } + onMount(async () => { installation.set(data.installation); repository.set(data.repository); @@ -82,6 +89,10 @@ entrypoint = detections.entrypoint; buildCommand = detections.commands; runtime = detections.runtime as Runtime; + const detectedVariables = normalizeDetectedVariables(detections?.variables); + if (detectedVariables.length) { + variables = mergeVariables(variables, detectedVariables); + } trackEvent(Submit.FrameworkDetect, { runtime, source: 'repository' }); } catch (error) { @@ -189,7 +200,11 @@ installationId={data.installation.$id} repositoryId={data.repository.id} /> - + diff --git a/src/routes/(console)/project-[region]-[project]/functions/create-function/repository-[repository]/configuration.svelte b/src/routes/(console)/project-[region]-[project]/functions/create-function/repository-[repository]/configuration.svelte index 8f3091f39..c6a5dbd74 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/create-function/repository-[repository]/configuration.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/create-function/repository-[repository]/configuration.svelte @@ -3,9 +3,13 @@ import { Link } from '$lib/elements'; import { InputText } from '$lib/elements/forms'; import { Accordion, Fieldset, Layout } from '@appwrite.io/pink-svelte'; + import type { Models } from '@appwrite.io/console'; + import { EnvironmentVariables } from '$lib/components/variables'; export let buildCommand = ''; export let roles: string[] = []; + export let variables: Partial[] = []; + export let isLoading = false;
@@ -31,5 +35,11 @@ +
diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/domains/add-domain/verify-[domain]/+page.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/domains/add-domain/verify-[domain]/+page.svelte index 07f3c49d4..7701ebd8d 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/domains/add-domain/verify-[domain]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/domains/add-domain/verify-[domain]/+page.svelte @@ -29,8 +29,8 @@ const ruleId = page.url.searchParams.get('rule'); const showCNAMETab = $derived( - Boolean($regionalConsoleVariables._APP_DOMAIN_FUNCTIONS) && - $regionalConsoleVariables._APP_DOMAIN_FUNCTIONS !== 'localhost' + Boolean($regionalConsoleVariables._APP_DOMAIN_TARGET_CNAME) && + $regionalConsoleVariables._APP_DOMAIN_TARGET_CNAME !== 'localhost' ); const showATab = $derived( !isCloud && diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/domains/retryDomainModal.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/domains/retryDomainModal.svelte index bc90344d6..02a09d4c4 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/domains/retryDomainModal.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/domains/retryDomainModal.svelte @@ -26,8 +26,8 @@ } = $props(); const showCNAMETab = $derived( - Boolean($regionalConsoleVariables._APP_DOMAIN_FUNCTIONS) && - $regionalConsoleVariables._APP_DOMAIN_FUNCTIONS !== 'localhost' + Boolean($regionalConsoleVariables._APP_DOMAIN_TARGET_CNAME) && + $regionalConsoleVariables._APP_DOMAIN_TARGET_CNAME !== 'localhost' ); const showATab = $derived( !isCloud && diff --git a/src/routes/(console)/project-[region]-[project]/overview/platforms/action.svelte b/src/routes/(console)/project-[region]-[project]/overview/platforms/action.svelte index 233742854..f08d2f0cc 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/platforms/action.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/platforms/action.svelte @@ -20,7 +20,7 @@ {#if $canWritePlatforms} {#if isLimited} - +
- You have reached the maximum number of platforms for your plan in a project. +
+ You have reached the maximum number of platforms for your plan in a project. +
{:else} diff --git a/src/routes/(console)/project-[region]-[project]/pausedProjectModal.svelte b/src/routes/(console)/project-[region]-[project]/pausedProjectModal.svelte new file mode 100644 index 000000000..4a2f39d48 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/pausedProjectModal.svelte @@ -0,0 +1,98 @@ + + + + + This project has been paused due to inactivity. + + Your data is safe and will remain intact. Resume the project to continue using it. + + + {#if error} + (error = null)}> + {error} + + {/if} + + + + + + + + + diff --git a/src/routes/(console)/project-[region]-[project]/settings/migrations/(import)/wizard.svelte b/src/routes/(console)/project-[region]-[project]/settings/migrations/(import)/wizard.svelte index a5eac4dbc..4dfd64719 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/migrations/(import)/wizard.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/migrations/(import)/wizard.svelte @@ -7,6 +7,12 @@ import { invalidate } from '$app/navigation'; import { Dependencies } from '$lib/constants'; import { migrationFormToResources, type Provider } from '$lib/stores/migration'; + import { + AppwriteMigrationResource, + FirebaseMigrationResource, + NHostMigrationResource, + SupabaseMigrationResource + } from '@appwrite.io/console'; import { started } from '../stores'; import { showMigrationBox } from '$lib/components/migrationBox.svelte'; import { addNotification } from '$lib/stores/notifications'; @@ -45,7 +51,7 @@ await sdk .forProject(page.params.region, page.params.project) .migrations.createAppwriteMigration({ - resources, + resources: resources as AppwriteMigrationResource[], endpoint: $provider.endpoint, projectId: $provider.projectID, apiKey: $provider.apiKey @@ -58,7 +64,7 @@ await sdk .forProject(page.params.region, page.params.project) .migrations.createSupabaseMigration({ - resources, + resources: resources as SupabaseMigrationResource[], endpoint: $provider.endpoint, apiKey: $provider.apiKey, databaseHost: $provider.host, @@ -73,7 +79,7 @@ await sdk .forProject(page.params.region, page.params.project) .migrations.createFirebaseMigration({ - resources, + resources: resources as FirebaseMigrationResource[], serviceAccount: $provider.serviceAccount }); await invalidate(Dependencies.MIGRATIONS); @@ -83,7 +89,7 @@ await sdk .forProject(page.params.region, page.params.project) .migrations.createNHostMigration({ - resources, + resources: resources as NHostMigrationResource[], subdomain: $provider.subdomain, region: $provider.region, adminSecret: $provider.adminSecret, @@ -206,7 +212,8 @@
Project settings are not imported + >Project settings are not imported + You will need to set service and project settings manually. @@ -218,7 +225,8 @@
Keep your organization plan's limits in mind + >Keep your organization plan's limits in mind + Make sure to have enough storage in your organization plan when importing files. @@ -256,9 +264,12 @@ variant="secondary" on:click={() => { showExitModal = true; - }}>Cancel + }} + >Cancel + Create + >Create + diff --git a/src/routes/(console)/project-[region]-[project]/settings/webhooks/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/webhooks/+page.svelte index da34dceff..4a2f324ae 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/webhooks/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/webhooks/+page.svelte @@ -39,7 +39,7 @@ {#if $canWriteWebhooks} - +
- You have reached the maximum number of webhooks for your plan. +
+ You have reached the maximum number of webhooks for your plan. +
{/if} diff --git a/src/routes/(console)/project-[region]-[project]/sites/create-site/configuration.svelte b/src/routes/(console)/project-[region]-[project]/sites/create-site/configuration.svelte index dc5d0a140..8b1a9ad3f 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/create-site/configuration.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/create-site/configuration.svelte @@ -1,38 +1,10 @@ diff --git a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/settings/updateRepository.svelte b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/settings/updateRepository.svelte index 56c871ac4..8386f5222 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/settings/updateRepository.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/settings/updateRepository.svelte @@ -116,30 +116,46 @@ } async function connect(selectedInstallationId: string, selectedRepository: string) { + let nextBranch = site?.providerBranch ?? 'main'; try { - await sdk.forProject(page.params.region, page.params.project).sites.update({ - siteId: site.$id, - name: site.name, - framework: site.framework as Framework, - enabled: site?.enabled, - logging: site?.logging || undefined, - timeout: site?.timeout, - installCommand: site?.installCommand, - buildCommand: site?.buildCommand, - outputDirectory: site?.outputDirectory, - buildRuntime: site?.buildRuntime as BuildRuntime, - adapter: site.adapter as Adapter, - fallbackFile: site?.fallbackFile, - installationId: selectedInstallationId, - providerRepositoryId: selectedRepository, - providerBranch: 'main', - specification: site?.specification || undefined - }); - - invalidate(Dependencies.SITE); + const branchList = await sdk + .forProject(page.params.region, page.params.project) + .vcs.listRepositoryBranches({ + installationId: selectedInstallationId, + providerRepositoryId: selectedRepository + }); + const sorted = sortBranches(branchList.branches); + nextBranch = + sorted.find((branch) => branch.name === site?.providerBranch)?.name ?? + sorted.find((branch) => branch.name === 'main' || branch.name === 'master')?.name ?? + sorted[0]?.name ?? + nextBranch; } catch { - return; + // Ignore branch lookup failures; fallback to default. } + + await sdk.forProject(page.params.region, page.params.project).sites.update({ + siteId: site.$id, + name: site.name, + framework: site.framework as Framework, + enabled: site?.enabled, + logging: site?.logging || undefined, + timeout: site?.timeout, + installCommand: site?.installCommand, + buildCommand: site?.buildCommand, + outputDirectory: site?.outputDirectory, + buildRuntime: site?.buildRuntime as BuildRuntime, + adapter: site.adapter as Adapter, + fallbackFile: site?.fallbackFile, + installationId: selectedInstallationId, + providerRepositoryId: selectedRepository, + providerBranch: nextBranch, + providerSilentMode: site?.providerSilentMode ?? undefined, + providerRootDirectory: site?.providerRootDirectory ?? undefined, + specification: site?.specification || undefined + }); + + invalidate(Dependencies.SITE); } $: if (site?.installationId && site?.providerRepositoryId) { diff --git a/src/routes/(console)/project-[region]-[project]/storage/+page.svelte b/src/routes/(console)/project-[region]-[project]/storage/+page.svelte index 42a1faac2..aa629e200 100644 --- a/src/routes/(console)/project-[region]-[project]/storage/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/storage/+page.svelte @@ -44,7 +44,7 @@ view={data.view} searchPlaceholder="Search by name or ID"> {#if $canWriteBuckets} - +
- You have reached the maximum number of buckets for your plan. +
+ You have reached the maximum number of buckets for your plan. +
{/if} diff --git a/src/routes/(console)/supportWizard.svelte b/src/routes/(console)/supportWizard.svelte index b25338521..9b540d96b 100644 --- a/src/routes/(console)/supportWizard.svelte +++ b/src/routes/(console)/supportWizard.svelte @@ -134,16 +134,17 @@ formData.append('subject', $supportData.subject); formData.append('firstName', ($user?.name || 'Unknown').slice(0, 40)); formData.append('message', $supportData.message); + formData.append('tags[]', 'cloud'); formData.append('tags[]', categoryTopicTag); formData.append( - 'customFields', - JSON.stringify([ - { id: '41612', value: $supportData.category }, - { id: '48492', value: $organization?.$id ?? '' }, - { id: '48491', value: $supportData?.project ?? '' }, - { id: '56023', value: $supportData?.severity ?? '' }, - { id: '56024', value: $organization?.billingPlanId ?? '' } - ]) + 'metaFields', + JSON.stringify({ + category: $supportData.category, + orgId: $organization?.$id ?? '', + projectId: $supportData?.project ?? '', + severity: $supportData?.severity ?? '', + billingPlan: $organization?.billingPlanId ?? '' + }) ); if (files && files.length > 0) { formData.append('attachment', files[0]); diff --git a/vite.config.ts b/vite.config.ts index 0db1215e0..f3112124b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,4 +1,4 @@ -import { defineConfig } from 'vite'; +import { defineConfig } from 'vitest/config'; import { sveltekit } from '@sveltejs/kit/vite'; import { sentrySvelteKit } from '@sentry/sveltekit'; import { svelteTesting } from '@testing-library/svelte/vite';