Implement transactional setup with explicit state machine and crash-safety
to prevent partial commits from leaving inconsistent state.
## Changes
### Core Implementation
1. **settings_repo.py**: Add atomic batch settings write
- New set_settings_batch() method: writes multiple settings in single
transaction (BEGIN IMMEDIATE ... COMMIT). Either all settings persist
or none do, preventing partial state if crash occurs mid-batch.
2. **setup_service.py**: Refactor run_setup() with transactional phases
- Phase 0: Compute password hash early (before any DB writes) to ensure
idempotency. Same hash is used throughout retries, preventing divergent
hashes from bcrypt's random salt.
- Phase 1 (Bootstrap DB transaction): Set setup_state=in_progress and
database_path, then commit. First checkpoint for crash detection.
- Phase 2 (Filesystem): Initialize runtime database (idempotent)
- Phase 3 (Runtime DB transaction): Batch-write all settings atomically
- Phase 4 (Bootstrap DB transaction): Set setup_state=complete and
setup_completed=1. Final commit point.
3. **protocols.py**: Add set_settings_batch to SettingsRepository protocol
### Testing
- Added 6 new transactionality tests covering:
- State machine transitions (None → in_progress → complete)
- Password hash idempotency across retries
- Atomic batch writes (all-or-nothing persistence)
- Bootstrap DB state tracking
- Database path propagation to both DBs
- Recovery on partial failure
- All 18 tests pass (12 existing + 6 new)
### Documentation
- Updated Docs/Architekture.md with new section 6:
- Setup state machine with state transitions
- Transaction boundary documentation
- Password hash idempotency rationale
- Backward compatibility notes
## Design Decisions
### Why This Approach
- Current code already idempotent via INSERT OR REPLACE, but password
hash non-idempotency created silent inconsistency risk
- Simpler than multi-state machine: 2 states sufficient for detection
- Maintains backward compatibility (setup_completed key still written)
- Explicit transactions make crash-safety obvious to future maintainers
### Crash Scenarios Now Handled
1. Crash after Phase 1 → detected by setup_state=in_progress on retry
2. Crash after Phase 2 → runtime DB may be partial, safe to retry
3. Crash after Phase 3 → runtime DB rolls back on next connection
4. Crash after Phase 4 → setup_completed detected, skipped
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The blocklist import service targets a dedicated jail called
'blocklist-import' (BLOCKLIST_JAIL constant in blocklist_service.py),
but that jail was never defined in the dev fail2ban configuration.
Every import attempt immediately failed with UnknownJailException.
Add Docker/fail2ban-dev-config/fail2ban/jail.d/blocklist-import.conf:
a manual-ban jail with no log-based detection that accepts banip
commands only, using iptables-allports with a 1-week bantime.
Also track the new file in .gitignore (whitelist) and fix a
pre-existing blank-line-with-whitespace lint error in setup_service.py.
- Add SetupGuard component: redirects to /setup if setup not complete,
shown as spinner while loading. All routes except /setup now wrapped.
- SetupPage redirects to /login on mount when setup already done.
- Fix async blocking: offload bcrypt.hashpw and bcrypt.checkpw to
run_in_executor so they never stall the asyncio event loop.
- Hash password with SHA-256 (SubtleCrypto) before transmission; added
src/utils/crypto.ts with sha256Hex(). Backend stores bcrypt(sha256).
- Add Makefile with make up/down/restart/logs/clean targets.
- Add tests: _check_password async, concurrent bcrypt, expired session,
login-without-setup, run_setup event-loop interleaving.
- Update Architekture.md and Features.md to reflect all changes.