Skip to content

Commit 5ad169d

Browse files
author
Dennis Braun
committed
Refactor: Enforce non-empty defaults in ConfigManager for critical MQTT keys
1 parent 8eead90 commit 5ad169d

File tree

4 files changed

+127
-1
lines changed

4 files changed

+127
-1
lines changed

CLAUDE.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
**Repo:** https://github.com/itsDNNS/docsight
8+
9+
DOCSight is a Python web application that monitors cable internet (DOCSIS) signals from AVM FritzBox routers 24/7, collecting evidence for ISP complaints. It runs entirely locally with no cloud dependencies.
10+
11+
**Stack:** Python 3.12+ / Flask / SQLite / Jinja2 / vanilla JS frontend / paho-mqtt
12+
13+
## Commands
14+
15+
```bash
16+
# Run locally
17+
python -m app.main
18+
19+
# Run tests (192 tests)
20+
python -m pytest tests/ -v
21+
22+
# Run a single test
23+
python -m pytest tests/test_analyzer.py::TestHealthGood::test_all_normal -v
24+
25+
# Docker dev (port 8766)
26+
docker compose -f docker-compose.dev.yml up -d --build
27+
28+
# Install dependencies
29+
pip install -r requirements.txt
30+
pip install pytest
31+
```
32+
33+
No linter or formatter is configured.
34+
35+
## Architecture
36+
37+
**Threading model:** `main.py` runs two threads — a polling loop (collects FritzBox data at configurable intervals) and a Flask/Waitress web server (default port 8765). The main thread idles and listens for shutdown via `threading.Event`.
38+
39+
**Data flow:**
40+
```
41+
FritzBox modem → fritzbox.py (HTTP/login.lua)
42+
→ analyzer.py (health assessment per channel using thresholds.json)
43+
→ event_detector.py (stateful anomaly detection between snapshots)
44+
→ storage.py (SQLite persistence)
45+
→ mqtt_publisher.py (Home Assistant Auto-Discovery)
46+
→ web.py (Flask API + dashboard)
47+
→ report.py (PDF incident reports via fpdf2)
48+
```
49+
50+
**Config precedence:** Environment variables > `/data/config.json` > hardcoded defaults. Secrets (passwords, tokens) are encrypted at rest with Fernet (AES-128-CBC). The encryption key lives at `/data/.config_key`.
51+
52+
**Signal thresholds** in `app/thresholds.json` are per-modulation (64QAM/256QAM/1024QAM) and per-DOCSIS-version, based on Vodafone Kabel Deutschland guidelines. The analyzer produces health statuses: Good/Marginal/Poor/Critical.
53+
54+
## Key Conventions
55+
56+
- **i18n:** All user-facing strings use translation files in `app/i18n/` (en.json, de.json, fr.json, es.json). When adding or changing UI strings, update **all 4 files**. Each has a `_meta` field with `language_name` and `flag`.
57+
- **Adding modem support:** Create a new module in `app/` implementing `login()`, `get_docsis_data()`, and `get_device_info()` matching the FritzBox API return format so the analyzer works unchanged.
58+
- **Storage:** SQLite database at `/data/docsis_history.db`. Tables: snapshots, bqm_graphs, speedtest_results, incidents, incident_attachments, events. Attachments stored as BLOBs (max 10 MB each, max 10 per incident).
59+
- **Frontend:** The dashboard is a single large Jinja2 template (`app/templates/index.html`, ~174 KB) with vanilla JavaScript. No frontend build step.
60+
61+
## Workflow & Process
62+
63+
**Role:** You are the **Lead Developer** (implementation). Nova (OpenClaw) is your PM/QA.
64+
65+
1. **Branching:**
66+
- ALWAYS work on `dev` branch.
67+
- NEVER commit directly to `main` (protected for releases).
68+
69+
2. **Definition of Done:**
70+
- All tests passed (`python -m pytest tests/ -v`).
71+
- If UI changed: i18n keys added to **all 4 languages** (en/de/fr/es).
72+
- If UI changed: Mobile responsiveness verified (in thought/code).
73+
- Code committed to `dev` with a clear message (e.g., `Add channel timeline feature`).
74+
75+
3. **Handoff (Wake Nova):**
76+
- When you are finished with a task and have committed to `dev`:
77+
- Run: `openclaw system event --text "Done: <summary of changes>" --mode now`
78+
- This wakes up Nova (PM/QA) to perform acceptance testing and release prep.
79+
80+
4. **Releases:**
81+
- Do NOT create tags or releases yourself. Nova manages the release cycle.
82+
83+
## Emergency / User Override
84+
85+
If Nova is offline or the user explicitly takes command:
86+
- You may perform releases if instructed: Merge `dev` -> `main`, tag `vYYYY-MM-DD.N`.
87+
- ALWAYS run full test suite before tagging.
88+
- Check `gh release list` first to ensure the next version number is free.
89+
- You become responsible for QA (self-verify thoroughly).

app/config.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@
7878

7979
INT_KEYS = {"mqtt_port", "poll_interval", "web_port", "history_days", "booked_download", "booked_upload"}
8080

81+
# Keys where an empty string should fall back to the DEFAULTS value
82+
_NON_EMPTY_KEYS = {"mqtt_topic_prefix", "mqtt_discovery_prefix"}
83+
8184

8285
class ConfigManager:
8386
"""Loads config from config.json, env vars override file values.
@@ -176,6 +179,9 @@ def get(self, key, default=None):
176179

177180
if key in self._file_config:
178181
val = self._file_config[key]
182+
# Keys that must not be empty: fall through to defaults
183+
if key in _NON_EMPTY_KEYS and not val:
184+
return DEFAULTS[key]
179185
if key in INT_KEYS and not isinstance(val, int):
180186
if val == "" or val is None:
181187
return default if default is not None else 0
@@ -213,6 +219,11 @@ def save(self, data):
213219
if key in data and data[key]:
214220
data[key] = self._encrypt(data[key])
215221

222+
# Replace empty strings with defaults for keys that require a value
223+
for key in _NON_EMPTY_KEYS:
224+
if key in data and not data[key]:
225+
data[key] = DEFAULTS[key]
226+
216227
# Merge with existing config
217228
self._file_config.update(data)
218229

app/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def polling_loop(config_mgr, storage, stop_event):
4343
user=mqtt_user,
4444
password=mqtt_password,
4545
topic_prefix=config["mqtt_topic_prefix"],
46-
ha_prefix=config["mqtt_discovery_prefix"] or "homeassistant",
46+
ha_prefix=config["mqtt_discovery_prefix"],
4747
)
4848
try:
4949
mqtt_pub.connect()

tests/test_config.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,32 @@ def test_is_mqtt_configured(self, config):
178178
config.save({"mqtt_host": "broker.local"})
179179
assert config.is_mqtt_configured() is True
180180

181+
def test_empty_discovery_prefix_returns_default(self, tmp_data_dir):
182+
"""Empty mqtt_discovery_prefix should fall back to 'homeassistant'."""
183+
config = ConfigManager(tmp_data_dir)
184+
config.save({"mqtt_discovery_prefix": ""})
185+
assert config.get("mqtt_discovery_prefix") == "homeassistant"
186+
187+
def test_empty_topic_prefix_returns_default(self, tmp_data_dir):
188+
"""Empty mqtt_topic_prefix should fall back to 'docsight'."""
189+
config = ConfigManager(tmp_data_dir)
190+
config.save({"mqtt_topic_prefix": ""})
191+
assert config.get("mqtt_topic_prefix") == "docsight"
192+
193+
def test_custom_discovery_prefix_preserved(self, tmp_data_dir):
194+
"""Non-empty mqtt_discovery_prefix should be preserved."""
195+
config = ConfigManager(tmp_data_dir)
196+
config.save({"mqtt_discovery_prefix": "custom_ha"})
197+
assert config.get("mqtt_discovery_prefix") == "custom_ha"
198+
199+
def test_empty_prefix_normalized_on_save(self, tmp_data_dir):
200+
"""Empty prefix keys should be replaced with defaults in config.json."""
201+
config = ConfigManager(tmp_data_dir)
202+
config.save({"mqtt_discovery_prefix": ""})
203+
with open(os.path.join(tmp_data_dir, "config.json")) as f:
204+
raw = json.load(f)
205+
assert raw["mqtt_discovery_prefix"] == "homeassistant"
206+
181207
def test_theme_validation(self, config):
182208
assert config.get_theme() == "dark"
183209
config.save({"theme": "light"})

0 commit comments

Comments
 (0)