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 619f700d91..9af261bb1a 100644
--- a/app/cli.php
+++ b/app/cli.php
@@ -157,7 +157,7 @@ $cli->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
@@ -178,7 +178,7 @@ $cli->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
@@ -201,9 +201,8 @@ $cli->setResource('getLogsDB', function (Group $pools, Cache $cache, Authorizati
$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;
}
@@ -219,8 +218,8 @@ $cli->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/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 8556cbeb0f..9edbe3c92c 100644
--- a/app/init/resources.php
+++ b/app/init/resources.php
@@ -593,7 +593,7 @@ $container->set('dbForProject', function (Group $pools, Database $dbForPlatform,
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
- ->setTenant((int) $project->getSequence())
+ ->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -852,7 +852,7 @@ $container->set('getProjectDB', function (Group $pools, Database $dbForPlatform,
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
- ->setTenant((int) $project->getSequence())
+ ->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -882,9 +882,8 @@ $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization
$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;
}
@@ -900,8 +899,8 @@ $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization
->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 f05133c875..435b0aaa1d 100644
--- a/app/realtime.php
+++ b/app/realtime.php
@@ -116,7 +116,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 cab557a1e8..65a70f96ba 100644
--- a/composer.json
+++ b/composer.json
@@ -13,7 +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"
+ "analyze": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G",
+ "bench": "vendor/bin/phpbench run --report=benchmark",
+ "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": {
@@ -90,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": "*",
diff --git a/composer.lock b/composer.lock
index 4d4f1015eb..fdde1cb0a3 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": "5848ab9fef2b8aefac2e80f54bd1f146",
+ "content-hash": "13579de3d747c541fdcce4f709df8e57",
"packages": [
{
"name": "adhocore/jwt",
@@ -3850,16 +3850,16 @@
},
{
"name": "utopia-php/database",
- "version": "dev-main",
+ "version": "dev-fix-collection-recreate",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
- "reference": "bb89e8c4a5d534fc7e650c4438aa796667fe160a"
+ "reference": "5208630969dfdfbe8eda9c34c6b28ce711ece7c4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/database/zipball/bb89e8c4a5d534fc7e650c4438aa796667fe160a",
- "reference": "bb89e8c4a5d534fc7e650c4438aa796667fe160a",
+ "url": "https://api.github.com/repos/utopia-php/database/zipball/5208630969dfdfbe8eda9c34c6b28ce711ece7c4",
+ "reference": "5208630969dfdfbe8eda9c34c6b28ce711ece7c4",
"shasum": ""
},
"require": {
@@ -3890,7 +3890,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"
],
@@ -3903,10 +3934,10 @@
"utopia"
],
"support": {
- "issues": "https://github.com/utopia-php/database/issues",
- "source": "https://github.com/utopia-php/database/tree/main"
+ "source": "https://github.com/utopia-php/database/tree/fix-collection-recreate",
+ "issues": "https://github.com/utopia-php/database/issues"
},
- "time": "2026-03-16T11:41:45+00:00"
+ "time": "2026-03-16T11:58:09+00:00"
},
{
"name": "utopia-php/detector",
@@ -5447,16 +5478,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
- "version": "1.11.8",
+ "version": "1.11.6",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
- "reference": "bf45bb91419f157e6d539d05f3f2c2d2120c90dc"
+ "reference": "f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/bf45bb91419f157e6d539d05f3f2c2d2120c90dc",
- "reference": "bf45bb91419f157e6d539d05f3f2c2d2120c90dc",
+ "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38",
+ "reference": "f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38",
"shasum": ""
},
"require": {
@@ -5492,9 +5523,9 @@
"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.8"
+ "source": "https://github.com/appwrite/sdk-generator/tree/1.11.6"
},
- "time": "2026-03-16T11:02:05+00:00"
+ "time": "2026-03-09T07:12:51+00:00"
},
{
"name": "brianium/paratest",
@@ -8444,21 +8475,14 @@
"aliases": [
{
"package": "utopia-php/database",
- "version": "dev-main",
+ "version": "dev-fix-collection-recreate",
"alias": "5.3.15",
"alias_normalized": "5.3.15.0"
- },
- {
- "package": "utopia-php/framework",
- "version": "dev-feat/coroutines-option",
- "alias": "0.34.15",
- "alias_normalized": "0.34.15.0"
}
],
"minimum-stability": "dev",
"stability-flags": {
- "utopia-php/database": 20,
- "utopia-php/framework": 20
+ "utopia-php/database": 20
},
"prefer-stable": true,
"prefer-lowest": false,
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/mongo-entrypoint.sh b/mongo-entrypoint.sh
old mode 100755
new mode 100644
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 8825208851..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\