chore: harden test isolation, gitignore stale src/data, mark shutdown action done
- ``tests/test_preferences_api.py`` no longer captures the auth API key at module-import time. The new ``client`` fixture resolves it inside its body and bakes the Bearer header into ``TestClient.headers``, so the e2e conftest swapping the global config singleton during collection cannot leave the test holding a stale 401-bound header. Same proven pattern as ``test_audio_processing_templates_api.py``. - ``.gitignore`` now anchors ``/server/src/data/`` defensively. If the server is launched from ``server/src/`` (uncommon but possible during ad-hoc debugging), its relative ``data/`` resolves there. Templates now live in SQLite (``capture_templates`` / ``pattern_templates`` / ``postprocessing_templates`` tables); any stale ``*.json`` that lands in that directory is a runtime export and must not be committed. - Three such stale exports were untracked at the start of the pre-merge audit and have been deleted from the working tree. - ``TODO.md`` flips the shutdown-action checklist to done and notes that real-hardware verification (WLED + serial after Ctrl+C) is still pending.
This commit is contained in:
@@ -68,6 +68,11 @@ logs/
|
||||
# shipped sound assets out of the CI tag checkout.
|
||||
/data/
|
||||
/server/data/
|
||||
# Defensive: if the server is launched from server/src/ (uncommon path),
|
||||
# its relative `data/` dir resolves to server/src/data/. Templates now
|
||||
# live in SQLite, so any *.json that lands here is stale runtime export
|
||||
# and must not be committed.
|
||||
/server/src/data/
|
||||
*.db
|
||||
*.sqlite
|
||||
*.json.bak
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# LedGrab TODO
|
||||
|
||||
## Server shutdown action
|
||||
|
||||
Let user choose what happens to LED targets on server shutdown.
|
||||
|
||||
- [x] Backend storage: `shutdown_action` in `db.settings` (`"stop_targets"` default | `"nothing"`)
|
||||
- [x] Backend route: `GET/PUT /api/v1/system/shutdown-action` in `system_settings.py`
|
||||
- [x] Backend schema: `ShutdownActionResponse/Request` in `schemas/system.py`
|
||||
- [x] Backend wiring: lifespan shutdown in `main.py` reads action, passes `restore_devices` flag to `processor_manager.stop_all()`
|
||||
- [x] `processor_manager.stop_all(restore_devices: bool = True)` — when False, calls public `proc.cancel_task()` (defined on `TargetProcessor`) which awaits cancellation without restoring device state; skips `_restore_device_idle_state` loop. No reach into private `_task` attribute.
|
||||
- [x] Frontend: hidden `<select>` + IconSelect in `settings.html` General tab (icons via `ICON_SQUARE` / `ICON_CIRCLE` from `core/icons.ts`)
|
||||
- [x] Frontend: load/save handlers in `features/settings.ts`, wired into `openSettingsModal()`
|
||||
- [x] i18n: en / ru / zh keys for label, hint, item descriptions
|
||||
- [ ] Real-hardware test pending — verify that "nothing" actually leaves a WLED + a serial device on the last frame after `Ctrl+C`/SIGTERM.
|
||||
|
||||
## WebUI Redesign — "Lumenworks" Studio-Console Aesthetic
|
||||
|
||||
Full-app UI/UX refresh. Design direction committed to by user 2026-04-24.
|
||||
|
||||
@@ -4,18 +4,25 @@ import pytest
|
||||
|
||||
from ledgrab.config import get_config
|
||||
|
||||
_config = get_config()
|
||||
_api_key = next(iter(_config.auth.api_keys.values()), "")
|
||||
AUTH_HEADERS = {"Authorization": f"Bearer {_api_key}"} if _api_key else {}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def client():
|
||||
"""TestClient with auth header read at fixture-build time.
|
||||
|
||||
The auth API key is resolved here (not at module import) so any
|
||||
config-singleton mutation that happens during pytest collection —
|
||||
notably ``server/tests/e2e/conftest.py`` reassigning the global
|
||||
config to a different test key during collection of e2e tests —
|
||||
cannot leave us holding a stale Bearer header that yields 401.
|
||||
"""
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from ledgrab.main import app
|
||||
|
||||
api_key = next(iter(get_config().auth.api_keys.values()), "")
|
||||
with TestClient(app, raise_server_exceptions=False) as c:
|
||||
if api_key:
|
||||
c.headers["Authorization"] = f"Bearer {api_key}"
|
||||
yield c
|
||||
|
||||
|
||||
@@ -61,8 +68,8 @@ def _minimal_layout() -> dict:
|
||||
def test_get_dashboard_layout_default_empty(client):
|
||||
"""When no layout has been saved, GET returns an empty object."""
|
||||
# Clear first so this test is order-independent.
|
||||
client.delete("/api/v1/preferences/dashboard-layout", headers=AUTH_HEADERS)
|
||||
resp = client.get("/api/v1/preferences/dashboard-layout", headers=AUTH_HEADERS)
|
||||
client.delete("/api/v1/preferences/dashboard-layout")
|
||||
resp = client.get("/api/v1/preferences/dashboard-layout")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == {}
|
||||
|
||||
@@ -70,15 +77,11 @@ def test_get_dashboard_layout_default_empty(client):
|
||||
def test_put_then_get_dashboard_layout(client):
|
||||
"""PUT a layout, GET it back unchanged."""
|
||||
layout = _minimal_layout()
|
||||
put = client.put(
|
||||
"/api/v1/preferences/dashboard-layout",
|
||||
json=layout,
|
||||
headers=AUTH_HEADERS,
|
||||
)
|
||||
put = client.put("/api/v1/preferences/dashboard-layout", json=layout)
|
||||
assert put.status_code == 200
|
||||
assert put.json() == {"ok": True}
|
||||
|
||||
got = client.get("/api/v1/preferences/dashboard-layout", headers=AUTH_HEADERS)
|
||||
got = client.get("/api/v1/preferences/dashboard-layout")
|
||||
assert got.status_code == 200
|
||||
body = got.json()
|
||||
assert body["version"] == 1
|
||||
@@ -90,11 +93,7 @@ def test_put_then_get_dashboard_layout(client):
|
||||
def test_put_rejects_missing_version(client):
|
||||
"""Body without numeric version field is rejected with 422."""
|
||||
bad = {"sections": []}
|
||||
resp = client.put(
|
||||
"/api/v1/preferences/dashboard-layout",
|
||||
json=bad,
|
||||
headers=AUTH_HEADERS,
|
||||
)
|
||||
resp = client.put("/api/v1/preferences/dashboard-layout", json=bad)
|
||||
assert resp.status_code == 422
|
||||
|
||||
|
||||
@@ -103,24 +102,16 @@ def test_put_rejects_non_object(client):
|
||||
resp = client.put(
|
||||
"/api/v1/preferences/dashboard-layout",
|
||||
json=["not", "an", "object"],
|
||||
headers=AUTH_HEADERS,
|
||||
)
|
||||
assert resp.status_code in (400, 422)
|
||||
|
||||
|
||||
def test_delete_clears_layout(client):
|
||||
"""DELETE wipes the saved layout so subsequent GET returns empty."""
|
||||
client.put(
|
||||
"/api/v1/preferences/dashboard-layout",
|
||||
json=_minimal_layout(),
|
||||
headers=AUTH_HEADERS,
|
||||
)
|
||||
deleted = client.delete(
|
||||
"/api/v1/preferences/dashboard-layout",
|
||||
headers=AUTH_HEADERS,
|
||||
)
|
||||
client.put("/api/v1/preferences/dashboard-layout", json=_minimal_layout())
|
||||
deleted = client.delete("/api/v1/preferences/dashboard-layout")
|
||||
assert deleted.status_code == 200
|
||||
after = client.get("/api/v1/preferences/dashboard-layout", headers=AUTH_HEADERS)
|
||||
after = client.get("/api/v1/preferences/dashboard-layout")
|
||||
assert after.status_code == 200
|
||||
assert after.json() == {}
|
||||
|
||||
@@ -139,7 +130,7 @@ def test_layout_round_trip_preserves_unknown_fields(client):
|
||||
"options": {"sensitivity": 0.7},
|
||||
}
|
||||
)
|
||||
client.put("/api/v1/preferences/dashboard-layout", json=layout, headers=AUTH_HEADERS)
|
||||
got = client.get("/api/v1/preferences/dashboard-layout", headers=AUTH_HEADERS).json()
|
||||
client.put("/api/v1/preferences/dashboard-layout", json=layout)
|
||||
got = client.get("/api/v1/preferences/dashboard-layout").json()
|
||||
assert got["futureField"] == {"foo": "bar"}
|
||||
assert any(s["key"] == "audio-meters" for s in got["sections"])
|
||||
|
||||
Reference in New Issue
Block a user