Skip to content
Open
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
39 changes: 39 additions & 0 deletions static/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1677,6 +1677,45 @@ pre code {
font-size: var(--text-md);
}

.setup-complete-btn {
min-width: 12.5rem;
}

.setup-complete-ready,
.setup-complete-pending {
align-items: center;
gap: var(--sp-2);
}

.setup-complete-ready {
display: inline-flex;
}

.setup-complete-pending {
display: none;
}

.setup-complete-btn.is-async-pending .setup-complete-ready {
display: none;
}

.setup-complete-btn.is-async-pending .setup-complete-pending {
display: inline-flex;
}

.setup-complete-spinner {
width: 1rem;
height: 1rem;
border: 2px solid hsla(0, 0%, 100%, 0.35);
border-top-color: currentColor;
border-radius: 50%;
animation: setup-complete-spin 0.8s linear infinite;
}

@keyframes setup-complete-spin {
to { transform: rotate(360deg); }
}

.setup-nav-note {
margin-top: var(--sp-4);
display: inline-flex;
Expand Down
22 changes: 17 additions & 5 deletions templates/setup.html
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,12 @@ <h3>Schedule & Bandwidth</h3>
<div class="invalid-feedback">Bandwidth limit must be a valid format (e.g. 4M, 1G, 500k).</div>
</div>
<div class="setup-actions">
<button onclick="saveSchedule()" class="btn btn-primary btn-lg">
<i class="fas fa-check me-1"></i> Complete Setup
<button id="setupCompleteBtn" type="button" onclick="saveSchedule()" class="btn btn-primary btn-lg setup-complete-btn">
<span class="setup-complete-ready"><i class="fas fa-check me-1"></i> Complete Setup</span>
<span class="setup-complete-pending">
<span class="setup-complete-spinner"></span>
Completing Setup
</span>
</button>
</div>
</div>
Expand Down Expand Up @@ -412,6 +416,7 @@ <h5 id="confirmationModalTitle">Confirm Action</h5>
<script>
let currentStep = 1;
const totalSteps = 6;
let setupCompletionPending = false;

// --- LocalStorage step persistence ---
function saveStepToStorage(step) {
Expand Down Expand Up @@ -721,21 +726,28 @@ <h5 id="confirmationModalTitle">Confirm Action</h5>

// --- Schedule ---
async function saveSchedule() {
if (setupCompletionPending) return;
hideError();
const valid = [validateBackupTime(), validateBandwidthLimit()];
if (valid.includes(false)) { showError('Please correct the highlighted fields.'); return; }
const time = document.getElementById('backupTime').value;
const bandwidthLimit = document.getElementById('bandwidthLimit').value;
if (!time) { showError('Please select a backup time.'); return; }
const saveBtn = document.querySelector('button[onclick="saveSchedule()"]');
const saveBtn = document.getElementById('setupCompleteBtn');
setupCompletionPending = true;
window.AsyncButtonState.start(saveBtn);
try {
await window.ApiClient.fetchJson('/api/setup/schedule', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ time, bandwidth_limit: bandwidthLimit }) });
hideError();
const completed = await completeSetup();
if (completed) window.AsyncButtonState.success(saveBtn);
else window.AsyncButtonState.error(saveBtn);
} catch (error) { window.AsyncButtonState.error(saveBtn); showError('Error saving schedule: ' + (error.message || error)); }
// Successful completion redirects away, so only clear this lock when the page stays here.
else { setupCompletionPending = false; window.AsyncButtonState.error(saveBtn); }
} catch (error) {
setupCompletionPending = false;
window.AsyncButtonState.error(saveBtn);
showError('Error saving schedule: ' + (error.message || error));
}
}

// --- Complete ---
Expand Down
21 changes: 21 additions & 0 deletions tests/test_app_factory_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,27 @@ def test_setup_title_keeps_product_name_before_server_name_is_chosen():
runtime._fake_state = previous_fake_state


def test_setup_complete_button_renders_busy_state_markup():
previous_runtime = runtime._runtime
previous_fake_state = runtime._fake_state
try:
with TemporaryDirectory() as temp_dir:
app = _create_fake_app(temp_dir)

with app.test_client() as client:
response = client.get("/setup")

assert response.status_code == 200
page = response.get_data(as_text=True)
assert 'id="setupCompleteBtn"' in page
assert "setup-complete-spinner" in page
assert "Completing Setup" in page
assert "setupCompletionPending" in page
finally:
runtime._runtime = previous_runtime
runtime._fake_state = previous_fake_state


def test_smb_share_list_returns_validation_error_for_malformed_sss_shares_file():
previous_runtime = runtime._runtime
previous_fake_state = runtime._fake_state
Expand Down
Loading