diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1421eeacc9..4bfe432c7e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -84,6 +84,31 @@ jobs:
sarif_file: 'trivy-fs-results.sarif'
category: 'trivy-source'
+ composer:
+ name: Checks / Composer
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+ tools: composer:v2
+ coverage: none
+
+ - name: Validate
+ run: composer validate
+
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-progress --ignore-platform-reqs
+
+ - name: Audit
+ env:
+ COMPOSER_NO_AUDIT: 0
+ run: composer audit
+
format:
name: Checks / Format
runs-on: ubuntu-latest
@@ -96,15 +121,18 @@ jobs:
- run: git checkout HEAD^2
if: github.event_name == 'pull_request'
- - name: Validate composer.json and composer.lock
- run: |
- docker run --rm -v $PWD:/app composer:2.8 sh -c \
- "composer validate"
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+ tools: composer:v2
+ coverage: none
+
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-progress --ignore-platform-reqs
- name: Run Linter
- run: |
- docker run --rm -v $PWD:/app composer:2.8 sh -c \
- "composer install --profile --ignore-platform-reqs && composer lint"
+ run: composer lint
analyze:
name: Checks / Analyze
@@ -113,15 +141,33 @@ jobs:
- name: Check out the repo
uses: actions/checkout@v6
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+ tools: composer:v2
+ coverage: none
+
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-progress --ignore-platform-reqs
+
- name: Run PHPStan
- run: |
- docker run --rm -v $PWD:/app composer:2.8 sh -c \
- "composer install --profile --ignore-platform-reqs && composer analyze"
+ run: composer analyze
+
+ locale:
+ name: Checks / Locale
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out the repo
+ uses: actions/checkout@v6
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: '24'
- name: Run Locale check
- run: |
- docker run --rm -v $PWD:/app node:24-alpine sh -c \
- "cd /app/.github/workflows/static-analysis/locale && node index.js"
+ run: node .github/workflows/static-analysis/locale/index.js
matrix:
name: Tests / Matrix
diff --git a/.gitignore b/.gitignore
index 6aac1bbbf4..d6e138a382 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,9 @@ appwrite.config.json
/app/config/specs/
/docs/examples/
.phpunit.cache
+playwright-report
+test-results
+docker-compose.web-installer.yml
+.env.web-installer
+docker-compose.web-installer.yml.**.backup
+tests/playwright/screenshots
diff --git a/Dockerfile b/Dockerfile
index 210c2bc3d9..7cb007c188 100755
--- a/Dockerfile
+++ b/Dockerfile
@@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
--no-plugins --no-scripts --prefer-dist \
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
-FROM appwrite/base:1.0.0 AS base
+FROM appwrite/base:1.0.1 AS base
LABEL maintainer="team@appwrite.io"
@@ -121,5 +121,6 @@ RUN if [ "$DEBUG" = "true" ]; then \
fi
EXPOSE 80
+EXPOSE 8080
CMD [ "php", "app/http.php" ]
diff --git a/app/cli.php b/app/cli.php
index ee134b9487..b8721320be 100644
--- a/app/cli.php
+++ b/app/cli.php
@@ -167,7 +167,7 @@ $setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $c
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
- ->setTenant((int) $project->getSequence())
+ ->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -188,7 +188,7 @@ $setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $c
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
- ->setTenant((int) $project->getSequence())
+ ->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -211,9 +211,8 @@ $setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $a
$database = null;
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
- if ($database !== null && $project !== null && ! $project->isEmpty() && $project->getId() !== 'console') {
- $database->setTenant((int) $project->getSequence());
-
+ if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
+ $database->setTenant($project->getSequence());
return $database;
}
@@ -229,8 +228,8 @@ $setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $a
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
// set tenant
- if ($project !== null && ! $project->isEmpty() && $project->getId() !== 'console') {
- $database->setTenant((int) $project->getSequence());
+ if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
+ $database->setTenant($project->getSequence());
}
return $database;
diff --git a/app/controllers/general.php b/app/controllers/general.php
index f77aa3ec52..1a099c4bde 100644
--- a/app/controllers/general.php
+++ b/app/controllers/general.php
@@ -555,7 +555,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
if (!empty($deployment->getAttribute('startCommand', ''))) {
- $startCommand = 'cd /usr/local/server/src/function/ && ' . $deployment->getAttribute('startCommand', '');
+ $startCommand = 'cd /usr/local/server/src/function/ && ' . str_replace(['"', '`', '$'], ['\\"', '\\`', '\\$'], $deployment->getAttribute('startCommand', ''));
}
$runtimeEntrypoint = match ($version) {
diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php
index c8824d3708..acc9c2dc9a 100644
--- a/app/controllers/shared/api.php
+++ b/app/controllers/shared/api.php
@@ -215,7 +215,7 @@ Http::init()
);
}
- if (! $dbKey) {
+ if (!$dbKey) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
diff --git a/app/init/resources.php b/app/init/resources.php
index 1bab4491a4..4ebe705e73 100644
--- a/app/init/resources.php
+++ b/app/init/resources.php
@@ -589,7 +589,7 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
- ->setTenant((int) $project->getSequence())
+ ->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -848,7 +848,7 @@ Http::setResource('getProjectDB', function (Group $pools, Database $dbForPlatfor
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
- ->setTenant((int) $project->getSequence())
+ ->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -878,9 +878,8 @@ Http::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorizati
$database = null;
return function (?Document $project = null) use ($pools, $cache, $authorization, &$database) {
- if ($database !== null && $project !== null && ! $project->isEmpty() && $project->getId() !== 'console') {
- $database->setTenant((int) $project->getSequence());
-
+ if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
+ $database->setTenant($project->getSequence());
return $database;
}
@@ -896,8 +895,8 @@ Http::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorizati
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
// set tenant
- if ($project !== null && ! $project->isEmpty() && $project->getId() !== 'console') {
- $database->setTenant((int) $project->getSequence());
+ if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
+ $database->setTenant($project->getSequence());
}
return $database;
diff --git a/app/realtime.php b/app/realtime.php
index 5addb2a78f..19dbdf6da2 100644
--- a/app/realtime.php
+++ b/app/realtime.php
@@ -115,7 +115,7 @@ if (!function_exists('getProjectDB')) {
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
- ->setTenant((int)$project->getSequence())
+ ->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml
index a62f6f7e8c..0f4df352bd 100644
--- a/app/views/install/compose.phtml
+++ b/app/views/install/compose.phtml
@@ -12,8 +12,12 @@ $version = $this->getParam('version', '');
$organization = $this->getParam('organization', '');
$image = $this->getParam('image', '');
$enableAssistant = $this->getParam('enableAssistant', false);
-$dbService = $this->getParam('database');
-
+$dbService = $this->getParam('database', 'mongodb');
+$allowedDbServices = ['mariadb', 'mongodb', 'postgresql'];
+if (!\in_array($dbService, $allowedDbServices, true)) {
+ $dbService = 'mongodb';
+}
+$hostPath = rtrim($this->getParam('hostPath', ''), '/');
?>services:
traefik:
image: traefik:3.6
@@ -63,6 +67,9 @@ $dbService = $this->getParam('database');
- traefik.http.routers.appwrite_api_https.service=appwrite_api
- traefik.http.routers.appwrite_api_https.tls=true
volumes:
+
+ - ":/usr/src/code:rw"
+
- appwrite-uploads:/storage/uploads:rw
- appwrite-imports:/storage/imports:rw
- appwrite-cache:/storage/cache:rw
@@ -72,8 +79,10 @@ $dbService = $this->getParam('database');
- appwrite-sites:/storage/sites:rw
- appwrite-builds:/storage/builds:rw
depends_on:
- - redis
- - = $dbService . "\n" ?>
+ redis:
+ condition: service_healthy
+ = $dbService ?>:
+ condition: service_healthy
# - clamav
environment:
- _APP_ENV
@@ -227,8 +236,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- - redis
- - = $dbService . "\n" ?>
+ redis:
+ condition: service_healthy
+ = $dbService ?>:
+ condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -258,8 +269,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- - redis
- - = $dbService . "\n" ?>
+ redis:
+ condition: service_healthy
+ = $dbService ?>:
+ condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -286,8 +299,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- - redis
- - = $dbService . "\n" ?>
+ redis:
+ condition: service_healthy
+ = $dbService ?>:
+ condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -316,8 +331,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- - redis
- - = $dbService . "\n" ?>
+ redis:
+ condition: service_healthy
+ = $dbService ?>:
+ condition: service_healthy
volumes:
- appwrite-uploads:/storage/uploads:rw
- appwrite-cache:/storage/cache:rw
@@ -381,8 +398,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- - redis
- - = $dbService . "\n" ?>
+ redis:
+ condition: service_healthy
+ = $dbService ?>:
+ condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -409,8 +428,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- - redis
- - = $dbService . "\n" ?>
+ redis:
+ condition: service_healthy
+ = $dbService ?>:
+ condition: service_healthy
volumes:
- appwrite-functions:/storage/functions:rw
- appwrite-sites:/storage/sites:rw
@@ -479,8 +500,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- - redis
- - = $dbService . "\n" ?>
+ redis:
+ condition: service_healthy
+ = $dbService ?>:
+ condition: service_healthy
volumes:
- appwrite-config:/storage/config:rw
- appwrite-certificates:/storage/certificates:rw
@@ -518,9 +541,12 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- - redis
- - = $dbService . "\n" ?>
- - openruntimes-executor
+ redis:
+ condition: service_healthy
+ = $dbService ?>:
+ condition: service_healthy
+ openruntimes-executor:
+ condition: service_started
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -559,8 +585,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- - redis
- - = $dbService . "\n" ?>
+ redis:
+ condition: service_healthy
+ = $dbService ?>:
+ condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -598,8 +626,10 @@ $dbService = $this->getParam('database');
volumes:
- appwrite-uploads:/storage/uploads:rw
depends_on:
- - redis
- - = $dbService . "\n" ?>
+ redis:
+ condition: service_healthy
+ = $dbService ?>:
+ condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -652,7 +682,8 @@ $dbService = $this->getParam('database');
volumes:
- appwrite-imports:/storage/imports:rw
depends_on:
- - = $dbService . "\n" ?>
+ = $dbService ?>:
+ condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -688,8 +719,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- - redis
- - = $dbService . "\n" ?>
+ redis:
+ condition: service_healthy
+ = $dbService ?>:
+ condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -730,8 +763,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- - redis
- - = $dbService . "\n" ?>
+ redis:
+ condition: service_healthy
+ = $dbService ?>:
+ condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -761,8 +796,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- - redis
- - = $dbService . "\n" ?>
+ redis:
+ condition: service_healthy
+ = $dbService ?>:
+ condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -791,8 +828,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- - redis
- - = $dbService . "\n" ?>
+ redis:
+ condition: service_healthy
+ = $dbService ?>:
+ condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -821,8 +860,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- - = $dbService . "\n" ?>
- - redis
+ = $dbService ?>:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -848,8 +889,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- - = $dbService . "\n" ?>
- - redis
+ = $dbService ?>:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -875,8 +918,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- - = $dbService . "\n" ?>
- - redis
+ = $dbService ?>:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -966,7 +1011,6 @@ $dbService = $this->getParam('database');
- OPR_EXECUTOR_STORAGE_WASABI_BUCKET=$_APP_STORAGE_WASABI_BUCKET
-
mariadb:
image: mariadb:10.11
container_name: appwrite-mariadb
@@ -982,6 +1026,12 @@ $dbService = $this->getParam('database');
- MYSQL_PASSWORD=${_APP_DB_PASS}
- MARIADB_AUTO_UPGRADE=1
command: 'mysqld --innodb-flush-method=fsync'
+ healthcheck:
+ test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
+ interval: 10s
+ timeout: 10s
+ retries: 10
+ start_period: 30s
@@ -1059,18 +1109,24 @@ $dbService = $this->getParam('database');
postgresql:
- image: postgres:18
+ image: appwrite/postgres:0.1.0
container_name: appwrite-postgresql
restart: unless-stopped
networks:
- appwrite
volumes:
- - appwrite-postgresql:/var/lib/postgresql/data:rw
+ - appwrite-postgresql:/var/lib/postgresql:rw
environment:
- POSTGRES_DB=${_APP_DB_SCHEMA}
- POSTGRES_USER=${_APP_DB_USER}
- POSTGRES_PASSWORD=${_APP_DB_PASS}
command: "postgres"
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER} -d ${_APP_DB_SCHEMA}"]
+ interval: 10s
+ timeout: 10s
+ retries: 10
+ start_period: 30s
@@ -1088,6 +1144,12 @@ $dbService = $this->getParam('database');
- appwrite
volumes:
- appwrite-redis:/data:rw
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ start_period: 10s
# clamav:
# image: appwrite/clamav:1.2.0
@@ -1114,6 +1176,7 @@ volumes:
appwrite-mongodb:
appwrite-mongodb-keyfile:
+ appwrite-mongodb-config:
appwrite-redis:
appwrite-cache:
diff --git a/app/views/install/env.phtml b/app/views/install/env.phtml
index c3ebb6f918..51bda7a6fb 100644
--- a/app/views/install/env.phtml
+++ b/app/views/install/env.phtml
@@ -3,6 +3,10 @@
$vars = $this->getParam('vars');
foreach ($vars as $key => $value) {
- echo $key.'='.$value."\n";
+ if ($value === null || $value === '') {
+ echo $key . "=\n";
+ } else {
+ echo $key . '="' . addcslashes((string) $value, '"\\$`') . '"' . "\n";
+ }
}
?>
\ No newline at end of file
diff --git a/app/views/install/installer.phtml b/app/views/install/installer.phtml
new file mode 100644
index 0000000000..3cf2b77aa9
--- /dev/null
+++ b/app/views/install/installer.phtml
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ data-locked-database=""
+
+ data-default-http-port=""
+ data-default-https-port=""
+ data-default-app-domain=""
+ data-default-email-certificates=""
+ data-default-secret-key=""
+ data-default-assistant-openai-key=""
+ data-default-database=""
+
+ data-dev-mode="true"
+
+>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/views/install/installer/css/styles.css b/app/views/install/installer/css/styles.css
new file mode 100644
index 0000000000..4601cfcb67
--- /dev/null
+++ b/app/views/install/installer/css/styles.css
@@ -0,0 +1,1743 @@
+/* Installer Styles - Pink v2 tokens and components */
+
+:root {
+ /* Neutral colors */
+ --neutral-0: rgba(255, 255, 255, 1);
+ --neutral-25: rgba(250, 250, 251, 1);
+ --neutral-40: rgba(244, 244, 247, 1);
+ --neutral-50: rgba(237, 237, 240, 1);
+ --neutral-100: rgba(228, 228, 231, 1);
+ --neutral-200: rgba(216, 216, 219, 1);
+ --neutral-300: rgba(173, 173, 176, 1);
+ --neutral-400: rgba(151, 151, 155, 1);
+ --neutral-500: rgba(129, 129, 134, 1);
+ --neutral-700: rgba(86, 86, 92, 1);
+ --neutral-800: rgba(45, 45, 49, 1);
+ --neutral-900: rgba(25, 25, 28, 1);
+
+ /* Warning colors */
+ --web-orange-200: rgba(255, 213, 194, 1);
+ --web-orange-500: rgba(254, 124, 67, 1);
+ --web-orange-700: rgba(97, 37, 10, 1);
+
+ /* Error colors */
+ --web-red-500: rgba(255, 69, 58, 1);
+ --web-red-700: rgba(179, 18, 18, 1);
+
+ /* Brand colors */
+ --brand-pink-500: rgba(253, 54, 110, 1);
+ --brand-pink-600: rgba(202, 43, 88, 1);
+ --brand-pink-700: rgba(152, 32, 66, 1);
+
+ /* Background colors */
+ --bgcolor-neutral-default: var(--neutral-25);
+ --bgcolor-neutral-primary: var(--neutral-0);
+ --bgcolor-neutral-secondary: var(--neutral-40);
+ --bgcolor-neutral-tertiary: var(--neutral-50);
+ --bgcolor-neutral-invert-weaker: var(--neutral-500);
+ --bgcolor-neutral-invert-weak: var(--neutral-700);
+ --bgcolor-accent: var(--brand-pink-500);
+ --bgcolor-accent-secondary: var(--brand-pink-600);
+ --bgcolor-accent-tertiary: var(--brand-pink-700);
+ --bgcolor-success-weak: rgba(16, 185, 129, 0.16);
+ --bgcolor-warning-weaker: rgba(254, 124, 67, 0.04);
+ --bgcolor-warning-weak: rgba(254, 124, 67, 0.16);
+ --bgcolor-error: var(--web-red-500);
+ --bgcolor-error-weaker: rgba(255, 69, 58, 0.04);
+
+ /* Foreground colors */
+ --fgcolor-neutral-primary: var(--neutral-800);
+ --fgcolor-neutral-secondary: var(--neutral-700);
+ --fgcolor-neutral-tertiary: var(--neutral-400);
+ --fgcolor-neutral-weak: var(--neutral-200);
+ --fgcolor-accent: var(--brand-pink-500);
+ --fgcolor-on-accent: var(--neutral-0);
+ --fgcolor-on-invert: var(--neutral-25);
+ --fgcolor-on-success-weak: rgba(10, 113, 79, 1);
+ --fgcolor-warning: rgba(97, 37, 10, 1);
+ --fgcolor-on-warning-weak: var(--web-orange-700);
+ --fgcolor-error: var(--web-red-700);
+ --fgcolor-on-error: var(--neutral-0);
+
+ /* Border colors */
+ --border-neutral: var(--neutral-50);
+ --border-neutral-strong: var(--neutral-200);
+ --border-neutral-stronger: var(--neutral-500);
+ --border-focus: var(--neutral-300);
+ --border-accent: var(--brand-pink-500);
+ --border-warning-weak: rgba(254, 124, 67, 0.32);
+ --border-error: var(--web-red-500);
+ --border-error-weak: rgba(255, 69, 58, 0.32);
+
+ /* Overlay colors */
+ --overlay-neutral-hover: rgba(25, 25, 28, 0.03);
+ --overlay-neutral-pressed: rgba(25, 25, 28, 0.04);
+ --overlay-scrim: rgba(25, 25, 28, 0.8);
+
+ /* Icon sizes */
+ --icon-size-s: var(--base-16);
+
+ /* Typography */
+ --font-family-brand: 'Aeonik Pro';
+ --font-family-sansserif: 'Inter';
+ --font-family-code: 'Fira Code';
+ --sans-fallbacks: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ --mono-fallbacks: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace;
+
+ --font-size-xs: 12px;
+ --font-size-s: 14px;
+ --font-size-m: 16px;
+ --font-size-l: 20px;
+ --label-line-height: 19.6px;
+
+ /* Motion */
+ --duration-fast: 150ms;
+ --duration-short: 160ms;
+ --duration-medium: 200ms;
+ --duration-extended: 250ms;
+ --duration-slow: 300ms;
+ --ease-standard: ease;
+ --ease-emphasized: cubic-bezier(0.32, 0.72, 0, 1);
+ --ease-in-out: ease-in-out;
+ --ease-out: ease-out;
+
+ /* Spacing */
+ --base-0: 0;
+ --base-2: 2px;
+ --base-4: 4px;
+ --base-6: 6px;
+ --base-8: 8px;
+ --base-10: 10px;
+ --base-12: 12px;
+ --base-16: 16px;
+ --base-20: 20px;
+ --base-24: 24px;
+ --base-32: 32px;
+ --base-40: 40px;
+ --base-48: 48px;
+
+ /* Component sizing */
+ --button-min-width-s: 60px;
+
+ --space-0: var(--base-0);
+ --space-1: var(--base-2);
+ --space-2: var(--base-4);
+ --space-3: var(--base-6);
+ --space-4: var(--base-8);
+ --space-5: var(--base-10);
+ --space-6: var(--base-12);
+ --space-7: var(--base-16);
+ --space-8: var(--base-20);
+ --space-9: var(--base-24);
+ --space-10: var(--base-32);
+ --space-11: var(--base-40);
+ --space-12: var(--base-48);
+
+ /* Gap scale (Pink tokens) */
+ --gap-none: var(--base-0);
+ --gap-xxxs: var(--base-2);
+ --gap-xxs: var(--base-4);
+ --gap-xs: var(--base-6);
+ --gap-s: var(--base-8);
+ --gap-m: var(--base-12);
+ --gap-l: var(--base-16);
+ --gap-xl: var(--base-20);
+ --gap-xxl: var(--base-32);
+ --gap-xxxl: var(--base-40);
+
+ /* Border radius */
+ --border-radius-s: 8px;
+ --border-radius-xs: 6px;
+ --border-radius-m: 12px;
+ --border-radius-l: 16px;
+
+ /* Border widths */
+ --border-width-s: 1px;
+ --border-width-l: 2px;
+
+ /* Installer layout vars */
+ --step-min-height: auto;
+ --divider-gap-top: var(--gap-xl);
+
+ /* Animation vars */
+ --spinner-rotation: 0deg;
+
+ /* Legacy aliases */
+ --bgColor-neutral-default: var(--bgcolor-neutral-default);
+ --bgColor-neutral-primary: var(--bgcolor-neutral-primary);
+ --bgColor-accent: var(--bgcolor-accent);
+ --fgColor-neutral-primary: var(--fgcolor-neutral-primary);
+ --fgColor-neutral-secondary: var(--fgcolor-neutral-secondary);
+ --fgColor-neutral-tertiary: var(--fgcolor-neutral-tertiary);
+ --fgColor-neutral-weak: var(--fgcolor-neutral-weak);
+ --fgColor-accent: var(--fgcolor-accent);
+ --fgColor-on-accent: var(--fgcolor-on-accent);
+}
+
+.installer-toast-stack {
+ position: fixed;
+ top: var(--base-12);
+ right: var(--base-12);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-4);
+ z-index: 60;
+ pointer-events: none;
+}
+
+.installer-toast {
+ inline-size: 24rem;
+ display: inline-flex;
+ align-items: start;
+ justify-content: space-between;
+ gap: var(--space-6);
+ padding: var(--space-4);
+ border-radius: var(--border-radius-m);
+ border: var(--border-width-s) solid var(--border-neutral);
+ background: var(--bgcolor-neutral-primary);
+ box-shadow:
+ 0 2px 12px 0 rgba(0, 0, 0, 0.02),
+ 0 6px 8px 0 rgba(0, 0, 0, 0.02);
+ pointer-events: auto;
+ opacity: 1;
+ transform: translate3d(0, 0, 0);
+ will-change: transform, opacity;
+ transition:
+ transform 400ms cubic-bezier(0.33, 1, 0.68, 1),
+ opacity 400ms cubic-bezier(0.33, 1, 0.68, 1);
+}
+
+.installer-toast.is-entering {
+ opacity: 0;
+ transform: translate3d(50px, 0, 0);
+}
+
+.installer-toast.is-leaving {
+ opacity: 0;
+ transform: translate3d(50px, 0, 0);
+ pointer-events: none;
+}
+
+.installer-toast-content {
+ display: flex;
+ align-items: start;
+ gap: var(--space-6);
+}
+
+.installer-toast-body {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2);
+ margin-block: auto;
+}
+
+.installer-toast-icon {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--border-radius-s);
+ background: var(--bgcolor-neutral-invert-weaker);
+ color: var(--fgcolor-on-invert);
+ flex-shrink: 0;
+}
+
+.installer-toast-icon svg {
+ width: 16px;
+ height: 16px;
+ display: block;
+}
+
+.installer-toast-icon[data-status='error'] {
+ background: var(--bgcolor-error);
+ color: var(--fgcolor-on-error);
+}
+
+.installer-toast-close {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--space-3);
+ border: var(--border-width-s) solid transparent;
+ border-radius: var(--border-radius-s);
+ background: transparent;
+ color: var(--fgcolor-neutral-tertiary);
+ cursor: pointer;
+ transition: all 0.15s ease-in-out;
+}
+
+.installer-toast-close svg {
+ width: 16px;
+ height: 16px;
+ display: block;
+}
+
+.installer-toast-close:hover {
+ color: var(--fgcolor-neutral-secondary);
+ background: var(--overlay-neutral-hover);
+}
+
+.installer-toast-close:active {
+ color: var(--fgcolor-neutral-secondary);
+ background: var(--overlay-neutral-pressed);
+}
+
+.installer-toast-close:focus-visible {
+ outline: var(--border-width-l) solid var(--border-focus);
+ outline-offset: var(--border-width-s);
+}
+
+.is-hidden {
+ display: none !important;
+}
+
+@media (min-width: 768px) {
+ .installer-toast-stack {
+ top: var(--base-24);
+ right: var(--base-24);
+ }
+}
+
+@media (max-width: 640px) {
+ .installer-toast-stack {
+ left: 0;
+ right: 0;
+ top: var(--base-12);
+ padding: 0 var(--space-4);
+ align-items: stretch;
+ }
+
+ .installer-toast {
+ inline-size: 100%;
+ }
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ color: var(--fgcolor-neutral-primary);
+ background: var(--bgcolor-neutral-default);
+ font-family: var(--font-family-sansserif), var(--sans-fallbacks), sans-serif;
+}
+
+.installer-page {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: var(--space-7);
+ gap: var(--gap-l);
+ background: var(--bgcolor-neutral-default);
+ position: relative;
+ overflow: hidden;
+}
+
+.installer-main {
+ flex: 1;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ z-index: 1;
+ min-height: 0;
+}
+
+.installer-backdrop {
+ position: fixed;
+ inset: 0;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity var(--duration-medium) var(--ease-standard);
+ z-index: 0;
+}
+
+.installer-page[data-step='5'] .installer-backdrop {
+ opacity: 1;
+}
+
+.installer-gradients {
+ top: 34%;
+ left: 17%;
+ width: 409px;
+ height: 194px;
+ opacity: 0.32;
+ position: absolute;
+ pointer-events: none;
+ transform: translateY(-50%) rotate(-25deg);
+}
+
+.installer-blob {
+ position: absolute;
+ left: 0;
+ top: 0;
+ --blob-x: 0;
+ --blob-y: 0;
+ transform: translate(var(--blob-x), var(--blob-y)) scale(1);
+ transform-origin: center;
+ animation: none;
+}
+
+.installer-blob.blob-one {
+ --blob-x: 268.3px;
+ --blob-y: 108.3px;
+}
+
+.installer-blob.blob-two {
+ --blob-x: 101.3px;
+ --blob-y: 101.3px;
+}
+
+.installer-card {
+ width: 100%;
+ max-width: 500px;
+ max-height: 100%;
+ padding: var(--space-8);
+ background: var(--bgcolor-neutral-primary);
+ border-radius: var(--border-radius-l);
+ outline: var(--border-width-s) solid var(--border-neutral);
+ outline-offset: -1px;
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ transition: opacity var(--duration-medium) var(--ease-standard);
+ overflow: hidden;
+}
+
+.installer-page[data-step='5'] .installer-card {
+ opacity: 0;
+ pointer-events: none;
+}
+
+.installer-page[data-install-locked='true'] .step-indicators {
+ opacity: 0.4;
+}
+
+.selector-card.is-disabled {
+ cursor: default;
+ pointer-events: auto;
+}
+
+.selector-card.is-disabled .selector-content,
+.selector-card.is-disabled .selector-icon {
+ opacity: 0.4;
+}
+
+.installer-step {
+ display: grid;
+ position: relative;
+ width: 100%;
+ min-height: var(--step-min-height, auto);
+ flex: 1 1 auto;
+ overflow: hidden;
+}
+
+.action-shell {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ margin-top: auto;
+ flex-shrink: 0;
+}
+
+.install-screen {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity var(--duration-medium) var(--ease-standard);
+ z-index: 1;
+}
+
+.installer-page[data-step='5'] .install-screen {
+ opacity: 1;
+ pointer-events: auto;
+}
+
+.install-screen-content {
+ width: 100%;
+ max-width: 500px;
+}
+
+.step-panel {
+ grid-area: 1 / 1;
+ width: 100%;
+ opacity: 1;
+ transition: opacity var(--duration-medium) var(--ease-standard);
+ will-change: opacity;
+}
+
+.step-panel:not(.is-measure) {
+ height: 100%;
+ overflow-y: auto;
+ scrollbar-gutter: stable;
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+}
+
+.step-panel:not(.is-measure)::-webkit-scrollbar {
+ width: 0;
+ height: 0;
+}
+
+.step-panel.is-entering {
+ opacity: 0;
+ pointer-events: none;
+}
+
+.step-panel.is-exiting {
+ opacity: 0;
+ pointer-events: none;
+}
+
+.step-panel.is-measure {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ opacity: 0;
+ visibility: hidden;
+ pointer-events: none;
+}
+
+.stack-none {
+ display: flex;
+ flex-direction: column;
+ gap: var(--gap-none);
+}
+
+.stack-xxxs {
+ display: flex;
+ flex-direction: column;
+ gap: var(--gap-xxxs);
+}
+
+.stack-xxs {
+ display: flex;
+ flex-direction: column;
+ gap: var(--gap-xxs);
+}
+
+.stack-xs {
+ display: flex;
+ flex-direction: column;
+ gap: var(--gap-xs);
+}
+
+.stack-s {
+ display: flex;
+ flex-direction: column;
+ gap: var(--gap-s);
+}
+
+.stack-m {
+ display: flex;
+ flex-direction: column;
+ gap: var(--gap-m);
+}
+
+.stack-l {
+ display: flex;
+ flex-direction: column;
+ gap: var(--gap-l);
+}
+
+.stack-xl {
+ display: flex;
+ flex-direction: column;
+ gap: var(--gap-xl);
+}
+
+.stack-xxl {
+ display: flex;
+ flex-direction: column;
+ gap: var(--gap-xxl);
+}
+
+.stack-xxxl {
+ display: flex;
+ flex-direction: column;
+ gap: var(--gap-xxxl);
+}
+
+.install-layout {
+ width: 100%;
+}
+
+.install-card {
+ width: 100%;
+ padding: var(--space-2);
+ background: var(--bgcolor-neutral-default);
+ border-radius: var(--border-radius-l);
+ border: var(--border-width-s) solid var(--border-neutral);
+}
+
+.install-panel {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ gap: var(--gap-xxxs);
+ transition: height 0.3s cubic-bezier(0.32, 0.72, 0, 1);
+}
+
+.install-header {
+ padding: var(--space-4);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: var(--gap-xxxs);
+}
+
+.install-list {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 0;
+}
+
+.install-row {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ padding: 0;
+ background: var(--bgcolor-neutral-primary);
+ border: var(--border-width-s) solid var(--border-neutral);
+ border-radius: 0;
+ overflow: hidden;
+ opacity: 1;
+ transform: translateY(0);
+ transition: opacity 0.2s ease,
+ transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
+ will-change: transform, opacity;
+}
+
+.install-row-main {
+ display: flex;
+ align-items: center;
+ gap: var(--gap-s);
+ height: 44px;
+ padding: 0 var(--space-6);
+ transition: height 0.3s cubic-bezier(0.32, 0.72, 0, 1),
+ opacity 0.2s ease,
+ transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
+}
+
+.install-row-label {
+ display: flex;
+ align-items: center;
+ gap: var(--gap-s);
+ min-width: 0;
+ min-height: 0;
+ padding-block-start: 0;
+}
+
+.install-text {
+ min-width: 0;
+ display: inline-flex;
+ transition: opacity 0.2s ease, transform 0.2s ease;
+}
+
+.install-text.is-enter {
+ opacity: 0;
+ transform: translateY(10px);
+}
+
+.install-row-toggle {
+ margin-left: auto;
+ width: 32px;
+ height: 32px;
+ border: none;
+ border-radius: var(--border-radius-xs);
+ background: transparent;
+ color: var(--fgcolor-neutral-secondary);
+ display: none;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ opacity: 0;
+ pointer-events: none;
+ transition: background-color var(--duration-short) var(--ease-standard),
+ color var(--duration-short) var(--ease-standard),
+ transform var(--duration-short) var(--ease-standard);
+}
+
+.install-row-toggle svg {
+ width: 20px;
+ height: 20px;
+}
+
+
+.install-row-details {
+ max-height: none;
+ opacity: 0;
+ overflow: hidden;
+ display: grid;
+ grid-template-rows: 0fr;
+ padding: 0;
+ border: none;
+ transition: grid-template-rows var(--duration-medium) var(--ease-standard),
+ opacity var(--duration-medium) var(--ease-standard);
+}
+
+.install-row:first-child {
+ border-top-left-radius: var(--border-radius-m);
+ border-top-right-radius: var(--border-radius-m);
+}
+
+.install-row + .install-row {
+ margin-top: -1px;
+}
+
+.install-row:last-child {
+ border-bottom-left-radius: var(--border-radius-m);
+ border-bottom-right-radius: var(--border-radius-m);
+}
+
+.install-row.is-entering {
+ opacity: 0;
+ transform: translateY(-20px);
+}
+
+.install-row.is-entering .install-row-main {
+ height: 0;
+ opacity: 0;
+ transform: translateY(-20px);
+}
+
+.install-icon {
+ width: 16px;
+ height: 16px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ position: relative;
+ overflow: hidden;
+}
+
+.install-icon-spinner,
+.install-icon-check {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ inset: 0;
+ transition: opacity 200ms cubic-bezier(0.32, 0.72, 0, 1);
+}
+
+.install-icon-error {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ inset: 0;
+ opacity: 0;
+ transition: opacity 200ms cubic-bezier(0.32, 0.72, 0, 1);
+ color: var(--fgcolor-error);
+}
+
+.install-icon-check {
+ opacity: 0;
+}
+
+.install-icon-spinner svg {
+ animation: none;
+ transform: rotate(var(--spinner-rotation));
+}
+
+.install-row[data-status='completed'] .install-icon-spinner {
+ opacity: 0;
+}
+
+.install-row[data-status='completed'] .install-icon-spinner svg {
+ animation: none;
+}
+
+.install-row[data-status='completed'] .install-icon-check {
+ opacity: 1;
+}
+
+/* Keep the checkmark subtle: rely on container fade only (no stroke drawing). */
+
+.install-row[data-status='error'] {
+ height: auto;
+ overflow: hidden;
+}
+
+.install-row[data-status='error'] .install-icon-spinner,
+.install-row[data-status='error'] .install-icon-check {
+ opacity: 0;
+}
+
+.install-row[data-status='error'] .install-icon-error {
+ opacity: 1;
+}
+
+.install-row[data-status='error'] .install-row-toggle {
+ opacity: 1;
+ pointer-events: auto;
+ display: inline-flex;
+}
+
+.install-row[data-status='error'] {
+ cursor: pointer;
+}
+
+.install-row[data-status='error'] .button {
+ cursor: pointer;
+}
+
+.install-row.is-open .install-row-toggle {
+ transform: rotate(180deg);
+}
+
+.install-row.is-open .install-row-details {
+ grid-template-rows: 1fr;
+ opacity: 1;
+}
+
+.install-row-details-inner {
+ position: relative;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ border-top: var(--border-width-s) solid var(--border-neutral);
+ border-left: none;
+ border-right: none;
+ border-radius: 0;
+ background: var(--bgcolor-neutral-primary);
+}
+
+.install-error-code {
+ margin: 0;
+ padding: var(--space-4) var(--space-6);
+ width: 100%;
+ background: var(--bgcolor-neutral-default);
+ border: none;
+ border-radius: 0;
+ font-family: var(--font-family-code), var(--mono-fallbacks), monospace;
+ font-size: var(--font-size-xs);
+ line-height: 140%;
+ letter-spacing: 0;
+ white-space: pre-wrap;
+ word-break: break-word;
+ overflow-wrap: break-word;
+ max-height: 160px;
+ overflow-y: auto;
+ overflow-x: auto;
+ color: var(--fgcolor-neutral-primary);
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+}
+
+.install-error-code::-webkit-scrollbar {
+ width: 0;
+ height: 0;
+}
+
+
+.install-error-actions {
+ padding: var(--space-3) var(--space-6);
+ background: var(--bgcolor-neutral-primary);
+ border-top: var(--border-width-s) solid var(--border-neutral);
+ border-radius: 0 0 var(--border-radius-m) var(--border-radius-m);
+ display: flex;
+ justify-content: flex-end;
+ flex-direction: row;
+ gap: var(--gap-m);
+}
+
+.install-error-details .button {
+ align-self: center;
+ margin-top: 0;
+}
+
+@keyframes install-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.typography-title-s {
+ font-family: var(--font-family-brand), var(--sans-fallbacks), sans-serif;
+ font-size: var(--font-size-l);
+ font-weight: 400;
+ line-height: 130%;
+ letter-spacing: -0.144px;
+}
+
+.typography-title-s,
+.typography-text-m-400,
+.typography-text-m-500 {
+ margin: 0;
+}
+
+.typography-text-m-400 {
+ font-family: var(--font-family-brand), var(--sans-fallbacks), sans-serif;
+ font-size: var(--font-size-s);
+ font-weight: 400;
+ line-height: 140%;
+ letter-spacing: -0.063px;
+}
+
+.typography-text-m-500 {
+ font-family: var(--font-family-brand), var(--sans-fallbacks), sans-serif;
+ font-size: var(--font-size-s);
+ font-weight: 500;
+ line-height: 140%;
+ letter-spacing: -0.063px;
+}
+
+.typography-text-xs-400 {
+ font-family: var(--font-family-sansserif), var(--sans-fallbacks), sans-serif;
+ font-size: var(--font-size-xs);
+ font-weight: 400;
+ line-height: 130%;
+ letter-spacing: -0.12px;
+}
+
+.typography-text-xs-500 {
+ font-family: var(--font-family-sansserif), var(--sans-fallbacks), sans-serif;
+ font-size: var(--font-size-xs);
+ font-weight: 500;
+ line-height: 130%;
+ letter-spacing: -0.12px;
+}
+
+.typography-caption-400 {
+ font-family: var(--font-family-sansserif), var(--sans-fallbacks), sans-serif;
+ font-size: var(--font-size-xs);
+ font-weight: 400;
+ line-height: 140%;
+ letter-spacing: -0.063px;
+}
+
+.typography-caption-500 {
+ font-family: var(--font-family-sansserif), var(--sans-fallbacks), sans-serif;
+ font-size: var(--font-size-xs);
+ font-weight: 500;
+ line-height: 140%;
+ letter-spacing: -0.063px;
+}
+
+.label-text {
+ display: flex;
+ align-items: center;
+ gap: var(--space-1);
+}
+
+.label-optional {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 var(--space-1);
+}
+
+.label-info-button {
+ width: 16px;
+ height: 16px;
+ padding: 0;
+ border: none;
+ background: transparent;
+ color: var(--fgcolor-neutral-secondary);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: default;
+}
+
+.label-info-button svg {
+ width: 16px;
+ height: 16px;
+}
+
+.text-neutral-primary {
+ color: var(--fgcolor-neutral-primary);
+}
+
+.text-neutral-secondary {
+ color: var(--fgcolor-neutral-secondary);
+}
+
+.text-neutral-tertiary {
+ color: var(--fgcolor-neutral-tertiary);
+}
+
+.text-warning {
+ color: var(--fgcolor-warning);
+}
+
+.text-error {
+ color: var(--fgcolor-error);
+}
+
+.text-on-success-weak {
+ color: var(--fgcolor-on-success-weak);
+}
+
+.text-on-invert {
+ color: var(--fgcolor-on-invert);
+}
+
+.input-group {
+ width: 100%;
+}
+
+.field-error {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--space-3);
+ color: var(--fgcolor-error);
+ max-height: 0;
+ opacity: 0;
+ overflow: hidden;
+ /*padding-inline-start: var(--space-2);*/
+ transition: max-height var(--duration-extended) var(--ease-standard),
+ opacity var(--duration-extended) var(--ease-standard);
+}
+
+.field-error-icon {
+ display: flex;
+ flex-shrink: 0;
+ align-items: center;
+}
+
+.field-error-icon svg {
+ width: 1rem;
+ height: 1rem;
+}
+
+.field-error.is-visible {
+ opacity: 1;
+ max-height: 64px;
+}
+
+.field-helper {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--space-3);
+ color: var(--fgcolor-neutral-secondary);
+}
+
+.field-helper-icon {
+ display: flex;
+ flex-shrink: 0;
+ align-items: center;
+}
+
+.field-helper-icon svg {
+ width: 1rem;
+ height: 1rem;
+}
+
+.input-field {
+ width: 100%;
+ padding: var(--space-3) var(--space-6);
+ background: var(--bgcolor-neutral-default);
+ border: var(--border-width-s) solid var(--border-neutral);
+ outline: none;
+ border-radius: var(--border-radius-s);
+ transition: all var(--duration-fast) var(--ease-in-out);
+}
+
+.input-field:focus {
+ border-color: var(--border-focus);
+ box-shadow: inset 0 0 0 1px var(--border-focus);
+}
+
+.input-field.is-error {
+ border-color: var(--border-error);
+ box-shadow: none;
+}
+
+.input-field.is-error:focus {
+ border-color: var(--border-error);
+ box-shadow: inset 0 0 0 1px var(--border-error);
+}
+
+.input-field::placeholder {
+ color: var(--fgcolor-neutral-tertiary);
+}
+
+.input-field[type='number'] {
+ appearance: textfield;
+ -moz-appearance: textfield;
+}
+
+.input-field[type='number']::-webkit-outer-spin-button,
+.input-field[type='number']::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+
+.selector-group {
+ display: flex;
+ gap: var(--gap-m);
+ width: 100%;
+ position: relative;
+ overflow: visible;
+}
+
+.selector-card {
+ position: relative;
+ flex: 1;
+ min-height: 52px;
+ display: flex;
+ align-items: center;
+ gap: var(--gap-s);
+ padding: var(--space-4) var(--space-6);
+ background: var(--bgcolor-neutral-default);
+ border: none;
+ box-shadow: inset 0 0 0 var(--border-width-s) var(--border-neutral);
+ border-radius: var(--border-radius-s);
+ cursor: pointer;
+ overflow: hidden;
+ transition: all var(--duration-fast) var(--ease-in-out);
+}
+
+.selector-card.has-tooltip {
+ overflow: visible;
+}
+
+.selector-card::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: var(--overlay-neutral-hover);
+ opacity: 0;
+ transition: opacity var(--duration-fast) var(--ease-in-out);
+}
+
+.selector-card:hover::before,
+.selector-card.selected::before {
+ opacity: 1;
+}
+
+.selector-card.is-disabled::before,
+.selector-card.is-disabled:hover::before {
+ opacity: 0;
+}
+
+.selector-group.is-locked .selector-card {
+ cursor: default;
+}
+
+.selector-group.is-locked .selector-card:hover::before {
+ opacity: 0;
+}
+
+.selector-card.has-tooltip .tooltip {
+ top: calc(100% + 6px);
+ bottom: auto;
+ transform: translateX(-50%) translateY(-8px);
+}
+
+.selector-card.has-tooltip {
+ z-index: 0;
+}
+
+.selector-card.has-tooltip:hover {
+ z-index: 3;
+}
+
+.selector-card.has-tooltip:hover .tooltip,
+.selector-card.has-tooltip:focus-within .tooltip {
+ opacity: 1;
+ visibility: visible;
+ transform: translateX(-50%) translateY(0);
+ transition: opacity var(--duration-short) var(--ease-standard),
+ transform var(--duration-short) var(--ease-standard),
+ visibility 0s;
+}
+
+.tooltip-db-locked {
+ width: 193px;
+ max-width: none;
+ min-width: 193px;
+ text-align: center;
+}
+.selector-card.selected {
+ box-shadow: inset 0 0 0 var(--border-width-s) var(--border-neutral-stronger);
+}
+
+.selector-card:focus-within {
+ outline: none;
+ box-shadow: inset 0 0 0 var(--border-width-s) var(--border-neutral),
+ inset 0 0 0 var(--border-width-l) var(--border-focus);
+}
+
+.selector-card.selected:focus-within {
+ box-shadow: inset 0 0 0 var(--border-width-s) var(--border-neutral-stronger),
+ inset 0 0 0 var(--border-width-l) var(--border-focus);
+}
+
+.selector-content {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ flex: 1;
+}
+
+.selector-icon {
+ position: relative;
+ width: 32px;
+ height: 32px;
+ flex-shrink: 0;
+}
+
+.accordion {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+}
+
+.accordion-toggle {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--gap-s);
+ width: 100%;
+ padding: 0;
+ border: none;
+ border-radius: var(--border-radius-s);
+ background: transparent;
+ cursor: pointer;
+ transition: background var(--duration-fast) var(--ease-in-out);
+ font-family: inherit;
+ font-size: inherit;
+ font-weight: inherit;
+ line-height: inherit;
+ letter-spacing: inherit;
+}
+
+.accordion-toggle:focus-visible {
+ outline: var(--border-width-l) solid var(--border-focus);
+}
+
+.accordion-chevron {
+ width: 20px;
+ height: 20px;
+ transition: transform var(--duration-slow) var(--ease-in-out);
+ color: var(--fgcolor-neutral-tertiary);
+}
+
+.accordion-chevron[data-open='true'] {
+ transform: rotate(180deg);
+}
+
+.accordion-content {
+ display: flex;
+ flex-direction: column;
+ gap: var(--gap-l);
+ width: 100%;
+ max-height: 0;
+ opacity: 0;
+ overflow: hidden;
+ transition: max-height var(--duration-medium) var(--ease-out),
+ opacity var(--duration-medium) var(--ease-out);
+}
+
+.accordion-content.open {
+ opacity: 1;
+ overflow: visible;
+ margin-top: var(--gap-m);
+}
+
+.divider {
+ width: 100%;
+ height: 1px;
+ background: var(--border-neutral);
+ margin-top: var(--divider-gap-top, var(--gap-xl));
+ margin-bottom: var(--gap-xl);
+}
+
+.button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--gap-xs);
+ padding: var(--space-3) var(--space-5);
+ min-height: var(--space-10);
+ min-width: var(--button-min-width-s);
+ border-radius: var(--border-radius-s);
+ border: none;
+ box-shadow: inset 0 0 0 var(--border-width-s) transparent;
+ cursor: pointer;
+ transition: all var(--duration-fast) var(--ease-in-out);
+ outline-offset: var(--border-width-s);
+ background: transparent;
+ color: var(--fgcolor-neutral-secondary);
+}
+
+.button-text {
+ display: inline-flex;
+ align-items: center;
+}
+
+.button:focus-visible {
+ outline: var(--border-width-l) solid var(--border-focus);
+}
+
+.button.primary {
+ background: var(--bgcolor-accent);
+ box-shadow: inset 0 0 0 var(--border-width-s) var(--bgcolor-accent);
+ color: var(--fgcolor-on-accent);
+}
+
+.button.primary:hover {
+ background: var(--bgcolor-accent-secondary);
+ box-shadow: inset 0 0 0 var(--border-width-s) var(--bgcolor-accent-secondary);
+}
+
+.button.primary:active {
+ background: var(--bgcolor-accent-tertiary);
+ box-shadow: inset 0 0 0 var(--border-width-s) var(--bgcolor-accent-tertiary);
+}
+
+.button.primary:disabled {
+ background: var(--bgcolor-neutral-invert-weaker);
+ box-shadow: inset 0 0 0 var(--border-width-s) var(--bgcolor-neutral-invert-weaker);
+}
+
+.button.secondary {
+ background: var(--bgcolor-neutral-primary);
+ box-shadow: inset 0 0 0 var(--border-width-s) var(--border-neutral);
+ color: var(--fgcolor-neutral-primary);
+}
+
+.button.secondary:hover {
+ background: var(--bgcolor-neutral-secondary);
+}
+
+.button.secondary:active {
+ background: var(--bgcolor-neutral-tertiary);
+}
+
+.button.secondary:disabled {
+ box-shadow: inset 0 0 0 var(--border-width-s) var(--border-neutral-strong);
+}
+
+.button:disabled {
+ opacity: 0.4;
+ pointer-events: none;
+}
+
+.action-bar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--gap-xl);
+ width: 100%;
+ flex-wrap: nowrap;
+}
+
+.step-indicators {
+ display: flex;
+ align-items: center;
+ line-height: 0;
+}
+
+.step-indicator {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ line-height: 0;
+}
+
+.step-indicator svg {
+ display: block;
+}
+
+.step-indicator .indicator-active {
+ display: none;
+}
+
+.step-indicator.is-active .indicator-active {
+ display: inline-flex;
+}
+
+.step-indicator.is-active .indicator-inactive {
+ display: none;
+}
+
+.step-indicator.is-hidden {
+ display: none;
+}
+
+.step-layout {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+}
+
+.inline-alert {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: var(--gap-m);
+ width: 100%;
+ padding: var(--space-6);
+ background: var(--bgcolor-neutral-default);
+ border-radius: var(--border-radius-s);
+ outline: var(--border-width-s) solid var(--border-neutral-strong);
+ outline-offset: -1px;
+ --alert-primary-color: var(--fgcolor-neutral-secondary);
+}
+
+.inline-alert--warning {
+ background: var(--bgcolor-warning-weaker);
+ outline-color: var(--border-warning-weak);
+ --alert-primary-color: var(--fgcolor-warning);
+}
+
+.inline-alert-content {
+ display: inline-flex;
+ align-items: flex-start;
+ gap: var(--gap-s);
+ align-self: stretch;
+}
+
+.inline-alert-icon {
+ width: 20px;
+ height: 20px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--alert-primary-color);
+ flex-shrink: 0;
+}
+
+.inline-alert-text {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-1);
+}
+
+.input-action {
+ display: flex;
+ align-items: center;
+ gap: var(--gap-l);
+ width: 100%;
+ padding: var(--space-3) var(--space-5) var(--space-3) var(--space-6);
+ background: var(--bgcolor-neutral-default);
+ border-radius: var(--border-radius-s);
+ border: var(--border-width-s) solid var(--border-neutral);
+ outline: none;
+ transition: all var(--duration-fast) var(--ease-in-out);
+}
+
+.input-action:focus-within {
+ border-color: var(--border-focus);
+ box-shadow: inset 0 0 0 1px var(--border-focus);
+}
+
+.input-action.is-error {
+ border-color: var(--border-error);
+ box-shadow: none;
+}
+
+.input-action.is-error:focus-within {
+ border-color: var(--border-error);
+ box-shadow: inset 0 0 0 1px var(--border-error);
+}
+
+.input-action-input {
+ flex: 1;
+ border: none;
+ background: transparent;
+ padding: 0;
+ outline: none;
+}
+
+.input-action-buttons {
+ display: flex;
+ align-items: center;
+ gap: var(--gap-m);
+}
+
+.tooltip-wrapper {
+ position: relative;
+ display: inline-flex;
+}
+
+.tooltip {
+ position: absolute;
+ left: 50%;
+ bottom: calc(100% + 6px);
+ transform: translateX(-50%) translateY(8px);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: max-content;
+ max-width: 11.25rem;
+ padding: var(--space-2) var(--space-4);
+ border-radius: var(--border-radius-s);
+ background: var(--bgcolor-neutral-invert-weak);
+ color: var(--fgcolor-on-invert);
+ opacity: 0;
+ visibility: hidden;
+ pointer-events: none;
+ transition: opacity var(--duration-short) var(--ease-standard),
+ transform var(--duration-short) var(--ease-standard),
+ visibility 0s linear var(--duration-short);
+ z-index: 5;
+}
+
+.tooltip.is-open {
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+ transition: opacity var(--duration-short) var(--ease-standard),
+ transform var(--duration-short) var(--ease-standard),
+ visibility 0s;
+}
+
+.tooltip-portal {
+ position: fixed;
+ left: 0;
+ top: 0;
+ bottom: auto;
+ transform: translateY(8px);
+}
+
+.tooltip-assistant {
+ width: 246px;
+ max-width: 246px;
+ text-align: left;
+}
+
+.tooltip-wrapper:hover .tooltip,
+.tooltip-wrapper:focus-within .tooltip,
+.tooltip-wrapper.is-open .tooltip {
+ opacity: 1;
+ visibility: visible;
+ transform: translateX(-50%) translateY(0);
+ transition: opacity var(--duration-short) var(--ease-standard),
+ transform var(--duration-short) var(--ease-standard),
+ visibility 0s;
+}
+
+.input-icon-button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ border: none;
+ background: transparent;
+ color: var(--fgcolor-neutral-secondary);
+ cursor: pointer;
+ border-radius: var(--border-radius-xs);
+}
+
+.input-icon-button svg {
+ width: 16px;
+ height: 16px;
+ display: block;
+}
+
+.input-icon-button:hover {
+ background: var(--overlay-neutral-hover);
+}
+
+.input-icon-button:active {
+ background: var(--overlay-neutral-pressed);
+}
+
+.password-toggle-icon {
+ display: inline-flex;
+}
+
+.password-toggle [data-password-icon="hide"] {
+ display: none;
+}
+
+.password-toggle.is-visible [data-password-icon="show"] {
+ display: none;
+}
+
+.password-toggle.is-visible [data-password-icon="hide"] {
+ display: inline-flex;
+}
+
+.icon-button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--space-3);
+ background: var(--bgcolor-neutral-primary);
+ border-radius: var(--border-radius-s);
+ outline: var(--border-width-s) solid var(--border-neutral-strong);
+ outline-offset: -1px;
+ border: none;
+ color: var(--fgcolor-neutral-tertiary);
+ cursor: pointer;
+}
+
+.icon-button.is-rotating svg {
+ animation: installer-rotate-once 0.35s linear;
+}
+
+.input-row {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--space-4);
+ width: 100%;
+}
+
+.input-row .input-group {
+ flex: 1;
+}
+
+.input-row .button {
+ margin-top: calc(var(--label-line-height) + var(--space-3));
+}
+
+.input-row .icon-button {
+ margin-top: calc(var(--label-line-height) + var(--space-3));
+}
+
+.review-card {
+ width: 100%;
+ padding: var(--space-6);
+ background: var(--bgcolor-neutral-default);
+ border-radius: var(--border-radius-m);
+ outline: var(--border-width-s) solid var(--border-neutral);
+ outline-offset: -1px;
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-5);
+}
+
+.review-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--gap-m);
+}
+
+.review-label {
+ text-align: right;
+}
+
+.badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--space-1) var(--space-2);
+ border-radius: 6px;
+}
+
+.badge-success {
+ background: var(--bgcolor-success-weak);
+ color: var(--fgcolor-on-success-weak);
+}
+
+.badge-warning {
+ background: var(--bgcolor-warning-weak);
+ color: var(--fgcolor-on-warning-weak);
+}
+
+.badge-neutral {
+ background: var(--bgcolor-neutral-tertiary);
+ color: var(--fgcolor-neutral-secondary);
+}
+
+.installer-footer {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ padding-bottom: var(--space-7);
+ position: relative;
+ z-index: 1;
+}
+
+.appwrite-logo {
+ width: 131px;
+ height: 25px;
+ position: relative;
+}
+
+.appwrite-logo svg {
+ width: 100%;
+ height: 100%;
+ display: block;
+}
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ * {
+ transition: none !important;
+ animation: none !important;
+ }
+}
+
+@keyframes installer-rotate-once {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (max-width: 640px) {
+ .installer-page {
+ padding: var(--space-6);
+ gap: var(--gap-m);
+ }
+
+ .installer-card {
+ padding: var(--space-6);
+ }
+
+ .installer-step {
+ min-height: 0;
+ }
+
+ .selector-group {
+ flex-direction: column;
+ }
+
+ .input-row {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .input-row .button {
+ align-self: flex-start;
+ margin-top: 0;
+ }
+
+ .input-row .icon-button {
+ margin-top: 0;
+ }
+
+ .step-layout[data-step='2'] .input-row {
+ flex-direction: row;
+ align-items: flex-end;
+ }
+
+ .action-bar {
+ gap: var(--gap-s);
+ }
+}
diff --git a/app/views/install/installer/icons/appwrite-logo.svg b/app/views/install/installer/icons/appwrite-logo.svg
new file mode 100644
index 0000000000..f0f2fa5af0
--- /dev/null
+++ b/app/views/install/installer/icons/appwrite-logo.svg
@@ -0,0 +1,13 @@
+
diff --git a/app/views/install/installer/icons/appwrite-mark.svg b/app/views/install/installer/icons/appwrite-mark.svg
new file mode 100644
index 0000000000..7dff2b5fc4
--- /dev/null
+++ b/app/views/install/installer/icons/appwrite-mark.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/views/install/installer/icons/chevron-down.svg b/app/views/install/installer/icons/chevron-down.svg
new file mode 100644
index 0000000000..74d9c14cfd
--- /dev/null
+++ b/app/views/install/installer/icons/chevron-down.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/views/install/installer/icons/copy.svg b/app/views/install/installer/icons/copy.svg
new file mode 100644
index 0000000000..008d89098a
--- /dev/null
+++ b/app/views/install/installer/icons/copy.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/views/install/installer/icons/exclamation-circle.svg b/app/views/install/installer/icons/exclamation-circle.svg
new file mode 100644
index 0000000000..3416951e48
--- /dev/null
+++ b/app/views/install/installer/icons/exclamation-circle.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/views/install/installer/icons/eye-off.svg b/app/views/install/installer/icons/eye-off.svg
new file mode 100644
index 0000000000..9c0b0a063b
--- /dev/null
+++ b/app/views/install/installer/icons/eye-off.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/views/install/installer/icons/eye.svg b/app/views/install/installer/icons/eye.svg
new file mode 100644
index 0000000000..897b425bf3
--- /dev/null
+++ b/app/views/install/installer/icons/eye.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/views/install/installer/icons/indicator-active.svg b/app/views/install/installer/icons/indicator-active.svg
new file mode 100644
index 0000000000..e620bdf6e9
--- /dev/null
+++ b/app/views/install/installer/icons/indicator-active.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/views/install/installer/icons/indicator-inactive.svg b/app/views/install/installer/icons/indicator-inactive.svg
new file mode 100644
index 0000000000..78b5ef3ac0
--- /dev/null
+++ b/app/views/install/installer/icons/indicator-inactive.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/views/install/installer/icons/info.svg b/app/views/install/installer/icons/info.svg
new file mode 100644
index 0000000000..6f0524fb49
--- /dev/null
+++ b/app/views/install/installer/icons/info.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/views/install/installer/icons/install-bg-1.svg b/app/views/install/installer/icons/install-bg-1.svg
new file mode 100644
index 0000000000..7291974bee
--- /dev/null
+++ b/app/views/install/installer/icons/install-bg-1.svg
@@ -0,0 +1,16 @@
+
diff --git a/app/views/install/installer/icons/install-bg-2.svg b/app/views/install/installer/icons/install-bg-2.svg
new file mode 100644
index 0000000000..9e49ddb99f
--- /dev/null
+++ b/app/views/install/installer/icons/install-bg-2.svg
@@ -0,0 +1,16 @@
+
diff --git a/app/views/install/installer/icons/install-check.svg b/app/views/install/installer/icons/install-check.svg
new file mode 100644
index 0000000000..7d43d007c6
--- /dev/null
+++ b/app/views/install/installer/icons/install-check.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/views/install/installer/icons/install-spinner.svg b/app/views/install/installer/icons/install-spinner.svg
new file mode 100644
index 0000000000..543d2c4b22
--- /dev/null
+++ b/app/views/install/installer/icons/install-spinner.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/views/install/installer/icons/lock.svg b/app/views/install/installer/icons/lock.svg
new file mode 100644
index 0000000000..6a87c21345
--- /dev/null
+++ b/app/views/install/installer/icons/lock.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/views/install/installer/icons/mariadb.svg b/app/views/install/installer/icons/mariadb.svg
new file mode 100644
index 0000000000..921fbbb0dc
--- /dev/null
+++ b/app/views/install/installer/icons/mariadb.svg
@@ -0,0 +1,10 @@
+
diff --git a/app/views/install/installer/icons/mongodb.svg b/app/views/install/installer/icons/mongodb.svg
new file mode 100644
index 0000000000..5470eccb38
--- /dev/null
+++ b/app/views/install/installer/icons/mongodb.svg
@@ -0,0 +1,6 @@
+
diff --git a/app/views/install/installer/icons/postgresql.svg b/app/views/install/installer/icons/postgresql.svg
new file mode 100644
index 0000000000..f891b75c37
--- /dev/null
+++ b/app/views/install/installer/icons/postgresql.svg
@@ -0,0 +1,27 @@
+
diff --git a/app/views/install/installer/icons/refresh.svg b/app/views/install/installer/icons/refresh.svg
new file mode 100644
index 0000000000..56f70b425a
--- /dev/null
+++ b/app/views/install/installer/icons/refresh.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/views/install/installer/icons/step-dot.svg b/app/views/install/installer/icons/step-dot.svg
new file mode 100644
index 0000000000..490a039d20
--- /dev/null
+++ b/app/views/install/installer/icons/step-dot.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/views/install/installer/icons/warning.svg b/app/views/install/installer/icons/warning.svg
new file mode 100644
index 0000000000..131b29171c
--- /dev/null
+++ b/app/views/install/installer/icons/warning.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/views/install/installer/js/constants.js b/app/views/install/installer/js/constants.js
new file mode 100644
index 0000000000..940c0bc780
--- /dev/null
+++ b/app/views/install/installer/js/constants.js
@@ -0,0 +1,11 @@
+(() => {
+ window.InstallerConstants = Object.freeze({
+ stepTransitionMs: 260,
+ errorClearMs: 180,
+ installPollIntervalMs: 4000,
+ installFallbackDelayMs: 12000,
+ redirectDelayMs: 2500,
+ progressTransitionDelayMs: 320,
+ progressCompleteDelayMs: 140,
+ });
+})();
diff --git a/app/views/install/installer/js/installer.js b/app/views/install/installer/js/installer.js
new file mode 100644
index 0000000000..463b7f6221
--- /dev/null
+++ b/app/views/install/installer/js/installer.js
@@ -0,0 +1,450 @@
+(() => {
+ const stepContainer = document.querySelector('.installer-step');
+ const installerCard = document.querySelector('.installer-card');
+ const backButton = document.querySelector('[data-action="back"]');
+ const nextButton = document.querySelector('[data-action="next"]');
+ const installScreen = document.querySelector('.install-screen-content');
+ const indicatorNodes = Array.from(document.querySelectorAll('.step-indicator'));
+ const STEP_TRANSITION_TIMEOUT = window.InstallerConstants?.stepTransitionMs ?? 260;
+
+ if (!stepContainer || !installerCard) return;
+
+ const { validateInstallRequest } = window.InstallerStepsProgress || {};
+
+ const isUpgrade = document.body?.dataset.upgrade === 'true';
+ const stepFlow = isUpgrade ? [1, 4, 5] : [1, 2, 3, 4, 5];
+ const cardSteps = stepFlow.filter((step) => step !== 5);
+
+ const normalizeStep = (step) => {
+ const numeric = clampStep(step);
+ if (stepFlow.includes(numeric)) return numeric;
+ if (numeric <= stepFlow[0]) return stepFlow[0];
+ for (let i = 0; i < stepFlow.length; i += 1) {
+ if (numeric < stepFlow[i]) {
+ return stepFlow[i];
+ }
+ }
+ return stepFlow[stepFlow.length - 1];
+ };
+
+ const buildStepConfig = () => {
+ const config = {};
+ stepFlow.forEach((step, index) => {
+ if (step === 5) {
+ config[step] = { back: { target: null }, next: { target: null } };
+ return;
+ }
+ const prev = stepFlow[index - 1] ?? null;
+ const next = stepFlow[index + 1] ?? null;
+ const label = next === 5 ? (isUpgrade ? 'Update' : 'Install') : 'Next';
+ config[step] = {
+ back: { target: prev },
+ next: { label, target: next }
+ };
+ });
+ return config;
+ };
+
+ const STEP_CONFIG = buildStepConfig();
+
+ const stepCache = new Map();
+ let maxStepHeight = 0;
+ let isTransitioning = false;
+ let pendingStep = null;
+ let pendingPushState = false;
+
+ const clampStep = (step) => Math.max(1, Math.min(5, step));
+ const isInstallLocked = () => Boolean(window.InstallerSteps?.isInstallLocked?.());
+
+ const scrollToFirstError = (panel) => {
+ if (!panel) return;
+ const getErrorNode = () => panel.querySelector('.field-error.is-visible')
+ || panel.querySelector('.field-error')
+ || panel.querySelector('.input-field.is-error, .input-action.is-error');
+ const container = panel.closest('.step-panel') || panel;
+ const attemptScroll = () => {
+ const target = getErrorNode();
+ if (!target || typeof target.getBoundingClientRect !== 'function') return false;
+ const targetRect = target.getBoundingClientRect();
+ const containerRect = container.getBoundingClientRect();
+ const targetTop = targetRect.top - containerRect.top + container.scrollTop;
+ const targetBottom = targetTop + targetRect.height;
+ const viewTop = container.scrollTop;
+ const viewBottom = viewTop + containerRect.height;
+ const padding = 12;
+
+ let nextScrollTop = viewTop;
+ if (targetTop < viewTop + padding) {
+ nextScrollTop = Math.max(0, targetTop - padding);
+ } else if (targetBottom > viewBottom - padding) {
+ nextScrollTop = Math.max(0, targetBottom - containerRect.height + padding);
+ }
+
+ if (Math.abs(nextScrollTop - viewTop) < 1) {
+ return false;
+ }
+
+ container.scrollTo({ top: nextScrollTop, behavior: 'smooth' });
+ return true;
+ };
+
+ let remaining = 20;
+ let lastScrollTop = -1;
+ const settle = () => {
+ if (remaining <= 0) return;
+ const moved = attemptScroll();
+ remaining -= 1;
+ const currentTop = container.scrollTop;
+ const delta = Math.abs(currentTop - lastScrollTop);
+ lastScrollTop = currentTop;
+ if (!moved && delta < 0.5) {
+ return;
+ }
+ requestAnimationFrame(settle);
+ };
+ requestAnimationFrame(settle);
+ };
+
+ const getStepFromUrl = () => {
+ const url = new URL(window.location.href);
+ const step = Number(url.searchParams.get('step') || 1);
+ return normalizeStep(Number.isNaN(step) ? 1 : step);
+ };
+
+ const buildStepUrl = (step) => {
+ const url = new URL(window.location.href);
+ url.searchParams.set('step', step);
+ return url;
+ };
+
+ const setStepInUrl = (step, pushState) => {
+ const url = new URL(window.location.href);
+ url.searchParams.set('step', step);
+
+ if (pushState) {
+ window.history.pushState({ step }, '', url.toString());
+ }
+
+ return url;
+ };
+
+ const updateActionBar = (step) => {
+ const config = STEP_CONFIG[step] || STEP_CONFIG[1];
+ if (!backButton || !nextButton) return;
+ const locked = isInstallLocked();
+
+ const setButtonLabel = (button, label) => {
+ if (!button) return;
+ let text = button.querySelector('.button-text');
+ if (!text) {
+ text = document.createElement('span');
+ text.className = 'button-text typography-text-m-500';
+ button.textContent = '';
+ button.appendChild(text);
+ }
+ text.textContent = label;
+ };
+
+ if (!locked && config.back?.target) {
+ backButton.disabled = false;
+ backButton.setAttribute('data-step-target', String(config.back.target));
+ } else {
+ backButton.disabled = true;
+ backButton.removeAttribute('data-step-target');
+ }
+ setButtonLabel(backButton, 'Back');
+
+ if (!locked && config.next?.target) {
+ setButtonLabel(nextButton, config.next?.label || 'Next');
+ nextButton.setAttribute('data-step-target', String(config.next?.target || 1));
+ nextButton.disabled = false;
+ } else {
+ setButtonLabel(nextButton, config.next?.label || 'Next');
+ nextButton.removeAttribute('data-step-target');
+ nextButton.disabled = true;
+ }
+
+ indicatorNodes.forEach((node, index) => {
+ const isVisible = index < cardSteps.length;
+ node.classList.toggle('is-hidden', !isVisible);
+ if (!isVisible) {
+ node.classList.remove('is-active');
+ return;
+ }
+ node.classList.toggle('is-active', cardSteps[index] === step);
+ });
+
+ installerCard.setAttribute('data-step', String(step));
+ document.body.dataset.step = String(step);
+ if (locked) {
+ document.body.dataset.installLocked = 'true';
+ } else {
+ delete document.body.dataset.installLocked;
+ }
+ };
+
+ const measureStepHeight = (panel) => {
+ if (!panel) return;
+ const height = panel.getBoundingClientRect().height;
+ if (!height) return;
+ maxStepHeight = Math.max(maxStepHeight, height);
+ stepContainer.style.setProperty('--step-min-height', `${maxStepHeight}px`);
+ };
+
+ const runStepInit = (step, rootElement) => {
+ if (!window.InstallerSteps || typeof window.InstallerSteps.initStep !== 'function') return;
+ const root = rootElement || stepContainer;
+ window.InstallerSteps.initStep(step, root);
+ updateActionBar(step);
+ };
+
+ const fetchStepHtml = (step, url) => {
+ if (stepCache.has(step)) {
+ return Promise.resolve(stepCache.get(step));
+ }
+
+ const fetchUrl = new URL(url);
+ fetchUrl.searchParams.set('partial', '1');
+
+ return fetch(fetchUrl.toString(), {
+ headers: {
+ 'X-Requested-With': 'XMLHttpRequest'
+ }
+ })
+ .then((response) => {
+ if (!response.ok) {
+ throw new Error('Failed to load step');
+ }
+ return response.text();
+ })
+ .then((html) => {
+ stepCache.set(step, html);
+ return html;
+ });
+ };
+
+ const preloadSteps = (steps) => {
+ const current = getStepFromUrl();
+ const targets = steps.filter((step) => step !== current);
+
+ return Promise.all(
+ targets.map((step) => {
+ const url = buildStepUrl(step);
+ return fetchStepHtml(step, url)
+ .then((html) => {
+ const panel = document.createElement('div');
+ panel.className = 'step-panel is-measure';
+ panel.innerHTML = html;
+ stepContainer.appendChild(panel);
+ panel.getBoundingClientRect();
+ measureStepHeight(panel);
+ panel.remove();
+ })
+ .catch(() => null);
+ })
+ );
+ };
+
+ const swapPanels = (step, html, onDone) => {
+ const activePanel = stepContainer.querySelector('.step-panel');
+
+ const measurePanel = document.createElement('div');
+ measurePanel.className = 'step-panel is-measure';
+ measurePanel.innerHTML = html;
+ stepContainer.appendChild(measurePanel);
+ measurePanel.getBoundingClientRect();
+ measureStepHeight(measurePanel);
+ measurePanel.remove();
+
+ const newPanel = document.createElement('div');
+ newPanel.className = 'step-panel is-entering';
+ newPanel.innerHTML = html;
+ stepContainer.appendChild(newPanel);
+ runStepInit(step, newPanel);
+
+ newPanel.getBoundingClientRect();
+
+ requestAnimationFrame(() => {
+ newPanel.classList.remove('is-entering');
+ newPanel.classList.add('is-active');
+ if (activePanel) {
+ activePanel.classList.add('is-exiting');
+ }
+ });
+
+ const finalize = () => {
+ if (activePanel && activePanel.parentNode) {
+ activePanel.parentNode.removeChild(activePanel);
+ }
+ newPanel.classList.remove('is-entering');
+ if (typeof onDone === 'function') {
+ onDone();
+ }
+ };
+
+ const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ if (prefersReduced) {
+ finalize();
+ return;
+ }
+
+ let finished = false;
+ const finishOnce = () => {
+ if (finished) return;
+ finished = true;
+ finalize();
+ };
+
+ newPanel.addEventListener(
+ 'transitionend',
+ (event) => {
+ if (event.propertyName === 'opacity') {
+ finishOnce();
+ }
+ },
+ { once: true }
+ );
+
+ setTimeout(finishOnce, STEP_TRANSITION_TIMEOUT);
+ };
+
+ const showInstallScreen = (step, html) => {
+ if (!installScreen) return;
+ installScreen.innerHTML = html;
+ runStepInit(step, installScreen);
+ };
+
+ const hideInstallScreen = () => {
+ if (!installScreen) return;
+ installScreen.innerHTML = '';
+ };
+
+ const loadStep = (step, pushState) => {
+ const targetStep = normalizeStep(Number(step));
+ const currentStep = getStepFromUrl();
+ if (targetStep === currentStep && pushState) return;
+
+ isTransitioning = true;
+ const url = setStepInUrl(targetStep, pushState);
+
+ fetchStepHtml(targetStep, url)
+ .then((html) => {
+ if (targetStep === 5) {
+ showInstallScreen(targetStep, html);
+ isTransitioning = false;
+ if (pendingStep !== null && pendingStep !== targetStep) {
+ const nextStep = pendingStep;
+ const nextPushState = pendingPushState;
+ pendingStep = null;
+ pendingPushState = false;
+ loadStep(nextStep, nextPushState);
+ return;
+ }
+ pendingStep = null;
+ pendingPushState = false;
+ return;
+ }
+
+ hideInstallScreen();
+ swapPanels(targetStep, html, () => {
+ isTransitioning = false;
+ if (pendingStep !== null && pendingStep !== targetStep) {
+ const nextStep = pendingStep;
+ const nextPushState = pendingPushState;
+ pendingStep = null;
+ pendingPushState = false;
+ loadStep(nextStep, nextPushState);
+ return;
+ }
+ pendingStep = null;
+ pendingPushState = false;
+ });
+ })
+ .catch(() => {
+ isTransitioning = false;
+ window.location.href = url.toString();
+ });
+ };
+
+ const requestStep = (step, pushState) => {
+ const targetStep = normalizeStep(Number(step));
+ if (isInstallLocked() && targetStep !== 5) {
+ loadStep(5, true);
+ return;
+ }
+ if (isTransitioning) {
+ pendingStep = targetStep;
+ pendingPushState = pendingPushState || pushState;
+ return;
+ }
+ loadStep(targetStep, pushState);
+ };
+
+ document.addEventListener('click', async (event) => {
+ const button = event.target.closest('[data-step-target]');
+ if (!button || button.disabled) return;
+ event.preventDefault();
+ const target = button.getAttribute('data-step-target');
+ if (!target) return;
+ const action = button.getAttribute('data-action');
+ if (action === 'next') {
+ const currentStep = getStepFromUrl();
+ const panel = stepContainer.querySelector('.step-panel') || stepContainer;
+ const validator = window.InstallerSteps?.validateStep;
+ if (typeof validator === 'function') {
+ const valid = validator(currentStep, panel);
+ if (!valid) {
+ scrollToFirstError(panel);
+ return;
+ }
+ }
+ }
+ if (action === 'next' && String(target) === '5' && typeof validateInstallRequest === 'function') {
+ const isValid = await validateInstallRequest();
+ if (!isValid) {
+ return;
+ }
+ }
+ if (isInstallLocked() && Number(target) !== 5) {
+ requestStep(5, true);
+ return;
+ }
+ requestStep(target, true);
+ });
+
+ window.addEventListener('popstate', (event) => {
+ const step = event.state?.step || getStepFromUrl();
+ if (isInstallLocked() && Number(step) !== 5) {
+ requestStep(5, false);
+ return;
+ }
+ requestStep(step, false);
+ });
+
+ document.addEventListener('DOMContentLoaded', () => {
+ let step = getStepFromUrl();
+ if (isInstallLocked() && step !== 5) {
+ const url = buildStepUrl(5);
+ window.history.replaceState({ step: 5 }, '', url.toString());
+ step = 5;
+ } else {
+ const url = buildStepUrl(step);
+ window.history.replaceState({ step }, '', url.toString());
+ }
+ const activePanel = stepContainer.querySelector('.step-panel') || stepContainer;
+ runStepInit(step, activePanel);
+ measureStepHeight(activePanel);
+ if (step === 5 && installScreen) {
+ runStepInit(step, installScreen);
+ }
+ const preload = () => {
+ measureStepHeight(activePanel);
+ preloadSteps(cardSteps);
+ };
+ if (document.fonts && document.fonts.ready) {
+ document.fonts.ready.then(preload).catch(preload);
+ } else {
+ preload();
+ }
+ });
+})();
diff --git a/app/views/install/installer/js/modules/context.js b/app/views/install/installer/js/modules/context.js
new file mode 100644
index 0000000000..bc08a8c8df
--- /dev/null
+++ b/app/views/install/installer/js/modules/context.js
@@ -0,0 +1,105 @@
+(() => {
+ const getBodyDataset = () => document.body?.dataset ?? {};
+ const isUpgradeMode = () => getBodyDataset().upgrade === 'true';
+ const getLockedDatabase = () => getBodyDataset().lockedDatabase || '';
+
+ const STEP_IDS = Object.freeze({
+ CONFIG_FILES: 'config-files',
+ DOCKER_COMPOSE: 'docker-compose',
+ ENV_VARS: 'env-vars',
+ DOCKER_CONTAINERS: 'docker-containers',
+ ACCOUNT_SETUP: 'account-setup'
+ });
+
+ const STATUS = Object.freeze({
+ IN_PROGRESS: 'in-progress',
+ COMPLETED: 'completed',
+ ERROR: 'error'
+ });
+
+ const SSE_EVENTS = Object.freeze({
+ PING: 'ping',
+ INSTALL_ID: 'install-id',
+ PROGRESS: 'progress',
+ DONE: 'done',
+ ERROR: 'error'
+ });
+
+ const buildInstallationSteps = (upgrade) => (upgrade ? [
+ {
+ id: STEP_IDS.CONFIG_FILES,
+ inProgress: 'Updating configuration files...',
+ done: 'Configuration files updated'
+ },
+ {
+ id: STEP_IDS.DOCKER_COMPOSE,
+ inProgress: 'Updating Docker Compose file...',
+ done: 'Docker Compose file updated'
+ },
+ {
+ id: STEP_IDS.ENV_VARS,
+ inProgress: 'Updating environment variables...',
+ done: 'Environment variables updated'
+ },
+ {
+ id: STEP_IDS.DOCKER_CONTAINERS,
+ inProgress: 'Restarting Docker containers...',
+ done: 'Docker containers restarted'
+ }
+ ] : [
+ {
+ id: STEP_IDS.CONFIG_FILES,
+ inProgress: 'Creating configuration files...',
+ done: 'Configuration files created'
+ },
+ {
+ id: STEP_IDS.DOCKER_COMPOSE,
+ inProgress: 'Generating Docker Compose file...',
+ done: 'Docker Compose file generated'
+ },
+ {
+ id: STEP_IDS.ENV_VARS,
+ inProgress: 'Configuring environment variables...',
+ done: 'Environment variables configured'
+ },
+ {
+ id: STEP_IDS.DOCKER_CONTAINERS,
+ inProgress: 'Starting Docker containers...',
+ done: 'Docker containers started'
+ },
+ {
+ id: STEP_IDS.ACCOUNT_SETUP,
+ inProgress: 'Creating Appwrite account...',
+ done: 'Appwrite account created (redirecting...)'
+ }
+ ]);
+
+ const INSTALLATION_STEPS = buildInstallationSteps(isUpgradeMode());
+ const CONSTANTS = window.InstallerConstants || {};
+ const TIMINGS = {
+ errorClear: CONSTANTS.errorClearMs ?? 180,
+ installPollInterval: CONSTANTS.installPollIntervalMs ?? 4000,
+ installFallbackDelay: CONSTANTS.installFallbackDelayMs ?? 12000,
+ redirectDelay: CONSTANTS.redirectDelayMs ?? 500,
+ progressTransitionDelay: CONSTANTS.progressTransitionDelayMs ?? 140,
+ progressCompleteDelay: CONSTANTS.progressCompleteDelayMs ?? 120
+ };
+
+ const clampStep = (step) => {
+ const numeric = Number(step);
+ if (Number.isNaN(numeric)) return 1;
+ return Math.max(1, Math.min(5, numeric));
+ };
+
+ window.InstallerStepsContext = Object.freeze({
+ getBodyDataset,
+ isUpgradeMode,
+ getLockedDatabase,
+ STEP_IDS,
+ STATUS,
+ SSE_EVENTS,
+ INSTALLATION_STEPS,
+ TIMINGS,
+ clampStep
+ });
+})();
diff --git a/app/views/install/installer/js/modules/progress.js b/app/views/install/installer/js/modules/progress.js
new file mode 100644
index 0000000000..a6e784ada9
--- /dev/null
+++ b/app/views/install/installer/js/modules/progress.js
@@ -0,0 +1,899 @@
+(() => {
+ const {
+ INSTALLATION_STEPS,
+ TIMINGS,
+ getBodyDataset,
+ isUpgradeMode,
+ STEP_IDS,
+ STATUS,
+ SSE_EVENTS
+ } = window.InstallerStepsContext;
+ const {
+ formState,
+ applyLockPayload,
+ applyBodyDefaults,
+ setInstallLock,
+ getInstallLock,
+ clearInstallLock,
+ isInstallLocked,
+ syncInstallLockFlag,
+ getStoredInstallId,
+ storeInstallId,
+ clearInstallId
+ } = window.InstallerStepsState || {};
+ const { extractHostname, isLocalHost } = window.InstallerStepsValidation || {};
+ const { generateSecretKey } = window.InstallerStepsUI || {};
+ const { showToast } = window.InstallerToast || {};
+
+ let activeInstall = null;
+ let unloadGuard = null;
+ const csrfToken = document.querySelector('meta[name="appwrite-installer-csrf"]')?.getAttribute('content') || '';
+
+ const withCsrfHeader = (headers = {}) => {
+ if (!csrfToken) {
+ return headers;
+ }
+ return { ...headers, 'X-Appwrite-Installer-CSRF': csrfToken };
+ };
+
+ const showCsrfToast = () => {
+ showToast?.({
+ status: 'error',
+ title: 'Session expired',
+ description: 'Refresh the page and try again.',
+ dismissible: true
+ });
+ };
+
+ const validateInstallRequest = async () => {
+ try {
+ const response = await fetch('/install/validate', {
+ method: 'POST',
+ headers: withCsrfHeader({
+ 'Content-Type': 'application/json'
+ })
+ });
+ if (!response.ok) {
+ showCsrfToast();
+ return false;
+ }
+ const data = await response.json().catch(() => ({}));
+ if (!data?.success) {
+ showCsrfToast();
+ return false;
+ }
+ return true;
+ } catch (error) {
+ showCsrfToast();
+ return false;
+ }
+ };
+
+ const setUnloadGuard = (enabled) => {
+ if (!enabled && unloadGuard) {
+ window.removeEventListener('beforeunload', unloadGuard);
+ unloadGuard = null;
+ return;
+ }
+
+ if (enabled && !unloadGuard) {
+ unloadGuard = (event) => {
+ event.preventDefault();
+ event.returnValue = '';
+ return '';
+ };
+ window.addEventListener('beforeunload', unloadGuard);
+ }
+ };
+
+ const cleanupInstallFlow = () => {
+ if (activeInstall?.controller) {
+ activeInstall.controller.abort();
+ if (activeInstall.pollTimer) {
+ clearInterval(activeInstall.pollTimer);
+ }
+ if (activeInstall.fallbackTimer) {
+ clearTimeout(activeInstall.fallbackTimer);
+ }
+ activeInstall = null;
+ }
+ stopSyncedSpinnerRotation();
+ setUnloadGuard(false);
+ };
+
+ const getStepDefinition = (id) => INSTALLATION_STEPS.find((step) => step.id === id);
+
+ const getProgressLabel = (step, status, message) => {
+ if (!step) return message || '';
+ if (status === STATUS.ERROR) {
+ const normalized = normalizeInstallError(message || '');
+ return normalized.summary || 'Installation failed.';
+ }
+ if (status === STATUS.COMPLETED) return step.done;
+ return step.inProgress;
+ };
+
+ const updateInstallRow = (row, step, status, message) => {
+ if (!row || !step) return;
+ row.dataset.status = status;
+ row.dataset.step = step.id;
+ if (status !== STATUS.ERROR) {
+ row.classList.remove('is-open');
+ const toggle = row.querySelector('[data-install-toggle]');
+ if (toggle) {
+ toggle.setAttribute('aria-expanded', 'false');
+ }
+ }
+ const label = getProgressLabel(step, status, message);
+ const text = row.querySelector('[data-install-text]');
+ if (text) {
+ if (text.textContent !== label) {
+ text.classList.remove('is-enter');
+ text.textContent = label;
+ text.classList.add('is-enter');
+ requestAnimationFrame(() => {
+ text.classList.remove('is-enter');
+ });
+ }
+ }
+
+ // Show/hide "Navigate to Console" button for account setup errors
+ const consoleBtn = row.querySelector('[data-install-console]');
+ if (consoleBtn) {
+ const shouldShow = step.id === STEP_IDS.ACCOUNT_SETUP && status === STATUS.ERROR;
+ consoleBtn.classList.toggle('is-hidden', !shouldShow);
+ }
+ };
+
+ const normalizeInstallError = (message) => {
+ const text = String(message || '').trim();
+ if (!text) {
+ return { summary: '', details: '' };
+ }
+ const colonIndex = text.indexOf(':');
+ if (colonIndex > 0 && colonIndex < 80) {
+ const summary = text.slice(0, colonIndex).trim();
+ const details = text.slice(colonIndex + 1).trim();
+ return { summary, details };
+ }
+ if (text.length > 180) {
+ return { summary: text.slice(0, 180).trim() + '…', details: text };
+ }
+ return { summary: text, details: '' };
+ };
+
+ let spinnerAnimationFrame = null;
+ const stopSyncedSpinnerRotation = () => {
+ if (spinnerAnimationFrame) {
+ cancelAnimationFrame(spinnerAnimationFrame);
+ spinnerAnimationFrame = null;
+ }
+ };
+
+ const startSyncedSpinnerRotation = (container) => {
+ stopSyncedSpinnerRotation();
+ if (!container) return;
+ let startTime = null;
+ const animate = (timestamp) => {
+ if (!startTime) startTime = timestamp;
+ const elapsed = timestamp - startTime;
+ const rotation = ((elapsed / 1000) * 360 * 1.5) % 360;
+ container.style.setProperty('--spinner-rotation', `${rotation}deg`);
+ spinnerAnimationFrame = requestAnimationFrame(animate);
+ };
+ spinnerAnimationFrame = requestAnimationFrame(animate);
+ };
+
+ const updateInstallErrorDetails = (row, error) => {
+ if (!row) return;
+ const traceNode = row.querySelector('[data-install-trace]');
+ const normalized = normalizeInstallError(error?.message || '');
+ const output = error?.output || '';
+ const trace = error?.trace || '';
+ const detailChunks = [];
+ if (normalized.details) detailChunks.push(normalized.details);
+ if (output) detailChunks.push(output);
+ if (trace) detailChunks.push(trace);
+ const detailText = detailChunks.join('\n\n');
+
+ if (traceNode) {
+ traceNode.textContent = detailText;
+ traceNode.style.display = detailText ? 'block' : 'none';
+ }
+ };
+
+ const createInstallRow = (template, step) => {
+ const fragment = template.content.cloneNode(true);
+ const row = fragment.querySelector('.install-row');
+ if (!row) return null;
+ const toggle = row.querySelector('[data-install-toggle]');
+ const setOpenState = (isOpen) => {
+ row.classList.toggle('is-open', isOpen);
+ if (toggle) {
+ toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
+ }
+ };
+ const toggleRow = () => {
+ if (!row.dataset.status || row.dataset.status !== STATUS?.ERROR) {
+ return;
+ }
+ setOpenState(!row.classList.contains('is-open'));
+ };
+ row.addEventListener('click', (event) => {
+ if (event.target.closest('[data-install-retry]')) {
+ return;
+ }
+ if (event.target.closest('[data-install-toggle]')) {
+ return;
+ }
+ if (event.target.closest('.install-row-details')) {
+ return;
+ }
+ toggleRow();
+ });
+ if (toggle) {
+ toggle.addEventListener('click', (event) => {
+ event.stopPropagation();
+ toggleRow();
+ });
+ }
+ updateInstallRow(row, step, STATUS.IN_PROGRESS);
+ return row;
+ };
+
+ const generateInstallId = () => {
+ if (window.crypto?.randomUUID) {
+ return window.crypto.randomUUID();
+ }
+ const bytes = new Uint8Array(16);
+ window.crypto.getRandomValues(bytes);
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
+ };
+
+ const buildRedirectUrl = () => {
+ const dataset = getBodyDataset?.() ?? {};
+ const rawDomain = (formState?.appDomain || dataset.defaultAppDomain || '').trim();
+ if (!rawDomain) return '';
+ const httpPort = (formState?.httpPort || dataset.defaultHttpPort || '').trim();
+ const httpsPort = (formState?.httpsPort || dataset.defaultHttpsPort || '').trim();
+ const hasPort = rawDomain.includes(':') || rawDomain.startsWith('[');
+ let host = rawDomain;
+ const hostForProtocol = extractHostname?.(rawDomain);
+ const normalizedHost = hostForProtocol?.toLowerCase?.() ?? '';
+ if (hostForProtocol === '0.0.0.0') {
+ host = rawDomain.replace('0.0.0.0', 'localhost');
+ } else if (normalizedHost === 'traefik') {
+ host = rawDomain.replace(hostForProtocol, 'localhost');
+ }
+ let protocol = 'http';
+ let port = httpPort;
+ if (httpsPort && httpsPort !== '0' && !isLocalHost?.(normalizedHost)) {
+ protocol = 'https';
+ port = httpsPort;
+ }
+ if (!hasPort && port && ((protocol === 'http' && port !== '80') || (protocol === 'https' && port !== '443'))) {
+ host = `${host}:${port}`;
+ }
+ return `${protocol}://${host}`;
+ };
+
+ const redirectToApp = () => {
+ const url = buildRedirectUrl();
+ if (!url) return;
+ // Fire-and-forget: tell the installer server it can shut down
+ fetch('/install/shutdown', { method: 'POST', headers: withCsrfHeader() }).catch(() => {});
+ window.location.href = url;
+ };
+
+ const notifyInstallComplete = (installId, session) => {
+ if (!installId) return Promise.resolve();
+ const payload = { installId };
+ const sessionSecret = session?.sessionSecret || session?.secret;
+ const sessionId = session?.sessionId || session?.id;
+ const sessionExpire = session?.sessionExpire || session?.expire;
+ if (sessionSecret) {
+ payload.sessionSecret = sessionSecret;
+ }
+ if (sessionId) {
+ payload.sessionId = sessionId;
+ }
+ if (sessionExpire) {
+ payload.sessionExpire = sessionExpire;
+ }
+ return fetch('/install/complete', {
+ method: 'POST',
+ headers: withCsrfHeader({
+ 'Content-Type': 'application/json'
+ }),
+ body: JSON.stringify(payload)
+ }).catch(() => {});
+ };
+
+ const buildInstallPayload = (installId) => {
+ const normalizedSecret = (formState?.opensslKey || '').trim();
+ if (!normalizedSecret && generateSecretKey && !isUpgradeMode?.()) {
+ formState.opensslKey = generateSecretKey();
+ }
+ const normalizedDomain = (formState?.appDomain || '').trim() || 'localhost';
+ const normalizedHttpPort = (formState?.httpPort || '').trim() || '80';
+ const normalizedHttpsPort = (formState?.httpsPort || '').trim() || '443';
+ const normalizedEmail = (formState?.emailCertificates || '').trim();
+ const normalizedAssistantKey = (formState?.assistantOpenAIKey || '').trim();
+ const normalizedAccountEmail = (formState?.accountEmail || '').trim();
+ const normalizedAccountPassword = (formState?.accountPassword || '').trim();
+
+ return {
+ installId,
+ httpPort: normalizedHttpPort,
+ httpsPort: normalizedHttpsPort,
+ database: formState?.database || 'mongodb',
+ appDomain: normalizedDomain,
+ emailCertificates: normalizedEmail,
+ opensslKey: (formState?.opensslKey || '').trim(),
+ assistantOpenAIKey: normalizedAssistantKey,
+ accountEmail: normalizedAccountEmail,
+ accountPassword: normalizedAccountPassword
+ };
+ };
+
+ const fetchInstallStatus = async (installId) => {
+ if (!installId) return null;
+ const response = await fetch(`/install/status?installId=${encodeURIComponent(installId)}`, {
+ cache: 'no-store'
+ });
+ if (!response.ok) return null;
+ const json = await response.json();
+ return json.progress || null;
+ };
+
+ const readEventStream = async (stream, onEvent) => {
+ const reader = stream.getReader();
+ const decoder = new TextDecoder('utf-8');
+ let buffer = '';
+
+ try {
+ const processEvent = (rawEvent) => {
+ if (!rawEvent) return;
+ const lines = rawEvent.split('\n');
+ let eventName = 'message';
+ let data = '';
+
+ lines.forEach((line) => {
+ if (line.startsWith('event:')) {
+ eventName = line.replace('event:', '').trim();
+ } else if (line.startsWith('data:')) {
+ data += line.replace('data:', '').trim();
+ }
+ });
+
+ if (data) {
+ try {
+ const parsed = JSON.parse(data);
+ onEvent(eventName, parsed);
+ } catch (error) {
+ onEvent(eventName, { message: data });
+ }
+ }
+ };
+
+ while (true) {
+ const { value, done } = await reader.read();
+ if (done) {
+ buffer = buffer.replace(/\r\n/g, '\n');
+ if (buffer.trim()) {
+ processEvent(buffer);
+ }
+ break;
+ }
+ buffer += decoder.decode(value, { stream: true });
+ buffer = buffer.replace(/\r\n/g, '\n');
+ let separatorIndex = buffer.indexOf('\n\n');
+
+ while (separatorIndex !== -1) {
+ const rawEvent = buffer.slice(0, separatorIndex);
+ buffer = buffer.slice(separatorIndex + 2);
+ processEvent(rawEvent);
+ separatorIndex = buffer.indexOf('\n\n');
+ }
+ }
+ } finally {
+ try {
+ reader.releaseLock();
+ } catch (error) {}
+ }
+ };
+
+ const initStep5 = (root) => {
+ if (!root) return;
+
+ if (activeInstall?.controller) {
+ activeInstall.controller.abort();
+ }
+ if (activeInstall?.pollTimer) {
+ clearInterval(activeInstall.pollTimer);
+ }
+ if (activeInstall?.fallbackTimer) {
+ clearTimeout(activeInstall.fallbackTimer);
+ }
+ activeInstall = null;
+
+ const list = root.querySelector('[data-install-list]');
+ const template = root.querySelector('#install-row-template');
+ if (!list || !template) return;
+ startSyncedSpinnerRotation(list);
+
+ list.innerHTML = '';
+ const rowsById = new Map();
+ const progressState = new Map();
+ syncInstallLockFlag?.();
+ applyLockPayload?.();
+ applyBodyDefaults?.();
+
+ const ensureRow = (step) => {
+ if (!step) return null;
+ if (rowsById.has(step.id)) {
+ return rowsById.get(step.id);
+ }
+ const row = createInstallRow(template, step);
+ if (!row) return null;
+ row.classList.add('is-entering');
+ list.appendChild(row);
+ row.getBoundingClientRect();
+ requestAnimationFrame(() => {
+ row.classList.remove('is-entering');
+ });
+ rowsById.set(step.id, row);
+ return row;
+ };
+
+ const installPanel = root.querySelector('.install-panel');
+ let panelHeightCleanup = null;
+ const animatePanelHeight = (mutate) => {
+ if (!installPanel) {
+ mutate();
+ return;
+ }
+ if (panelHeightCleanup) {
+ panelHeightCleanup();
+ panelHeightCleanup = null;
+ }
+ const currentHeight = installPanel.getBoundingClientRect().height;
+ installPanel.style.height = `${currentHeight}px`;
+ installPanel.getBoundingClientRect();
+ mutate();
+ const nextHeight = installPanel.getBoundingClientRect().height;
+ if (currentHeight === nextHeight) {
+ installPanel.style.height = '';
+ return;
+ }
+ installPanel.style.height = `${currentHeight}px`;
+ installPanel.getBoundingClientRect();
+ installPanel.style.height = `${nextHeight}px`;
+ const cleanup = () => {
+ installPanel.style.height = '';
+ installPanel.removeEventListener('transitionend', onEnd);
+ };
+ const onEnd = (event) => {
+ if (event.propertyName === 'height') {
+ cleanup();
+ }
+ };
+ panelHeightCleanup = cleanup;
+ installPanel.addEventListener('transitionend', onEnd);
+ };
+
+ const renderProgress = () => {
+ animatePanelHeight(() => {
+ const visibleSteps = [];
+ for (const step of INSTALLATION_STEPS) {
+ const state = progressState.get(step.id);
+ if (!state) break;
+ visibleSteps.push(step);
+ }
+
+ visibleSteps.forEach((step) => {
+ const state = progressState.get(step.id);
+ if (!state) return;
+ const row = ensureRow(step);
+ if (row) {
+ updateInstallRow(row, step, state.status || STATUS.IN_PROGRESS, state.message);
+ if (state.status === STATUS?.ERROR) {
+ updateInstallErrorDetails(row, {
+ message: state.message,
+ trace: state.details?.trace,
+ output: state.details?.output
+ });
+ }
+ }
+ });
+ });
+ };
+
+ const firstStep = INSTALLATION_STEPS[0];
+ if (firstStep) {
+ progressState.set(firstStep.id, {
+ status: STATUS.IN_PROGRESS,
+ message: firstStep.inProgress
+ });
+ }
+ renderProgress();
+
+ const applyProgress = (payload) => {
+ const step = getStepDefinition(payload.step) || {
+ id: payload.step,
+ inProgress: payload.message || payload.step,
+ done: payload.message || payload.step
+ };
+ progressState.set(step.id, {
+ status: payload.status || STATUS.IN_PROGRESS,
+ message: payload.message,
+ details: payload.details
+ });
+ renderProgress();
+ if (activeInstall) {
+ activeInstall.lastEventAt = Date.now();
+ if (payload.status === STATUS.ERROR) {
+ if (activeInstall.pollTimer) {
+ clearInterval(activeInstall.pollTimer);
+ activeInstall.pollTimer = null;
+ }
+ if (activeInstall.fallbackTimer) {
+ clearTimeout(activeInstall.fallbackTimer);
+ activeInstall.fallbackTimer = null;
+ }
+ }
+ }
+ scheduleFallback();
+ };
+
+ const handleProgress = (payload) => {
+ if (!payload || !payload.step) return;
+
+ const existingState = progressState.get(payload.step);
+ if (existingState && existingState.status === STATUS.COMPLETED && payload.status === STATUS.IN_PROGRESS) {
+ return;
+ }
+
+ const step = getStepDefinition(payload.step) || {
+ id: payload.step,
+ inProgress: payload.message || payload.step,
+ done: payload.message || payload.step
+ };
+ if (payload.status === STATUS.IN_PROGRESS) {
+ const currentIndex = INSTALLATION_STEPS.findIndex((candidate) => candidate.id === step.id);
+ if (currentIndex > 0) {
+ for (let i = 0; i < currentIndex; i += 1) {
+ const previousStep = INSTALLATION_STEPS[i];
+ const previousState = progressState.get(previousStep.id);
+ if (previousState && previousState.status !== STATUS.COMPLETED) {
+ progressState.set(previousStep.id, {
+ status: STATUS.COMPLETED,
+ message: previousStep.done,
+ details: previousState.details
+ });
+ }
+ }
+ }
+ }
+ applyProgress(payload);
+ };
+
+ const applySnapshot = (snapshot) => {
+ if (!snapshot || !snapshot.steps) return;
+ INSTALLATION_STEPS.forEach((step) => {
+ const detail = snapshot.steps[step.id];
+ if (!detail) return;
+ progressState.set(step.id, {
+ status: detail.status,
+ message: detail.message,
+ details: snapshot.details?.[step.id]
+ });
+ });
+ renderProgress();
+ };
+
+ const checkAllCompleted = () => {
+ const allDone = INSTALLATION_STEPS.every((step) => {
+ const state = progressState.get(step.id);
+ return state && state.status === STATUS.COMPLETED;
+ });
+ if (!allDone) return;
+ const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP);
+ const sessionDetails = accountState?.details;
+ finalizeInstall();
+ notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
+ setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0);
+ });
+ };
+
+ const startPolling = () => {
+ if (!activeInstall || activeInstall.pollTimer) return;
+ activeInstall.pollTimer = setInterval(async () => {
+ if (!activeInstall || activeInstall.completed) return;
+ const snapshot = await fetchInstallStatus(activeInstall.installId);
+ if (snapshot) {
+ applySnapshot(snapshot);
+ checkAllCompleted();
+ }
+ }, TIMINGS?.installPollInterval ?? 0);
+ };
+
+ const scheduleFallback = () => {
+ if (!activeInstall) return;
+ if (activeInstall.fallbackTimer) {
+ clearTimeout(activeInstall.fallbackTimer);
+ }
+ activeInstall.fallbackTimer = setTimeout(() => {
+ if (!activeInstall) return;
+ startPolling();
+ }, TIMINGS?.installFallbackDelay ?? 0);
+ };
+
+ const finalizeInstall = () => {
+ if (!activeInstall) return;
+ activeInstall.completed = true;
+ if (activeInstall.pollTimer) {
+ clearInterval(activeInstall.pollTimer);
+ }
+ if (activeInstall.fallbackTimer) {
+ clearTimeout(activeInstall.fallbackTimer);
+ }
+ stopSyncedSpinnerRotation();
+ setUnloadGuard(false);
+ };
+
+ const startInstallStream = async (installId, options = {}) => {
+ const isValid = await validateInstallRequest();
+ if (!isValid) {
+ return;
+ }
+ activeInstall = {
+ installId,
+ controller: new AbortController(),
+ lastEventAt: Date.now(),
+ pollTimer: null,
+ fallbackTimer: null,
+ completed: false
+ };
+
+ const payload = buildInstallPayload(installId);
+ if (options.retryStep) {
+ payload.retryStep = options.retryStep;
+ }
+ setInstallLock?.(installId, payload);
+ setUnloadGuard(true);
+
+ try {
+ scheduleFallback();
+ const response = await fetch('/install', {
+ method: 'POST',
+ headers: withCsrfHeader({
+ 'Content-Type': 'application/json',
+ 'Accept': 'text/event-stream'
+ }),
+ body: JSON.stringify(payload),
+ signal: activeInstall.controller.signal
+ });
+
+ if (!response.ok || !response.body) {
+ let errorMessage = null;
+ try {
+ const contentType = response.headers.get('Content-Type') || '';
+ if (contentType.includes('application/json')) {
+ const data = await response.json();
+ errorMessage = data?.message || null;
+ }
+ } catch (error) {
+ errorMessage = null;
+ }
+ if (errorMessage) {
+ handleProgress({
+ step: STEP_IDS.CONFIG_FILES,
+ status: STATUS.ERROR,
+ message: errorMessage
+ });
+ finalizeInstall();
+ return;
+ }
+ startPolling();
+ return;
+ }
+
+ await readEventStream(response.body, (event, data) => {
+ if (!activeInstall) return;
+ if (event === SSE_EVENTS.INSTALL_ID && data?.installId) {
+ activeInstall.installId = data.installId;
+ storeInstallId?.(data.installId);
+ return;
+ }
+ if (event === SSE_EVENTS.PROGRESS) {
+ handleProgress(data);
+ return;
+ }
+ if (event === SSE_EVENTS.DONE) {
+ // Mark every step as completed (preserving details
+ // from earlier progress events, e.g. session info).
+ INSTALLATION_STEPS.forEach((step) => {
+ const existing = progressState.get(step.id);
+ if (!existing || (existing.status !== STATUS.COMPLETED && existing.status !== STATUS.ERROR)) {
+ progressState.set(step.id, {
+ status: STATUS.COMPLETED,
+ message: step.done,
+ details: existing?.details
+ });
+ }
+ });
+ renderProgress();
+
+ // If any step ended in error (e.g. account creation
+ // failed), stay on the progress screen so the user can
+ // see the error and choose to retry or navigate to the
+ // console manually — don't auto-redirect.
+ const hasErrors = INSTALLATION_STEPS.some((step) => {
+ const state = progressState.get(step.id);
+ return state && state.status === STATUS.ERROR;
+ });
+
+ if (hasErrors) {
+ finalizeInstall();
+ return;
+ }
+
+ const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP);
+ const sessionDetails = accountState?.details;
+ finalizeInstall();
+ notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
+ setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0);
+ });
+ return;
+ }
+ if (event === SSE_EVENTS.ERROR) {
+ if (data?.message) {
+ const existingError = Array.from(progressState.values()).some((state) => state?.status === STATUS.ERROR);
+ if (data.step || !existingError) {
+ let targetStep = data.step;
+ if (!targetStep) {
+ for (const candidate of INSTALLATION_STEPS) {
+ const state = progressState.get(candidate.id);
+ if (!state || state.status !== STATUS.COMPLETED) {
+ targetStep = candidate.id;
+ break;
+ }
+ }
+ }
+ handleProgress({
+ step: targetStep || STEP_IDS.CONFIG_FILES,
+ status: STATUS.ERROR,
+ message: data.message,
+ details: data.details
+ });
+ }
+ }
+ finalizeInstall();
+ }
+ });
+ if (activeInstall && !activeInstall.completed) {
+ // Stream ended without a "done" event (e.g. browser
+ // throttled the background tab). Check if we're done.
+ checkAllCompleted();
+ if (!activeInstall?.completed) {
+ startPolling();
+ }
+ }
+ } catch (error) {
+ if (!activeInstall || activeInstall.controller.signal.aborted) {
+ return;
+ }
+ startPolling();
+ }
+ };
+
+ const resumeInstall = async (installId) => {
+ const snapshot = await fetchInstallStatus(installId);
+ if (!snapshot) return false;
+ activeInstall = {
+ installId,
+ controller: new AbortController(),
+ lastEventAt: Date.now(),
+ pollTimer: null,
+ fallbackTimer: null,
+ completed: false
+ };
+ applySnapshot(snapshot);
+ startPolling();
+ setUnloadGuard(true);
+ return true;
+ };
+
+ const resetProgressFrom = (stepId) => {
+ const index = INSTALLATION_STEPS.findIndex((step) => step.id === stepId);
+ if (index === -1) return;
+ INSTALLATION_STEPS.slice(index).forEach((step) => {
+ progressState.delete(step.id);
+ const row = rowsById.get(step.id);
+ if (row && row.parentNode) {
+ row.parentNode.removeChild(row);
+ }
+ rowsById.delete(step.id);
+ });
+ };
+
+ const retryInstallStep = (stepId) => {
+ if (!stepId) return;
+ if (activeInstall?.controller) {
+ activeInstall.controller.abort();
+ }
+ if (activeInstall?.pollTimer) {
+ clearInterval(activeInstall.pollTimer);
+ }
+ if (activeInstall?.fallbackTimer) {
+ clearTimeout(activeInstall.fallbackTimer);
+ }
+
+ resetProgressFrom(stepId);
+
+ const step = getStepDefinition(stepId);
+ progressState.set(stepId, {
+ status: STATUS.IN_PROGRESS,
+ message: step?.inProgress || 'Retrying...'
+ });
+
+ const row = ensureRow(step);
+ if (row) {
+ updateInstallRow(row, step, STATUS.IN_PROGRESS, step.inProgress || 'Retrying...');
+ }
+
+ const installId = activeInstall?.installId || getInstallLock?.()?.installId || generateInstallId();
+ storeInstallId?.(installId);
+ startInstallStream(installId, { retryStep: stepId });
+ };
+
+ list.addEventListener('click', (event) => {
+ const consoleButton = event.target.closest('[data-install-console]');
+ const retryButton = event.target.closest('[data-install-retry]');
+
+ if (consoleButton) {
+ redirectToApp();
+ return;
+ }
+
+ if (retryButton) {
+ const row = retryButton.closest('.install-row');
+ const stepId = row?.dataset.step;
+ retryInstallStep(stepId);
+ }
+ });
+
+ // When the user switches back to this tab, check if installation
+ // finished while the tab was in the background.
+ document.addEventListener('visibilitychange', () => {
+ if (document.visibilityState === 'visible' && activeInstall && !activeInstall.completed) {
+ checkAllCompleted();
+ }
+ });
+
+ const lock = getInstallLock?.();
+ const existingInstallId = lock?.installId || getStoredInstallId?.();
+ if (existingInstallId) {
+ resumeInstall(existingInstallId).then((resumed) => {
+ if (!resumed) {
+ clearInstallId?.();
+ clearInstallLock?.();
+ const newInstallId = generateInstallId();
+ storeInstallId?.(newInstallId);
+ startInstallStream(newInstallId);
+ }
+ });
+ } else {
+ const newInstallId = generateInstallId();
+ storeInstallId?.(newInstallId);
+ startInstallStream(newInstallId);
+ }
+ };
+
+ window.InstallerStepsProgress = {
+ initStep5,
+ cleanupInstallFlow,
+ validateInstallRequest
+ };
+})();
diff --git a/app/views/install/installer/js/modules/state.js b/app/views/install/installer/js/modules/state.js
new file mode 100644
index 0000000000..9fcf9969a8
--- /dev/null
+++ b/app/views/install/installer/js/modules/state.js
@@ -0,0 +1,158 @@
+(() => {
+ const {
+ getBodyDataset,
+ isUpgradeMode,
+ getLockedDatabase
+ } = window.InstallerStepsContext || {};
+
+ const INSTALL_LOCK_KEY = 'appwrite-install-lock';
+ const INSTALL_ID_KEY = 'appwrite-install-id';
+
+ const formState = {
+ appDomain: null,
+ database: null,
+ httpPort: null,
+ httpsPort: null,
+ emailCertificates: null,
+ opensslKey: null,
+ assistantOpenAIKey: null,
+ accountEmail: null,
+ accountPassword: null
+ };
+
+ const dispatchStateChange = (key) => {
+ if (!key || typeof document === 'undefined') return;
+ try {
+ document.dispatchEvent(new CustomEvent('installer:state-change', {
+ detail: { key, value: formState[key] }
+ }));
+ } catch (error) {}
+ };
+
+ const setStateIfEmpty = (key, value) => {
+ if (value === null || value === undefined || value === '') return;
+ if (formState[key] === null || formState[key] === undefined || formState[key] === '') {
+ formState[key] = value;
+ }
+ };
+
+ const applyBodyDefaults = () => {
+ const data = getBodyDataset?.() ?? {};
+ setStateIfEmpty('appDomain', data.defaultAppDomain);
+ setStateIfEmpty('httpPort', data.defaultHttpPort);
+ setStateIfEmpty('httpsPort', data.defaultHttpsPort);
+ setStateIfEmpty('emailCertificates', data.defaultEmailCertificates);
+ setStateIfEmpty('opensslKey', data.defaultSecretKey);
+ setStateIfEmpty('assistantOpenAIKey', data.defaultAssistantOpenaiKey);
+ if (data.lockedDatabase) {
+ formState.database = data.lockedDatabase;
+ }
+ if (!isUpgradeMode?.()) {
+ setStateIfEmpty('database', data.defaultDatabase);
+ }
+ };
+
+ const getInstallLock = () => {
+ try {
+ const raw = sessionStorage.getItem(INSTALL_LOCK_KEY);
+ if (!raw) return null;
+ const parsed = JSON.parse(raw);
+ if (!parsed || typeof parsed !== 'object') return null;
+ return parsed;
+ } catch (error) {
+ return null;
+ }
+ };
+
+ const setInstallLock = (installId, payload) => {
+ const sanitizedPayload = payload ? { ...payload } : null;
+ if (sanitizedPayload) {
+ delete sanitizedPayload.opensslKey;
+ delete sanitizedPayload.accountPassword;
+ delete sanitizedPayload.assistantOpenAIKey;
+ }
+ const lock = {
+ installId,
+ payload: sanitizedPayload,
+ startedAt: Date.now()
+ };
+ try {
+ sessionStorage.setItem(INSTALL_LOCK_KEY, JSON.stringify(lock));
+ } catch (error) {}
+ if (document.body) {
+ document.body.dataset.installLocked = 'true';
+ }
+ return lock;
+ };
+
+ const clearInstallLock = () => {
+ try {
+ sessionStorage.removeItem(INSTALL_LOCK_KEY);
+ } catch (error) {}
+ if (document.body) {
+ delete document.body.dataset.installLocked;
+ }
+ };
+
+ const isInstallLocked = () => {
+ return Boolean(getInstallLock());
+ };
+
+ const syncInstallLockFlag = () => {
+ if (!document.body) return;
+ if (isInstallLocked()) {
+ document.body.dataset.installLocked = 'true';
+ } else {
+ delete document.body.dataset.installLocked;
+ }
+ };
+
+ const applyLockPayload = () => {
+ const lock = getInstallLock();
+ if (!lock || !lock.payload) return;
+ const payload = lock.payload;
+ setStateIfEmpty('appDomain', payload.appDomain);
+ setStateIfEmpty('database', payload.database);
+ setStateIfEmpty('httpPort', payload.httpPort);
+ setStateIfEmpty('httpsPort', payload.httpsPort);
+ setStateIfEmpty('emailCertificates', payload.emailCertificates);
+ setStateIfEmpty('accountEmail', payload.accountEmail);
+ };
+
+ const getStoredInstallId = () => {
+ try {
+ return sessionStorage.getItem(INSTALL_ID_KEY);
+ } catch (error) {
+ return null;
+ }
+ };
+
+ const storeInstallId = (installId) => {
+ try {
+ sessionStorage.setItem(INSTALL_ID_KEY, installId);
+ } catch (error) {}
+ };
+
+ const clearInstallId = () => {
+ try {
+ sessionStorage.removeItem(INSTALL_ID_KEY);
+ } catch (error) {}
+ };
+
+ window.InstallerStepsState = {
+ formState,
+ dispatchStateChange,
+ setStateIfEmpty,
+ applyBodyDefaults,
+ applyLockPayload,
+ getInstallLock,
+ setInstallLock,
+ clearInstallLock,
+ isInstallLocked,
+ syncInstallLockFlag,
+ getStoredInstallId,
+ storeInstallId,
+ clearInstallId,
+ getLockedDatabase: getLockedDatabase || (() => '')
+ };
+})();
diff --git a/app/views/install/installer/js/modules/toast.js b/app/views/install/installer/js/modules/toast.js
new file mode 100644
index 0000000000..5a8eb55f41
--- /dev/null
+++ b/app/views/install/installer/js/modules/toast.js
@@ -0,0 +1,95 @@
+(() => {
+ const TOAST_STACK_ID = 'installer-toast-stack';
+ const DEFAULT_TIMEOUT = 5000;
+ const MAX_TOASTS = 3;
+ const ICONS = {
+ error: '',
+ close: ''
+ };
+
+ const getStack = () => document.getElementById(TOAST_STACK_ID);
+
+ const dismissToast = (toast) => {
+ if (!toast) return;
+ if (toast.classList.contains('is-leaving')) return;
+ toast.classList.add('is-leaving');
+ const remove = () => toast.remove();
+ toast.addEventListener('transitionend', remove, { once: true });
+ setTimeout(remove, 450);
+ };
+
+ const showToast = ({
+ title = '',
+ description = '',
+ status = 'error',
+ dismissible = true,
+ timeout = DEFAULT_TIMEOUT
+ } = {}) => {
+ const stack = getStack();
+ if (!stack) return;
+ const visibleToasts = Array.from(
+ stack.querySelectorAll('.installer-toast:not(.is-leaving)')
+ );
+ if (visibleToasts.length >= MAX_TOASTS) {
+ dismissToast(visibleToasts[0]);
+ }
+
+ const toast = document.createElement('div');
+ toast.className = 'installer-toast is-entering';
+ toast.dataset.status = status;
+ toast.setAttribute('role', status === 'error' ? 'alert' : 'status');
+
+ const content = document.createElement('div');
+ content.className = 'installer-toast-content';
+
+ const icon = document.createElement('span');
+ icon.className = 'installer-toast-icon';
+ icon.dataset.status = status;
+ icon.innerHTML = ICONS.error;
+ content.appendChild(icon);
+
+ const body = document.createElement('section');
+ body.className = 'installer-toast-body';
+
+ if (title) {
+ const titleNode = document.createElement('p');
+ titleNode.className = 'installer-toast-title typography-text-m-500';
+ titleNode.textContent = title;
+ body.appendChild(titleNode);
+ }
+
+ if (description) {
+ const descNode = document.createElement('p');
+ descNode.className = 'installer-toast-description typography-text-m-400';
+ descNode.textContent = description;
+ body.appendChild(descNode);
+ }
+
+ content.appendChild(body);
+ toast.appendChild(content);
+
+ if (dismissible) {
+ const close = document.createElement('button');
+ close.type = 'button';
+ close.className = 'installer-toast-close';
+ close.setAttribute('aria-label', 'Dismiss notification');
+ close.innerHTML = ICONS.close;
+ close.addEventListener('click', () => dismissToast(toast));
+ toast.appendChild(close);
+ }
+
+ stack.appendChild(toast);
+ toast.getBoundingClientRect();
+ requestAnimationFrame(() => {
+ toast.classList.remove('is-entering');
+ });
+
+ if (timeout > 0) {
+ setTimeout(() => dismissToast(toast), timeout);
+ }
+ };
+
+ window.InstallerToast = Object.freeze({
+ showToast
+ });
+})();
diff --git a/app/views/install/installer/js/modules/ui.js b/app/views/install/installer/js/modules/ui.js
new file mode 100644
index 0000000000..bde4cb7c44
--- /dev/null
+++ b/app/views/install/installer/js/modules/ui.js
@@ -0,0 +1,281 @@
+(() => {
+ const { TIMINGS } = window.InstallerStepsContext || {};
+ const { formState } = window.InstallerStepsState || {};
+
+ const clearFieldErrors = (root) => {
+ if (!root) return;
+ root.querySelectorAll('.field-error').forEach((node) => {
+ node.classList.remove('is-visible');
+ });
+ root.querySelectorAll('.input-field.is-error, .input-action.is-error').forEach((node) => {
+ node.classList.remove('is-error');
+ });
+ root.querySelectorAll('.field-helper').forEach((helper) => {
+ helper.style.display = '';
+ });
+ };
+
+ const setFieldError = (input, message) => {
+ if (!input) return;
+ const group = input.closest('.input-group');
+ if (!group) return;
+ let error = group.querySelector('.field-error');
+ let errorText = error?.querySelector('.field-error-text');
+ const hasSameMessage = Boolean(errorText && errorText.textContent === message);
+ const alreadyVisible = Boolean(error && error.classList.contains('is-visible'));
+
+ if (hasSameMessage && alreadyVisible) {
+ return;
+ }
+
+ if (!error) {
+ const template = document.getElementById('field-error-template');
+ if (template && template.content) {
+ const fragment = template.content.cloneNode(true);
+ error = fragment.querySelector('.field-error');
+ group.appendChild(fragment);
+ }
+ errorText = error?.querySelector('.field-error-text');
+ }
+ if (errorText) {
+ errorText.textContent = message;
+ }
+
+ if (!alreadyVisible) {
+ requestAnimationFrame(() => {
+ error.classList.add('is-visible');
+ });
+ }
+
+ input.classList.add('is-error');
+ const actionWrapper = input.closest('.input-action');
+ if (actionWrapper) {
+ actionWrapper.classList.add('is-error');
+ }
+ const helper = group.querySelector('.field-helper');
+ if (helper) {
+ helper.style.display = 'none';
+ }
+ };
+
+ const bindErrorClear = (input) => {
+ if (!input) return;
+ const handler = () => {
+ const group = input.closest('.input-group');
+ const error = group?.querySelector('.field-error');
+ if (error) {
+ error.classList.remove('is-visible');
+ }
+ input.classList.remove('is-error');
+ const actionWrapper = input.closest('.input-action');
+ if (actionWrapper) {
+ actionWrapper.classList.remove('is-error');
+ }
+ const helper = group?.querySelector('.field-helper');
+ if (helper) {
+ helper.style.display = '';
+ }
+ };
+ input.addEventListener('input', handler);
+ input.addEventListener('change', handler);
+ };
+
+ const toDatabaseLabel = (value) => {
+ if (!value) return '';
+ const lower = value.toLowerCase();
+ if (lower === 'mariadb') return 'MariaDB';
+ if (lower === 'postgresql') return 'PostgreSQL';
+ return 'MongoDB';
+ };
+
+ const updateDatabaseSelection = (radio, root) => {
+ if (!radio || !root) return;
+ const allOptions = root.querySelectorAll('.selector-card');
+ allOptions.forEach((option) => option.classList.remove('selected'));
+ const selectedOption = radio.closest('.selector-card');
+ if (selectedOption) {
+ selectedOption.classList.add('selected');
+ }
+ };
+
+ const syncResetButton = (input, button) => {
+ const defaultValue = input.dataset.default ?? '';
+ button.disabled = input.value === defaultValue;
+ };
+
+ const setupResetButtons = (root) => {
+ const inputs = root.querySelectorAll('.input-field[data-default]');
+ inputs.forEach((input) => {
+ const button = root.querySelector(`[data-reset-target="${input.id}"]`);
+ if (!button) return;
+
+ syncResetButton(input, button);
+
+ input.addEventListener('input', () => syncResetButton(input, button));
+ button.addEventListener('click', () => {
+ input.value = input.dataset.default ?? '';
+ syncResetButton(input, button);
+ input.dispatchEvent(new Event('input', { bubbles: true }));
+ });
+ });
+ };
+
+ const toggleAccordion = (button) => {
+ const content = button.nextElementSibling;
+ const icon = button.querySelector('.accordion-chevron');
+ const isOpen = button.classList.contains('is-open');
+
+ button.classList.toggle('is-open', !isOpen);
+ button.setAttribute('aria-expanded', String(!isOpen));
+
+ if (content) {
+ if (!isOpen) {
+ content.classList.add('open');
+ content.style.maxHeight = `${content.scrollHeight}px`;
+ } else {
+ content.style.maxHeight = '0px';
+ content.classList.remove('open');
+ }
+ }
+
+ if (icon) {
+ icon.setAttribute('data-open', String(!isOpen));
+ }
+ };
+
+ const setupAccordion = (root) => {
+ const buttons = root.querySelectorAll('.accordion-toggle');
+ buttons.forEach((button) => {
+ button.addEventListener('click', () => toggleAccordion(button));
+ });
+ };
+
+ const openAccordion = (root) => {
+ const toggle = root.querySelector('.accordion-toggle');
+ const content = root.querySelector('.accordion-content');
+ if (!toggle || !content) return;
+ if (!toggle.classList.contains('is-open')) {
+ toggle.classList.add('is-open');
+ toggle.setAttribute('aria-expanded', 'true');
+ content.classList.add('open');
+ content.style.maxHeight = `${content.scrollHeight}px`;
+ }
+ };
+
+ const disableControls = (root) => {
+ const inputs = root.querySelectorAll('input, select, textarea');
+ inputs.forEach((input) => {
+ if (input.type === 'radio' || input.type === 'checkbox') {
+ input.disabled = true;
+ } else {
+ input.readOnly = true;
+ input.setAttribute('aria-disabled', 'true');
+ }
+ });
+
+ const buttons = root.querySelectorAll('button');
+ buttons.forEach((button) => {
+ if (button.matches('[data-copy-target]')) return;
+ button.disabled = true;
+ button.setAttribute('aria-disabled', 'true');
+ });
+
+ root.classList.add('is-locked');
+ };
+
+ const generateSecretKey = () => {
+ const array = new Uint8Array(32);
+ window.crypto.getRandomValues(array);
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join('');
+ };
+
+ const copyToClipboard = (value, input) => {
+ if (!value) return;
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(value);
+ return;
+ }
+ if (input) {
+ input.select();
+ document.execCommand('copy');
+ input.setSelectionRange(0, 0);
+ return;
+ }
+ const textArea = document.createElement('textarea');
+ textArea.value = value;
+ textArea.style.position = 'fixed';
+ textArea.style.top = '-9999px';
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+ try {
+ document.execCommand('copy');
+ } catch (error) {} finally {
+ document.body.removeChild(textArea);
+ }
+ };
+
+ const setTooltipText = (wrapper, message) => {
+ if (!wrapper) return;
+ const tooltip = wrapper.querySelector('.tooltip');
+ if (tooltip && message) {
+ tooltip.textContent = message;
+ }
+ };
+
+ const resetTooltipText = (wrapper) => {
+ if (!wrapper) return;
+ const defaultText = wrapper.dataset.tooltipDefault;
+ if (!defaultText) return;
+ setTooltipText(wrapper, defaultText);
+ };
+
+ const updateReviewSummary = (root) => {
+ if (!root) return;
+ const valueNodes = root.querySelectorAll('[data-review-value]');
+ valueNodes.forEach((node) => {
+ const key = node.dataset.reviewValue;
+ if (!key) return;
+ let value = formState?.[key];
+ if (key === 'database') {
+ value = toDatabaseLabel(formState?.database);
+ }
+ if (value) {
+ node.textContent = value;
+ }
+ });
+
+ const badge = root.querySelector('[data-review-badge]');
+ if (badge) {
+ const hasKey = Boolean((formState?.opensslKey || '').trim());
+ badge.textContent = hasKey ? 'Generated' : 'Missing';
+ badge.classList.remove('badge-success', 'badge-warning');
+ badge.classList.add(hasKey ? 'badge-success' : 'badge-warning');
+ }
+
+ const assistantBadge = root.querySelector('[data-review-assistant-badge]');
+ if (assistantBadge) {
+ const hasAssistantKey = Boolean((formState?.assistantOpenAIKey || '').trim());
+ assistantBadge.textContent = hasAssistantKey ? 'Enabled' : 'Disabled';
+ assistantBadge.classList.remove('badge-success', 'badge-neutral');
+ assistantBadge.classList.add(hasAssistantKey ? 'badge-success' : 'badge-neutral');
+ }
+ };
+
+ window.InstallerStepsUI = {
+ clearFieldErrors,
+ setFieldError,
+ bindErrorClear,
+ toDatabaseLabel,
+ updateDatabaseSelection,
+ setupResetButtons,
+ setupAccordion,
+ openAccordion,
+ disableControls,
+ generateSecretKey,
+ copyToClipboard,
+ setTooltipText,
+ resetTooltipText,
+ updateReviewSummary
+ };
+})();
diff --git a/app/views/install/installer/js/modules/validation.js b/app/views/install/installer/js/modules/validation.js
new file mode 100644
index 0000000000..13ab60ef4e
--- /dev/null
+++ b/app/views/install/installer/js/modules/validation.js
@@ -0,0 +1,117 @@
+(() => {
+ const isValidEmail = (email) => {
+ if (!email) return false;
+ const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return re.test(email);
+ };
+
+ const isValidPort = (value) => {
+ const numeric = Number(value);
+ if (!Number.isInteger(numeric)) return false;
+ return numeric >= 1 && numeric <= 65535;
+ };
+
+ const isValidPassword = (value) => {
+ if (!value) return false;
+ return value.length >= 8 && /\S/.test(value);
+ };
+
+ const isValidIPv4 = (host) => {
+ if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return false;
+ return host.split('.').every((part) => {
+ const num = Number(part);
+ return num >= 0 && num <= 255;
+ });
+ };
+
+ const isValidIPv6 = (host) => {
+ try {
+ const url = new URL(`http://[${host}]`);
+ return url.hostname.toLowerCase() === host.toLowerCase();
+ } catch (error) {
+ return false;
+ }
+ };
+
+ const isValidHostnameLabel = (label) => {
+ if (!label || label.length > 63) return false;
+ if (label.startsWith('-') || label.endsWith('-')) return false;
+ return /^[a-zA-Z0-9-]+$/.test(label);
+ };
+
+ const isValidDomain = (host) => {
+ if (host.length > 253) return false;
+ const labels = host.split('.');
+ return labels.every((label) => isValidHostnameLabel(label));
+ };
+
+ const isValidHost = (host) => {
+ if (host === 'localhost') return true;
+ if (isValidIPv4(host)) return true;
+ if (isValidIPv6(host)) return true;
+ return isValidDomain(host);
+ };
+
+ const isValidHostnameInput = (value) => {
+ if (!value) return false;
+ const trimmed = value.trim();
+ if (!trimmed) return false;
+
+ let host = trimmed;
+ let port = null;
+
+ if (trimmed.startsWith('[')) {
+ const match = trimmed.match(/^\[([^\]]+)\](?::(\d+))?$/);
+ if (!match) return false;
+ host = match[1] || '';
+ port = match[2] || null;
+ } else {
+ const parts = trimmed.split(':');
+ if (parts.length > 2) return false;
+ if (parts.length === 2) {
+ host = parts[0];
+ port = parts[1];
+ }
+ }
+
+ if (port !== null && port !== '' && !isValidPort(port)) {
+ return false;
+ }
+
+ return isValidHost(host);
+ };
+
+ const extractHostname = (value) => {
+ if (!value) return '';
+ const trimmed = value.trim();
+ if (trimmed.startsWith('[')) {
+ const end = trimmed.indexOf(']');
+ if (end !== -1) {
+ return trimmed.slice(1, end);
+ }
+ return trimmed;
+ }
+ const colonCount = (trimmed.match(/:/g) || []).length;
+ if (colonCount === 1) {
+ return trimmed.split(':')[0];
+ }
+ return trimmed;
+ };
+
+ const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '0.0.0.0']);
+
+ const isLocalHost = (host) => {
+ if (!host) return false;
+ const normalized = host.toLowerCase();
+ return LOCAL_HOSTS.has(normalized);
+ };
+
+ window.InstallerStepsValidation = {
+ isValidEmail,
+ isValidPort,
+ isValidPassword,
+ isValidHostnameInput,
+ extractHostname,
+ isLocalHost
+ };
+})();
diff --git a/app/views/install/installer/js/steps.js b/app/views/install/installer/js/steps.js
new file mode 100644
index 0000000000..d8b0621761
--- /dev/null
+++ b/app/views/install/installer/js/steps.js
@@ -0,0 +1,435 @@
+(() => {
+ const Context = window.InstallerStepsContext || {};
+ const State = window.InstallerStepsState || {};
+ const Validation = window.InstallerStepsValidation || {};
+ const UI = window.InstallerStepsUI || {};
+ const Progress = window.InstallerStepsProgress || {};
+ const Tooltips = window.InstallerTooltips || null;
+
+ const {
+ INSTALLATION_STEPS,
+ clampStep,
+ isUpgradeMode
+ } = Context;
+
+ const {
+ formState,
+ dispatchStateChange,
+ applyBodyDefaults,
+ applyLockPayload,
+ clearInstallLock,
+ clearInstallId,
+ isInstallLocked,
+ syncInstallLockFlag,
+ getInstallLock,
+ getLockedDatabase
+ } = State;
+
+ const {
+ isValidEmail,
+ isValidPort,
+ isValidHostnameInput,
+ isValidPassword
+ } = Validation;
+
+ const {
+ clearFieldErrors,
+ setFieldError,
+ bindErrorClear,
+ updateDatabaseSelection,
+ setupResetButtons,
+ setupAccordion,
+ openAccordion,
+ disableControls,
+ generateSecretKey,
+ copyToClipboard,
+ setTooltipText,
+ resetTooltipText,
+ updateReviewSummary
+ } = UI;
+
+ let reviewListener = null;
+
+ const bindInputToState = (input, key) => {
+ if (!input) return;
+ const update = () => {
+ formState[key] = input.value;
+ dispatchStateChange?.(key);
+ };
+ input.addEventListener('input', update);
+ input.addEventListener('change', update);
+ update();
+ };
+
+ const lockDatabaseSelection = (root, lockedDatabase) => {
+ if (lockedDatabase) {
+ const radios = root.querySelectorAll('input[name="database"]');
+ radios.forEach((radio) => {
+ const isLockedChoice = radio.value === lockedDatabase;
+ const card = radio.closest('.selector-card');
+ radio.disabled = !isLockedChoice;
+ if (card) {
+ card.classList.toggle('is-disabled', !isLockedChoice);
+ }
+ if (isLockedChoice) {
+ radio.checked = true;
+ updateDatabaseSelection?.(radio, root);
+ }
+ });
+ }
+ };
+
+ const bindDatabaseSelection = (root) => {
+ const radios = root.querySelectorAll('input[name="database"]');
+ radios.forEach((radio) => {
+ radio.addEventListener('change', () => {
+ formState.database = radio.value;
+ updateDatabaseSelection?.(radio, root);
+ });
+ });
+ };
+
+ const hydrateStep1State = (root) => {
+ State.setStateIfEmpty?.('appDomain', root.querySelector('#hostname')?.value);
+ State.setStateIfEmpty?.('database', root.querySelector('input[name="database"]:checked')?.value);
+ State.setStateIfEmpty?.('httpPort', root.querySelector('#http-port')?.value);
+ State.setStateIfEmpty?.('httpsPort', root.querySelector('#https-port')?.value);
+ State.setStateIfEmpty?.('emailCertificates', root.querySelector('#ssl-email')?.value);
+ State.setStateIfEmpty?.('assistantOpenAIKey', root.querySelector('#assistant-openai-key')?.value);
+ };
+
+ const applyStep1State = (root) => {
+ const hostname = root.querySelector('#hostname');
+ if (hostname && formState.appDomain) hostname.value = formState.appDomain;
+
+ const httpPort = root.querySelector('#http-port');
+ if (httpPort && formState.httpPort) httpPort.value = formState.httpPort;
+
+ const httpsPort = root.querySelector('#https-port');
+ if (httpsPort && formState.httpsPort) httpsPort.value = formState.httpsPort;
+
+ const sslEmail = root.querySelector('#ssl-email');
+ if (sslEmail && formState.emailCertificates) sslEmail.value = formState.emailCertificates;
+
+ const assistantKey = root.querySelector('#assistant-openai-key');
+ if (assistantKey && formState.assistantOpenAIKey) {
+ assistantKey.value = formState.assistantOpenAIKey;
+ }
+
+ if (formState.database) {
+ const radio = root.querySelector(`input[name="database"][value="${formState.database}"]`);
+ if (radio) {
+ radio.checked = true;
+ updateDatabaseSelection?.(radio, root);
+ }
+ }
+ };
+
+ const initStep1 = (root) => {
+ if (!root) return;
+ syncInstallLockFlag?.();
+ applyLockPayload?.();
+ applyBodyDefaults?.();
+ hydrateStep1State(root);
+ applyStep1State(root);
+
+ if (isInstallLocked?.()) {
+ openAccordion?.(root);
+ disableControls?.(root);
+ return;
+ }
+
+ const lockedDatabase = getLockedDatabase?.() || '';
+ if (lockedDatabase) {
+ lockDatabaseSelection(root, lockedDatabase);
+ } else {
+ bindDatabaseSelection(root);
+ }
+
+ const hostname = root.querySelector('#hostname');
+ const httpPort = root.querySelector('#http-port');
+ const httpsPort = root.querySelector('#https-port');
+ const sslEmail = root.querySelector('#ssl-email');
+ const assistantKey = root.querySelector('#assistant-openai-key');
+
+ bindInputToState(hostname, 'appDomain');
+ bindInputToState(httpPort, 'httpPort');
+ bindInputToState(httpsPort, 'httpsPort');
+ bindInputToState(sslEmail, 'emailCertificates');
+ bindInputToState(assistantKey, 'assistantOpenAIKey');
+
+ bindErrorClear?.(hostname);
+ bindErrorClear?.(httpPort);
+ bindErrorClear?.(httpsPort);
+ bindErrorClear?.(sslEmail);
+ bindErrorClear?.(assistantKey);
+
+ const checked = root.querySelector('input[name="database"]:checked');
+ if (checked) {
+ updateDatabaseSelection?.(checked, root);
+ }
+
+ setupResetButtons?.(root);
+ setupAccordion?.(root);
+ Tooltips?.setupTooltipPortals?.(root);
+ };
+
+ const hydrateStep2State = (root) => {
+ const value = root.querySelector('#secret-key')?.value;
+ if (formState.opensslKey) return;
+ if (value) {
+ formState.opensslKey = value;
+ }
+ };
+
+ const applyStep2State = (root) => {
+ const input = root.querySelector('#secret-key');
+ if (input && formState.opensslKey) {
+ input.value = formState.opensslKey;
+ }
+ };
+
+ const initStep2 = (root) => {
+ if (!root) return;
+ syncInstallLockFlag?.();
+ applyLockPayload?.();
+ applyBodyDefaults?.();
+ hydrateStep2State(root);
+ if (!isUpgradeMode?.() && (!formState.opensslKey || !formState.opensslKey.trim())) {
+ formState.opensslKey = generateSecretKey?.();
+ dispatchStateChange?.('opensslKey');
+ }
+ applyStep2State(root);
+
+ const input = root.querySelector('#secret-key');
+ if (input) {
+ bindInputToState(input, 'opensslKey');
+ bindErrorClear?.(input);
+ }
+
+ const copyButton = root.querySelector('[data-copy-target]');
+ const tooltipWrapper = copyButton?.closest('.tooltip-wrapper');
+
+ if (tooltipWrapper) {
+ tooltipWrapper.addEventListener('mouseenter', () => resetTooltipText?.(tooltipWrapper));
+ tooltipWrapper.addEventListener('focusin', () => resetTooltipText?.(tooltipWrapper));
+ }
+
+ if (copyButton) {
+ copyButton.addEventListener('click', () => {
+ const targetId = copyButton.getAttribute('data-copy-target');
+ const targetInput = targetId ? root.querySelector(`#${targetId}`) : null;
+ const value = targetInput?.value || '';
+ copyToClipboard?.(value, targetInput);
+ copyButton.blur();
+
+ if (tooltipWrapper) {
+ const successText = tooltipWrapper.dataset.tooltipSuccess || 'Copied';
+ setTooltipText?.(tooltipWrapper, successText);
+ }
+ });
+ }
+
+ const regenerateButton = root.querySelector('[data-regenerate-target]');
+ if (regenerateButton && !isInstallLocked?.()) {
+ regenerateButton.addEventListener('click', () => {
+ const targetId = regenerateButton.getAttribute('data-regenerate-target');
+ const targetInput = targetId ? root.querySelector(`#${targetId}`) : null;
+ if (!targetInput) return;
+ regenerateButton.classList.remove('is-rotating');
+ void regenerateButton.offsetWidth;
+ regenerateButton.classList.add('is-rotating');
+ const handleAnimationEnd = () => {
+ regenerateButton.classList.remove('is-rotating');
+ };
+ regenerateButton.addEventListener('animationend', handleAnimationEnd, { once: true });
+ targetInput.value = generateSecretKey?.();
+ targetInput.dispatchEvent(new Event('input', { bubbles: true }));
+ });
+ }
+
+ if (isInstallLocked?.()) {
+ disableControls?.(root);
+ }
+ };
+
+ const hydrateStep3State = (root) => {
+ State.setStateIfEmpty?.('accountEmail', root.querySelector('#account-email')?.value);
+ State.setStateIfEmpty?.('accountPassword', root.querySelector('#account-password')?.value);
+ };
+
+ const applyStep3State = (root) => {
+ const email = root.querySelector('#account-email');
+ if (email && formState.accountEmail) email.value = formState.accountEmail;
+
+ const password = root.querySelector('#account-password');
+ if (password && formState.accountPassword) password.value = formState.accountPassword;
+ };
+
+ const initStep3 = (root) => {
+ if (!root) return;
+ syncInstallLockFlag?.();
+ applyLockPayload?.();
+ applyBodyDefaults?.();
+ hydrateStep3State(root);
+ applyStep3State(root);
+
+ const email = root.querySelector('#account-email');
+ const password = root.querySelector('#account-password');
+ const passwordToggle = root.querySelector('[data-password-toggle="account-password"]');
+
+ bindInputToState(email, 'accountEmail');
+ bindInputToState(password, 'accountPassword');
+
+ bindErrorClear?.(email);
+ bindErrorClear?.(password);
+
+ if (password && passwordToggle) {
+ passwordToggle.addEventListener('click', () => {
+ const isVisible = passwordToggle.classList.toggle('is-visible');
+ password.type = isVisible ? 'text' : 'password';
+ passwordToggle.setAttribute('aria-label', isVisible ? 'Hide password' : 'Show password');
+ });
+ }
+
+ if (isInstallLocked?.()) {
+ disableControls?.(root);
+ }
+ };
+
+ const initStep4 = (root) => {
+ if (!root) return;
+ syncInstallLockFlag?.();
+ applyLockPayload?.();
+ applyBodyDefaults?.();
+ updateReviewSummary?.(root);
+ if (reviewListener) {
+ document.removeEventListener('installer:state-change', reviewListener);
+ }
+ reviewListener = () => updateReviewSummary?.(root);
+ document.addEventListener('installer:state-change', reviewListener);
+ if (isInstallLocked?.()) {
+ disableControls?.(root);
+ }
+ };
+
+ const initStep = (step, container) => {
+ if (!container) return;
+ const root = container.querySelector('.step-layout') || container;
+ const normalized = clampStep?.(step) ?? 1;
+ Tooltips?.cleanupTooltipPortals?.();
+ if (normalized !== 4 && reviewListener) {
+ document.removeEventListener('installer:state-change', reviewListener);
+ reviewListener = null;
+ }
+ if (normalized !== 5) {
+ Progress.cleanupInstallFlow?.();
+ }
+ if (normalized === 1) initStep1(root);
+ if (normalized === 2) initStep2(root);
+ if (normalized === 3) initStep3(root);
+ if (normalized === 4) initStep4(root);
+ if (normalized === 5) Progress.initStep5?.(root);
+ };
+
+ window.InstallerSteps = {
+ initStep1,
+ initStep2,
+ initStep3,
+ initStep4,
+ initStep5: Progress.initStep5,
+ installationSteps: INSTALLATION_STEPS || [],
+ isInstallLocked,
+ getInstallLock,
+ clearInstallLock,
+ initStep,
+ validateStep: (step, container) => {
+ const root = container?.querySelector('.step-layout') || container;
+ const normalized = clampStep?.(step) ?? 1;
+ if (normalized === 1) {
+ clearFieldErrors?.(root);
+ let valid = true;
+ const hostname = root?.querySelector('#hostname');
+ const httpPort = root?.querySelector('#http-port');
+ const httpsPort = root?.querySelector('#https-port');
+ const sslEmail = root?.querySelector('#ssl-email');
+
+ if (!hostname || !hostname.value.trim()) {
+ setFieldError?.(hostname, 'Please enter your Appwrite hostname');
+ valid = false;
+ } else if (!isValidHostnameInput?.(hostname.value.trim())) {
+ setFieldError?.(hostname, 'Please enter a valid hostname');
+ valid = false;
+ }
+
+ const parsePort = (input, label) => {
+ const value = input?.value;
+ if (!value || !isValidPort?.(value)) {
+ setFieldError?.(input, `Please enter a valid ${label} port (1-65535)`);
+ return false;
+ }
+ return true;
+ };
+
+ if (!parsePort(httpPort, 'HTTP')) valid = false;
+ if (!parsePort(httpsPort, 'HTTPS')) valid = false;
+
+ if (!sslEmail || !sslEmail.value.trim()) {
+ setFieldError?.(sslEmail, 'Please enter an email address for SSL certificates');
+ valid = false;
+ } else if (!isValidEmail?.(sslEmail.value.trim())) {
+ setFieldError?.(sslEmail, 'Please enter a valid email address');
+ valid = false;
+ }
+
+ if (!valid) {
+ openAccordion?.(root);
+ }
+
+ return valid;
+ }
+
+ if (normalized === 2) {
+ clearFieldErrors?.(root);
+ const secretKey = root?.querySelector('#secret-key');
+ const secretValue = secretKey?.value.trim() || '';
+ if (!secretKey || !secretValue) {
+ setFieldError?.(secretKey, 'Please enter or generate a secret API key');
+ return false;
+ }
+ if (secretValue.length > 64) {
+ setFieldError?.(secretKey, 'Secret API key must be 1-64 characters');
+ return false;
+ }
+ }
+
+ if (normalized === 3) {
+ clearFieldErrors?.(root);
+ let valid = true;
+ const email = root?.querySelector('#account-email');
+ const password = root?.querySelector('#account-password');
+
+ if (!email || !email.value.trim()) {
+ setFieldError?.(email, 'This field is required');
+ valid = false;
+ } else if (!isValidEmail?.(email.value.trim())) {
+ setFieldError?.(email, 'Please enter a valid email address');
+ valid = false;
+ }
+
+ const passwordValue = password?.value ?? '';
+ if (!password || !/\S/.test(passwordValue)) {
+ setFieldError?.(password, 'This field is required');
+ valid = false;
+ } else if (!isValidPassword?.(passwordValue)) {
+ setFieldError?.(password, 'Password must be at least 8 characters long');
+ valid = false;
+ }
+
+ return valid;
+ }
+
+ return true;
+ }
+ };
+})();
diff --git a/app/views/install/installer/js/tooltips.js b/app/views/install/installer/js/tooltips.js
new file mode 100644
index 0000000000..96b637b9a3
--- /dev/null
+++ b/app/views/install/installer/js/tooltips.js
@@ -0,0 +1,77 @@
+(() => {
+ const tooltipPortals = new Set();
+
+ const positionTooltipPortal = (tooltip, anchor) => {
+ if (!tooltip || !anchor) return;
+ const rect = anchor.getBoundingClientRect();
+ const tooltipRect = tooltip.getBoundingClientRect();
+ const offset = Number(tooltip.dataset.tooltipOffset || 6);
+ const padding = 8;
+ let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2);
+ left = Math.max(padding, Math.min(left, window.innerWidth - tooltipRect.width - padding));
+ const top = rect.bottom + offset;
+ tooltip.style.left = `${left}px`;
+ tooltip.style.top = `${top}px`;
+ };
+
+ const attachTooltipPortal = (tooltip) => {
+ if (!tooltip || tooltip.dataset.portalInitialized === 'true') return;
+ const anchor = tooltip.parentElement;
+ if (!anchor) return;
+
+ tooltip.dataset.portalInitialized = 'true';
+ tooltip.classList.add('tooltip-portal');
+ document.body.appendChild(tooltip);
+
+ const show = () => {
+ tooltip.classList.add('is-open');
+ positionTooltipPortal(tooltip, anchor);
+ };
+ const hide = () => {
+ tooltip.classList.remove('is-open');
+ };
+ const refresh = () => {
+ if (tooltip.classList.contains('is-open')) {
+ positionTooltipPortal(tooltip, anchor);
+ }
+ };
+
+ anchor.addEventListener('mouseenter', show);
+ anchor.addEventListener('mouseleave', hide);
+ anchor.addEventListener('focusin', show);
+ anchor.addEventListener('focusout', hide);
+ window.addEventListener('scroll', refresh, true);
+ window.addEventListener('resize', refresh);
+
+ tooltipPortals.add({
+ tooltip,
+ cleanup: () => {
+ anchor.removeEventListener('mouseenter', show);
+ anchor.removeEventListener('mouseleave', hide);
+ anchor.removeEventListener('focusin', show);
+ anchor.removeEventListener('focusout', hide);
+ window.removeEventListener('scroll', refresh, true);
+ window.removeEventListener('resize', refresh);
+ if (tooltip.parentElement) {
+ tooltip.parentElement.removeChild(tooltip);
+ }
+ }
+ });
+ };
+
+ const setupTooltipPortals = (root) => {
+ if (!root) return;
+ const portalTooltips = root.querySelectorAll('.tooltip[data-tooltip-portal]');
+ portalTooltips.forEach((tooltip) => attachTooltipPortal(tooltip));
+ };
+
+ const cleanupTooltipPortals = () => {
+ tooltipPortals.forEach((entry) => entry.cleanup());
+ tooltipPortals.clear();
+ };
+
+ window.InstallerTooltips = {
+ setupTooltipPortals,
+ cleanupTooltipPortals
+ };
+})();
diff --git a/app/views/install/installer/templates/steps/step-1.phtml b/app/views/install/installer/templates/steps/step-1.phtml
new file mode 100644
index 0000000000..ae7674c3bf
--- /dev/null
+++ b/app/views/install/installer/templates/steps/step-1.phtml
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/views/install/installer/templates/steps/step-2.phtml b/app/views/install/installer/templates/steps/step-2.phtml
new file mode 100644
index 0000000000..9b7eed11b1
--- /dev/null
+++ b/app/views/install/installer/templates/steps/step-2.phtml
@@ -0,0 +1,58 @@
+
+
+
+
+
Secure your app
+
+
+
+
+
+
+
+
+
+
+
+
+
Save your key
+
You won't be able to see this key again. Copy it somewhere safe before continuing.
+
+
+
+
+
+
+
+
+
diff --git a/app/views/install/installer/templates/steps/step-3.phtml b/app/views/install/installer/templates/steps/step-3.phtml
new file mode 100644
index 0000000000..8eaf0e044b
--- /dev/null
+++ b/app/views/install/installer/templates/steps/step-3.phtml
@@ -0,0 +1,61 @@
+
+
+
+
+
Create your account
+
+ Set up the email and password for your Appwrite account. You can use these
+ credentials to sign in later.
+
+
+
+
+
+
diff --git a/app/views/install/installer/templates/steps/step-4.phtml b/app/views/install/installer/templates/steps/step-4.phtml
new file mode 100644
index 0000000000..07dc865257
--- /dev/null
+++ b/app/views/install/installer/templates/steps/step-4.phtml
@@ -0,0 +1,76 @@
+ 'MariaDB',
+ 'postgresql' => 'PostgreSQL',
+ default => 'MongoDB',
+};
+$badgeLabel = $defaultSecretKey !== '' ? 'Generated' : 'Missing';
+$badgeClass = $defaultSecretKey !== '' ? 'badge-success' : 'badge-warning';
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
SSL certificate email
+
+
+
Disabled
+
Appwrite Assistant
+
+
+
+
+
+
+
+
diff --git a/app/views/install/installer/templates/steps/step-5.phtml b/app/views/install/installer/templates/steps/step-5.phtml
new file mode 100644
index 0000000000..8fa810b259
--- /dev/null
+++ b/app/views/install/installer/templates/steps/step-5.phtml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/worker.php b/app/worker.php
index db036b6a99..840231f16c 100644
--- a/app/worker.php
+++ b/app/worker.php
@@ -112,7 +112,7 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
- ->setTenant((int) $project->getSequence())
+ ->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -152,7 +152,7 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
- ->setTenant((int) $project->getSequence())
+ ->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -174,7 +174,7 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
- ->setTenant((int) $project->getSequence())
+ ->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -196,9 +196,8 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authoriza
$database = null;
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
- if ($database !== null && $project !== null && ! $project->isEmpty() && $project->getId() !== 'console') {
- $database->setTenant((int) $project->getSequence());
-
+ if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
+ $database->setTenant($project->getSequence());
return $database;
}
@@ -213,9 +212,8 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authoriza
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
- // set tenant
- if ($project !== null && ! $project->isEmpty() && $project->getId() !== 'console') {
- $database->setTenant((int) $project->getSequence());
+ if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
+ $database->setTenant($project->getSequence());
}
return $database;
diff --git a/composer.json b/composer.json
index 057118645e..06ee153574 100644
--- a/composer.json
+++ b/composer.json
@@ -13,8 +13,11 @@
"test": "vendor/bin/phpunit",
"lint": "vendor/bin/pint --test --config pint.json",
"format": "vendor/bin/pint --config pint.json",
+ "analyze": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G",
"bench": "vendor/bin/phpbench run --report=benchmark",
- "analyze": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G"
+ "check": "./vendor/bin/phpstan analyse -c phpstan.neon",
+ "installer:clean": "php src/Appwrite/Platform/Installer/Server.php --clean",
+ "installer:dev": "docker compose build && composer installer:clean && php src/Appwrite/Platform/Installer/Server.php --docker"
},
"autoload": {
"psr-4": {
@@ -58,7 +61,7 @@
"utopia-php/compression": "0.1.*",
"utopia-php/config": "1.*",
"utopia-php/console": "0.1.*",
- "utopia-php/database": "5.*",
+ "utopia-php/database": "dev-fix-collection-recreate as 5.3.15",
"utopia-php/detector": "0.2.*",
"utopia-php/domains": "1.*",
"utopia-php/emails": "0.6.*",
@@ -91,8 +94,15 @@
"spomky-labs/otphp": "11.*",
"webonyx/graphql-php": "14.11.*",
"league/csv": "9.14.*",
- "enshrined/svg-sanitize": "0.22.*"
+ "enshrined/svg-sanitize": "0.22.*",
+ "utopia-php/di": "0.1.0"
},
+ "repositories": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/utopia-php/database"
+ }
+ ],
"require-dev": {
"ext-fileinfo": "*",
"appwrite/sdk-generator": "*",
@@ -102,8 +112,7 @@
"phpstan/phpstan": "^2.0",
"textalk/websocket": "1.5.*",
"czproject/git-php": "4.*",
- "laravel/pint": "1.*",
- "phpbench/phpbench": "1.*"
+ "laravel/pint": "1.*"
},
"provide": {
"ext-phpiredis": "*"
diff --git a/composer.lock b/composer.lock
index c9128f3a1f..314265f3e0 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "392d5bd2621b3eb57f8b1bf38d9e4bc3",
+ "content-hash": "13579de3d747c541fdcce4f709df8e57",
"packages": [
{
"name": "adhocore/jwt",
@@ -3606,16 +3606,16 @@
},
{
"name": "utopia-php/cache",
- "version": "1.0.0",
+ "version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/cache.git",
- "reference": "7068870c086a6aea16173563a26b93ef3e408439"
+ "reference": "05ceba981436a4022553f7aaa2a05fa049d0f71c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/cache/zipball/7068870c086a6aea16173563a26b93ef3e408439",
- "reference": "7068870c086a6aea16173563a26b93ef3e408439",
+ "url": "https://api.github.com/repos/utopia-php/cache/zipball/05ceba981436a4022553f7aaa2a05fa049d0f71c",
+ "reference": "05ceba981436a4022553f7aaa2a05fa049d0f71c",
"shasum": ""
},
"require": {
@@ -3652,9 +3652,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/cache/issues",
- "source": "https://github.com/utopia-php/cache/tree/1.0.0"
+ "source": "https://github.com/utopia-php/cache/tree/1.0.1"
},
- "time": "2026-01-28T10:55:44+00:00"
+ "time": "2026-03-12T03:39:09+00:00"
},
{
"name": "utopia-php/cli",
@@ -3850,16 +3850,16 @@
},
{
"name": "utopia-php/database",
- "version": "5.3.8",
+ "version": "dev-fix-collection-recreate",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
- "reference": "4920bb60afb98d4bd81f4d331765716ae1d40255"
+ "reference": "5208630969dfdfbe8eda9c34c6b28ce711ece7c4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/database/zipball/4920bb60afb98d4bd81f4d331765716ae1d40255",
- "reference": "4920bb60afb98d4bd81f4d331765716ae1d40255",
+ "url": "https://api.github.com/repos/utopia-php/database/zipball/5208630969dfdfbe8eda9c34c6b28ce711ece7c4",
+ "reference": "5208630969dfdfbe8eda9c34c6b28ce711ece7c4",
"shasum": ""
},
"require": {
@@ -3888,7 +3888,38 @@
"Utopia\\Database\\": "src/Database"
}
},
- "notification-url": "https://packagist.org/downloads/",
+ "autoload-dev": {
+ "psr-4": {
+ "Tests\\E2E\\": "tests/e2e",
+ "Tests\\Unit\\": "tests/unit"
+ }
+ },
+ "scripts": {
+ "build": [
+ "Composer\\Config::disableProcessTimeout",
+ "docker compose build"
+ ],
+ "start": [
+ "Composer\\Config::disableProcessTimeout",
+ "docker compose up -d"
+ ],
+ "test": [
+ "Composer\\Config::disableProcessTimeout",
+ "docker compose exec tests vendor/bin/phpunit --configuration phpunit.xml"
+ ],
+ "lint": [
+ "php -d memory_limit=2G ./vendor/bin/pint --test"
+ ],
+ "format": [
+ "php -d memory_limit=2G ./vendor/bin/pint"
+ ],
+ "check": [
+ "./vendor/bin/phpstan analyse --level 7 src tests --memory-limit 2G"
+ ],
+ "coverage": [
+ "./vendor/bin/coverage-check ./tmp/clover.xml 90"
+ ]
+ },
"license": [
"MIT"
],
@@ -3901,10 +3932,10 @@
"utopia"
],
"support": {
- "issues": "https://github.com/utopia-php/database/issues",
- "source": "https://github.com/utopia-php/database/tree/5.3.8"
+ "source": "https://github.com/utopia-php/database/tree/fix-collection-recreate",
+ "issues": "https://github.com/utopia-php/database/issues"
},
- "time": "2026-03-11T01:03:34+00:00"
+ "time": "2026-03-16T11:58:09+00:00"
},
{
"name": "utopia-php/detector",
@@ -4058,16 +4089,16 @@
},
{
"name": "utopia-php/domains",
- "version": "1.0.2",
+ "version": "1.0.5",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/domains.git",
- "reference": "b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6"
+ "reference": "0edf6bb2b07f30db849a267027077bf5abb994c6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/domains/zipball/b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6",
- "reference": "b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6",
+ "url": "https://api.github.com/repos/utopia-php/domains/zipball/0edf6bb2b07f30db849a267027077bf5abb994c6",
+ "reference": "0edf6bb2b07f30db849a267027077bf5abb994c6",
"shasum": ""
},
"require": {
@@ -4114,9 +4145,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/domains/issues",
- "source": "https://github.com/utopia-php/domains/tree/1.0.2"
+ "source": "https://github.com/utopia-php/domains/tree/1.0.5"
},
- "time": "2026-02-25T08:18:25+00:00"
+ "time": "2026-03-03T09:20:50+00:00"
},
{
"name": "utopia-php/dsn",
@@ -4167,16 +4198,16 @@
},
{
"name": "utopia-php/emails",
- "version": "0.6.8",
+ "version": "0.6.9",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/emails.git",
- "reference": "25dfcd46ed47b862d2a7e7c98d92a3a4680b6f1b"
+ "reference": "3a59fb392a03a88f5497e5fdb0ea84a252a4dfdf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/emails/zipball/25dfcd46ed47b862d2a7e7c98d92a3a4680b6f1b",
- "reference": "25dfcd46ed47b862d2a7e7c98d92a3a4680b6f1b",
+ "url": "https://api.github.com/repos/utopia-php/emails/zipball/3a59fb392a03a88f5497e5fdb0ea84a252a4dfdf",
+ "reference": "3a59fb392a03a88f5497e5fdb0ea84a252a4dfdf",
"shasum": ""
},
"require": {
@@ -4222,9 +4253,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/emails/issues",
- "source": "https://github.com/utopia-php/emails/tree/0.6.8"
+ "source": "https://github.com/utopia-php/emails/tree/0.6.9"
},
- "time": "2026-02-09T12:31:56+00:00"
+ "time": "2026-03-14T13:52:56+00:00"
},
{
"name": "utopia-php/fetch",
@@ -4572,16 +4603,16 @@
},
{
"name": "utopia-php/mongo",
- "version": "1.0.0",
+ "version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/mongo.git",
- "reference": "45bedf36c2c946ec7a0a3e59b9f12f772de0b01d"
+ "reference": "83dbcde768d5fb40241f5ca8aa5ed8ca140a7469"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/mongo/zipball/45bedf36c2c946ec7a0a3e59b9f12f772de0b01d",
- "reference": "45bedf36c2c946ec7a0a3e59b9f12f772de0b01d",
+ "url": "https://api.github.com/repos/utopia-php/mongo/zipball/83dbcde768d5fb40241f5ca8aa5ed8ca140a7469",
+ "reference": "83dbcde768d5fb40241f5ca8aa5ed8ca140a7469",
"shasum": ""
},
"require": {
@@ -4627,9 +4658,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/mongo/issues",
- "source": "https://github.com/utopia-php/mongo/tree/1.0.0"
+ "source": "https://github.com/utopia-php/mongo/tree/1.0.1"
},
- "time": "2026-02-12T05:54:06+00:00"
+ "time": "2026-03-13T07:29:24+00:00"
},
{
"name": "utopia-php/platform",
@@ -5059,16 +5090,16 @@
},
{
"name": "utopia-php/system",
- "version": "0.10.0",
+ "version": "0.10.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/system.git",
- "reference": "6441a9c180958a373e5ddb330264dd638539dfdb"
+ "reference": "7c1669533bb9c285de19191270c8c1439161a78a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/system/zipball/6441a9c180958a373e5ddb330264dd638539dfdb",
- "reference": "6441a9c180958a373e5ddb330264dd638539dfdb",
+ "url": "https://api.github.com/repos/utopia-php/system/zipball/7c1669533bb9c285de19191270c8c1439161a78a",
+ "reference": "7c1669533bb9c285de19191270c8c1439161a78a",
"shasum": ""
},
"require": {
@@ -5109,9 +5140,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/system/issues",
- "source": "https://github.com/utopia-php/system/tree/0.10.0"
+ "source": "https://github.com/utopia-php/system/tree/0.10.1"
},
- "time": "2025-10-15T19:12:00+00:00"
+ "time": "2026-03-15T21:07:41+00:00"
},
{
"name": "utopia-php/telemetry",
@@ -5215,29 +5246,28 @@
},
{
"name": "utopia-php/vcs",
- "version": "2.0.0",
+ "version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/vcs.git",
- "reference": "058049326e04a2a0c2f0ce8ad00c7e84825aba14"
+ "reference": "5769679308bad498f2777547d48ab332166c4c0b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/vcs/zipball/058049326e04a2a0c2f0ce8ad00c7e84825aba14",
- "reference": "058049326e04a2a0c2f0ce8ad00c7e84825aba14",
+ "url": "https://api.github.com/repos/utopia-php/vcs/zipball/5769679308bad498f2777547d48ab332166c4c0b",
+ "reference": "5769679308bad498f2777547d48ab332166c4c0b",
"shasum": ""
},
"require": {
"adhocore/jwt": "^1.1",
"php": ">=8.0",
- "utopia-php/cache": "1.0.*",
- "utopia-php/framework": "0.*.*",
- "utopia-php/system": "0.10.*"
+ "utopia-php/cache": "1.0.*"
},
"require-dev": {
"laravel/pint": "1.*.*",
"phpstan/phpstan": "1.*.*",
- "phpunit/phpunit": "^9.4"
+ "phpunit/phpunit": "^9.4",
+ "utopia-php/system": "0.10.*"
},
"type": "library",
"autoload": {
@@ -5258,9 +5288,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/vcs/issues",
- "source": "https://github.com/utopia-php/vcs/tree/2.0.0"
+ "source": "https://github.com/utopia-php/vcs/tree/2.0.2"
},
- "time": "2026-02-25T11:36:45+00:00"
+ "time": "2026-03-13T15:25:16+00:00"
},
{
"name": "utopia-php/websocket",
@@ -5438,16 +5468,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
- "version": "1.11.1",
+ "version": "1.11.6",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
- "reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb"
+ "reference": "f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6ff411f26f2750eea05c7598c14bb3a2ada898cb",
- "reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb",
+ "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38",
+ "reference": "f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38",
"shasum": ""
},
"require": {
@@ -5483,22 +5513,22 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
- "source": "https://github.com/appwrite/sdk-generator/tree/1.11.1"
+ "source": "https://github.com/appwrite/sdk-generator/tree/1.11.6"
},
- "time": "2026-02-25T07:15:19+00:00"
+ "time": "2026-03-09T07:12:51+00:00"
},
{
"name": "brianium/paratest",
- "version": "v7.19.0",
+ "version": "v7.19.2",
"source": {
"type": "git",
"url": "https://github.com/paratestphp/paratest.git",
- "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6"
+ "reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/paratestphp/paratest/zipball/7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6",
- "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6",
+ "url": "https://api.github.com/repos/paratestphp/paratest/zipball/66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9",
+ "reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9",
"shasum": ""
},
"require": {
@@ -5512,9 +5542,9 @@
"phpunit/php-code-coverage": "^12.5.3 || ^13.0.1",
"phpunit/php-file-iterator": "^6.0.1 || ^7",
"phpunit/php-timer": "^8 || ^9",
- "phpunit/phpunit": "^12.5.9 || ^13",
+ "phpunit/phpunit": "^12.5.14 || ^13.0.5",
"sebastian/environment": "^8.0.3 || ^9",
- "symfony/console": "^7.4.4 || ^8.0.4",
+ "symfony/console": "^7.4.7 || ^8.0.7",
"symfony/process": "^7.4.5 || ^8.0.5"
},
"require-dev": {
@@ -5522,11 +5552,11 @@
"ext-pcntl": "*",
"ext-pcov": "*",
"ext-posix": "*",
- "phpstan/phpstan": "^2.1.38",
- "phpstan/phpstan-deprecation-rules": "^2.0.3",
- "phpstan/phpstan-phpunit": "^2.0.12",
- "phpstan/phpstan-strict-rules": "^2.0.8",
- "symfony/filesystem": "^7.4.0 || ^8.0.1"
+ "phpstan/phpstan": "^2.1.40",
+ "phpstan/phpstan-deprecation-rules": "^2.0.4",
+ "phpstan/phpstan-phpunit": "^2.0.16",
+ "phpstan/phpstan-strict-rules": "^2.0.10",
+ "symfony/filesystem": "^7.4.6 || ^8.0.6"
},
"bin": [
"bin/paratest",
@@ -5566,7 +5596,7 @@
],
"support": {
"issues": "https://github.com/paratestphp/paratest/issues",
- "source": "https://github.com/paratestphp/paratest/tree/v7.19.0"
+ "source": "https://github.com/paratestphp/paratest/tree/v7.19.2"
},
"funding": [
{
@@ -5578,7 +5608,7 @@
"type": "paypal"
}
],
- "time": "2026-02-06T10:53:26+00:00"
+ "time": "2026-03-09T14:33:17+00:00"
},
{
"name": "czproject/git-php",
@@ -5644,160 +5674,6 @@
],
"time": "2025-11-10T07:24:07+00:00"
},
- {
- "name": "doctrine/annotations",
- "version": "2.0.2",
- "source": {
- "type": "git",
- "url": "https://github.com/doctrine/annotations.git",
- "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/doctrine/annotations/zipball/901c2ee5d26eb64ff43c47976e114bf00843acf7",
- "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7",
- "shasum": ""
- },
- "require": {
- "doctrine/lexer": "^2 || ^3",
- "ext-tokenizer": "*",
- "php": "^7.2 || ^8.0",
- "psr/cache": "^1 || ^2 || ^3"
- },
- "require-dev": {
- "doctrine/cache": "^2.0",
- "doctrine/coding-standard": "^10",
- "phpstan/phpstan": "^1.10.28",
- "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
- "symfony/cache": "^5.4 || ^6.4 || ^7",
- "vimeo/psalm": "^4.30 || ^5.14"
- },
- "suggest": {
- "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Guilherme Blanco",
- "email": "guilhermeblanco@gmail.com"
- },
- {
- "name": "Roman Borschel",
- "email": "roman@code-factory.org"
- },
- {
- "name": "Benjamin Eberlei",
- "email": "kontakt@beberlei.de"
- },
- {
- "name": "Jonathan Wage",
- "email": "jonwage@gmail.com"
- },
- {
- "name": "Johannes Schmitt",
- "email": "schmittjoh@gmail.com"
- }
- ],
- "description": "Docblock Annotations Parser",
- "homepage": "https://www.doctrine-project.org/projects/annotations.html",
- "keywords": [
- "annotations",
- "docblock",
- "parser"
- ],
- "support": {
- "issues": "https://github.com/doctrine/annotations/issues",
- "source": "https://github.com/doctrine/annotations/tree/2.0.2"
- },
- "abandoned": true,
- "time": "2024-09-05T10:17:24+00:00"
- },
- {
- "name": "doctrine/lexer",
- "version": "3.0.1",
- "source": {
- "type": "git",
- "url": "https://github.com/doctrine/lexer.git",
- "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd",
- "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd",
- "shasum": ""
- },
- "require": {
- "php": "^8.1"
- },
- "require-dev": {
- "doctrine/coding-standard": "^12",
- "phpstan/phpstan": "^1.10",
- "phpunit/phpunit": "^10.5",
- "psalm/plugin-phpunit": "^0.18.3",
- "vimeo/psalm": "^5.21"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Doctrine\\Common\\Lexer\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Guilherme Blanco",
- "email": "guilhermeblanco@gmail.com"
- },
- {
- "name": "Roman Borschel",
- "email": "roman@code-factory.org"
- },
- {
- "name": "Johannes Schmitt",
- "email": "schmittjoh@gmail.com"
- }
- ],
- "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
- "homepage": "https://www.doctrine-project.org/projects/lexer.html",
- "keywords": [
- "annotations",
- "docblock",
- "lexer",
- "parser",
- "php"
- ],
- "support": {
- "issues": "https://github.com/doctrine/lexer/issues",
- "source": "https://github.com/doctrine/lexer/tree/3.0.1"
- },
- "funding": [
- {
- "url": "https://www.doctrine-project.org/sponsorship.html",
- "type": "custom"
- },
- {
- "url": "https://www.patreon.com/phpdoctrine",
- "type": "patreon"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer",
- "type": "tidelift"
- }
- ],
- "time": "2024-02-05T11:56:58+00:00"
- },
{
"name": "fidry/cpu-core-counter",
"version": "1.3.0",
@@ -5921,16 +5797,16 @@
},
{
"name": "laravel/pint",
- "version": "v1.27.1",
+ "version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
- "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5"
+ "reference": "bdec963f53172c5e36330f3a400604c69bf02d39"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/pint/zipball/54cca2de13790570c7b6f0f94f37896bee4abcb5",
- "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5",
+ "url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39",
+ "reference": "bdec963f53172c5e36330f3a400604c69bf02d39",
"shasum": ""
},
"require": {
@@ -5941,13 +5817,14 @@
"php": "^8.2.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^3.93.1",
- "illuminate/view": "^12.51.0",
- "larastan/larastan": "^3.9.2",
+ "friendsofphp/php-cs-fixer": "^3.94.2",
+ "illuminate/view": "^12.54.1",
+ "larastan/larastan": "^3.9.3",
"laravel-zero/framework": "^12.0.5",
"mockery/mockery": "^1.6.12",
- "nunomaduro/termwind": "^2.3.3",
- "pestphp/pest": "^3.8.5"
+ "nunomaduro/termwind": "^2.4.0",
+ "pestphp/pest": "^3.8.6",
+ "shipfastlabs/agent-detector": "^1.1.0"
},
"bin": [
"builds/pint"
@@ -5984,7 +5861,7 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
- "time": "2026-02-10T20:00:20+00:00"
+ "time": "2026-03-12T15:51:39+00:00"
},
{
"name": "matthiasmullie/minify",
@@ -6345,155 +6222,6 @@
},
"time": "2022-02-21T01:04:05+00:00"
},
- {
- "name": "phpbench/container",
- "version": "2.2.3",
- "source": {
- "type": "git",
- "url": "https://github.com/phpbench/container.git",
- "reference": "0c7b2d36c1ea53fe27302fb8873ded7172047196"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/phpbench/container/zipball/0c7b2d36c1ea53fe27302fb8873ded7172047196",
- "reference": "0c7b2d36c1ea53fe27302fb8873ded7172047196",
- "shasum": ""
- },
- "require": {
- "psr/container": "^1.0|^2.0",
- "symfony/options-resolver": "^4.2 || ^5.0 || ^6.0 || ^7.0 || ^8.0"
- },
- "require-dev": {
- "php-cs-fixer/shim": "^3.89",
- "phpstan/phpstan": "^0.12.52",
- "phpunit/phpunit": "^8"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "PhpBench\\DependencyInjection\\": "lib/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Daniel Leech",
- "email": "daniel@dantleech.com"
- }
- ],
- "description": "Simple, configurable, service container.",
- "support": {
- "issues": "https://github.com/phpbench/container/issues",
- "source": "https://github.com/phpbench/container/tree/2.2.3"
- },
- "time": "2025-11-06T09:05:13+00:00"
- },
- {
- "name": "phpbench/phpbench",
- "version": "1.4.3",
- "source": {
- "type": "git",
- "url": "https://github.com/phpbench/phpbench.git",
- "reference": "b641dde59d969ea42eed70a39f9b51950bc96878"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/phpbench/phpbench/zipball/b641dde59d969ea42eed70a39f9b51950bc96878",
- "reference": "b641dde59d969ea42eed70a39f9b51950bc96878",
- "shasum": ""
- },
- "require": {
- "doctrine/annotations": "^2.0",
- "ext-dom": "*",
- "ext-json": "*",
- "ext-pcre": "*",
- "ext-reflection": "*",
- "ext-spl": "*",
- "ext-tokenizer": "*",
- "php": "^8.1",
- "phpbench/container": "^2.2",
- "psr/log": "^1.1 || ^2.0 || ^3.0",
- "seld/jsonlint": "^1.1",
- "symfony/console": "^6.1 || ^7.0 || ^8.0",
- "symfony/filesystem": "^6.1 || ^7.0 || ^8.0",
- "symfony/finder": "^6.1 || ^7.0 || ^8.0",
- "symfony/options-resolver": "^6.1 || ^7.0 || ^8.0",
- "symfony/process": "^6.1 || ^7.0 || ^8.0",
- "webmozart/glob": "^4.6"
- },
- "require-dev": {
- "dantleech/invoke": "^2.0",
- "ergebnis/composer-normalize": "^2.39",
- "jangregor/phpstan-prophecy": "^1.0",
- "php-cs-fixer/shim": "^3.9",
- "phpspec/prophecy": "^1.22",
- "phpstan/extension-installer": "^1.1",
- "phpstan/phpstan": "^1.0",
- "phpstan/phpstan-phpunit": "^1.0",
- "phpunit/phpunit": "^10.4 || ^11.0",
- "rector/rector": "^1.2",
- "symfony/error-handler": "^6.1 || ^7.0 || ^8.0",
- "symfony/var-dumper": "^6.1 || ^7.0 || ^8.0"
- },
- "suggest": {
- "ext-xdebug": "For Xdebug profiling extension."
- },
- "bin": [
- "bin/phpbench"
- ],
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.2-dev"
- }
- },
- "autoload": {
- "files": [
- "lib/Report/Func/functions.php"
- ],
- "psr-4": {
- "PhpBench\\": "lib/",
- "PhpBench\\Extensions\\XDebug\\": "extensions/xdebug/lib/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Daniel Leech",
- "email": "daniel@dantleech.com"
- }
- ],
- "description": "PHP Benchmarking Framework",
- "keywords": [
- "benchmarking",
- "optimization",
- "performance",
- "profiling",
- "testing"
- ],
- "support": {
- "issues": "https://github.com/phpbench/phpbench/issues",
- "source": "https://github.com/phpbench/phpbench/tree/1.4.3"
- },
- "funding": [
- {
- "url": "https://github.com/dantleech",
- "type": "github"
- }
- ],
- "time": "2025-11-06T19:07:31+00:00"
- },
{
"name": "phpstan/phpstan",
"version": "2.1.40",
@@ -6999,55 +6727,6 @@
],
"time": "2026-02-18T12:38:40+00:00"
},
- {
- "name": "psr/cache",
- "version": "3.0.0",
- "source": {
- "type": "git",
- "url": "https://github.com/php-fig/cache.git",
- "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
- "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
- "shasum": ""
- },
- "require": {
- "php": ">=8.0.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Psr\\Cache\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "PHP-FIG",
- "homepage": "https://www.php-fig.org/"
- }
- ],
- "description": "Common interface for caching libraries",
- "keywords": [
- "cache",
- "psr",
- "psr-6"
- ],
- "support": {
- "source": "https://github.com/php-fig/cache/tree/3.0.0"
- },
- "time": "2021-02-03T23:26:27+00:00"
- },
{
"name": "sebastian/cli-parser",
"version": "4.2.0",
@@ -7336,16 +7015,16 @@
},
{
"name": "sebastian/environment",
- "version": "8.0.3",
+ "version": "8.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
- "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68"
+ "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/24a711b5c916efc6d6e62aa65aa2ec98fef77f68",
- "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/7b8842c2d8e85d0c3a5831236bf5869af6ab2a11",
+ "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11",
"shasum": ""
},
"require": {
@@ -7388,7 +7067,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/environment/issues",
"security": "https://github.com/sebastianbergmann/environment/security/policy",
- "source": "https://github.com/sebastianbergmann/environment/tree/8.0.3"
+ "source": "https://github.com/sebastianbergmann/environment/tree/8.0.4"
},
"funding": [
{
@@ -7408,7 +7087,7 @@
"type": "tidelift"
}
],
- "time": "2025-08-12T14:11:56+00:00"
+ "time": "2026-03-15T07:05:40+00:00"
},
{
"name": "sebastian/exporter",
@@ -7945,70 +7624,6 @@
],
"time": "2025-02-07T05:00:38+00:00"
},
- {
- "name": "seld/jsonlint",
- "version": "1.11.0",
- "source": {
- "type": "git",
- "url": "https://github.com/Seldaek/jsonlint.git",
- "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/1748aaf847fc731cfad7725aec413ee46f0cc3a2",
- "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2",
- "shasum": ""
- },
- "require": {
- "php": "^5.3 || ^7.0 || ^8.0"
- },
- "require-dev": {
- "phpstan/phpstan": "^1.11",
- "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13"
- },
- "bin": [
- "bin/jsonlint"
- ],
- "type": "library",
- "autoload": {
- "psr-4": {
- "Seld\\JsonLint\\": "src/Seld/JsonLint/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Jordi Boggiano",
- "email": "j.boggiano@seld.be",
- "homepage": "https://seld.be"
- }
- ],
- "description": "JSON Linter",
- "keywords": [
- "json",
- "linter",
- "parser",
- "validator"
- ],
- "support": {
- "issues": "https://github.com/Seldaek/jsonlint/issues",
- "source": "https://github.com/Seldaek/jsonlint/tree/1.11.0"
- },
- "funding": [
- {
- "url": "https://github.com/Seldaek",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint",
- "type": "tidelift"
- }
- ],
- "time": "2024-07-11T14:55:45+00:00"
- },
{
"name": "staabm/side-effects-detector",
"version": "1.0.5",
@@ -8095,16 +7710,16 @@
},
{
"name": "symfony/console",
- "version": "v8.0.4",
+ "version": "v8.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b"
+ "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/ace03c4cf9805080ff40cbeec69fca180c339a3b",
- "reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b",
+ "url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a",
+ "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a",
"shasum": ""
},
"require": {
@@ -8161,7 +7776,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v8.0.4"
+ "source": "https://github.com/symfony/console/tree/v8.0.7"
},
"funding": [
{
@@ -8181,216 +7796,7 @@
"type": "tidelift"
}
],
- "time": "2026-01-13T13:06:50+00:00"
- },
- {
- "name": "symfony/filesystem",
- "version": "v8.0.1",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/filesystem.git",
- "reference": "d937d400b980523dc9ee946bb69972b5e619058d"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/d937d400b980523dc9ee946bb69972b5e619058d",
- "reference": "d937d400b980523dc9ee946bb69972b5e619058d",
- "shasum": ""
- },
- "require": {
- "php": ">=8.4",
- "symfony/polyfill-ctype": "~1.8",
- "symfony/polyfill-mbstring": "~1.8"
- },
- "require-dev": {
- "symfony/process": "^7.4|^8.0"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Symfony\\Component\\Filesystem\\": ""
- },
- "exclude-from-classmap": [
- "/Tests/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Fabien Potencier",
- "email": "fabien@symfony.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Provides basic utilities for the filesystem",
- "homepage": "https://symfony.com",
- "support": {
- "source": "https://github.com/symfony/filesystem/tree/v8.0.1"
- },
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://github.com/nicolas-grekas",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2025-12-01T09:13:36+00:00"
- },
- {
- "name": "symfony/finder",
- "version": "v8.0.5",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/finder.git",
- "reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/8bd576e97c67d45941365bf824e18dc8538e6eb0",
- "reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0",
- "shasum": ""
- },
- "require": {
- "php": ">=8.4"
- },
- "require-dev": {
- "symfony/filesystem": "^7.4|^8.0"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Symfony\\Component\\Finder\\": ""
- },
- "exclude-from-classmap": [
- "/Tests/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Fabien Potencier",
- "email": "fabien@symfony.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Finds files and directories via an intuitive fluent interface",
- "homepage": "https://symfony.com",
- "support": {
- "source": "https://github.com/symfony/finder/tree/v8.0.5"
- },
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://github.com/nicolas-grekas",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2026-01-26T15:08:38+00:00"
- },
- {
- "name": "symfony/options-resolver",
- "version": "v8.0.0",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/options-resolver.git",
- "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7",
- "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7",
- "shasum": ""
- },
- "require": {
- "php": ">=8.4",
- "symfony/deprecation-contracts": "^2.5|^3"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Symfony\\Component\\OptionsResolver\\": ""
- },
- "exclude-from-classmap": [
- "/Tests/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Fabien Potencier",
- "email": "fabien@symfony.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Provides an improved replacement for the array_replace PHP function",
- "homepage": "https://symfony.com",
- "keywords": [
- "config",
- "configuration",
- "options"
- ],
- "support": {
- "source": "https://github.com/symfony/options-resolver/tree/v8.0.0"
- },
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://github.com/nicolas-grekas",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2025-11-12T15:55:31+00:00"
+ "time": "2026-03-06T14:06:22+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -8789,16 +8195,16 @@
},
{
"name": "symfony/string",
- "version": "v8.0.4",
+ "version": "v8.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "758b372d6882506821ed666032e43020c4f57194"
+ "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194",
- "reference": "758b372d6882506821ed666032e43020c4f57194",
+ "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
+ "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
"shasum": ""
},
"require": {
@@ -8855,7 +8261,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v8.0.4"
+ "source": "https://github.com/symfony/string/tree/v8.0.6"
},
"funding": [
{
@@ -8875,7 +8281,7 @@
"type": "tidelift"
}
],
- "time": "2026-01-12T12:37:40+00:00"
+ "time": "2026-02-09T10:14:57+00:00"
},
{
"name": "textalk/websocket",
@@ -9054,60 +8460,20 @@
}
],
"time": "2024-11-07T12:36:22+00:00"
- },
- {
- "name": "webmozart/glob",
- "version": "4.7.0",
- "source": {
- "type": "git",
- "url": "https://github.com/webmozarts/glob.git",
- "reference": "8a2842112d6916e61e0e15e316465b611f3abc17"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/webmozarts/glob/zipball/8a2842112d6916e61e0e15e316465b611f3abc17",
- "reference": "8a2842112d6916e61e0e15e316465b611f3abc17",
- "shasum": ""
- },
- "require": {
- "php": "^7.3 || ^8.0.0"
- },
- "require-dev": {
- "phpunit/phpunit": "^9.5",
- "symfony/filesystem": "^5.3"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.1-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Webmozart\\Glob\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Bernhard Schussek",
- "email": "bschussek@gmail.com"
- }
- ],
- "description": "A PHP implementation of Ant's glob.",
- "support": {
- "issues": "https://github.com/webmozarts/glob/issues",
- "source": "https://github.com/webmozarts/glob/tree/4.7.0"
- },
- "time": "2024-03-07T20:33:40+00:00"
}
],
- "aliases": [],
+ "aliases": [
+ {
+ "package": "utopia-php/database",
+ "version": "dev-fix-collection-recreate",
+ "alias": "5.3.15",
+ "alias_normalized": "5.3.15.0"
+ }
+ ],
"minimum-stability": "dev",
- "stability-flags": {},
+ "stability-flags": {
+ "utopia-php/database": 20
+ },
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
diff --git a/docker-compose.yml b/docker-compose.yml
index c0b0560a7f..7d64dfa867 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1122,6 +1122,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
+ - _APP_DATABASE_SHARED_TABLES
appwrite-task-scheduler-messages:
entrypoint: schedule-messages
@@ -1300,7 +1301,7 @@ services:
networks:
- appwrite
volumes:
- - appwrite-postgresql:/var/lib/postgresql/data:rw
+ - appwrite-postgresql:/var/lib/postgresql:rw
ports:
- "5432:5432"
environment:
diff --git a/docs/sdks/python/GETTING_STARTED.md b/docs/sdks/python/GETTING_STARTED.md
index 2732ef8483..09569d3eb0 100644
--- a/docs/sdks/python/GETTING_STARTED.md
+++ b/docs/sdks/python/GETTING_STARTED.md
@@ -20,10 +20,16 @@ client = Client()
### Make Your First Request
Once your SDK object is set, create any of the Appwrite service objects and choose any request to send. Full documentation for any service method you would like to use can be found in your SDK documentation or in the [API References](https://appwrite.io/docs) section.
+All service methods return typed Pydantic models, so you can access response fields as attributes:
+
```python
users = Users(client)
-result = users.create(ID.unique(), email = "email@example.com", phone = "+123456789", password = "password", name = "Walter O'Brien")
+user = users.create(ID.unique(), email = "email@example.com", phone = "+123456789", password = "password", name = "Walter O'Brien")
+
+print(user.name) # "Walter O'Brien"
+print(user.email) # "email@example.com"
+print(user.id) # The generated user ID
```
### Full Example
@@ -43,7 +49,60 @@ client = Client()
users = Users(client)
-result = users.create(ID.unique(), email = "email@example.com", phone = "+123456789", password = "password", name = "Walter O'Brien")
+user = users.create(ID.unique(), email = "email@example.com", phone = "+123456789", password = "password", name = "Walter O'Brien")
+
+print(user.name) # Access fields as attributes
+print(user.to_dict()) # Convert to dictionary if needed
+```
+
+### Type Safety with Models
+
+The Appwrite Python SDK provides type safety when working with database rows through generic methods. Methods like `get_row`, `list_rows`, and others accept a `model_type` parameter that allows you to specify your custom Pydantic model for full type safety.
+
+```python
+from pydantic import BaseModel
+from datetime import datetime
+from typing import Optional
+from appwrite.client import Client
+from appwrite.services.tables_db import TablesDB
+
+# Define your custom model matching your table schema
+class Post(BaseModel):
+ postId: int
+ authorId: int
+ title: str
+ content: str
+ createdAt: datetime
+ updatedAt: datetime
+ isPublished: bool
+ excerpt: Optional[str] = None
+
+client = Client()
+# ... configure your client ...
+
+tables_db = TablesDB(client)
+
+# Fetch a single row with type safety
+row = tables_db.get_row(
+ database_id="your-database-id",
+ table_id="your-table-id",
+ row_id="your-row-id",
+ model_type=Post # Pass your custom model type
+)
+
+print(row.data.title) # Fully typed - IDE autocomplete works
+print(row.data.postId) # int type, not Any
+print(row.data.createdAt) # datetime type
+
+# Fetch multiple rows with type safety
+result = tables_db.list_rows(
+ database_id="your-database-id",
+ table_id="your-table-id",
+ model_type=Post
+)
+
+for row in result.rows:
+ print(f"{row.data.title} by {row.data.authorId}")
```
### Error Handling
@@ -52,7 +111,8 @@ The Appwrite Python SDK raises `AppwriteException` object with `message`, `code`
```python
users = Users(client)
try:
- result = users.create(ID.unique(), email = "email@example.com", phone = "+123456789", password = "password", name = "Walter O'Brien")
+ user = users.create(ID.unique(), email = "email@example.com", phone = "+123456789", password = "password", name = "Walter O'Brien")
+ print(user.name)
except AppwriteException as e:
print(e.message)
```
diff --git a/mongo-entrypoint.sh b/mongo-entrypoint.sh
old mode 100755
new mode 100644
diff --git a/phpbench.json b/phpbench.json
deleted file mode 100644
index adc40d1294..0000000000
--- a/phpbench.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "$schema":"vendor/phpbench/phpbench/phpbench.schema.json",
- "runner.bootstrap": "vendor/autoload.php",
- "runner.path": "tests",
- "runner.file_pattern": "*Bench.php"
-}
\ No newline at end of file
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 99ff5018d3..0219836405 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -72,12 +72,6 @@ parameters:
count: 1
path: app/cli.php
- -
- message: '#^Parameter \#1 \$name of class Utopia\\Queue\\Queue constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/cli.php
-
-
message: '#^Parameter \#1 \$name of method Utopia\\DI\\Injection\:\:inject\(\) expects string, mixed given\.$#'
identifier: argument.type
@@ -102,12 +96,6 @@ parameters:
count: 1
path: app/cli.php
- -
- message: '#^Parameter \#1 \$version of method Utopia\\Logger\\Log\:\:setVersion\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/cli.php
-
-
message: '#^Parameter \#2 \$cache of class Utopia\\Database\\Database constructor expects Utopia\\Cache\\Cache, mixed given\.$#'
identifier: argument.type
@@ -126,12 +114,6 @@ parameters:
count: 1
path: app/cli.php
- -
- message: '#^Parameter \#2 \$string of function explode expects string, string\|null given\.$#'
- identifier: argument.type
- count: 2
- path: app/cli.php
-
-
message: '#^Result of && is always false\.$#'
identifier: booleanAnd.alwaysFalse
@@ -192,18 +174,6 @@ parameters:
count: 14
path: app/config/frameworks.php
- -
- message: '#^Parameter \#1 \$haystack of function str_contains expects string, string\|null given\.$#'
- identifier: argument.type
- count: 2
- path: app/config/platform.php
-
- -
- message: '#^Parameter \#2 \$string of function explode expects string, string\|null given\.$#'
- identifier: argument.type
- count: 3
- path: app/config/platform.php
-
-
message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string\|null, int given\.$#'
identifier: argument.type
@@ -372,12 +342,6 @@ parameters:
count: 1
path: app/config/templates/function.php
- -
- message: '#^Parameter \#2 \$string of function explode expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/config/templates/function.php
-
-
message: '#^Array has 2 duplicate keys with value ''outputDirectory'' \(''outputDirectory'', ''outputDirectory''\)\.$#'
identifier: array.duplicateKey
@@ -2658,12 +2622,6 @@ parameters:
count: 12
path: app/controllers/api/projects.php
- -
- message: '#^Parameter \#1 \$name of method Utopia\\Locale\\Locale\:\:setFallback\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/controllers/api/projects.php
-
-
message: '#^Parameter \#1 \$queries of static method Utopia\\Database\\Query\:\:parseQueries\(\) expects array\, array given\.$#'
identifier: argument.type
@@ -3174,12 +3132,6 @@ parameters:
count: 1
path: app/controllers/general.php
- -
- message: '#^Binary operation "\." between ''cd /usr/local…'' and mixed results in an error\.$#'
- identifier: binaryOp.invalid
- count: 1
- path: app/controllers/general.php
-
-
message: '#^Binary operation "\." between ''http\://''\|''https\://'' and mixed results in an error\.$#'
identifier: binaryOp.invalid
@@ -3540,24 +3492,12 @@ parameters:
count: 1
path: app/controllers/general.php
- -
- message: '#^Parameter \#1 \$version of method Utopia\\Logger\\Log\:\:setVersion\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/controllers/general.php
-
-
message: '#^Parameter \#2 \$array of function array_key_exists expects array, mixed given\.$#'
identifier: argument.type
count: 2
path: app/controllers/general.php
- -
- message: '#^Parameter \#2 \$default of method Appwrite\\Utopia\\Request\:\:getHeader\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 2
- path: app/controllers/general.php
-
-
message: '#^Parameter \#2 \$default of method Utopia\\DSN\\DSN\:\:getParam\(\) expects string, float given\.$#'
identifier: argument.type
@@ -3588,12 +3528,6 @@ parameters:
count: 1
path: app/controllers/general.php
- -
- message: '#^Parameter \#2 \$string of function explode expects string, string\|null given\.$#'
- identifier: argument.type
- count: 2
- path: app/controllers/general.php
-
-
message: '#^Parameter \#2 \$value of method Utopia\\Http\\Response\:\:addCookie\(\) expects string\|null, mixed given\.$#'
identifier: argument.type
@@ -3606,6 +3540,12 @@ parameters:
count: 2
path: app/controllers/general.php
+ -
+ message: '#^Parameter \#3 \$subject of function str_replace expects array\\|string, mixed given\.$#'
+ identifier: argument.type
+ count: 1
+ path: app/controllers/general.php
+
-
message: '#^Parameter \$body of method Executor\\Executor\:\:createExecution\(\) expects string\|null, string\|false\|null given\.$#'
identifier: argument.type
@@ -4698,12 +4638,6 @@ parameters:
count: 1
path: app/http.php
- -
- message: '#^Parameter \#1 \$namespace of method Utopia\\Database\\Database\:\:setNamespace\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/http.php
-
-
message: '#^Parameter \#1 \$pool of class Utopia\\Database\\Adapter\\Pool constructor expects Utopia\\Pools\\Pool\, Utopia\\Pools\\Pool\ given\.$#'
identifier: argument.type
@@ -4767,7 +4701,7 @@ parameters:
-
message: '#^Parameter \#2 \$string of function explode expects string, string\|null given\.$#'
identifier: argument.type
- count: 4
+ count: 1
path: app/http.php
-
@@ -4783,7 +4717,7 @@ parameters:
path: app/http.php
-
- message: '#^Parameter \$port of class Swoole\\Http\\Server constructor expects int, string\|null given\.$#'
+ message: '#^Parameter \$port of class Swoole\\Http\\Server constructor expects int, string given\.$#'
identifier: argument.type
count: 1
path: app/http.php
@@ -5053,7 +4987,7 @@ parameters:
path: app/init/registers.php
-
- message: '#^Binary operation "/" between string\|null and string\|null results in an error\.$#'
+ message: '#^Binary operation "/" between string and string results in an error\.$#'
identifier: binaryOp.invalid
count: 1
path: app/init/registers.php
@@ -5070,6 +5004,12 @@ parameters:
count: 1
path: app/init/registers.php
+ -
+ message: '#^Offset ''dsns'' on array\{type\: ''cache'', dsns\: non\-falsy\-string, multiple\: true, schemes\: array\{''redis''\}\}\|array\{type\: ''consumer''\|''publisher''\|''pubsub'', dsns\: non\-falsy\-string, multiple\: false, schemes\: array\{''redis''\}\}\|array\{type\: ''database'', dsns\: string, multiple\: bool, schemes\: array\{''mariadb'', ''mongodb'', ''mysql'', ''postgresql''\}\} on left side of \?\? always exists and is not nullable\.$#'
+ identifier: nullCoalesce.offset
+ count: 1
+ path: app/init/registers.php
+
-
message: '#^Offset ''host'' might not exist on array\{key\: string\|null, projectId\: string, host\: non\-falsy\-string\}\|array\{key\: string\}\|array\{ticket\: string, host\: string\}\.$#'
identifier: offsetAccess.notFound
@@ -5095,7 +5035,7 @@ parameters:
path: app/init/registers.php
-
- message: '#^Offset ''multiple'' on array\{type\: ''cache'', dsns\: non\-falsy\-string, multiple\: true, schemes\: array\{''redis''\}\}\|array\{type\: ''consumer''\|''publisher''\|''pubsub'', dsns\: non\-falsy\-string, multiple\: false, schemes\: array\{''redis''\}\}\|array\{type\: ''database'', dsns\: string\|null, multiple\: bool, schemes\: array\{''mariadb'', ''mongodb'', ''mysql'', ''postgresql''\}\} on left side of \?\? always exists and is not nullable\.$#'
+ message: '#^Offset ''multiple'' on array\{type\: ''cache'', dsns\: non\-falsy\-string, multiple\: true, schemes\: array\{''redis''\}\}\|array\{type\: ''consumer''\|''publisher''\|''pubsub'', dsns\: non\-falsy\-string, multiple\: false, schemes\: array\{''redis''\}\}\|array\{type\: ''database'', dsns\: string, multiple\: bool, schemes\: array\{''mariadb'', ''mongodb'', ''mysql'', ''postgresql''\}\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: app/init/registers.php
@@ -5113,7 +5053,7 @@ parameters:
path: app/init/registers.php
-
- message: '#^Offset ''schemes'' on array\{type\: ''cache'', dsns\: non\-falsy\-string, multiple\: true, schemes\: array\{''redis''\}\}\|array\{type\: ''consumer''\|''publisher''\|''pubsub'', dsns\: non\-falsy\-string, multiple\: false, schemes\: array\{''redis''\}\}\|array\{type\: ''database'', dsns\: string\|null, multiple\: bool, schemes\: array\{''mariadb'', ''mongodb'', ''mysql'', ''postgresql''\}\} on left side of \?\? always exists and is not nullable\.$#'
+ message: '#^Offset ''schemes'' on array\{type\: ''cache'', dsns\: non\-falsy\-string, multiple\: true, schemes\: array\{''redis''\}\}\|array\{type\: ''consumer''\|''publisher''\|''pubsub'', dsns\: non\-falsy\-string, multiple\: false, schemes\: array\{''redis''\}\}\|array\{type\: ''database'', dsns\: string, multiple\: bool, schemes\: array\{''mariadb'', ''mongodb'', ''mysql'', ''postgresql''\}\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: app/init/registers.php
@@ -5131,7 +5071,7 @@ parameters:
path: app/init/registers.php
-
- message: '#^Offset ''type'' on array\{type\: ''cache'', dsns\: non\-falsy\-string, multiple\: true, schemes\: array\{''redis''\}\}\|array\{type\: ''consumer''\|''publisher''\|''pubsub'', dsns\: non\-falsy\-string, multiple\: false, schemes\: array\{''redis''\}\}\|array\{type\: ''database'', dsns\: string\|null, multiple\: bool, schemes\: array\{''mariadb'', ''mongodb'', ''mysql'', ''postgresql''\}\} on left side of \?\? always exists and is not nullable\.$#'
+ message: '#^Offset ''type'' on array\{type\: ''cache'', dsns\: non\-falsy\-string, multiple\: true, schemes\: array\{''redis''\}\}\|array\{type\: ''consumer''\|''publisher''\|''pubsub'', dsns\: non\-falsy\-string, multiple\: false, schemes\: array\{''redis''\}\}\|array\{type\: ''database'', dsns\: string, multiple\: bool, schemes\: array\{''mariadb'', ''mongodb'', ''mysql'', ''postgresql''\}\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: app/init/registers.php
@@ -5142,18 +5082,6 @@ parameters:
count: 1
path: app/init/registers.php
- -
- message: '#^Parameter \#1 \$address of method PHPMailer\\PHPMailer\\PHPMailer\:\:addReplyTo\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/registers.php
-
- -
- message: '#^Parameter \#1 \$address of method PHPMailer\\PHPMailer\\PHPMailer\:\:setFrom\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/registers.php
-
-
message: '#^Parameter \#1 \$client of class Appwrite\\PubSub\\Adapter\\Redis constructor expects Redis, Redis\|Swoole\\Database\\PDOProxy\|Utopia\\Mongo\\Client given\.$#'
identifier: argument.type
@@ -5169,7 +5097,7 @@ parameters:
-
message: '#^Parameter \#1 \$database of class Utopia\\Mongo\\Client constructor expects string, string\|null given\.$#'
identifier: argument.type
- count: 2
+ count: 1
path: app/init/registers.php
-
@@ -5196,30 +5124,12 @@ parameters:
count: 1
path: app/init/registers.php
- -
- message: '#^Parameter \#1 \$string of function urldecode expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/registers.php
-
- -
- message: '#^Parameter \#1 \$value of static method Utopia\\Http\\Http\:\:setMode\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/registers.php
-
-
message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string\|null, int given\.$#'
identifier: argument.type
count: 7
path: app/init/registers.php
- -
- message: '#^Parameter \#2 \$host of class Utopia\\Mongo\\Client constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/registers.php
-
-
message: '#^Parameter \#2 \$key of class Utopia\\Logger\\Adapter\\Sentry constructor expects string, string\|null given\.$#'
identifier: argument.type
@@ -5241,18 +5151,12 @@ parameters:
-
message: '#^Parameter \#4 \$user of class Utopia\\Mongo\\Client constructor expects string, string\|null given\.$#'
identifier: argument.type
- count: 2
+ count: 1
path: app/init/registers.php
-
message: '#^Parameter \#5 \$password of class Utopia\\Mongo\\Client constructor expects string, string\|null given\.$#'
identifier: argument.type
- count: 2
- path: app/init/registers.php
-
- -
- message: '#^Property PHPMailer\\PHPMailer\\PHPMailer\:\:\$Host \(string\) does not accept string\|null\.$#'
- identifier: assign.propertyType
count: 1
path: app/init/registers.php
@@ -5263,13 +5167,7 @@ parameters:
path: app/init/registers.php
-
- message: '#^Property PHPMailer\\PHPMailer\\PHPMailer\:\:\$Port \(int\) does not accept string\|null\.$#'
- identifier: assign.propertyType
- count: 1
- path: app/init/registers.php
-
- -
- message: '#^Property PHPMailer\\PHPMailer\\PHPMailer\:\:\$SMTPSecure \(string\) does not accept string\|null\.$#'
+ message: '#^Property PHPMailer\\PHPMailer\\PHPMailer\:\:\$Port \(int\) does not accept string\.$#'
identifier: assign.propertyType
count: 1
path: app/init/registers.php
@@ -5493,7 +5391,7 @@ parameters:
-
message: '#^Expression on left side of \?\? is not nullable\.$#'
identifier: nullCoalesce.expr
- count: 1
+ count: 2
path: app/init/resources.php
-
@@ -5544,12 +5442,6 @@ parameters:
count: 1
path: app/init/resources.php
- -
- message: '#^Parameter \#1 \$default of class Utopia\\Locale\\Locale constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
-
message: '#^Parameter \#1 \$dsn of class Utopia\\DSN\\DSN constructor expects string, mixed given\.$#'
identifier: argument.type
@@ -5562,12 +5454,6 @@ parameters:
count: 8
path: app/init/resources.php
- -
- message: '#^Parameter \#1 \$host of method Redis\:\:pconnect\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
-
message: '#^Parameter \#1 \$input of class Utopia\\Database\\Document constructor expects array\, mixed given\.$#'
identifier: argument.type
@@ -5586,18 +5472,6 @@ parameters:
count: 1
path: app/init/resources.php
- -
- message: '#^Parameter \#1 \$name of class Utopia\\Queue\\Queue constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#1 \$name of method Utopia\\Locale\\Locale\:\:setFallback\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
-
message: '#^Parameter \#1 \$name of method Utopia\\Pools\\Group\:\:get\(\) expects string, mixed given\.$#'
identifier: argument.type
@@ -5658,42 +5532,6 @@ parameters:
count: 1
path: app/init/resources.php
- -
- message: '#^Parameter \#2 \$accessKey of class Utopia\\Storage\\Device\\AWS constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#2 \$accessKey of class Utopia\\Storage\\Device\\Backblaze constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#2 \$accessKey of class Utopia\\Storage\\Device\\DOSpaces constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#2 \$accessKey of class Utopia\\Storage\\Device\\Linode constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#2 \$accessKey of class Utopia\\Storage\\Device\\S3 constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#2 \$accessKey of class Utopia\\Storage\\Device\\Wasabi constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
-
message: '#^Parameter \#2 \$allowedSchemes of class Appwrite\\Network\\Validator\\Origin constructor expects array\, array given\.$#'
identifier: argument.type
@@ -5778,12 +5616,6 @@ parameters:
count: 2
path: app/init/resources.php
- -
- message: '#^Parameter \#2 \$string of function explode expects string, string\|null given\.$#'
- identifier: argument.type
- count: 3
- path: app/init/resources.php
-
-
message: '#^Parameter \#2 \$value of method Appwrite\\Usage\\Context\:\:addMetric\(\) expects int, mixed given\.$#'
identifier: argument.type
@@ -5796,114 +5628,12 @@ parameters:
count: 1
path: app/init/resources.php
- -
- message: '#^Parameter \#3 \$secretKey of class Utopia\\Storage\\Device\\AWS constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#3 \$secretKey of class Utopia\\Storage\\Device\\Backblaze constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#3 \$secretKey of class Utopia\\Storage\\Device\\DOSpaces constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#3 \$secretKey of class Utopia\\Storage\\Device\\Linode constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#3 \$secretKey of class Utopia\\Storage\\Device\\S3 constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#3 \$secretKey of class Utopia\\Storage\\Device\\Wasabi constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#4 \$bucket of class Utopia\\Storage\\Device\\AWS constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#4 \$bucket of class Utopia\\Storage\\Device\\Backblaze constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#4 \$bucket of class Utopia\\Storage\\Device\\DOSpaces constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#4 \$bucket of class Utopia\\Storage\\Device\\Linode constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#4 \$bucket of class Utopia\\Storage\\Device\\Wasabi constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
-
message: '#^Parameter \#5 \$queueForFunctions of closure expects Appwrite\\Event\\Func, Appwrite\\Event\\Event given\.$#'
identifier: argument.type
count: 1
path: app/init/resources.php
- -
- message: '#^Parameter \#5 \$region of class Utopia\\Storage\\Device\\AWS constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#5 \$region of class Utopia\\Storage\\Device\\Backblaze constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#5 \$region of class Utopia\\Storage\\Device\\DOSpaces constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#5 \$region of class Utopia\\Storage\\Device\\Linode constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#5 \$region of class Utopia\\Storage\\Device\\S3 constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Parameter \#5 \$region of class Utopia\\Storage\\Device\\Wasabi constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/init/resources.php
-
-
message: '#^Parameter \#6 \$queueForWebhooks of closure expects Appwrite\\Event\\Webhook, Appwrite\\Event\\Event given\.$#'
identifier: argument.type
@@ -6234,12 +5964,6 @@ parameters:
count: 1
path: app/realtime.php
- -
- message: '#^Parameter \#1 \$host of method Redis\:\:pconnect\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/realtime.php
-
-
message: '#^Parameter \#1 \$json of function json_decode expects string, mixed given\.$#'
identifier: argument.type
@@ -6348,12 +6072,6 @@ parameters:
count: 4
path: app/realtime.php
- -
- message: '#^Parameter \#1 \$version of method Utopia\\Logger\\Log\:\:setVersion\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/realtime.php
-
-
message: '#^Parameter \#2 \$array of function array_key_exists expects array, mixed given\.$#'
identifier: argument.type
@@ -6396,12 +6114,6 @@ parameters:
count: 5
path: app/realtime.php
- -
- message: '#^Parameter \#2 \$string of function explode expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/realtime.php
-
-
message: '#^Parameter \#2 \$value of method Utopia\\Abuse\\Adapter\:\:setParam\(\) expects string, int given\.$#'
identifier: argument.type
@@ -6433,7 +6145,7 @@ parameters:
path: app/realtime.php
-
- message: '#^Parameter \$port of class Utopia\\WebSocket\\Adapter\\Swoole constructor expects int, string\|null given\.$#'
+ message: '#^Parameter \$port of class Utopia\\WebSocket\\Adapter\\Swoole constructor expects int, string given\.$#'
identifier: argument.type
count: 1
path: app/realtime.php
@@ -6463,7 +6175,7 @@ parameters:
path: app/worker.php
-
- message: '#^Binary operation "\*" between \-1 and string\|null results in an error\.$#'
+ message: '#^Binary operation "\*" between \-1 and string results in an error\.$#'
identifier: binaryOp.invalid
count: 3
path: app/worker.php
@@ -6552,24 +6264,12 @@ parameters:
count: 1
path: app/worker.php
- -
- message: '#^Parameter \#1 \$host of method Redis\:\:pconnect\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/worker.php
-
-
message: '#^Parameter \#1 \$input of class Utopia\\Database\\Document constructor expects array\, mixed given\.$#'
identifier: argument.type
count: 1
path: app/worker.php
- -
- message: '#^Parameter \#1 \$name of class Utopia\\Queue\\Queue constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: app/worker.php
-
-
message: '#^Parameter \#1 \$pool of class Utopia\\Cache\\Adapter\\Pool constructor expects Utopia\\Pools\\Pool\, mixed given\.$#'
identifier: argument.type
@@ -6594,12 +6294,6 @@ parameters:
count: 2
path: app/worker.php
- -
- message: '#^Parameter \#1 \$version of method Utopia\\Logger\\Log\:\:setVersion\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 2
- path: app/worker.php
-
-
message: '#^Parameter \#2 \$cache of class Utopia\\Database\\Database constructor expects Utopia\\Cache\\Cache, mixed given\.$#'
identifier: argument.type
@@ -6624,12 +6318,6 @@ parameters:
count: 3
path: app/worker.php
- -
- message: '#^Parameter \#2 \$string of function explode expects string, string\|null given\.$#'
- identifier: argument.type
- count: 3
- path: app/worker.php
-
-
message: '#^Result of && is always false\.$#'
identifier: booleanAnd.alwaysFalse
@@ -10908,66 +10596,24 @@ parameters:
count: 1
path: src/Appwrite/Event/Audit.php
- -
- message: '#^Parameter \#1 \$class of method Appwrite\\Event\\Event\:\:setClass\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Audit.php
-
- -
- message: '#^Parameter \#1 \$queue of method Appwrite\\Event\\Event\:\:setQueue\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Audit.php
-
-
message: '#^Method Appwrite\\Event\\Build\:\:preparePayload\(\) return type has no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
count: 1
path: src/Appwrite/Event/Build.php
- -
- message: '#^Parameter \#1 \$class of method Appwrite\\Event\\Event\:\:setClass\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Build.php
-
- -
- message: '#^Parameter \#1 \$queue of method Appwrite\\Event\\Event\:\:setQueue\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Build.php
-
-
message: '#^Method Appwrite\\Event\\Certificate\:\:preparePayload\(\) return type has no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
count: 1
path: src/Appwrite/Event/Certificate.php
- -
- message: '#^Parameter \#1 \$class of method Appwrite\\Event\\Event\:\:setClass\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Certificate.php
-
- -
- message: '#^Parameter \#1 \$queue of method Appwrite\\Event\\Event\:\:setQueue\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Certificate.php
-
-
message: '#^Method Appwrite\\Event\\Database\:\:preparePayload\(\) return type has no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
count: 1
path: src/Appwrite/Event/Database.php
- -
- message: '#^Parameter \#1 \$class of method Appwrite\\Event\\Event\:\:setClass\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Database.php
-
-
message: '#^Parameter \#1 \$dsn of class Utopia\\DSN\\DSN constructor expects string, mixed given\.$#'
identifier: argument.type
@@ -10992,18 +10638,6 @@ parameters:
count: 1
path: src/Appwrite/Event/Delete.php
- -
- message: '#^Parameter \#1 \$class of method Appwrite\\Event\\Event\:\:setClass\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Delete.php
-
- -
- message: '#^Parameter \#1 \$queue of method Appwrite\\Event\\Event\:\:setQueue\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Delete.php
-
-
message: '#^Method Appwrite\\Event\\Event\:\:generateEvents\(\) has parameter \$params with no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
@@ -11202,18 +10836,6 @@ parameters:
count: 1
path: src/Appwrite/Event/Func.php
- -
- message: '#^Parameter \#1 \$class of method Appwrite\\Event\\Event\:\:setClass\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Func.php
-
- -
- message: '#^Parameter \#1 \$queue of method Appwrite\\Event\\Event\:\:setQueue\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Func.php
-
-
message: '#^Property Appwrite\\Event\\Func\:\:\$headers type has no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
@@ -11340,18 +10962,6 @@ parameters:
count: 1
path: src/Appwrite/Event/Mail.php
- -
- message: '#^Parameter \#1 \$class of method Appwrite\\Event\\Event\:\:setClass\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Mail.php
-
- -
- message: '#^Parameter \#1 \$queue of method Appwrite\\Event\\Event\:\:setQueue\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Mail.php
-
-
message: '#^Property Appwrite\\Event\\Mail\:\:\$attachment type has no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
@@ -11484,18 +11094,6 @@ parameters:
count: 1
path: src/Appwrite/Event/Messaging.php
- -
- message: '#^Parameter \#1 \$class of method Appwrite\\Event\\Event\:\:setClass\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Messaging.php
-
- -
- message: '#^Parameter \#1 \$queue of method Appwrite\\Event\\Event\:\:setQueue\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Messaging.php
-
-
message: '#^Property Appwrite\\Event\\Messaging\:\:\$recipients type has no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
@@ -11508,18 +11106,6 @@ parameters:
count: 1
path: src/Appwrite/Event/Migration.php
- -
- message: '#^Parameter \#1 \$class of method Appwrite\\Event\\Event\:\:setClass\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Migration.php
-
- -
- message: '#^Parameter \#1 \$queue of method Appwrite\\Event\\Event\:\:setQueue\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Migration.php
-
-
message: '#^Cannot call method getId\(\) on Utopia\\Database\\Document\|null\.$#'
identifier: method.nonObject
@@ -11586,36 +11172,12 @@ parameters:
count: 1
path: src/Appwrite/Event/Screenshot.php
- -
- message: '#^Parameter \#1 \$class of method Appwrite\\Event\\Event\:\:setClass\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Screenshot.php
-
- -
- message: '#^Parameter \#1 \$queue of method Appwrite\\Event\\Event\:\:setQueue\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Screenshot.php
-
-
message: '#^Method Appwrite\\Event\\StatsResources\:\:preparePayload\(\) return type has no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
count: 1
path: src/Appwrite/Event/StatsResources.php
- -
- message: '#^Parameter \#1 \$class of method Appwrite\\Event\\Event\:\:setClass\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/StatsResources.php
-
- -
- message: '#^Parameter \#1 \$queue of method Appwrite\\Event\\Event\:\:setQueue\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/StatsResources.php
-
-
message: '#^Cannot access offset ''\$resource'' on mixed\.$#'
identifier: offsetAccess.nonOffsetAccessible
@@ -11676,18 +11238,6 @@ parameters:
count: 1
path: src/Appwrite/Event/Webhook.php
- -
- message: '#^Parameter \#1 \$class of method Appwrite\\Event\\Event\:\:setClass\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Webhook.php
-
- -
- message: '#^Parameter \#1 \$queue of method Appwrite\\Event\\Event\:\:setQueue\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Event/Webhook.php
-
-
message: '#^Cannot access offset ''code'' on mixed\.$#'
identifier: offsetAccess.nonOffsetAccessible
@@ -15054,6 +14604,438 @@ parameters:
count: 1
path: src/Appwrite/Platform/Action.php
+ -
+ message: '#^Cannot access offset ''appDomain'' on mixed\.$#'
+ identifier: offsetAccess.nonOffsetAccessible
+ count: 2
+ path: src/Appwrite/Platform/Installer/Http/Installer/Install.php
+
+ -
+ message: '#^Cannot access offset ''appDomain''\|''database''\|''emailCertificates''\|''httpPort''\|''httpsPort'' on mixed\.$#'
+ identifier: offsetAccess.nonOffsetAccessible
+ count: 1
+ path: src/Appwrite/Platform/Installer/Http/Installer/Install.php
+
+ -
+ message: '#^Cannot access offset ''assistantOpenAIKey''\|''opensslKey'' on mixed\.$#'
+ identifier: offsetAccess.nonOffsetAccessible
+ count: 2
+ path: src/Appwrite/Platform/Installer/Http/Installer/Install.php
+
+ -
+ message: '#^Cannot access offset ''assistantOpenAIKeyHash''\|''opensslKeyHash'' on mixed\.$#'
+ identifier: offsetAccess.nonOffsetAccessible
+ count: 2
+ path: src/Appwrite/Platform/Installer/Http/Installer/Install.php
+
+ -
+ message: '#^Cannot access offset ''database'' on mixed\.$#'
+ identifier: offsetAccess.nonOffsetAccessible
+ count: 1
+ path: src/Appwrite/Platform/Installer/Http/Installer/Install.php
+
+ -
+ message: '#^Cannot access offset ''emailCertificates'' on mixed\.$#'
+ identifier: offsetAccess.nonOffsetAccessible
+ count: 1
+ path: src/Appwrite/Platform/Installer/Http/Installer/Install.php
+
+ -
+ message: '#^Cannot access offset ''httpPort'' on mixed\.$#'
+ identifier: offsetAccess.nonOffsetAccessible
+ count: 1
+ path: src/Appwrite/Platform/Installer/Http/Installer/Install.php
+
+ -
+ message: '#^Cannot access offset ''httpsPort'' on mixed\.$#'
+ identifier: offsetAccess.nonOffsetAccessible
+ count: 1
+ path: src/Appwrite/Platform/Installer/Http/Installer/Install.php
+
+ -
+ message: '#^Cannot cast mixed to int\.$#'
+ identifier: cast.int
+ count: 2
+ path: src/Appwrite/Platform/Installer/Http/Installer/Install.php
+
+ -
+ message: '#^Cannot cast mixed to string\.$#'
+ identifier: cast.string
+ count: 3
+ path: src/Appwrite/Platform/Installer/Http/Installer/Install.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Http\\Installer\\Install\:\:action\(\) has parameter \$paths with no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Http/Installer/Install.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Http\\Installer\\Install\:\:buildErrorDetails\(\) return type has no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Http/Installer/Install.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Http\\Installer\\Install\:\:writeSseEvent\(\) has parameter \$payload with no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Http/Installer/Install.php
+
+ -
+ message: '#^Offset ''payload'' might not exist on array\|null\.$#'
+ identifier: offsetAccess.notFound
+ count: 1
+ path: src/Appwrite/Platform/Installer/Http/Installer/Install.php
+
+ -
+ message: '#^Offset 0 on non\-empty\-list\ on left side of \?\? always exists and is not nullable\.$#'
+ identifier: nullCoalesce.offset
+ count: 1
+ path: src/Appwrite/Platform/Installer/Http/Installer/Install.php
+
+ -
+ message: '#^Parameter \#1 \$httpPort of method Appwrite\\Platform\\Tasks\\Install\:\:performInstallation\(\) expects string, int\\|int\<1, max\>\|string given\.$#'
+ identifier: argument.type
+ count: 1
+ path: src/Appwrite/Platform/Installer/Http/Installer/Install.php
+
+ -
+ message: '#^Parameter \#1 \$string of function ucfirst expects string, string\|null given\.$#'
+ identifier: argument.type
+ count: 1
+ path: src/Appwrite/Platform/Installer/Http/Installer/Install.php
+
+ -
+ message: '#^Parameter \#2 \$httpsPort of method Appwrite\\Platform\\Tasks\\Install\:\:performInstallation\(\) expects string, int\\|int\<1, max\>\|string given\.$#'
+ identifier: argument.type
+ count: 1
+ path: src/Appwrite/Platform/Installer/Http/Installer/Install.php
+
+ -
+ message: '#^Strict comparison using \!\=\= between non\-empty\-string and '''' will always evaluate to true\.$#'
+ identifier: notIdentical.alwaysTrue
+ count: 3
+ path: src/Appwrite/Platform/Installer/Http/Installer/Install.php
+
+ -
+ message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#'
+ identifier: function.alreadyNarrowedType
+ count: 2
+ path: src/Appwrite/Platform/Installer/Http/Installer/Status.php
+
+ -
+ message: '#^Binary operation "\." between mixed and ''/installer\.phtml'' results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 1
+ path: src/Appwrite/Platform/Installer/Http/Installer/View.php
+
+ -
+ message: '#^Binary operation "\." between mixed and ''/installer…'' results in an error\.$#'
+ identifier: binaryOp.invalid
+ count: 2
+ path: src/Appwrite/Platform/Installer/Http/Installer/View.php
+
+ -
+ message: '#^Cannot access offset ''default'' on mixed\.$#'
+ identifier: offsetAccess.nonOffsetAccessible
+ count: 1
+ path: src/Appwrite/Platform/Installer/Http/Installer/View.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Http\\Installer\\View\:\:action\(\) has parameter \$paths with no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Http/Installer/View.php
+
+ -
+ message: '#^Parameter \#1 \$data of method Utopia\\Http\\Response\:\:html\(\) expects string, string\|false given\.$#'
+ identifier: argument.type
+ count: 2
+ path: src/Appwrite/Platform/Installer/Http/Installer/View.php
+
+ -
+ message: '#^Cannot cast mixed to string\.$#'
+ identifier: cast.string
+ count: 6
+ path: src/Appwrite/Platform/Installer/Runtime/Config.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Runtime\\Config\:\:__construct\(\) has parameter \$values with no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/Config.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Runtime\\Config\:\:apply\(\) has parameter \$values with no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/Config.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Runtime\\Config\:\:containsKnownKeys\(\) has parameter \$values with no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/Config.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Runtime\\Config\:\:getVars\(\) return type has no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/Config.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Runtime\\Config\:\:hasValidStringValue\(\) has parameter \$values with no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/Config.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Runtime\\Config\:\:setVars\(\) has parameter \$vars with no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/Config.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Runtime\\Config\:\:toArray\(\) return type has no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/Config.php
+
+ -
+ message: '#^Property Appwrite\\Platform\\Installer\\Runtime\\Config\:\:\$vars type has no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/Config.php
+
+ -
+ message: '#^Cannot access offset ''installId'' on mixed\.$#'
+ identifier: offsetAccess.nonOffsetAccessible
+ count: 2
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Cannot access offset mixed on mixed\.$#'
+ identifier: offsetAccess.nonOffsetAccessible
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Cannot cast mixed to int\.$#'
+ identifier: cast.int
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Cannot cast mixed to string\.$#'
+ identifier: cast.string
+ count: 2
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Runtime\\State\:\:__construct\(\) has parameter \$paths with no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Runtime\\State\:\:applyEnvConfig\(\) has parameter \$cfg with no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Runtime\\State\:\:buildConfig\(\) has parameter \$overrides with no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Runtime\\State\:\:isGlobalLockActive\(\) has parameter \$lock with no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Runtime\\State\:\:isValidPort\(\) has parameter \$value with no type specified\.$#'
+ identifier: missingType.parameter
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Runtime\\State\:\:parseEnvFile\(\) return type has no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Runtime\\State\:\:readConfigFile\(\) return type has no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Runtime\\State\:\:readProgressFile\(\) return type has no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Runtime\\State\:\:sanitizeInstallId\(\) has parameter \$value with no type specified\.$#'
+ identifier: missingType.parameter
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Runtime\\State\:\:withGlobalLock\(\) has no return type specified\.$#'
+ identifier: missingType.return
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Runtime\\State\:\:writeProgressFile\(\) has parameter \$payload with no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Offset 1 on array\{0\: non\-falsy\-string, 1\: non\-empty\-string, 2\?\: numeric\-string\} on left side of \?\? always exists and is not nullable\.$#'
+ identifier: nullCoalesce.offset
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Parameter \#1 \$lock of method Appwrite\\Platform\\Installer\\Runtime\\State\:\:isGlobalLockActive\(\) expects array\|null, mixed given\.$#'
+ identifier: argument.type
+ count: 2
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Parameter \#1 \$stream of function ftruncate expects resource, mixed given\.$#'
+ identifier: argument.type
+ count: 2
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Parameter \#1 \$stream of function fwrite expects resource, mixed given\.$#'
+ identifier: argument.type
+ count: 2
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Parameter \#1 \$stream of function rewind expects resource, mixed given\.$#'
+ identifier: argument.type
+ count: 2
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Parameter \#1 \$string of function trim expects string, string\|false given\.$#'
+ identifier: argument.type
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Parameter \#2 \$data of function fwrite expects string, string\|false given\.$#'
+ identifier: argument.type
+ count: 2
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Possibly invalid array key type mixed\.$#'
+ identifier: offsetAccess.invalidOffset
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Property Appwrite\\Platform\\Installer\\Runtime\\State\:\:\$paths is never read, only written\.$#'
+ identifier: property.onlyWritten
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Property Appwrite\\Platform\\Installer\\Runtime\\State\:\:\$paths type has no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Runtime/State.php
+
+ -
+ message: '#^Cannot cast mixed to int\.$#'
+ identifier: cast.int
+ count: 1
+ path: src/Appwrite/Platform/Installer/Server.php
+
+ -
+ message: '#^Cannot cast non\-empty\-list\\|non\-falsy\-string to string\.$#'
+ identifier: cast.string
+ count: 2
+ path: src/Appwrite/Platform/Installer/Server.php
+
+ -
+ message: '#^Method Appwrite\\Platform\\Installer\\Server\:\:startDockerInstaller\(\) has parameter \$opts with no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Server.php
+
+ -
+ message: '#^Parameter \#1 \$directory of method Utopia\\Http\\Files\:\:load\(\) expects string, mixed given\.$#'
+ identifier: argument.type
+ count: 1
+ path: src/Appwrite/Platform/Installer/Server.php
+
+ -
+ message: '#^Parameter \#1 \$filename of function unlink expects string, string\|false given\.$#'
+ identifier: argument.type
+ count: 1
+ path: src/Appwrite/Platform/Installer/Server.php
+
+ -
+ message: '#^Parameter \#1 \$path of function realpath expects string, mixed given\.$#'
+ identifier: argument.type
+ count: 1
+ path: src/Appwrite/Platform/Installer/Server.php
+
+ -
+ message: '#^Parameter \#1 \$value of method Appwrite\\Platform\\Installer\\Runtime\\Config\:\:setLockedDatabase\(\) expects string\|null, list\\|string given\.$#'
+ identifier: argument.type
+ count: 1
+ path: src/Appwrite/Platform/Installer/Server.php
+
+ -
+ message: '#^Parameter \#1 \$value of method Appwrite\\Platform\\Installer\\Runtime\\Config\:\:setLockedDatabase\(\) expects string\|null, mixed given\.$#'
+ identifier: argument.type
+ count: 1
+ path: src/Appwrite/Platform/Installer/Server.php
+
+ -
+ message: '#^Parameter \#2 \$port of class Utopia\\Http\\Adapter\\Swoole\\Server@anonymous/src/Appwrite/Platform/Installer/Server\.php\:156 constructor expects string\|null, int given\.$#'
+ identifier: argument.type
+ count: 1
+ path: src/Appwrite/Platform/Installer/Server.php
+
+ -
+ message: '#^Parameter \#2 \$port of method Appwrite\\Platform\\Installer\\Server\:\:printInstallerUrl\(\) expects string, mixed given\.$#'
+ identifier: argument.type
+ count: 2
+ path: src/Appwrite/Platform/Installer/Server.php
+
+ -
+ message: '#^Parameter \#3 \$readyFile of method Appwrite\\Platform\\Installer\\Server\:\:startSwooleServer\(\) expects string\|null, mixed given\.$#'
+ identifier: argument.type
+ count: 1
+ path: src/Appwrite/Platform/Installer/Server.php
+
+ -
+ message: '#^Property Appwrite\\Platform\\Installer\\Server\:\:\$paths type has no value type specified in iterable type array\.$#'
+ identifier: missingType.iterableValue
+ count: 1
+ path: src/Appwrite/Platform/Installer/Server.php
+
+ -
+ message: '#^Offset 1 on array\{0\: non\-falsy\-string, 1\: non\-empty\-string, 2\?\: numeric\-string\} on left side of \?\? always exists and is not nullable\.$#'
+ identifier: nullCoalesce.offset
+ count: 1
+ path: src/Appwrite/Platform/Installer/Validator/AppDomain.php
+
-
message: '#^Parameter \#1 \$issuer of method Appwrite\\Auth\\MFA\\Type\:\:setIssuer\(\) expects string, mixed given\.$#'
identifier: argument.type
@@ -16225,13 +16207,13 @@ parameters:
path: src/Appwrite/Platform/Modules/Compute/Base.php
-
- message: '#^Parameter \#3 \$maxCpus of class Appwrite\\Platform\\Modules\\Compute\\Validator\\Specification constructor expects float, string\|null given\.$#'
+ message: '#^Parameter \#3 \$maxCpus of class Appwrite\\Platform\\Modules\\Compute\\Validator\\Specification constructor expects float, string given\.$#'
identifier: argument.type
count: 1
path: src/Appwrite/Platform/Modules/Compute/Base.php
-
- message: '#^Parameter \#4 \$maxMemory of class Appwrite\\Platform\\Modules\\Compute\\Validator\\Specification constructor expects int, string\|null given\.$#'
+ message: '#^Parameter \#4 \$maxMemory of class Appwrite\\Platform\\Modules\\Compute\\Validator\\Specification constructor expects int, string given\.$#'
identifier: argument.type
count: 1
path: src/Appwrite/Platform/Modules/Compute/Base.php
@@ -16350,12 +16332,6 @@ parameters:
count: 1
path: src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php
- -
- message: '#^Parameter \#2 \$string of function explode expects string, string\|null given\.$#'
- identifier: argument.type
- count: 3
- path: src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php
-
-
message: '#^Method Appwrite\\Platform\\Modules\\Console\\Http\\Variables\\Get\:\:action\(\) has no return type specified\.$#'
identifier: missingType.return
@@ -16374,12 +16350,6 @@ parameters:
count: 1
path: src/Appwrite/Platform/Modules/Console/Http/Variables/Get.php
- -
- message: '#^Parameter \#2 \$string of function explode expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Platform/Modules/Console/Http/Variables/Get.php
-
-
message: '#^Unary operation "\+" on string\|null results in an error\.$#'
identifier: unaryOp.invalid
@@ -21432,12 +21402,6 @@ parameters:
count: 1
path: src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php
- -
- message: '#^Parameter \#2 \$default of method Utopia\\Http\\Request\:\:getHeader\(\) expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php
-
-
message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string\|null, int given\.$#'
identifier: argument.type
@@ -21456,12 +21420,6 @@ parameters:
count: 2
path: src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php
- -
- message: '#^Parameter \#2 \$string of function explode expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php
-
-
message: '#^Parameter \#2 \$value of method Utopia\\Http\\Response\:\:addHeader\(\) expects string, \(float\|int\) given\.$#'
identifier: argument.type
@@ -21475,13 +21433,13 @@ parameters:
path: src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php
-
- message: '#^Parameter \#3 \$maxCpus of class Appwrite\\Platform\\Modules\\Compute\\Validator\\Specification constructor expects float, string\|null given\.$#'
+ message: '#^Parameter \#3 \$maxCpus of class Appwrite\\Platform\\Modules\\Compute\\Validator\\Specification constructor expects float, string given\.$#'
identifier: argument.type
count: 2
path: src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php
-
- message: '#^Parameter \#4 \$maxMemory of class Appwrite\\Platform\\Modules\\Compute\\Validator\\Specification constructor expects int, string\|null given\.$#'
+ message: '#^Parameter \#4 \$maxMemory of class Appwrite\\Platform\\Modules\\Compute\\Validator\\Specification constructor expects int, string given\.$#'
identifier: argument.type
count: 2
path: src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php
@@ -21631,13 +21589,13 @@ parameters:
path: src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php
-
- message: '#^Parameter \#3 \$maxCpus of class Appwrite\\Platform\\Modules\\Compute\\Validator\\Specification constructor expects float, string\|null given\.$#'
+ message: '#^Parameter \#3 \$maxCpus of class Appwrite\\Platform\\Modules\\Compute\\Validator\\Specification constructor expects float, string given\.$#'
identifier: argument.type
count: 2
path: src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php
-
- message: '#^Parameter \#4 \$maxMemory of class Appwrite\\Platform\\Modules\\Compute\\Validator\\Specification constructor expects int, string\|null given\.$#'
+ message: '#^Parameter \#4 \$maxMemory of class Appwrite\\Platform\\Modules\\Compute\\Validator\\Specification constructor expects int, string given\.$#'
identifier: argument.type
count: 2
path: src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php
@@ -21708,12 +21666,6 @@ parameters:
count: 1
path: src/Appwrite/Platform/Modules/Functions/Http/Runtimes/XList.php
- -
- message: '#^Parameter \#2 \$string of function explode expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Platform/Modules/Functions/Http/Runtimes/XList.php
-
-
message: '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\.$#'
identifier: foreach.nonIterable
@@ -22860,12 +22812,6 @@ parameters:
count: 1
path: src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php
- -
- message: '#^Parameter \#1 \$host of class Appwrite\\ClamAV\\Network constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Platform/Modules/Health/Http/Health/AntiVirus/Get.php
-
-
message: '#^Parameter \#2 \$default of static method Utopia\\System\\System\:\:getEnv\(\) expects string\|null, int given\.$#'
identifier: argument.type
@@ -23304,12 +23250,6 @@ parameters:
count: 1
path: src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php
- -
- message: '#^Parameter \#2 \$string of function explode expects string, string\|null given\.$#'
- identifier: argument.type
- count: 5
- path: src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php
-
-
message: '#^Possibly invalid array key type mixed\.$#'
identifier: offsetAccess.invalidOffset
@@ -23598,12 +23538,6 @@ parameters:
count: 1
path: src/Appwrite/Platform/Modules/Proxy/Action.php
- -
- message: '#^Parameter \#1 \$domain of class Utopia\\Domains\\Domain constructor expects string, string\|null given\.$#'
- identifier: argument.type
- count: 1
- path: src/Appwrite/Platform/Modules/Proxy/Action.php
-
-
message: '#^Parameter \#1 \$validators of class Utopia\\Validator\\AnyOf constructor expects array\, list\