Skip to content

feat: port web-based configuration editor from main to develop#319

Merged
tomquist merged 18 commits into
tomquist:developfrom
Hutch67:feature/web-config
Apr 12, 2026
Merged

feat: port web-based configuration editor from main to develop#319
tomquist merged 18 commits into
tomquist:developfrom
Hutch67:feature/web-config

Conversation

@Hutch67
Copy link
Copy Markdown
Contributor

@Hutch67 Hutch67 commented Apr 9, 2026

Summary

  • Ports the web-based config editor feature from the main branch into develop, adapted to work with the async aiohttp architecture and src/astrameter/ package layout
  • Adds src/astrameter/web_config.py — HTML editor UI and config read/write helpers (atomic writes, comment preservation, duplicate-section validation)
  • Extends HealthCheckService with new aiohttp routes: GET /config (editor UI), GET /api/config, POST /api/config, POST /api/restart
  • Wires WEB_CONFIG_ENABLED config option in main.py (opt-in, default False)
  • Documents WEB_CONFIG_ENABLED in config.ini.example and CHANGELOG.md

Test plan

  • All 413 existing tests pass (uv run pytest)
  • ruff format and ruff check clean
  • mypy src/ clean
  • Set WEB_CONFIG_ENABLED = True in [GENERAL], start astrameter, open http://localhost:52500/config — editor loads, sections/keys editable, Save works, Save & Restart restarts the process

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Opt-in web configuration editor at http://:52500/config with type-aware inputs, preserves comments/layout, and a “Save & Restart” flow that waits for service health and reloads.
    • Embedded web server exposes health and config APIs plus a restart endpoint.
  • Tests

    • New test suite covering config I/O, validation, round-trip writes, and atomic write behavior.
  • Documentation

    • CHANGELOG and example config updated with WEB_CONFIG_ENABLED and WEB_SERVER_PORT settings.
  • Chore

    • Added runtime dependency for config handling.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 9, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0040cec8-b22b-48a3-bc45-c9de2e4c99d7

📥 Commits

Reviewing files that changed from the base of the PR and between b82a354 and 1d5c8be.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • CHANGELOG.md
  • config.ini.example
  • ha_addon/run.sh
  • pyproject.toml
  • src/astrameter/health_service.py
  • src/astrameter/main.py
  • src/astrameter/static/config_editor.html
  • src/astrameter/web_config.py
  • src/astrameter/web_config_test.py
  • src/astrameter/web_server.py
💤 Files with no reviewable changes (1)
  • src/astrameter/health_service.py
✅ Files skipped from review due to trivial changes (4)
  • pyproject.toml
  • CHANGELOG.md
  • config.ini.example
  • src/astrameter/static/config_editor.html
🚧 Files skipped from review as they are similar to previous changes (5)
  • ha_addon/run.sh
  • src/astrameter/web_config_test.py
  • src/astrameter/web_config.py
  • src/astrameter/web_server.py
  • src/astrameter/main.py

Walkthrough

Adds an opt-in embedded web server and browser-based configuration editor (enabled via GENERAL.WEB_CONFIG_ENABLED) with routes /config, /api/config, /api/key-types, /api/restart, INI read/write utilities that preserve comments/order, and restart signaling via a delayed SIGUSR1.

Changes

Cohort / File(s) Summary
Docs & Example Config
CHANGELOG.md, config.ini.example
Changelog entry for the opt-in web config editor; example config adds commented WEB_CONFIG_ENABLED and WEB_SERVER_PORT keys.
Embedded Web Server
src/astrameter/web_server.py, src/astrameter/health_service.py
New WebServer replaces former health service: serves /health, /api/*, optional /config UI and config APIs, supports restart scheduling (delayed SIGUSR1). health_service.py removed.
Web Config Backend
src/astrameter/web_config.py
New module exposing CONFIG_EDITOR_HTML, INI read->dict, config JSON, validated write with atomic-write fallbacks, config validation, and key-type metadata for the UI.
Static Web UI
src/astrameter/static/config_editor.html
New single-file browser UI that fetches /api/config and /api/key-types, renders editable sections/keys with type-appropriate inputs, supports add/remove/reorder, POSTs /api/config, and performs "Save & Restart" via /api/restart with health polling.
Startup / Main Loop
src/astrameter/main.py
Replaced HealthCheckService integration with conditional WebServer; extracted device-resolution helpers (_apply_cli_overrides, _resolve_device_config); added SIGUSR1 restart handling and restart-aware run loop to reload config on signal.
Tests
src/astrameter/web_config_test.py
New pytest suite exercising INI read/write, payload validation, atomic write fallbacks, comment/order preservation, and config JSON behavior.
Packaging & Scripts
pyproject.toml, ha_addon/run.sh
Added runtime dependency configupdater>=3.2; HA add-on template default switched from ENABLE_HEALTH_CHECK to ENABLE_WEB_SERVER.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Browser
    participant Web as WebServer
    participant Backend as web_config
    participant File as Config File
    participant Proc as Application Process

    Browser->>Web: GET /config (HTML)
    Browser->>Web: GET /api/config
    Web->>Backend: config_to_json(config_path)
    Backend->>File: read INI
    File-->>Backend: INI contents
    Backend-->>Web: JSON payload
    Web-->>Browser: 200 OK (config JSON)

    Browser->>Web: POST /api/config (sections, order)
    Web->>Backend: write_config_from_dict(config_path, sections, order)
    Backend->>File: atomic write (with fallback)
    File-->>Backend: success
    Backend-->>Web: 200 OK

    Browser->>Web: POST /api/restart
    Web-->>Browser: 200 OK
    Web->>Proc: schedule delayed SIGUSR1 (threading.Timer -> os.kill)
    Note right of Proc: Supervisor expected to restart process
    loop Poll health until ready
        Browser->>Web: GET /health
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the PR: porting a web-based configuration editor from main to develop branch.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Hutch67 Hutch67 marked this pull request as draft April 9, 2026 11:40
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/astrameter/health_service.py (1)

50-58: Consider security implications: endpoints expose config (including secrets) without authentication.

When WEB_CONFIG_ENABLED=True, anyone with network access to port 52500 can:

  • Read all config values including PASSWORD, ACCESSTOKEN, and other secrets via GET /api/config
  • Modify the configuration via POST /api/config
  • Trigger a service restart via POST /api/restart

Since this is opt-in and likely intended for trusted local networks, this may be acceptable. Consider adding a note in the documentation warning users about the security implications, or binding to 127.0.0.1 by default when web config is enabled.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/astrameter/health_service.py` around lines 50 - 58, The web config routes
added when enable_web_config is true expose secrets; update the health_service
to restrict access by either binding the web UI to localhost by default and/or
requiring a configurable token; specifically, when registering routes (where
enable_web_config is checked and app.router.add_* calls occur) ensure the server
listen/bind defaults to "127.0.0.1" unless an explicit host override is
provided, or add a short auth check inside the handlers (_handle_api_config_get,
_handle_api_config_post, _handle_api_restart, _handle_config_ui) that validates
a secret token from an env var (e.g., WEB_CONFIG_TOKEN) before returning config
or performing changes, returning 401 on failure; update documentation to note
the security implications and how to override the default host or set the token.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/astrameter/health_service.py`:
- Around line 166-175: The handler _handle_api_restart currently uses
threading.Timer(..., lambda: os._exit(0)) which forcefully terminates the
process and bypasses cleanup; replace this with a graceful shutdown trigger:
instead of os._exit, schedule a shutdown/signaling mechanism (e.g., call a
shared shutdown coroutine or set an asyncio.Event) that the main loop listens
for and then runs graceful cleanup of MQTT, powermeters and other services
before exiting; update _handle_api_restart to log the restart request,
set/dispatch the shutdown signal (or post a SIGTERM to the process) via the
existing async shutdown path, and remove the direct os._exit call so all finally
blocks and cleanup handlers execute.

In `@src/astrameter/web_config.py`:
- Around line 564-586: In collectConfig(), currently any .section-card whose
.section-name-input is empty is silently skipped; change this so you validate
section names before building the payload: iterate over all .section-card
elements (same loop in collectConfig), collect any cards with empty trimmed
section-name-input values (and optionally duplicate names using seenSections),
and if any are found return an error or throw an exception (or call a UI
alert/confirm) to block the save and surface which sections are unnamed; ensure
you still reference the same identifiers (collectConfig, .section-card,
.section-name-input, seenSections, order) so the check runs before result/ order
are returned.

---

Nitpick comments:
In `@src/astrameter/health_service.py`:
- Around line 50-58: The web config routes added when enable_web_config is true
expose secrets; update the health_service to restrict access by either binding
the web UI to localhost by default and/or requiring a configurable token;
specifically, when registering routes (where enable_web_config is checked and
app.router.add_* calls occur) ensure the server listen/bind defaults to
"127.0.0.1" unless an explicit host override is provided, or add a short auth
check inside the handlers (_handle_api_config_get, _handle_api_config_post,
_handle_api_restart, _handle_config_ui) that validates a secret token from an
env var (e.g., WEB_CONFIG_TOKEN) before returning config or performing changes,
returning 401 on failure; update documentation to note the security implications
and how to override the default host or set the token.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 18818849-ab25-4a85-87e4-8566a97baeca

📥 Commits

Reviewing files that changed from the base of the PR and between 9fdaa0e and 7fa41fd.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • config.ini.example
  • src/astrameter/health_service.py
  • src/astrameter/main.py
  • src/astrameter/web_config.py

Comment thread src/astrameter/web_server.py
Comment thread src/astrameter/web_config.py Outdated
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 9, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":500,"request":{"method":"PATCH","url":"https://api.github.com/repos/tomquist/AstraMeter/issues/comments/4213859862","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- This is an auto-generated comment: skip review by coderabbit.ai -->\n\n> [!IMPORTANT]\n> ## Review skipped\n> \n> Draft detected.\n> \n> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.\n> \n> <details>\n> <summary>⚙️ Run configuration</summary>\n> \n> **Configuration used**: Organization UI\n> \n> **Review profile**: CHILL\n> \n> **Plan**: Pro\n> \n> **Run ID**: `597d4469-070b-4761-b431-28f585480b79`\n> \n> </details>\n> \n> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.\n> \n> Use the checkbox below for a quick retry:\n> - [ ] <!-- {\"checkboxId\": \"e9bb8d72-00e8-4f67-9cb2-caf3b22574fe\"} --> 🔍 Trigger review\n\n<!-- end of auto-generated comment: skip review by coderabbit.ai -->\n\n<!-- walkthrough_start -->\n\n## Walkthrough\n\nAdded a web-based configuration editor feature to the application. The editor is controlled by a `WEB_CONFIG_ENABLED` config flag and serves an interactive UI at `/config` with API endpoints for reading, writing, and restarting the application via the health check service.\n\n## Changes\n\n|Cohort / File(s)|Summary|\n|---|---|\n|**Documentation & Configuration** <br> `CHANGELOG.md`, `config.ini.example`|Added changelog entry and example configuration option describing the opt-in web-based configuration editor feature served at `http://<host>:52500/config`.|\n|**Web Config Editor Module** <br> `src/astrameter/web_config.py`|New module implementing web configuration editor UI (embedded HTML/JavaScript) and backend functions: `read_config_as_dict()` parses config into a dict, `write_config_from_dict()` atomically writes changes back while preserving comments and structure, and `config_to_json()` serializes for the UI. Includes typed input controls for booleans, integers, floats, passwords, and enums, plus section/key management and \"Save & Restart\" workflow.|\n|**Health Service Integration** <br> `src/astrameter/health_service.py`, `src/astrameter/main.py`|Updated `HealthCheckService` to accept `config_path` and `enable_web_config` parameters and conditionally register routes: `/config` (editor UI), `/api/config` (GET/POST config operations), and `/api/restart` (POST triggers process exit). Modified `main.py` to read `GENERAL.WEB_CONFIG_ENABLED` flag from config and pass it to service initialization.|\n\n## Sequence Diagram(s)\n\n```mermaid\nsequenceDiagram\n    participant User as Browser User\n    participant UI as Web UI<br/>(/config)\n    participant API as Health Service API\n    participant Config as Config File\n    participant App as Application Process\n\n    User->>UI: Load config editor page\n    UI->>API: GET /api/config\n    API->>Config: read_config_as_dict()\n    Config-->>API: Return config + order\n    API-->>UI: JSON response\n    UI-->>User: Display form with current values\n\n    User->>UI: Edit configuration values\n    User->>UI: Click \"Save & Restart\"\n    UI->>API: POST /api/config\n    API->>Config: write_config_from_dict()\n    Config-->>API: Config updated\n    API-->>UI: Success response\n\n    UI->>API: POST /api/restart\n    API->>App: Schedule os._exit(0) after 0.5s\n    API-->>UI: Success response\n    App->>App: Process terminates\n    \n    UI->>UI: Show restart overlay + countdown\n    loop Poll until available\n        UI->>API: GET /health\n    end\n    \n    Note over App: Application restarts<br/>with new config\n    API-->>UI: Health check succeeds\n    UI->>UI: Reload page\n    UI-->>User: Display updated configuration\n```\n\n## Estimated code review effort\n\n🎯 4 (Complex) | ⏱️ ~50 minutes\n\n<!-- walkthrough_end -->\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n<details>\n<summary>🚥 Pre-merge checks | ✅ 2 | ❌ 1</summary>\n\n### ❌ Failed checks (1 warning)\n\n|     Check name     | Status     | Explanation                                                                           | Resolution                                                                         |\n| :----------------: | :--------- | :------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------- |\n| Docstring Coverage | ⚠️ Warning | Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |\n\n<details>\n<summary>✅ Passed checks (2 passed)</summary>\n\n|     Check name    | Status   | Explanation                                                                                                                                                                                                                            |\n| :---------------: | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled.                                                                                                                                                                            |\n|    Title check    | ✅ Passed | The title 'feat: port web-based configuration editor from main to develop' accurately describes the main objective of the pull request: porting the web-based configuration editor feature from the main branch to the develop branch. |\n\n</details>\n\n<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing Touches</summary>\n\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=tomquist/AstraMeter&utm_content=319)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- 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= -->\n\n<!-- internal state end -->"},"request":{"retryCount":3,"signal":{},"retries":3,"retryAfter":16}}}

@Hutch67 Hutch67 marked this pull request as ready for review April 9, 2026 19:58
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/astrameter/health_service.py`:
- Around line 35-38: HealthCheckService currently defaults bind_address to
"0.0.0.0" and when enable_web_config is true it exposes unauthenticated config
editor endpoints (including /api/restart) on that listener; change the logic
that attaches the config editor routes (the code path gated by enable_web_config
in HealthCheckService) so that routes are only mounted when either (a) the
bind_address is loopback (127.0.0.1 or ::1) or (b) an explicit auth/CSRF
protection mechanism is provided; enforce this by checking the bind_address
and/or a new required auth_config parameter before calling the function that
registers the config editor endpoints (referenced as the enable_web_config
gating and the /api/restart handler), and if neither condition is met, refuse to
attach the routes and log/raise an error instructing the operator to use
loopback or provide auth.
- Around line 139-145: The handlers that catch exceptions and currently return
raw str(e) in the response should be changed to log the full exception
server-side (use logger.exception(...) or logger.error with traceback) and avoid
leaking exception text to clients; map client-side errors (e.g., ValueError,
json.JSONDecodeError) to a 400 response with a generic message like
{"error":"Invalid request"} and map all other unexpected exceptions to a 500
response with {"error":"Internal server error"} instead of str(e); update both
exception blocks (the one shown and the similar one around lines 167-173) to
implement this pattern so they log full details but return safe, generic JSON
error bodies and appropriate status codes.

In `@src/astrameter/web_config.py`:
- Around line 583-588: The code that builds pairs by iterating
card.querySelectorAll('.key-value-body tr') silently overwrites duplicate keys
via pairs[key] = val; update the logic in that block to detect duplicates
instead: when extracting key from tr.querySelector('.key-input').value.trim(),
check if key already exists in the pairs map and if so mark the card/row as
invalid and surface a validation error (same UX as duplicate section-name
checks) and prevent form submission; otherwise set pairs[key] = val. Ensure you
reference the key-input/val-input rows and reuse the existing duplicate-section
validation mechanism to display the error and block the request.
- Around line 784-813: The temp file is left behind when
shutil.copyfile(tmp_path, config_path) succeeds because transferred is set True
and the finally cleanup is skipped; after a successful copyfile you should
remove tmp_path to avoid leaking temps. Modify the block that calls
shutil.copyfile in the try (around shutil.copyfile(tmp_path, config_path)) so
that on success you call contextlib.suppress(OSError): os.unlink(tmp_path) (or
equivalent safe unlink) and then set transferred = True; keep the existing
retryable-exception handling for the copyfile failure unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 67002d66-dd87-4520-881c-9a1475595a81

📥 Commits

Reviewing files that changed from the base of the PR and between 7fa41fd and 24052f3.

📒 Files selected for processing (2)
  • src/astrameter/health_service.py
  • src/astrameter/web_config.py

Comment thread src/astrameter/web_server.py
Comment thread src/astrameter/health_service.py Outdated
Comment thread src/astrameter/web_config.py Outdated
Comment thread src/astrameter/web_config.py
@Hutch67 Hutch67 marked this pull request as draft April 10, 2026 22:04
@Hutch67 Hutch67 marked this pull request as ready for review April 10, 2026 22:26
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/astrameter/health_service.py`:
- Around line 160-164: The handler currently assumes request.json() returns a
dict and that "sections" is a mapping and "order" is a list; validate the parsed
payload before using .get() by checking that data is a dict, sections is a dict
(or mapping) and order is a list (or sequence) and if any check fails return a
400 error instead of proceeding to call write_config_from_dict(self.config_path,
sections, order); use isinstance checks on the result of await request.json(),
on data.get("sections") and data.get("order") (or coerce/normalize them) to
avoid AttributeError/list-item issues and locate the logic around
request.json(), sections, order, and write_config_from_dict to implement this
validation and error response.

In `@src/astrameter/web_config.py`:
- Around line 531-565: When a section is renamed, update the client-side state
instead of leaving the old name: in the rename handler (the event attached to
the '.section-name-input' inside renderSection), read oldName from the card's
dataset and newName from the input, validate newName (non-empty and not already
in sectionOrder), then set currentConfig[newName] = currentConfig[oldName] (or
{} if missing), delete currentConfig[oldName], replace the entry in sectionOrder
(find index of oldName and set it to newName), and update the DOM card by
setting card.dataset.section = newName (and any visible labels) so
removeSection() and addSection() see the updated name; ensure addSection() still
checks sectionOrder for duplicates so new names won't collide.
- Around line 824-935: Reject unsafe names/values before emitting the INI: in
write_config_from_dict validate every section name, key, and value (iterate over
write_order and sections) and raise ValueError if a section contains '\n', '\r',
or ']' or if a key contains '\n' or '\r' (and optionally '=') or if a value
contains '\n' or '\r'; perform this single validation at the top of
write_config_from_dict so both the "file missing" branch and the
_update_section/new-section emission paths are protected (refer to symbols
write_config_from_dict, write_order, sections, keys/values, and
_update_section).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8b9321d9-5763-4d32-a5ae-ce6b1a6f1f74

📥 Commits

Reviewing files that changed from the base of the PR and between c0519bf and 6886305.

📒 Files selected for processing (2)
  • src/astrameter/health_service.py
  • src/astrameter/web_config.py

Comment thread src/astrameter/web_server.py
Comment thread src/astrameter/web_config.py Outdated
Comment thread src/astrameter/web_config.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/astrameter/health_service.py`:
- Around line 202-205: The restart timer currently sends SIGTERM via
os.kill(os.getpid(), signal.SIGTERM) which doesn't trigger the in-process
restart path in main.py (which sets restart_requested=True only on SIGUSR1);
change the signal sent by the timer to signal.SIGUSR1 so the lambda called by
threading.Timer triggers the same SIGUSR1 handler used in src/astrameter/main.py
and allows the process to perform the in-process "Save & Restart" flow instead
of exiting for an external supervisor to restart it.

In `@src/astrameter/main.py`:
- Around line 578-589: The loop reloads cfg on restart but fails to recompute
config-derived runtime state, so device_types, device_ids and skip_test remain
stale; after reloading cfg (inside the KeyboardInterrupt restart branch and
anywhere restart_requested can trigger a reload) rebuild device_types,
device_ids and skip_test using the same logic you used before entering the loop
(the code that originally derives GENERAL.DEVICE_TYPE, DEVICE_IDS, and
SKIP_POWERMETER_TEST) so the subsequent call to asyncio.run(async_main(...))
sees the updated values and CT port / shellypro3em expansion checks reflect the
new config.

In `@src/astrameter/web_config.py`:
- Around line 162-175: The read-modify-write of the config must be protected by
_CONFIG_WRITE_LOCK for the entire transaction: move acquisition of
_CONFIG_WRITE_LOCK so it is obtained before checking os.path.exists(config_path)
and before reading any existing file contents (and keep it held until after
_atomic_write_lines(config_path, lines) completes); ensure functions like
_validate_config_payload, the computation of write_order, and building of lines
based on sections/order all occur while the lock is held (apply the same locking
change to the later block covering lines ~178-260) so concurrent requests cannot
both read the old file and clobber each other.
- Around line 129-133: In _validate_config_payload, add a check that the order
list contains no duplicate section names and raise ValueError if duplicates
exist; specifically, inside the _validate_config_payload(sections: dict, order:
list) function validate that len(order) == len(set(order)) (or use
collections.Counter to detect repeats) and raise a clear ValueError like
"'order' contains duplicate section names" so write_order cannot emit the same
section twice; keep the existing type checks and perform this duplicate check
after validating the type of order.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aea2decd-8ca9-4b5f-808e-31345bd64373

📥 Commits

Reviewing files that changed from the base of the PR and between 6886305 and d8ab84c.

📒 Files selected for processing (4)
  • src/astrameter/health_service.py
  • src/astrameter/main.py
  • src/astrameter/static/config_editor.html
  • src/astrameter/web_config.py

Comment thread src/astrameter/web_server.py
Comment thread src/astrameter/health_service.py Outdated
Comment thread src/astrameter/main.py
Comment thread src/astrameter/web_config.py
Comment thread src/astrameter/web_config.py Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/astrameter/web_server.py (1)

161-186: ⚠️ Potential issue | 🟠 Major

Validate the updated config before returning success.

Line 181 writes the live file, and Line 183 immediately returns {"success": true}. write_config_from_dict() only guarantees INI structure; invalid values can still be persisted and then break the next restart. src/astrameter/web_config.py already adds validate_config(), so this handler should validate before committing or restore the previous file on failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/astrameter/web_server.py` around lines 161 - 186, The handler
_handle_api_config_post currently calls write_config_from_dict(...) and
immediately returns success; instead, after preparing sections and order call
validate_config (from src/astrameter/web_config.py) to ensure the new config is
valid before committing, or write to a temporary file and run validate_config
against that temp file, and only atomically replace the live config_path if
validation passes; if validation fails, restore the original file (or delete the
temp) and return an error response with validation details. Ensure you reference
and use write_config_from_dict, validate_config, and self.config_path in the
flow so invalid values are never persisted as the live config.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/astrameter/main.py`:
- Around line 584-595: The restart path only reloads cfg and device lists but
skips the one-time pre-run setup (CLI THROTTLE_INTERVAL override and MARSTEK
auto-registration) done before the loop; move that full startup preparation into
the restart cycle by extracting the pre-run setup into a helper (e.g.,
prepare_startup(cfg, args) or similar) that applies the THROTTLE_INTERVAL
override and performs the MARSTEK auto-registration, then call that helper both
before the first asyncio.run(async_main(...)) and again after reloading cfg in
the except KeyboardInterrupt branch (before calling asyncio.run), ensuring
restart_requested is handled as before.

In `@src/astrameter/web_config.py`:
- Around line 264-293: The CT002 schema fields marked as "float" (e.g.,
DEDUPE_TIME_WINDOW, CONSUMER_TTL, MAX_SMOOTH_STEP, MIN_EFFICIENT_POWER,
EFFICIENCY_ROTATION_INTERVAL) are read with cfg.getint(...) in runtime (see
src/astrameter/main.py), causing mismatches; either change those keys in the
CT002 block to "type": "integer" to match getint(), or update the runtime calls
that read them (the cfg.getint(...) usages) to cfg.getfloat(...) so the parser
accepts decimals—pick one approach and apply it consistently for all listed keys
so UI metadata and runtime parsing align.

---

Duplicate comments:
In `@src/astrameter/web_server.py`:
- Around line 161-186: The handler _handle_api_config_post currently calls
write_config_from_dict(...) and immediately returns success; instead, after
preparing sections and order call validate_config (from
src/astrameter/web_config.py) to ensure the new config is valid before
committing, or write to a temporary file and run validate_config against that
temp file, and only atomically replace the live config_path if validation
passes; if validation fails, restore the original file (or delete the temp) and
return an error response with validation details. Ensure you reference and use
write_config_from_dict, validate_config, and self.config_path in the flow so
invalid values are never persisted as the live config.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 12f6dab9-b09d-4ea8-a4db-6f93b0275d6c

📥 Commits

Reviewing files that changed from the base of the PR and between b4ff31e and b82a354.

📒 Files selected for processing (6)
  • config.ini.example
  • ha_addon/run.sh
  • src/astrameter/main.py
  • src/astrameter/static/config_editor.html
  • src/astrameter/web_config.py
  • src/astrameter/web_server.py
✅ Files skipped from review due to trivial changes (2)
  • config.ini.example
  • src/astrameter/static/config_editor.html

Comment thread src/astrameter/main.py
Comment on lines +584 to +595
while True:
restart_requested = False
try:
asyncio.run(async_main(cfg, args, device_types, device_ids, skip_test))
break # clean exit
except KeyboardInterrupt:
if not restart_requested:
break
logger.info("Restarting service…")
cfg = configparser.ConfigParser(dict_type=OrderedDict, interpolation=None)
cfg.read(args.config)
device_types, device_ids, skip_test = _resolve_device_config(cfg, args)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Move the full startup preparation into the restart cycle.

This branch only reloads cfg and recomputes device_types / device_ids / skip_test. The setup above the loop is still one-shot, so a web-triggered restart skips the CLI THROTTLE_INTERVAL override (Lines 509-513) and the MARSTEK auto-registration path (Lines 515-566). That makes “Save & Restart” behave differently from a real restart.

Please rerun that preparation after reloading the config, or wrap the whole pre-run setup in a helper used on every loop iteration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/astrameter/main.py` around lines 584 - 595, The restart path only reloads
cfg and device lists but skips the one-time pre-run setup (CLI THROTTLE_INTERVAL
override and MARSTEK auto-registration) done before the loop; move that full
startup preparation into the restart cycle by extracting the pre-run setup into
a helper (e.g., prepare_startup(cfg, args) or similar) that applies the
THROTTLE_INTERVAL override and performs the MARSTEK auto-registration, then call
that helper both before the first asyncio.run(async_main(...)) and again after
reloading cfg in the except KeyboardInterrupt branch (before calling
asyncio.run), ensuring restart_requested is handled as before.

Comment thread src/astrameter/web_config.py
hutch and others added 18 commits April 12, 2026 16:08
Adapts the web_config feature from the main branch to work with the
develop branch's async aiohttp architecture and src/astrameter layout.

- Add src/astrameter/web_config.py with HTML editor and config read/write helpers
- Extend HealthCheckService with /config, /api/config (GET/POST), /api/restart
- Wire WEB_CONFIG_ENABLED setting in main.py (opt-in, default False)
- Document WEB_CONFIG_ENABLED in config.ini.example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e threshold

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace os._exit(0) with SIGTERM in restart handler so asyncio cleanup
  (powermeters, MQTT insights, finally blocks) runs before process exits
- Raise an error in collectConfig() when any section has an empty name,
  preventing silent data loss on save

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On overlayfs / bind-mount filesystems (e.g. Home Assistant add-ons),
rename(2) returns EBUSY when the destination is a bind-mounted file.
Handle errno.EBUSY by copying the temp file over the destination in-place
instead of renaming it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Some container setups return EPERM instead of EBUSY from rename(2)
when the destination is a bind-mounted file.  Extend the fallback to
handle both errno values.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On some overlayfs setups the bind-mounted destination file cannot be
opened for writing (EPERM) even though the directory is writable.
Add a third strategy: unlink the destination first (which creates an
overlayfs whiteout in the upper layer), then rename the temp file into
place.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…table

When all write strategies fail (rename, copyfile, unlink+rename), surface
a human-readable error explaining the cause and how to fix it, instead of
a raw [Errno 13] string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'Permission denied' is EACCES (13); EPERM is 'Operation not permitted'
(1).  All fallback checks were testing the wrong errno constant, so
every permission error was being re-raised before the next strategy
could run.  Consolidate retryable errno values into a single tuple and
check it consistently across all three strategies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…trategies

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- In _atomic_write_lines, unlink the temp file after a successful
  shutil.copyfile so it is not left behind when rename is unavailable.
- In collectConfig (JS), throw on duplicate keys within a section using
  the same error-surfacing path as duplicate section-name detection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…responses

- Emit a WARNING log when WEB_CONFIG_ENABLED=true so the operator is
  reminded that unauthenticated read/write access is active
- Use logger.exception() for unexpected errors and return generic 500
  bodies instead of leaking internal error details to the client
- Distinguish 400 (bad request) from 500 (server error) in the save
  handler
- Add SVG favicon to the config editor page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Validate JSON payload shape (dict/list/dict) before calling .get() in
  the POST /api/config handler so malformed bodies return 400 not 500
- Add _validate_config_payload() to web_config.py that rejects section
  names, keys, or values containing \r, \n, or ] (section names), which
  could corrupt the emitted INI and have the API report success
- Fix stale sectionOrder state in the JS editor: addSection() now reads
  live names from the DOM via getCurrentSectionNames() to detect
  duplicates after a rename; removeSection() uses the current input
  value instead of card.dataset.section for the confirm prompt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…_write_lines

Strategy 1 (shutil.copyfile) copies the temp file into place but leaves it
on disk.  The previous code tried to unlink it inline with contextlib.suppress,
then set transferred=True; if the unlink was silently suppressed the finally
guard (if not transferred) would skip cleanup and leak the file.

Introduce a temp_consumed flag that is only set True when Strategy 2
(os.unlink + os.replace) physically renames the temp file away.  The finally
block now always cleans up the temp file unless it was consumed by a rename,
covering both the Strategy 1 success path and all failure paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d restart loop

Move the inline HTML template from web_config.py into
src/astrameter/static/config_editor.html, loaded at import time via
importlib.resources. Add a SIGUSR1-based restart loop in main() so the
service re-reads config and restarts in-process (useful when no external
supervisor is available). Also add validate_config() for trial-loading
config files.
The hand-rolled line-level parser for comment-preserving config writes
is replaced by the configupdater library, which handles comment/blank
preservation, key updates, and section management natively. Adds tests
for web_config.py covering read, write, validation, round-trip, and
atomic-write helpers.
- Send SIGUSR1 instead of SIGTERM from the web UI restart handler so
  the process reloads config in-place instead of exiting.
- Re-derive device_types, device_ids and skip_test after reloading cfg
  on restart so changed DEVICE_TYPE / DEVICE_IDS take effect.
- Hold _CONFIG_WRITE_LOCK for the entire read-modify-write transaction
  to prevent concurrent requests from clobbering each other.
- Reject duplicate section names in the order list.
…section-aware

Rename health_service.py → web_server.py and HealthCheckService → WebServer
to reflect its broader role (health endpoint, config editor, restart API).
Rename ENABLE_HEALTH_CHECK config key to ENABLE_WEB_SERVER. Add configurable
WEB_SERVER_PORT option (default 52500).

Move key-type metadata from inline JS to Python (web_config.py) and serve
via GET /api/key-types. Make key types section-based with prefix matching
so each section gets appropriate input widgets and auto-suggestions. Add
datalist-based key auto-completion filtered by section and excluding
already-used keys.
…T002 schema types

- Validate config against a temp copy before persisting in the POST
  /api/config handler; return the validation error on failure.
- Extract _apply_cli_overrides() so --throttle-interval is re-applied
  after a web-UI-triggered restart (was lost on config reload).
- Fix 13 CT002 key-type declarations from float to integer to match
  the getint() calls in main.py.
- Add PID controller keys (PID_KP/KI/KD/OUTPUT_MAX/MODE) to GENERAL
  and _PM_COMMON schemas and config.ini.example.
@tomquist tomquist force-pushed the feature/web-config branch from b82a354 to 1d5c8be Compare April 12, 2026 14:10
@tomquist tomquist merged commit 9b5bd20 into tomquist:develop Apr 12, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants