|
111 | 111 | return normalized.summary || 'Installation failed.'; |
112 | 112 | } |
113 | 113 | if (status === STATUS.COMPLETED) return step.done; |
114 | | - return step.inProgress; |
| 114 | + return message || step.inProgress; |
115 | 115 | }; |
116 | 116 |
|
117 | | - const updateInstallRow = (row, step, status, message) => { |
| 117 | + const updateInstallRow = (row, step, status, message, details) => { |
118 | 118 | if (!row || !step) return; |
119 | 119 | row.dataset.status = status; |
120 | 120 | row.dataset.step = step.id; |
|
138 | 138 | } |
139 | 139 | } |
140 | 140 |
|
| 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 | + |
141 | 150 | // Show/hide "Navigate to Console" button for account setup errors |
142 | 151 | const consoleBtn = row.querySelector('[data-install-console]'); |
143 | 152 | if (consoleBtn) { |
|
349 | 358 | const normalizedDomain = (formState?.appDomain || '').trim() || 'localhost'; |
350 | 359 | const normalizedHttpPort = (formState?.httpPort || '').trim() || '80'; |
351 | 360 | const normalizedHttpsPort = (formState?.httpsPort || '').trim() || '443'; |
352 | | - const normalizedEmail = (formState?.emailCertificates || '').trim(); |
| 361 | + const normalizedEmail = (formState?.emailCertificates || '').trim() || (formState?.accountEmail || '').trim(); |
353 | 362 | const normalizedAssistantKey = (formState?.assistantOpenAIKey || '').trim(); |
354 | 363 | const normalizedAccountEmail = (formState?.accountEmail || '').trim(); |
355 | 364 | const normalizedAccountPassword = (formState?.accountPassword || '').trim(); |
|
529 | 538 | if (!state) return; |
530 | 539 | const row = ensureRow(step); |
531 | 540 | 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); |
533 | 542 | if (state.status === STATUS?.ERROR) { |
534 | 543 | updateInstallErrorDetails(row, { |
535 | 544 | message: state.message, |
|
579 | 588 | } |
580 | 589 | } |
581 | 590 | } |
| 591 | + if (payload.status === STATUS.ERROR) { |
| 592 | + showGlobalActions(); |
| 593 | + } |
582 | 594 | scheduleFallback(); |
583 | 595 | }; |
584 | 596 |
|
|
616 | 628 |
|
617 | 629 | const applySnapshot = (snapshot) => { |
618 | 630 | if (!snapshot || !snapshot.steps) return; |
| 631 | + let hasErrors = false; |
619 | 632 | INSTALLATION_STEPS.forEach((step) => { |
620 | 633 | const detail = snapshot.steps[step.id]; |
621 | 634 | if (!detail) return; |
|
624 | 637 | message: detail.message, |
625 | 638 | details: snapshot.details?.[step.id] |
626 | 639 | }); |
| 640 | + if (detail.status === STATUS.ERROR) { |
| 641 | + hasErrors = true; |
| 642 | + } |
627 | 643 | }); |
628 | 644 | renderProgress(); |
| 645 | + if (hasErrors) { |
| 646 | + showGlobalActions(); |
| 647 | + } |
629 | 648 | }; |
630 | 649 |
|
631 | 650 | const checkAllCompleted = () => { |
|
674 | 693 | } |
675 | 694 | stopSyncedSpinnerRotation(); |
676 | 695 | setUnloadGuard(false); |
| 696 | + clearInstallLock?.(); |
677 | 697 | }; |
678 | 698 |
|
679 | 699 | const SSL_STEP = { |
|
890 | 910 | } |
891 | 911 | }; |
892 | 912 |
|
| 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 | + |
893 | 926 | const resumeInstall = async (installId) => { |
894 | 927 | const snapshot = await fetchInstallStatus(installId); |
895 | | - if (!snapshot) return false; |
| 928 | + if (!snapshot || isSnapshotTerminal(snapshot)) return false; |
896 | 929 | activeInstall = { |
897 | 930 | installId, |
898 | 931 | controller: new AbortController(), |
|
966 | 999 | } |
967 | 1000 | }); |
968 | 1001 |
|
| 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 | + |
969 | 1056 | // When the user switches back to this tab, check if installation |
970 | 1057 | // finished while the tab was in the background. |
971 | 1058 | document.addEventListener('visibilitychange', () => { |
|
974 | 1061 | } |
975 | 1062 | }); |
976 | 1063 |
|
| 1064 | + const startFreshInstall = () => { |
| 1065 | + clearInstallId?.(); |
| 1066 | + clearInstallLock?.(); |
| 1067 | + const newInstallId = generateInstallId(); |
| 1068 | + storeInstallId?.(newInstallId); |
| 1069 | + startInstallStream(newInstallId); |
| 1070 | + }; |
| 1071 | + |
977 | 1072 | const lock = getInstallLock?.(); |
978 | 1073 | const existingInstallId = lock?.installId || getStoredInstallId?.(); |
979 | 1074 | if (existingInstallId) { |
980 | 1075 | resumeInstall(existingInstallId).then((resumed) => { |
981 | 1076 | if (!resumed) { |
982 | | - clearInstallId?.(); |
983 | | - clearInstallLock?.(); |
984 | | - const newInstallId = generateInstallId(); |
985 | | - storeInstallId?.(newInstallId); |
986 | | - startInstallStream(newInstallId); |
| 1077 | + startFreshInstall(); |
987 | 1078 | } |
988 | 1079 | }); |
989 | 1080 | } else { |
990 | | - const newInstallId = generateInstallId(); |
991 | | - storeInstallId?.(newInstallId); |
992 | | - startInstallStream(newInstallId); |
| 1081 | + startFreshInstall(); |
993 | 1082 | } |
994 | 1083 | }; |
995 | 1084 |
|
|
0 commit comments