Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/views/install/compose.phtml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ $organization = $this->getParam('organization', '');
$image = $this->getParam('image', '');
$enableAssistant = $this->getParam('enableAssistant', false);
$dbService = $this->getParam('database', 'mongodb');
$allowedDbServices = ['mariadb', 'mongodb', 'postgresql'];
$allowedDbServices = ['mariadb', 'mongodb'];
if (!\in_array($dbService, $allowedDbServices, true)) {
$dbService = 'mongodb';
}
Expand Down Expand Up @@ -194,7 +194,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: <?php echo $organization; ?>/console:7.6.4
image: <?php echo $organization; ?>/console:7.8.26
restart: unless-stopped
networks:
- appwrite
Expand Down
24 changes: 24 additions & 0 deletions app/views/install/installer/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,19 @@ body {
transform: translateY(10px);
}

.install-counter {
margin-left: auto;
opacity: 0;
transition: opacity 0.2s ease;
white-space: nowrap;
user-select: none;
color: var(--fgcolor-neutral-secondary);
}

.install-row[data-status='in-progress'] .install-counter:not(:empty) {
opacity: 1;
}

.install-row-toggle {
margin-left: auto;
width: 32px;
Expand Down Expand Up @@ -897,6 +910,17 @@ body {
gap: var(--gap-m);
}

.install-global-actions {
display: flex;
justify-content: center;
gap: var(--gap-m);
padding: var(--space-4) 0;
}

.install-global-actions.is-hidden {
display: none;
}

.install-error-details .button {
align-self: center;
margin-top: 0;
Expand Down
115 changes: 102 additions & 13 deletions app/views/install/installer/js/modules/progress.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,10 @@
return normalized.summary || 'Installation failed.';
}
if (status === STATUS.COMPLETED) return step.done;
return step.inProgress;
return message || step.inProgress;
};

const updateInstallRow = (row, step, status, message) => {
const updateInstallRow = (row, step, status, message, details) => {
if (!row || !step) return;
row.dataset.status = status;
row.dataset.step = step.id;
Expand All @@ -138,6 +138,15 @@
}
}

const counter = row.querySelector('[data-install-counter]');
if (counter) {
const started = details?.containerStarted ?? 0;
const total = details?.containerTotal;
counter.textContent = (status === STATUS.IN_PROGRESS && total > 0 && started < total)
? `${started}/${total}`
: '';
}

// Show/hide "Navigate to Console" button for account setup errors
const consoleBtn = row.querySelector('[data-install-console]');
if (consoleBtn) {
Expand Down Expand Up @@ -349,7 +358,7 @@
const normalizedDomain = (formState?.appDomain || '').trim() || 'localhost';
const normalizedHttpPort = (formState?.httpPort || '').trim() || '80';
const normalizedHttpsPort = (formState?.httpsPort || '').trim() || '443';
const normalizedEmail = (formState?.emailCertificates || '').trim();
const normalizedEmail = (formState?.emailCertificates || '').trim() || (formState?.accountEmail || '').trim();
const normalizedAssistantKey = (formState?.assistantOpenAIKey || '').trim();
const normalizedAccountEmail = (formState?.accountEmail || '').trim();
const normalizedAccountPassword = (formState?.accountPassword || '').trim();
Expand Down Expand Up @@ -529,7 +538,7 @@
if (!state) return;
const row = ensureRow(step);
if (row) {
updateInstallRow(row, step, state.status || STATUS.IN_PROGRESS, state.message);
updateInstallRow(row, step, state.status || STATUS.IN_PROGRESS, state.message, state.details);
if (state.status === STATUS?.ERROR) {
updateInstallErrorDetails(row, {
message: state.message,
Expand Down Expand Up @@ -579,6 +588,9 @@
}
}
}
if (payload.status === STATUS.ERROR) {
showGlobalActions();
}
scheduleFallback();
};

Expand Down Expand Up @@ -616,6 +628,7 @@

const applySnapshot = (snapshot) => {
if (!snapshot || !snapshot.steps) return;
let hasErrors = false;
INSTALLATION_STEPS.forEach((step) => {
const detail = snapshot.steps[step.id];
if (!detail) return;
Expand All @@ -624,8 +637,14 @@
message: detail.message,
details: snapshot.details?.[step.id]
});
if (detail.status === STATUS.ERROR) {
hasErrors = true;
}
});
renderProgress();
if (hasErrors) {
showGlobalActions();
}
};

const checkAllCompleted = () => {
Expand Down Expand Up @@ -674,6 +693,7 @@
}
stopSyncedSpinnerRotation();
setUnloadGuard(false);
clearInstallLock?.();
};

const SSL_STEP = {
Expand Down Expand Up @@ -890,9 +910,22 @@
}
};

const isSnapshotTerminal = (snapshot) => {
if (!snapshot?.steps) return true;
const stepEntries = Object.values(snapshot.steps);
if (stepEntries.length === 0) return true;
const hasError = stepEntries.some((s) => s.status === STATUS.ERROR);
if (hasError) return true;
const allCompleted = INSTALLATION_STEPS.every((step) => {
const detail = snapshot.steps[step.id];
return detail && detail.status === STATUS.COMPLETED;
});
return allCompleted;
};

const resumeInstall = async (installId) => {
const snapshot = await fetchInstallStatus(installId);
if (!snapshot) return false;
if (!snapshot || isSnapshotTerminal(snapshot)) return false;
activeInstall = {
installId,
controller: new AbortController(),
Expand Down Expand Up @@ -966,6 +999,60 @@
}
});

const globalActions = root.querySelector('[data-install-global-actions]');

const showGlobalActions = () => {
if (globalActions) {
globalActions.classList.remove('is-hidden');
}
};

const performReset = async (hard) => {
const installId = activeInstall?.installId || getInstallLock?.()?.installId || getStoredInstallId?.();

try {
const res = await fetch('/install/reset', {
method: 'POST',
headers: withCsrfHeader({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ installId: installId || '', hard })
});
if (hard && !res.ok) {
const data = await res.json().catch(() => ({}));
showToast?.({
status: 'error',
title: 'Reset failed',
description: data?.message || 'Could not stop containers. Try running "docker compose down -v" manually.',
dismissible: true
});
return;
}
} catch (e) {
console.error('Reset request failed:', e);
}

clearInstallLock?.();
clearInstallId?.();
cleanupInstallFlow();
window.location.href = '/?step=1';
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const startOverButton = root.querySelector('[data-install-start-over]');
if (startOverButton) {
startOverButton.addEventListener('click', () => performReset(false));
}

const hardResetButton = root.querySelector('[data-install-hard-reset]');
if (hardResetButton) {
hardResetButton.addEventListener('click', () => {
const confirmed = window.confirm(
'This will stop all containers, remove all volumes (including database data, uploads, and certificates), and delete configuration files.\n\nThis action cannot be undone. Continue?'
);
if (confirmed) {
performReset(true);
}
});
}

// When the user switches back to this tab, check if installation
// finished while the tab was in the background.
document.addEventListener('visibilitychange', () => {
Expand All @@ -974,22 +1061,24 @@
}
});

const startFreshInstall = () => {
clearInstallId?.();
clearInstallLock?.();
const newInstallId = generateInstallId();
storeInstallId?.(newInstallId);
startInstallStream(newInstallId);
};

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);
startFreshInstall();
}
});
} else {
const newInstallId = generateInstallId();
storeInstallId?.(newInstallId);
startInstallStream(newInstallId);
startFreshInstall();
}
};

Expand Down
50 changes: 39 additions & 11 deletions app/views/install/installer/js/modules/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

const INSTALL_LOCK_KEY = 'appwrite-install-lock';
const INSTALL_ID_KEY = 'appwrite-install-id';
const INSTALL_LOCK_LOCAL_KEY = 'appwrite-install-lock-backup';
const INSTALL_ID_LOCAL_KEY = 'appwrite-install-id-backup';

const formState = {
appDomain: null,
Expand Down Expand Up @@ -55,13 +57,24 @@
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;
}
if (raw) {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') return parsed;
}
} catch (error) {}

try {
const raw = localStorage.getItem(INSTALL_LOCK_LOCAL_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') {
sessionStorage.setItem(INSTALL_LOCK_KEY, raw);
return parsed;
}
}
} catch (error) {}

return null;
};

const setInstallLock = (installId, payload) => {
Expand All @@ -79,6 +92,9 @@
try {
sessionStorage.setItem(INSTALL_LOCK_KEY, JSON.stringify(lock));
} catch (error) {}
try {
localStorage.setItem(INSTALL_LOCK_LOCAL_KEY, JSON.stringify(lock));
} catch (error) {}
if (document.body) {
document.body.dataset.installLocked = 'true';
}
Expand All @@ -89,6 +105,9 @@
try {
sessionStorage.removeItem(INSTALL_LOCK_KEY);
} catch (error) {}
try {
localStorage.removeItem(INSTALL_LOCK_LOCAL_KEY);
} catch (error) {}
if (document.body) {
delete document.body.dataset.installLocked;
}
Expand Down Expand Up @@ -121,22 +140,31 @@

const getStoredInstallId = () => {
try {
return sessionStorage.getItem(INSTALL_ID_KEY);
} catch (error) {
return null;
}
const val = sessionStorage.getItem(INSTALL_ID_KEY);
if (val) return val;
} catch (error) {}
try {
return localStorage.getItem(INSTALL_ID_LOCAL_KEY);
} catch (error) {}
return null;
};

const storeInstallId = (installId) => {
try {
sessionStorage.setItem(INSTALL_ID_KEY, installId);
} catch (error) {}
try {
localStorage.setItem(INSTALL_ID_LOCAL_KEY, installId);
} catch (error) {}
};

const clearInstallId = () => {
try {
sessionStorage.removeItem(INSTALL_ID_KEY);
} catch (error) {}
try {
localStorage.removeItem(INSTALL_ID_LOCAL_KEY);
} catch (error) {}
};

window.InstallerStepsState = {
Expand Down
3 changes: 3 additions & 0 deletions app/views/install/installer/js/modules/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@
if (key === 'database') {
value = toDatabaseLabel(formState?.database);
}
if (key === 'emailCertificates' && !value) {
value = formState?.accountEmail;
}
if (value) {
node.textContent = value;
}
Expand Down
5 changes: 1 addition & 4 deletions app/views/install/installer/js/steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -390,10 +390,7 @@
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())) {
if (sslEmail && sslEmail.value.trim() && !isValidEmail?.(sslEmail.value.trim())) {
setFieldError?.(sslEmail, 'Please enter a valid email address');
valid = false;
}
Expand Down
Loading
Loading