Skip to content

Commit a92ad3a

Browse files
authored
Merge pull request #11624 from appwrite/fix-installer-state
2 parents 49688a6 + 4de9ec7 commit a92ad3a

17 files changed

Lines changed: 645 additions & 88 deletions

File tree

app/views/install/compose.phtml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ $organization = $this->getParam('organization', '');
1313
$image = $this->getParam('image', '');
1414
$enableAssistant = $this->getParam('enableAssistant', false);
1515
$dbService = $this->getParam('database', 'mongodb');
16-
$allowedDbServices = ['mariadb', 'mongodb', 'postgresql'];
16+
$allowedDbServices = ['mariadb', 'mongodb'];
1717
if (!\in_array($dbService, $allowedDbServices, true)) {
1818
$dbService = 'mongodb';
1919
}
@@ -194,7 +194,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
194194
appwrite-console:
195195
<<: *x-logging
196196
container_name: appwrite-console
197-
image: <?php echo $organization; ?>/console:7.6.4
197+
image: <?php echo $organization; ?>/console:7.8.26
198198
restart: unless-stopped
199199
networks:
200200
- appwrite

app/views/install/installer/css/styles.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,19 @@ body {
691691
transform: translateY(10px);
692692
}
693693

694+
.install-counter {
695+
margin-left: auto;
696+
opacity: 0;
697+
transition: opacity 0.2s ease;
698+
white-space: nowrap;
699+
user-select: none;
700+
color: var(--fgcolor-neutral-secondary);
701+
}
702+
703+
.install-row[data-status='in-progress'] .install-counter:not(:empty) {
704+
opacity: 1;
705+
}
706+
694707
.install-row-toggle {
695708
margin-left: auto;
696709
width: 32px;
@@ -897,6 +910,17 @@ body {
897910
gap: var(--gap-m);
898911
}
899912

913+
.install-global-actions {
914+
display: flex;
915+
justify-content: center;
916+
gap: var(--gap-m);
917+
padding: var(--space-4) 0;
918+
}
919+
920+
.install-global-actions.is-hidden {
921+
display: none;
922+
}
923+
900924
.install-error-details .button {
901925
align-self: center;
902926
margin-top: 0;

app/views/install/installer/js/modules/progress.js

Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,10 @@
111111
return normalized.summary || 'Installation failed.';
112112
}
113113
if (status === STATUS.COMPLETED) return step.done;
114-
return step.inProgress;
114+
return message || step.inProgress;
115115
};
116116

117-
const updateInstallRow = (row, step, status, message) => {
117+
const updateInstallRow = (row, step, status, message, details) => {
118118
if (!row || !step) return;
119119
row.dataset.status = status;
120120
row.dataset.step = step.id;
@@ -138,6 +138,15 @@
138138
}
139139
}
140140

141+
const counter = row.querySelector('[data-install-counter]');
142+
if (counter) {
143+
const started = details?.containerStarted ?? 0;
144+
const total = details?.containerTotal;
145+
counter.textContent = (status === STATUS.IN_PROGRESS && total > 0 && started < total)
146+
? `${started}/${total}`
147+
: '';
148+
}
149+
141150
// Show/hide "Navigate to Console" button for account setup errors
142151
const consoleBtn = row.querySelector('[data-install-console]');
143152
if (consoleBtn) {
@@ -349,7 +358,7 @@
349358
const normalizedDomain = (formState?.appDomain || '').trim() || 'localhost';
350359
const normalizedHttpPort = (formState?.httpPort || '').trim() || '80';
351360
const normalizedHttpsPort = (formState?.httpsPort || '').trim() || '443';
352-
const normalizedEmail = (formState?.emailCertificates || '').trim();
361+
const normalizedEmail = (formState?.emailCertificates || '').trim() || (formState?.accountEmail || '').trim();
353362
const normalizedAssistantKey = (formState?.assistantOpenAIKey || '').trim();
354363
const normalizedAccountEmail = (formState?.accountEmail || '').trim();
355364
const normalizedAccountPassword = (formState?.accountPassword || '').trim();
@@ -529,7 +538,7 @@
529538
if (!state) return;
530539
const row = ensureRow(step);
531540
if (row) {
532-
updateInstallRow(row, step, state.status || STATUS.IN_PROGRESS, state.message);
541+
updateInstallRow(row, step, state.status || STATUS.IN_PROGRESS, state.message, state.details);
533542
if (state.status === STATUS?.ERROR) {
534543
updateInstallErrorDetails(row, {
535544
message: state.message,
@@ -579,6 +588,9 @@
579588
}
580589
}
581590
}
591+
if (payload.status === STATUS.ERROR) {
592+
showGlobalActions();
593+
}
582594
scheduleFallback();
583595
};
584596

@@ -616,6 +628,7 @@
616628

617629
const applySnapshot = (snapshot) => {
618630
if (!snapshot || !snapshot.steps) return;
631+
let hasErrors = false;
619632
INSTALLATION_STEPS.forEach((step) => {
620633
const detail = snapshot.steps[step.id];
621634
if (!detail) return;
@@ -624,8 +637,14 @@
624637
message: detail.message,
625638
details: snapshot.details?.[step.id]
626639
});
640+
if (detail.status === STATUS.ERROR) {
641+
hasErrors = true;
642+
}
627643
});
628644
renderProgress();
645+
if (hasErrors) {
646+
showGlobalActions();
647+
}
629648
};
630649

631650
const checkAllCompleted = () => {
@@ -674,6 +693,7 @@
674693
}
675694
stopSyncedSpinnerRotation();
676695
setUnloadGuard(false);
696+
clearInstallLock?.();
677697
};
678698

679699
const SSL_STEP = {
@@ -890,9 +910,22 @@
890910
}
891911
};
892912

913+
const isSnapshotTerminal = (snapshot) => {
914+
if (!snapshot?.steps) return true;
915+
const stepEntries = Object.values(snapshot.steps);
916+
if (stepEntries.length === 0) return true;
917+
const hasError = stepEntries.some((s) => s.status === STATUS.ERROR);
918+
if (hasError) return true;
919+
const allCompleted = INSTALLATION_STEPS.every((step) => {
920+
const detail = snapshot.steps[step.id];
921+
return detail && detail.status === STATUS.COMPLETED;
922+
});
923+
return allCompleted;
924+
};
925+
893926
const resumeInstall = async (installId) => {
894927
const snapshot = await fetchInstallStatus(installId);
895-
if (!snapshot) return false;
928+
if (!snapshot || isSnapshotTerminal(snapshot)) return false;
896929
activeInstall = {
897930
installId,
898931
controller: new AbortController(),
@@ -966,6 +999,60 @@
966999
}
9671000
});
9681001

1002+
const globalActions = root.querySelector('[data-install-global-actions]');
1003+
1004+
const showGlobalActions = () => {
1005+
if (globalActions) {
1006+
globalActions.classList.remove('is-hidden');
1007+
}
1008+
};
1009+
1010+
const performReset = async (hard) => {
1011+
const installId = activeInstall?.installId || getInstallLock?.()?.installId || getStoredInstallId?.();
1012+
1013+
try {
1014+
const res = await fetch('/install/reset', {
1015+
method: 'POST',
1016+
headers: withCsrfHeader({ 'Content-Type': 'application/json' }),
1017+
body: JSON.stringify({ installId: installId || '', hard })
1018+
});
1019+
if (hard && !res.ok) {
1020+
const data = await res.json().catch(() => ({}));
1021+
showToast?.({
1022+
status: 'error',
1023+
title: 'Reset failed',
1024+
description: data?.message || 'Could not stop containers. Try running "docker compose down -v" manually.',
1025+
dismissible: true
1026+
});
1027+
return;
1028+
}
1029+
} catch (e) {
1030+
console.error('Reset request failed:', e);
1031+
}
1032+
1033+
clearInstallLock?.();
1034+
clearInstallId?.();
1035+
cleanupInstallFlow();
1036+
window.location.href = '/?step=1';
1037+
};
1038+
1039+
const startOverButton = root.querySelector('[data-install-start-over]');
1040+
if (startOverButton) {
1041+
startOverButton.addEventListener('click', () => performReset(false));
1042+
}
1043+
1044+
const hardResetButton = root.querySelector('[data-install-hard-reset]');
1045+
if (hardResetButton) {
1046+
hardResetButton.addEventListener('click', () => {
1047+
const confirmed = window.confirm(
1048+
'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?'
1049+
);
1050+
if (confirmed) {
1051+
performReset(true);
1052+
}
1053+
});
1054+
}
1055+
9691056
// When the user switches back to this tab, check if installation
9701057
// finished while the tab was in the background.
9711058
document.addEventListener('visibilitychange', () => {
@@ -974,22 +1061,24 @@
9741061
}
9751062
});
9761063

1064+
const startFreshInstall = () => {
1065+
clearInstallId?.();
1066+
clearInstallLock?.();
1067+
const newInstallId = generateInstallId();
1068+
storeInstallId?.(newInstallId);
1069+
startInstallStream(newInstallId);
1070+
};
1071+
9771072
const lock = getInstallLock?.();
9781073
const existingInstallId = lock?.installId || getStoredInstallId?.();
9791074
if (existingInstallId) {
9801075
resumeInstall(existingInstallId).then((resumed) => {
9811076
if (!resumed) {
982-
clearInstallId?.();
983-
clearInstallLock?.();
984-
const newInstallId = generateInstallId();
985-
storeInstallId?.(newInstallId);
986-
startInstallStream(newInstallId);
1077+
startFreshInstall();
9871078
}
9881079
});
9891080
} else {
990-
const newInstallId = generateInstallId();
991-
storeInstallId?.(newInstallId);
992-
startInstallStream(newInstallId);
1081+
startFreshInstall();
9931082
}
9941083
};
9951084

app/views/install/installer/js/modules/state.js

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
const INSTALL_LOCK_KEY = 'appwrite-install-lock';
99
const INSTALL_ID_KEY = 'appwrite-install-id';
10+
const INSTALL_LOCK_LOCAL_KEY = 'appwrite-install-lock-backup';
11+
const INSTALL_ID_LOCAL_KEY = 'appwrite-install-id-backup';
1012

1113
const formState = {
1214
appDomain: null,
@@ -55,13 +57,24 @@
5557
const getInstallLock = () => {
5658
try {
5759
const raw = sessionStorage.getItem(INSTALL_LOCK_KEY);
58-
if (!raw) return null;
59-
const parsed = JSON.parse(raw);
60-
if (!parsed || typeof parsed !== 'object') return null;
61-
return parsed;
62-
} catch (error) {
63-
return null;
64-
}
60+
if (raw) {
61+
const parsed = JSON.parse(raw);
62+
if (parsed && typeof parsed === 'object') return parsed;
63+
}
64+
} catch (error) {}
65+
66+
try {
67+
const raw = localStorage.getItem(INSTALL_LOCK_LOCAL_KEY);
68+
if (raw) {
69+
const parsed = JSON.parse(raw);
70+
if (parsed && typeof parsed === 'object') {
71+
sessionStorage.setItem(INSTALL_LOCK_KEY, raw);
72+
return parsed;
73+
}
74+
}
75+
} catch (error) {}
76+
77+
return null;
6578
};
6679

6780
const setInstallLock = (installId, payload) => {
@@ -79,6 +92,9 @@
7992
try {
8093
sessionStorage.setItem(INSTALL_LOCK_KEY, JSON.stringify(lock));
8194
} catch (error) {}
95+
try {
96+
localStorage.setItem(INSTALL_LOCK_LOCAL_KEY, JSON.stringify(lock));
97+
} catch (error) {}
8298
if (document.body) {
8399
document.body.dataset.installLocked = 'true';
84100
}
@@ -89,6 +105,9 @@
89105
try {
90106
sessionStorage.removeItem(INSTALL_LOCK_KEY);
91107
} catch (error) {}
108+
try {
109+
localStorage.removeItem(INSTALL_LOCK_LOCAL_KEY);
110+
} catch (error) {}
92111
if (document.body) {
93112
delete document.body.dataset.installLocked;
94113
}
@@ -121,22 +140,31 @@
121140

122141
const getStoredInstallId = () => {
123142
try {
124-
return sessionStorage.getItem(INSTALL_ID_KEY);
125-
} catch (error) {
126-
return null;
127-
}
143+
const val = sessionStorage.getItem(INSTALL_ID_KEY);
144+
if (val) return val;
145+
} catch (error) {}
146+
try {
147+
return localStorage.getItem(INSTALL_ID_LOCAL_KEY);
148+
} catch (error) {}
149+
return null;
128150
};
129151

130152
const storeInstallId = (installId) => {
131153
try {
132154
sessionStorage.setItem(INSTALL_ID_KEY, installId);
133155
} catch (error) {}
156+
try {
157+
localStorage.setItem(INSTALL_ID_LOCAL_KEY, installId);
158+
} catch (error) {}
134159
};
135160

136161
const clearInstallId = () => {
137162
try {
138163
sessionStorage.removeItem(INSTALL_ID_KEY);
139164
} catch (error) {}
165+
try {
166+
localStorage.removeItem(INSTALL_ID_LOCAL_KEY);
167+
} catch (error) {}
140168
};
141169

142170
window.InstallerStepsState = {

app/views/install/installer/js/modules/ui.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,9 @@
240240
if (key === 'database') {
241241
value = toDatabaseLabel(formState?.database);
242242
}
243+
if (key === 'emailCertificates' && !value) {
244+
value = formState?.accountEmail;
245+
}
243246
if (value) {
244247
node.textContent = value;
245248
}

app/views/install/installer/js/steps.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -390,10 +390,7 @@
390390
if (!parsePort(httpPort, 'HTTP')) valid = false;
391391
if (!parsePort(httpsPort, 'HTTPS')) valid = false;
392392

393-
if (!sslEmail || !sslEmail.value.trim()) {
394-
setFieldError?.(sslEmail, 'Please enter an email address for SSL certificates');
395-
valid = false;
396-
} else if (!isValidEmail?.(sslEmail.value.trim())) {
393+
if (sslEmail && sslEmail.value.trim() && !isValidEmail?.(sslEmail.value.trim())) {
397394
setFieldError?.(sslEmail, 'Please enter a valid email address');
398395
valid = false;
399396
}

0 commit comments

Comments
 (0)