Matches the 1.0.0-38 product display rename. Touches every
infrastructure identifier:
- container_name: truenas-burnin → nas-burnin
- forge URL in /api/v1/updates/check
- security-scan: REPO_URL, REPO, DEPLOY_DIR, systemd unit description
- run-tests.sh default container name
- doc paths in README/SPEC/CLAUDE
- in-app instruction strings (login.html, settings.html, auth_cli.py)
Maple migration done in lockstep:
docker compose down (truenas-burnin)
mv ~/docker/stacks/{truenas-burnin,nas-burnin}
systemd unit ExecStart updated + daemon-reload
docker compose up -d --build → container nas-burnin
Old image truenas-burnin-app removed (~12 GB reclaimed)
Stale top-level orphans cleaned (config.py, poller.py, routes.py,
truenas.py, tests/) — all dead since pre-split refactors
Forge repo rename (git.hellocomputer.xyz/brandon/truenas-burnin →
nas-burnin) is a separate UI-only step. Forgejo redirects the old
URL after rename, so this commit can be pushed to the existing
remote first; remote URL gets updated locally once you rename.
Closes the four remaining items from the post-Codex hardening list.
#1 Rate-limit unlock + change-password endpoints (1.0.0-33)
* Generalised the existing login limiter into a reusable
`_RateLimiter` class in app/auth.py. Atomic check-then-increment
in synchronous code so a parallel asyncio burst can't slip past
the threshold.
* `unlock_limiter` (5 attempts in 10 min → 10 min lockout) gates
POST /api/v1/drives/{id}/unlock per-drive AND per-source-IP.
* `pwchange_limiter` (5 in 10 min → 15 min lockout) gates
POST /api/v1/auth/change-password per-user AND per-IP.
* Both clear on successful operation. The login limiter keeps its
existing `register_login_attempt` / `clear_login_failures`
facade names so external callers don't change.
#3 mypy in security-scan (1.0.0-33)
* Added a 4th tool to the daily scan + forge workflow. Runs in a
throwaway python:3.12-slim container against the deploy dir,
exit code is informational only (NOT included in the
`TOTAL_EXIT` failure sum). Findings land in
~/security-scans/scan-YYYY-MM-DD/mypy.txt for ratchet-down
work over time.
* Forge job uses `continue-on-error: true` so it doesn't fail the
workflow until the type-debt baseline is annotated down.
#4 Lifecycle test coverage (1.0.0-33)
* New tests/test_lifecycle.py with 15 cases:
- TestCommonHelpers (7 tests): _start_stage, _finish_stage
success/failure/error-preservation, _recalculate_progress
weighted math, _is_cancelled, _append_stage_log.
- TestStartCancelJob (4 tests): start_job inserts queued row +
correct stage list, duplicate-active rejection, cancel marks
state, cancel returns False on terminal-state jobs.
- TestRateLimiter (4 tests): under-threshold ok, trips at
threshold, clear removes both counter + lockout, separate
keys don't interfere.
* Total goes from 44 to 59 tests; closes the orchestration-path
coverage gap Codex flagged.
#2 Partial routes.py split (1.0.0-34)
* routes.py → routes/ package. Same staged-extraction pattern as
the burnin.py split.
* routes/auth.py — login/logout/setup/change-password (170 LoC).
* routes/system.py — /health, /ws/terminal, /api/v1/updates/check
(136 LoC).
* routes/_helpers.py — shared utilities used by both extracted
modules and the still-monolithic remainder: client_ip,
operator_for, is_stale, stale_context, secret_status,
SECRET_FIELDS (97 LoC).
* routes/__init__.py shrank from 1568 LoC to 1261. Future slices
can extract drives, burnin, history, settings the same way.
* GOTCHA recorded in commit body: `from app import auth` at the
top of __init__.py binds `auth` as an attribute on the package
namespace, so `from . import auth as _auth_routes` finds the
OUTER module and yields `app.auth` instead of the submodule.
Fix is `import app.routes.auth as _auth_routes` (absolute).
This bit me once at deploy time; container failed to start
with `module 'app.auth' has no attribute 'router'`.
Verification: 59/59 tests pass (44 existing + 15 new); container
boots clean at 1.0.0-34; /health 200 with all checks green; security
scan still clean (mypy informational findings ignored from totals).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>