Setup wizard internals¶
The setup wizard is implemented across three modules that together answer one question on every request: has this user finished setting up the application?
Module responsibilities¶
| Module | Responsibility |
|---|---|
setup_state.py |
Derives readiness from disk + session; exposes get_setup_status() and is_setup_complete() |
setup_guard.py |
setup_complete_required decorator — redirects to /setup/ when incomplete |
routes/setup_wizard.py |
Flask blueprint — four step pages + POST /setup/complete |
setup_state.py¶
The four checks¶
Each check is evaluated freshly on every call to get_setup_status(). Checks 1, 2, and 4 read the filesystem; check 3 reads the Flask session.
1. config¶
_check_config(app) returns True when:
CONFIG_PATHexists, and- it parses as valid YAML, and
- all five required keys are present:
dir_backup_local,dir_backup_remote,email_sender,email_report,tasks.
The same structural check is used by the config editor route before saving — the web UI and the setup state are always in agreement.
2. ssh_key¶
_check_ssh_key(app) returns True when at least one *.pub file in SSH_KEY_DIR has a matching private key file alongside it.
Auto-satisfied when has_remote_sources is False (all task sources are local paths).
3. connection¶
_connection_tested() returns True when session["setup_connection_tested"] is truthy.
This flag is written by mark_connection_tested(), which is called from connections.test_ssh and connections.test_email after any successful test.
Auto-satisfied when has_remote_sources is False.
4. schedule¶
_check_schedule(app) returns True when DATA_ROOT/schedule.json exists and its "enabled" field is truthy.
has_remote_sources¶
_has_remote_sources(app) scans the tasks list in the saved config and returns True if any dir_source contains @ (i.e. user@host:/path form). This drives the skip logic for steps 2 and 3.
Completion logic¶
complete = (config_ok and ssh_ok and connection_ok and schedule_ok) \
or session.get("setup_explicitly_complete")
Two independent paths reach complete = True:
- Automatic — all four checks pass simultaneously (re-evaluated on every request).
- Explicit — the user clicked Finish setup anyway, which sets
session["setup_explicitly_complete"]viamark_setup_complete().
Session scope¶
Because completion state lives in the browser session, it resets when:
- The server restarts (Flask sessions are signed with
SECRET_KEYbut not server-side — losing the key invalidates all sessions). - The user logs out (
session.clear()). - The cookie expires.
After a reset, is_setup_complete() re-evaluates the filesystem checks. If config, SSH key, and schedule are all still on disk, setup is automatically complete again — the only check that requires re-doing is the connection test (which has no on-disk representation).
setup_guard.py¶
The decorator calls is_setup_complete(app) and redirects to url_for("setup.index") if it returns False. It is applied after @login_required in the decorator stack, so the evaluation order is:
Unauthenticated users hit the login wall before the setup check — they never see the wizard URL directly.
Which routes are gated¶
| Route | Gated |
|---|---|
dashboard.index, dashboard.history |
Yes |
progress.index, progress.state_json, progress.stream |
Yes |
schedule.index, schedule.run_now, schedule.status |
Yes |
schedule.save, schedule.remove |
No — wizard step 4 uses these |
config_editor.*, connections.*, ssh_keys.* |
No — used during wizard |
setup.* |
No — would cause a redirect loop |
routes/setup_wizard.py¶
Blueprint¶
Registered at /setup. All routes require @login_required but are explicitly excluded from setup_complete_required.
Step routing¶
/setup/ (index) reads get_setup_status(), builds the list of visible steps via _visible_steps(has_remote), finds the first incomplete one, and redirects there. If setup is already complete it redirects to the dashboard.
Steps 2 and 3 (/setup/ssh-keys and /setup/connections) redirect themselves to /setup/schedule when has_remote_sources is False — they never render.
?next= parameter¶
Wizard step templates post forms to the existing route handlers for config, SSH keys, and schedule, not to new wizard-specific endpoints. Each form action includes ?next=<current-wizard-step-url>:
<form method="post"
action="{{ url_for('config_editor.save') }}?next={{ url_for('setup.step_config') }}">
The underlying route handlers (config_editor.save, ssh_keys.generate, ssh_keys.delete, schedule.save, schedule.remove) honour this parameter:
This avoids duplicating the save/validate logic in the wizard and keeps the standalone pages fully functional at the same time.
Finish endpoint¶
POST /setup/complete calls mark_setup_complete() and redirects to the dashboard. It is the target of the Finish setup and Finish setup anyway buttons on the Schedule step.
Stepper state¶
Each step template extends wizard_base.html, which renders the horizontal stepper bar. The stepper receives:
| Variable | Type | Source |
|---|---|---|
wizard_steps |
list[dict] |
_visible_steps(has_remote_sources) |
wizard_current |
int |
_current_step_index(status, steps) |
setup_status |
dict |
get_setup_status(app) |
Steps before wizard_current render with a green check bubble; the active step renders with a blue outlined bubble; future steps render grey.
Adding a new setup step¶
- Add an entry to
WIZARD_STEPSinroutes/setup_wizard.pywith a uniqueid,label,icon, androute. - Add the corresponding check to
setup_state.py— a filesystem or session check that returns abool. - Include the new
idin thecompletecalculation inget_setup_status(). - Create the route function and template (extending
wizard_base.html, filling{% block wizard_content %}). - If the step can be skipped under certain conditions, add the skip logic inside the route function (redirect to the next step's URL).
- Add tests in
tests/test_setup_wizard.pycovering the new check and the HTTP behaviour.