This commit is contained in:
@@ -166,6 +166,10 @@ pytest tests/
|
|||||||
107 tests covering config loading, display processing (crop, resize, rotation),
|
107 tests covering config loading, display processing (crop, resize, rotation),
|
||||||
trigger atomicity, MQTT parsing, and all 19 API routes.
|
trigger atomicity, MQTT parsing, and all 19 API routes.
|
||||||
|
|
||||||
|
A [Gitea Actions](.gitea/workflows/test.yml) workflow runs tests on every push.
|
||||||
|
|
||||||
|
See [bug-tracking.md](bug-tracking.md) for known issues and recent fixes.
|
||||||
|
|
||||||
## Project structure
|
## Project structure
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -189,6 +193,9 @@ spectra/
|
|||||||
│ ├── models.py # SQLAlchemy models
|
│ ├── models.py # SQLAlchemy models
|
||||||
│ ├── templates/ # 6 Jinja2 pages
|
│ ├── templates/ # 6 Jinja2 pages
|
||||||
│ └── static/ # CSS + JS
|
│ └── static/ # CSS + JS
|
||||||
|
├── .gitea/
|
||||||
|
│ └── workflows/
|
||||||
|
│ └── test.yml # CI workflow
|
||||||
├── tests/
|
├── tests/
|
||||||
│ ├── test_api.py
|
│ ├── test_api.py
|
||||||
│ ├── test_config.py
|
│ ├── test_config.py
|
||||||
|
|||||||
153
bug-tracking.md
153
bug-tracking.md
@@ -1,148 +1,15 @@
|
|||||||
# Bug Tracking
|
# Bug Tracking
|
||||||
|
|
||||||
## Status Legend
|
No open bugs. All known issues have been fixed or accepted.
|
||||||
- **Fixed** — resolved and verified
|
|
||||||
- **Open** — not yet addressed
|
## Wontfix / Design Notes
|
||||||
- **Wontfix** — accepted design/known limitation
|
|
||||||
|
### `_assert_safe_path` TOCTOU race
|
||||||
|
`spectra/web/server.py` — Path check and `send_file()` are not atomic. An attacker needs write access to the uploads dir to exploit. Mitigation would add complexity with negligible security gain.
|
||||||
|
|
||||||
|
### No cascade delete on Rotation FK
|
||||||
|
`spectra/web/models.py:31` — `Rotation.image_id` has no `ondelete="CASCADE"`. The single delete route handles cleanup explicitly. Add cascade if new deletion paths are added.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Critical
|
For a full history of fixes, see `git log`.
|
||||||
|
|
||||||
### 1. ~~All settings forms broken (send URL-encoded, API expected JSON)~~
|
|
||||||
|
|
||||||
**Fixed:** Every htmx form on the settings page (saturation, orientation, MQTT, schedule, Unsplash) sent URL-encoded form data (`application/x-www-form-urlencoded`) but the `/api/config/<section>/<key>` PUT endpoint used `request.get_json(force=True, silent=True)`. URL-encoded data is not valid JSON, so `get_json` returned `None` → the endpoint aborted with 400. Result: **every single settings form returned 400 in the browser**. The MQTT checkbox bug (old #1) was a symptom of this larger issue.
|
|
||||||
|
|
||||||
**Fix** (`spectra/web/server.py`): `api_config_set()` now tries JSON first, then falls back to `request.form.get("value")`. A `_coerce_form_value(raw, current)` helper infers the correct Python type (bool/int/float/str) from the form string by comparing against the current config value's type.
|
|
||||||
|
|
||||||
### 2. ~~API config tests wrote to real `~/.config/spectra/config.yaml`~~
|
|
||||||
|
|
||||||
**Fixed:** `ConfigManager._write()` resolved the config path by searching `CONFIG_SEARCH_PATHS`, and when no file existed (test environment), fell back to `~/.config/spectra/config.yaml`. Every test that called `PATCH /api/config` or `PUT /api/config/<section>/<key>` wrote test data into the real user's config file — **potential data corruption**.
|
|
||||||
|
|
||||||
**Fix** (`tests/conftest.py`): The `app` fixture now writes a minimal `config.yaml` into the temp directory and passes its path to `create_app(config_path=...)`. The ConfigManager writes back to the same temp file.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## High
|
|
||||||
|
|
||||||
### 3. `_lookup_image_path` crashes if DB/table doesn't exist
|
|
||||||
|
|
||||||
**File:** `spectra/mqtt.py:19–25`
|
|
||||||
|
|
||||||
**Problem:** When the web server hasn't created the database yet (or the `image` table is missing), `_lookup_image_path` raises `sqlite3.OperationalError: no such table: image`. The MQTT message handler catches this in paho, but the error is unhandled at the application level.
|
|
||||||
|
|
||||||
**Fixed:** Wrapped the `conn.execute()` call in `try/except sqlite3.OperationalError`, logging a warning and returning `None`.
|
|
||||||
|
|
||||||
### 4. `_MIME_MAP` dict redefined on every file download request
|
|
||||||
|
|
||||||
**File:** `spectra/web/server.py:240`
|
|
||||||
|
|
||||||
**Problem:** The `api_image_file()` route handler defined a `_MIME_MAP` dict inside the function body on every request. This is a minor performance issue — dictionary creation is cheap, but the dict is constant data that should be a module-level constant.
|
|
||||||
|
|
||||||
**Fixed:** Moved `_MIME_MAP` to module level.
|
|
||||||
|
|
||||||
### 5. Dead link `/docs/mqtt.md` in settings page
|
|
||||||
|
|
||||||
**File:** `spectra/web/templates/settings.html:138`
|
|
||||||
|
|
||||||
**Problem:** The settings template has `<a href="/docs/mqtt.md">MQTT docs</a>`. No Flask route serves this path, so browsers get a 404.
|
|
||||||
|
|
||||||
**Fixed:** Replaced with text: `<code>docs/mqtt.md</code>` (no link).
|
|
||||||
|
|
||||||
### 6. `_generate_thumbnail` leaks file descriptor
|
|
||||||
|
|
||||||
**File:** `spectra/web/server.py:400–406`
|
|
||||||
|
|
||||||
**Problem:** `PILImage.open(src)` without a context manager leaves the file handle open until garbage collection. On long-running servers serving many thumbnails, this can exhaust file descriptors.
|
|
||||||
|
|
||||||
**Fixed:** Changed to `with PILImage.open(src) as img:`.
|
|
||||||
|
|
||||||
### 7. Dead code: `TRIGGER_PATH` and `_trigger_path()`
|
|
||||||
|
|
||||||
**File:** `spectra/web/server.py:69–70`
|
|
||||||
|
|
||||||
**Problem:** `app.config["TRIGGER_PATH"]` was set but never read by any code. The `_trigger_path()` function was only used for that config key.
|
|
||||||
|
|
||||||
**Fixed:** Removed both the config key and the function.
|
|
||||||
|
|
||||||
### 8. Redundant `section_data` variable in `api_config_set`
|
|
||||||
|
|
||||||
**File:** `spectra/web/server.py:122–124`
|
|
||||||
|
|
||||||
**Problem:** `section_data = cm.config.get(section, {})` was assigned but only used for `isinstance(section_data, dict)` check. The old redundant mutation `section_data[key] = data["value"]` was already removed in a previous fix, leaving the variable as dead code.
|
|
||||||
|
|
||||||
**Fixed:** Removed the `section_data` check entirely — `cm.set_nested()` handles missing sections.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Medium
|
|
||||||
|
|
||||||
### 9. `api_display_status` always reports `simulate: false`
|
|
||||||
|
|
||||||
**File:** `spectra/web/server.py:148`
|
|
||||||
|
|
||||||
**Problem:** `cm.get("display", "simulate")` reads a config key that doesn't exist (`display.simulate` is set via CLI `--simulate`, not in the config file). The field always reports `false` even when the display loop runs with `--simulate`.
|
|
||||||
|
|
||||||
**Fixed:** Removed the `simulate` field from the API response and the dashboard template. The web server and CLI are separate processes; the web server cannot know the display loop's `--simulate` state.
|
|
||||||
|
|
||||||
### 10. `_assert_safe_path` TOCTOU race
|
|
||||||
|
|
||||||
**File:** `spectra/web/server.py:319–323`
|
|
||||||
|
|
||||||
**Problem:** The path safety check (`real_file.startswith(real_store + os.sep)`) and the subsequent `send_file()` are not atomic. A symlink could be swapped in between the check and the file read. An attacker needs write access to the uploads directory to exploit this.
|
|
||||||
|
|
||||||
**Status:** **Wontfix** — requires filesystem write access to exploit; mitigation would add complexity with minimal security gain.
|
|
||||||
|
|
||||||
### 11. No cascade delete on Rotation FK
|
|
||||||
|
|
||||||
**File:** `spectra/web/models.py:31`
|
|
||||||
|
|
||||||
**Problem:** `Rotation.image_id` has a foreign key to `image.id` but no `ondelete="CASCADE"`. Image deletion manually cleans up rotation entries (`Rotation.query.filter_by(image_id=...).delete()`). If a new deletion path is added without this cleanup, the FK constraint raises an error.
|
|
||||||
|
|
||||||
**Status:** **Wontfix** — explicit cleanup in the single delete route is sufficient for current code. Add cascade if new deletion paths are added.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Low
|
|
||||||
|
|
||||||
### 12. Object URLs never revoked in upload page
|
|
||||||
|
|
||||||
**File:** `spectra/web/templates/upload.html:156`
|
|
||||||
|
|
||||||
**Problem:** `URL.createObjectURL(file)` creates blob URLs for file previews but never releases them with `URL.revokeObjectURL()`. For typical sessions (a few files) the memory impact is negligible.
|
|
||||||
|
|
||||||
**Status:** **Wontfix** — mitigated by the upload page: when a file's upload completes or fails, the `updateFileStatus` function checks `badgeClass === 'badge-ok' || badgeClass === 'badge-err'` and calls `URL.revokeObjectURL(blobUrl)` (lines 180–183). Blob URLs are already cleaned up per-file.
|
|
||||||
|
|
||||||
### 13. Unsplash filename collision on duplicate fetch
|
|
||||||
|
|
||||||
**File:** `spectra/library.py:56`
|
|
||||||
|
|
||||||
**Problem:** Original filename `f"unsplash_{unsplash_id}.png"` could collide if the same photo is fetched twice. **Already fixed** in previous session: filename now uses `f"unsplash_{unsplash_id}_{uuid.uuid4().hex[:8]}.png"` and a dedup check on `unsplash_id` prevents duplicate DB records.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Previously Fixed
|
|
||||||
|
|
||||||
### Old #1 — MQTT checkbox can never be unchecked
|
|
||||||
**Fixed:** Hidden `<input type="hidden" name="value" value="false">` added before the checkbox. Resolved as a side-effect of Bug #1 (form data now properly accepted with type coercion).
|
|
||||||
|
|
||||||
### Old #2 — Gallery delete shows "Saved"
|
|
||||||
**Fixed:** Delete button uses `hx-target="closest article" hx-swap="delete"` with `hx-confirm`. The card is removed from the DOM directly.
|
|
||||||
|
|
||||||
### Old #3 — `MqttClient.reconfigure()` calls `__init__` directly
|
|
||||||
**Fixed:** Extracted `_setup()` and `_create_client()` methods from `__init__`, called by both `__init__` and `reconfigure()`.
|
|
||||||
|
|
||||||
### Old #4 — Unsplash filename collision
|
|
||||||
**Fixed:** See #13 above.
|
|
||||||
|
|
||||||
### Old #5 — Object URLs never revoked
|
|
||||||
**Fixed:** See #12 above — already handled in per-file cleanup.
|
|
||||||
|
|
||||||
### Old #6 — Dead CSS `#upload-result:empty`
|
|
||||||
**Fixed:** Selector removed from `app.css`.
|
|
||||||
|
|
||||||
### Old #8 — `api_config_set` double-mutates config
|
|
||||||
**Fixed:** Redundant `section_data[key] = data["value"]` mutation removed.
|
|
||||||
|
|
||||||
### Old #9 — Gallery delete test missing
|
|
||||||
**Fixed:** `tests/test_gallery.py` added with upload-delete flow E2E test.
|
|
||||||
|
|||||||
@@ -23,10 +23,10 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
Raspberry Pi
|
Raspberry Pi
|
||||||
┌────────────────────────────────────────────────────────────┐
|
┌───────────────────────────────────────────────────────────┐
|
||||||
│ │
|
│ │
|
||||||
│ ┌──────────┐ ┌──────────────┐ ┌─────────────────┐ │
|
│ ┌──────────┐ ┌──────────────┐ ┌─────────────────┐ │
|
||||||
│ │ Browser │◄──►│ Flask App │───►│ Display Loop │ │
|
│ │ Browser │◄──►│ Flask App │───►│ Display Loop │ │
|
||||||
│ │ (GUI) │ │ (port 5000) │ │ (cli.py) │ │
|
│ │ (GUI) │ │ (port 5000) │ │ (cli.py) │ │
|
||||||
│ └──────────┘ │ │ │ │ │
|
│ └──────────┘ │ │ │ │ │
|
||||||
│ │ /api/* │ │ ┌───────────┐ │ │
|
│ │ /api/* │ │ ┌───────────┐ │ │
|
||||||
@@ -36,15 +36,15 @@
|
|||||||
│ └──────┬───────┘ └────────┬────────┘ │
|
│ └──────┬───────┘ └────────┬────────┘ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ ┌────▼─────┐ ┌─────▼──────┐ │
|
│ ┌────▼─────┐ ┌─────▼──────┐ │
|
||||||
│ │ SQLite │ │ trigger │ │
|
│ │ SQLite │ │ trigger │ │
|
||||||
│ │ web.db │ │ .json │ │
|
│ │ web.db │ │ .json │ │
|
||||||
│ └──────────┘ └────────────┘ │
|
│ └──────────┘ └────────────┘ │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ ┌────▼─────┐ │
|
│ ┌────▼─────┐ │
|
||||||
│ │ uploads/ │ │
|
│ │ uploads/ │ │
|
||||||
│ │ (images) │ │
|
│ │ (images) │ │
|
||||||
│ └──────────┘ │
|
│ └──────────┘ │
|
||||||
└────────────────────────────────────────────────────────────┘
|
└───────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key decisions
|
### Key decisions
|
||||||
|
|||||||
Reference in New Issue
Block a user