Skip to content

Add beginner-friendly web config generator with PR previews#399

Open
tomquist wants to merge 18 commits into
developfrom
claude/peaceful-cori-9cx7P
Open

Add beginner-friendly web config generator with PR previews#399
tomquist wants to merge 18 commits into
developfrom
claude/peaceful-cori-9cx7P

Conversation

@tomquist
Copy link
Copy Markdown
Owner

@tomquist tomquist commented May 28, 2026

What this adds

A static, dummy-proof website (web/) that walks anyone — including a complete beginner — through a few questions about their power meter and generates a ready-to-use Python config.ini or ESPHome YAML, with a live preview that updates as you type. It runs entirely in the browser (nothing is uploaded).

Flow (progressive disclosure — sections appear only when relevant)

  1. How will you run it? — Home Assistant/Docker/PC (config.ini) vs. ESP32 (ESPHome YAML), each explained.
  2. What meter to impersonate — device-type pills (Shelly Pro 3EM … CT002/CT003) with guidance, or ESP32 board + CT type for ESPHome.
  3. Your power meter — all 18 sources, each with a plain-language blurb, a doc link, and (in ESPHome mode) a support badge (native / HTTP / alternate / not-yet-supported). Single vs. three-phase toggle expands to L1/L2/L3 inputs.
  4. Battery steering (CT002/CT003) — only when relevant; collapsible groups for balancing, efficiency, and saturation, all pre-explained at their defaults.
  5. Optional extras — Marstek cloud registration + MQTT Insights / Home Assistant discovery.
  6. Save your work — auto-saved to the browser, plus download/load a project file and copy a share link (full state encoded in the URL) so users can iterate later.

Every option from config.ini.example, esphome.example.yaml, the two docs/powermeters*.md references, and the README config section is covered — general settings, per-meter tuning (throttle, smoothing, deadband, value transform, Hampel, PID), CT002/CT003 active-control tuning, multi-meter NETMASK, Marstek, and MQTT Insights.

Architecture

  • web/js/schema.js — single data source for every meter/field/help string.
  • web/js/generate.js — pure config.ini / ESPHome YAML generators (no DOM, Node-testable).
  • web/js/app.js — renders the form from the schema, holds state, live preview, save/load/share.
  • web/js/generate.test.mjs — 47 assertions, run in CI before every deploy.

Testing it directly from this PR

A per-PR preview workflow deploys the site to pr-preview/pr-<number>/ on the gh-pages branch and posts a comment here with the live URL — click it to test the real generator. The preview is torn down when the PR closes.

One-time repo setup required for the URLs to resolve: Settings → Pages → Build and deployment → Source → "Deploy from a branch" → gh-pages / / (root). After that, pushes to main/develop publish the production site and PRs get live previews.

Notes

  • Correctness detail: EmLog needs JSON_POWER_CALCULATE = True for signed grid power, so the generator honors each field's schema default rather than Python's blanket False fallback.
  • No Python code touched; existing CI is unaffected.
  • Changelog: one ## Next bullet added; README gains a Configuration callout linking to the generator.

https://claude.ai/code/session_01VJwSDFQexrv2DHQKoEy7Ft


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Launched a GitHub Pages site with a Config Generator (live preview, copy/download, import/export, share links, save/load) and step‑by‑step ESPHome flashing guidance; added landing page with overview, supported devices, and FAQs.
  • Style

    • Complete dark-mode and responsive site styling, sticky nav, and improved layout/components.
  • Tests

    • Added lightweight generator, schema, and state tests + local typecheck/test scripts.
  • Chores

    • Added CI workflows for full-build deploy and PR preview (runs typecheck/test/build before publishing).

Review Change Stack

claude added 2 commits May 28, 2026 18:56
A static, data-driven site (web/) that walks users through a few questions
about their power meter and generates either a Python config.ini or an
ESPHome YAML, with per-option help text, a live preview, and save/load/share
of the UI state. Deployed via a new GitHub Pages workflow.

- web/js/schema.js: single source of truth for every powermeter and option
- web/js/generate.js: pure config.ini / ESPHome YAML generators (Node-testable)
- web/js/app.js: progressive-disclosure form, live preview, localStorage +
  project file + share-link persistence
- web/js/generate.test.mjs: assertions run in CI before deploy
- .github/workflows/pages.yml: test + deploy web/ to GitHub Pages
Switch the Pages deploy to publish web/ to a gh-pages branch (keep_files so
PR previews survive), and add a pr-preview workflow that deploys each PR to
pr-preview/pr-<number>/ and comments the live URL, so the site can be tested
directly from the PR.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

Walkthrough

This PR introduces a fully in-browser configuration generator for AstraMeter that produces config.ini or ESPHome YAML from schema-driven state. It includes a landing site, interactive generator page, dark-mode styling, comprehensive test coverage, build tooling, and GitHub Actions workflows for production and PR preview deployments to gh-pages.

Changes

Web Configuration Generator

Layer / File(s) Summary
Configuration Schema and Catalog
web/ts/schema.ts
Exports schema types and catalogs (POWERMETERS, PER_METER_TUNING, CT groups, MARSTEK_FIELDS, MQTT_INSIGHTS_FIELDS, ESP_BOARDS, HARDWARE) used by UI and generators.
State Model and Migration
web/ts/state.ts, web/ts/state.test.ts
Typed State/Meter shapes, safeParse to block prototype pollution, cleanMeter sanitization, migrate() backfill/coercion; tests cover defaults, hostile inputs, and migration.
Config and ESPHome Generators
web/ts/generate.ts, web/ts/generate.test.ts
Pure generator functions: INI section builders and ESPHome YAML construction (sensor/top blocks, phase handling, CT002 filters, optional mqtt_insights/marstek); smoke tests validate numerous scenarios.
Schema Validation Tests
web/ts/schema.test.ts
Node-run validator for POWERMETERS and option groups enforcing shapes, uniqueness, conditional esphome properties, and ESP_BOARDS correctness.
Interactive Generator UI
web/ts/app.ts
Schema-driven multi-step form, localStorage/URL/JSON persistence, live preview with copy/download, export/import/share/reset, and UI utilities (DOM helpers, toasts, rerender).
GitHub Links & Site Behavior
web/ts/links.ts, web/ts/site.ts
Build-time GH_REF injection with runtime fallback, helpers to resolve data-gh links, mobile nav toggle, scroll styling, and landing-page feature/powermeter rendering.
Styling
web/css/styles.css
Dark-mode theme variables, typography, complete generator UI and site marketing styles, utilities, and responsive breakpoints at 920px and 760px.
HTML Pages
web/index.html, web/generator.html
Landing page and generator page with head metadata, navigation, content sections, noscript fallback for generator, and module script entry points.
Build & Project Config
web/build.mjs, web/package.json, web/tsconfig.json, web/.gitignore, .dockerignore
Build script using esbuild with GH_REF injection, npm scripts (typecheck/test/build), strict tsconfig, and ignore rules for node_modules/dist.
CI Workflows
.github/workflows/pages.yml, .github/workflows/pr-preview.yml
Pages workflow: runs tests/build and publishes web/dist to gh-pages (maps develop→/develop/, preserves other dirs). PR preview workflow: builds per-PR previews with same-repo gating and cleanup on close.
Documentation
README.md, web/README.md, CHANGELOG.md
User-facing README link and description for the generator, detailed web/README.md developer guide, and CHANGELOG entry announcing the web site and generator.

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a beginner-friendly web-based configuration generator with support for PR previews. It accurately reflects the primary purpose and scope of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/peaceful-cori-9cx7P

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.

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: 8

🧹 Nitpick comments (1)
web/js/generate.test.mjs (1)

112-153: ⚡ Quick win

Add regression assertions for multi-phase filters and dual-MQTT configuration

Please add explicit tests for:

  • 3-phase ESPHome output applying offset/multiply filters to grid_l1, grid_l2, and grid_l3.
  • MQTT meter + MQTT Insights together, including broker mismatch behavior (either validated or intentionally canonicalized).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/js/generate.test.mjs` around lines 112 - 153, Add two regression
assertions: for the 3-phase ESPHome case (eyHa from generateEsphome) assert that
filters for grid_l1, grid_l2 and grid_l3 include the offset/multiply lines
(e.g., check for "- offset: -20" and any multiply filter strings applied to
grid_l1/grid_l2/grid_l3 using has(eyHa, ...)). For the MQTT case (eyMqtt from
generateEsphome) add assertions that the mqtt_insights block and
marstek_registration block both appear and explicitly assert broker/topic
behavior when MQTT meter and mqttInsights are both set (e.g., check presence of
"broker: 192.168.1.10" or whatever canonicalized broker string you expect, and
"mqtt_insights:" plus "platform: mqtt_subscribe" / "topic: home/p") to cover
broker mismatch/canonicalization scenarios.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/pages.yml:
- Around line 32-45: Pin the third-party GitHub Actions to immutable commit SHAs
and disable checkout credential persistence: replace the mutable tags
actions/checkout@v4, actions/setup-node@v4, and peaceiris/actions-gh-pages@v4
with their corresponding commit SHAs, and add persist-credentials: false to the
actions/checkout step (the checkout step referenced as uses: actions/checkout).
Ensure the other action inputs remain unchanged while swapping tags for the SHA
pins.

In @.github/workflows/pr-preview.yml:
- Around line 33-41: Update the workflow steps that use actions/checkout,
actions/setup-node, and rossjrw/pr-preview-action to pin them to their full
commit SHAs (replace the mutable tags like `@v4` and `@v1` with the corresponding
full SHA strings), and add persist-credentials: false under the actions/checkout
step so the checkout step does not persist GitHub credentials; ensure you modify
the steps referencing actions/checkout, actions/setup-node, and
rossjrw/pr-preview-action accordingly (and keep the node-version: "20" and the
existing Run config generator tests and Deploy preview step logic intact).

In `@CHANGELOG.md`:
- Line 5: Consolidate the multiple bullets under the "## Next" section into a
single summary bullet by folding the new sentence about the beginner-friendly
web-based config generator into that one overall item; locate the "## Next"
header and replace the separate bullet containing the line about "a
beginner-friendly web-based config generator (`web/`, deployable to GitHub
Pages)..." with a concise single summary bullet that captures that feature plus
any other pending items for this branch, merging/editting (not appending) so
there remains exactly one bullet under "## Next".

In `@web/js/app.js`:
- Around line 441-451: copyConfig() and downloadConfig() call generate(state)
directly so a thrown generation error can crash the action with no user
feedback; wrap the generate(state) call in a try/catch in both functions, on
success proceed (for copyConfig: call
navigator.clipboard.writeText(...).then(...) as before; for downloadConfig: call
downloadText(name, text)); on error call toast with an error message (e.g.,
"Generation failed" and mark as error) and avoid calling clipboard/download
functions. Reference functions: copyConfig, downloadConfig, generate, toast,
downloadText.
- Around line 453-459: The downloadText function creates an object URL with
URL.createObjectURL(blob) but never revokes it, leaking resources; update
downloadText to store the object URL in a variable (e.g., const url =
URL.createObjectURL(blob)), assign it to the anchor href, then after triggering
the download call URL.revokeObjectURL(url) (use a short setTimeout (e.g.,
0-1000ms) or the anchor's click event callback to revoke so the download has
time to start), and ensure you still remove the anchor element; reference:
downloadText, blob, a, URL.createObjectURL, URL.revokeObjectURL.
- Around line 138-143: The password inputs are being forced to "text" by the
expression type: field.type === "password" ? "text" : field.type; in the code
that creates the input element (look for control creation using field.type and
the variable control), so change it to preserve the original field.type (i.e.,
use type: field.type) so password fields render with type="password"; keep the
existing secret class addition (control.classList.add("secret") when field.type
=== "password") and leave the input event handling
(control.addEventListener("input", ...)) as-is.

In `@web/js/generate.js`:
- Around line 193-200: The current filter generation builds filterLines and
filterBlock using tuning.POWER_OFFSET, tuning.POWER_MULTIPLIER and
tuning.THROTTLE_INTERVAL but only emits them for the first sensor (L1); update
the logic that builds and inserts filterLines/filterBlock (symbols: filterLines,
filterBlock, tuning.POWER_OFFSET, tuning.POWER_MULTIPLIER,
tuning.THROTTLE_INTERVAL, IND) so it constructs and injects the same filter
block for every phase sensor (L1/L2/L3) when in 3-phase mode instead of
reusing/placing it only once; ensure you apply the same fix to the other similar
blocks mentioned (the instances around the other occurrences) so each phase
sensor gets identical offset/multiply/throttle entries.
- Around line 447-452: The mqtt insights code currently unconditionally emits
the insights broker/port when wantInsights is true, which can override a meter’s
own mqtt settings (esp.kind === "mqtt"); update the generate logic to detect
when a meter has esp.kind === "mqtt" and both meter-level mqtt config and
state.mqttInsights are present, then either (a) compare mf.BROKER/mf.PORT with
the meter’s broker/port and emit a processLogger.warn or throw if they differ,
or (b) deterministically merge by selecting a canonical source (e.g., prefer
meter-specific values and fall back to mf), and write the merged broker/port to
out (use the existing wantInsights, mf and esp.kind checks and the same out.push
path so output remains explicit and deterministic).

---

Nitpick comments:
In `@web/js/generate.test.mjs`:
- Around line 112-153: Add two regression assertions: for the 3-phase ESPHome
case (eyHa from generateEsphome) assert that filters for grid_l1, grid_l2 and
grid_l3 include the offset/multiply lines (e.g., check for "- offset: -20" and
any multiply filter strings applied to grid_l1/grid_l2/grid_l3 using has(eyHa,
...)). For the MQTT case (eyMqtt from generateEsphome) add assertions that the
mqtt_insights block and marstek_registration block both appear and explicitly
assert broker/topic behavior when MQTT meter and mqttInsights are both set
(e.g., check presence of "broker: 192.168.1.10" or whatever canonicalized broker
string you expect, and "mqtt_insights:" plus "platform: mqtt_subscribe" /
"topic: home/p") to cover broker mismatch/canonicalization scenarios.
🪄 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: 1877e289-91b7-4785-968e-19d8e24d4423

📥 Commits

Reviewing files that changed from the base of the PR and between 11e358c and adb52d3.

📒 Files selected for processing (11)
  • .github/workflows/pages.yml
  • .github/workflows/pr-preview.yml
  • CHANGELOG.md
  • README.md
  • web/README.md
  • web/css/styles.css
  • web/index.html
  • web/js/app.js
  • web/js/generate.js
  • web/js/generate.test.mjs
  • web/js/schema.js

Comment thread .github/workflows/pages.yml
Comment thread .github/workflows/pr-preview.yml
Comment thread CHANGELOG.md Outdated

## Next

- **Added** a beginner-friendly web-based config generator (`web/`, deployable to GitHub Pages) that walks you through a few questions about your power meter and produces a ready-to-use Python `config.ini` or ESPHome YAML, with a live preview and save/load/share of your answers.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 28, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Consolidate ## Next to a single summary bullet.

Line 5 adds/edits one entry, but ## Next now contains multiple bullets. Please keep exactly one overall summary bullet for this branch and fold this text into that single item.

As per coding guidelines, CHANGELOG.md should “keep one bullet under ## Next ... and edit (not append) on later iterations if scope shifts.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` at line 5, Consolidate the multiple bullets under the "## Next"
section into a single summary bullet by folding the new sentence about the
beginner-friendly web-based config generator into that one overall item; locate
the "## Next" header and replace the separate bullet containing the line about
"a beginner-friendly web-based config generator (`web/`, deployable to GitHub
Pages)..." with a concise single summary bullet that captures that feature plus
any other pending items for this branch, merging/editting (not appending) so
there remains exactly one bullet under "## Next".

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This is already compliant with the guideline as written: the rule is "keep one bullet under ## Next that summarizes the overall outcome of that branch" — i.e. one bullet per branch. This PR adds exactly one bullet for its branch; the other bullets under ## Next are from previously-merged branches and shouldn't be folded together (they describe unrelated changes already on develop). So I'm leaving this as-is.


Generated by Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@tomquist, you're right — I misread the guideline. The rule is one bullet per branch under ## Next, not one consolidated bullet for all branches. This PR correctly adds a single bullet for its own branch, and the other bullets belong to independent branches already on develop. My comment was incorrect; sorry for the noise!


✏️ Learnings added
Learnt from: tomquist
Repo: tomquist/AstraMeter PR: 399
File: CHANGELOG.md:5-5
Timestamp: 2026-05-28T19:46:43.804Z
Learning: In the AstraMeter repository (`CHANGELOG.md`), the project convention for the `## Next` section is **one bullet per branch** (not one bullet total). Each branch/PR contributes exactly one bullet summarizing that branch's overall outcome. Multiple bullets under `## Next` are expected when multiple branches have been merged to `develop` and are awaiting release. Reviewers should not flag this as a violation.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread web/js/app.js Outdated
Comment thread web/js/app.js Outdated
Comment thread web/js/app.js Outdated
Comment thread web/js/generate.js Outdated
Comment thread web/js/generate.js Outdated
…/download

- app.js: render password fields as type=password (were plain text); guard
  copy/download against generator errors; revoke object URLs after download
- generate.js: apply offset/multiply/throttle filters to every phase sensor in
  3-phase ESPHome output (single value applies to all phases, per-phase list
  maps per phase) — previously only L1 was filtered; warn and reuse the meter's
  broker when an MQTT meter and MQTT Insights share ESPHome's single mqtt: block
- tests: cover per-phase filter mapping
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 28, 2026

PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://tomquist.github.io/AstraMeter/pr-preview/pr-399/

Built to branch gh-pages at 2026-05-30 14:16 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/js/generate.js (1)

484-492: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Duplicate http_request: block when HTTP meter and Marstek are both enabled.

HTTP-based meters (Shelly, json_http) add http_request: to topBlocks at line 273. When Marstek is also enabled, another http_request: block is added at line 485. ESPHome allows only one top-level http_request: component, so this produces invalid YAML.

Apply the same deduplication pattern used for mqtt:.

Proposed fix
+  // http_request: is needed by Marstek *or* HTTP-based meters; emit once.
+  const meterUsesHttp = topBlocks.some((b) => b.startsWith("http_request:"));
   if (wantMarstek) {
-    out.push(`http_request:\n${IND}timeout: 20s`);
+    if (!meterUsesHttp) {
+      out.push(`http_request:\n${IND}timeout: 20s`);
+    }
   }

   // top blocks from the sensor (api/mqtt/uart/etc.) — de-dup api/mqtt if already added
   for (const b of topBlocks) {
     if (wantInsights && b.startsWith("mqtt:")) continue;
+    // http_request: already emitted for Marstek with longer timeout
+    if (wantMarstek && b.startsWith("http_request:")) continue;
     out.push(b);
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/js/generate.js` around lines 484 - 492, When Marstek is enabled you
always push a second http_request: block causing duplicate top-level components;
change the Marstek branch so it only pushes the http_request: block if topBlocks
does not already contain an http_request entry. Concretely, in the wantMarstek
branch check topBlocks.some(b => b.startsWith("http_request:")) (use the same
startsWith pattern used for mqtt) and only call
out.push(`http_request:\n${IND}timeout: 20s`) when that check is false; keep the
existing IND and out variables and leave the existing for-loop handling of
topBlocks/wantInsights unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@web/js/generate.js`:
- Around line 484-492: When Marstek is enabled you always push a second
http_request: block causing duplicate top-level components; change the Marstek
branch so it only pushes the http_request: block if topBlocks does not already
contain an http_request entry. Concretely, in the wantMarstek branch check
topBlocks.some(b => b.startsWith("http_request:")) (use the same startsWith
pattern used for mqtt) and only call out.push(`http_request:\n${IND}timeout:
20s`) when that check is false; keep the existing IND and out variables and
leave the existing for-loop handling of topBlocks/wantInsights unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fa15ec93-cc4e-46b0-ace8-c01953f6a8cf

📥 Commits

Reviewing files that changed from the base of the PR and between adb52d3 and 3a2c2c8.

📒 Files selected for processing (3)
  • web/js/app.js
  • web/js/generate.js
  • web/js/generate.test.mjs
🚧 Files skipped from review as they are similar to previous changes (2)
  • web/js/generate.test.mjs
  • web/js/app.js

For the ESPHome target the UI now explains what ESPHome is (assuming zero
prior knowledge), recommends the ESP32-S3 DevKitC-1 with purchase links,
warns that the board selection must match the hardware, and adds a step-by-step
'how to flash this' card (install ESPHome, replace the file, set WiFi secrets,
match the board, flash over USB, point the battery at it). The generated YAML
header carries the same numbered quick-start. Default board is now the
recommended ESP32-S3 DevKitC-1.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

Actionable comments posted: 0

Add a marketing landing page (index.html) with hero, feature grid, supported
devices, a power-meter grid and feature list rendered from the shared schema,
how-it-works, installation options, FAQ, and a shared sticky nav + footer.
Move the config generator to generator.html (linked from the nav and hero).
Add js/site.js for shared nav behaviour and schema-driven landing content, and
extend styles.css with the site/nav/footer/landing design. Update docs and the
changelog bullet to describe the broader website.
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: 0

🧹 Nitpick comments (1)
web/js/site.js (1)

55-62: ⚡ Quick win

Tier-label lookup map matches all esphome.tier values in web/js/schema.js

web/js/schema.js only declares esphome.tier as native | generic | alternate | unsupported, so the current 4-key lookup won’t produce undefined badges for any schema entries.

Optional future-proofing
-    const tierLabel = { native: "ESP32 native", generic: "ESP32 via HTTP", alternate: "ESP32 alt.", unsupported: "Python only" }[tier];
+    const tierLabel = { native: "ESP32 native", generic: "ESP32 via HTTP", alternate: "ESP32 alt.", unsupported: "Python only" }[tier] || "Python only";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/js/site.js` around lines 55 - 62, The lookup for tierLabel and tierCls
currently assumes pm.esphome.tier will always be one of the four known values,
which is true today but could break on new values; update the code around
POWERMETERS iteration (use the pm.esphome.tier value) to provide safe fallbacks:
replace the direct map access with a const tierMap = { native: "...", generic:
"...", alternate: "...", unsupported: "..." }; then compute const tierLabel =
tierMap[tier] ?? "Unknown tier"; and compute tierCls with a default branch (e.g.
const tierCls = tier === "unsupported" ? "bad" : tier === "alternate" ? "warn" :
tier === "native" || tier === "generic" ? "ok" : "unknown"); this keeps existing
behavior for known tiers while preventing undefined badges for unexpected
pm.esphome.tier values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@web/js/site.js`:
- Around line 55-62: The lookup for tierLabel and tierCls currently assumes
pm.esphome.tier will always be one of the four known values, which is true today
but could break on new values; update the code around POWERMETERS iteration (use
the pm.esphome.tier value) to provide safe fallbacks: replace the direct map
access with a const tierMap = { native: "...", generic: "...", alternate: "...",
unsupported: "..." }; then compute const tierLabel = tierMap[tier] ?? "Unknown
tier"; and compute tierCls with a default branch (e.g. const tierCls = tier ===
"unsupported" ? "bad" : tier === "alternate" ? "warn" : tier === "native" ||
tier === "generic" ? "ok" : "unknown"); this keeps existing behavior for known
tiers while preventing undefined badges for unexpected pm.esphome.tier values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4f37e7f3-95a4-4ac3-b24c-ae08fb80aae3

📥 Commits

Reviewing files that changed from the base of the PR and between 7cd5aa4 and a47d0b1.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • README.md
  • web/README.md
  • web/css/styles.css
  • web/generator.html
  • web/index.html
  • web/js/site.js
✅ Files skipped from review due to trivial changes (2)
  • README.md
  • web/README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

claude added 3 commits May 30, 2026 06:00
- Remove the last pm.id special-cases from generate.js; meter-specific ESPHome
  behaviour is now declarative on the schema (esphome.warn, esphome.headersField,
  esphome.haEntity), so editing/adding a meter is a one-file change in schema.js
- Add schema.test.mjs: structural validation that fails on typos, bad field
  types, selects without options, unknown esphome.kind, duplicate sections, and
  stray property names; wired into both Pages workflows
- Add JSDoc typedefs (Field / EsphomeSpec / Powermeter) for editor autocomplete
- Document 'Adding or editing a powermeter' in web/README with a worked example
- Cover the declarative paths with new generator assertions
…able

XSS audit found no exploitable issue: every restored/user value reaches only
textContent, input .value, or setAttribute — never an HTML sink. This adds
defense-in-depth so the 'safe today' invariant can't silently rot:

- Extract the pure state model + persistence helpers into web/js/state.js
  (defaultState, migrate, cleanMeter, prototype-pollution-safe safeParse)
- safeParse drops __proto__/constructor/prototype keys on share-link/project
  restore; migrate constrains restored input: meter.type -> known id, target ->
  python|esphome, phases -> 1|3, string/object coercion of value-bearing fields
- Add web/js/state.test.mjs (runs in plain Node) covering the hostile cases;
  wire it into both Pages workflows
- Comment the one interpolated innerHTML site noting only static schema
  constants may be interpolated there
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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/js/state.js`:
- Around line 79-81: In migrate(), coerce restored sections ct, marstek, and
mqttInsights so their fields become plain objects and
marstek/mqttInsights.enabled become booleans: instead of directly spreading
...(s.ct||{}) etc., ensure you only accept object-like maps (reject arrays) for
ct.fields, marstek.fields and mqttInsights.fields and default to {} otherwise,
and coerce marstek.enabled and mqttInsights.enabled to !!value; modify the
migrate() logic (and reuse or add a small helper like isPlainObject/cleanMeter
if present) to perform these checks before constructing the returned state so
downstream code that accesses state.ct.fields / state.mqttInsights.fields can
safely assume a plain object and enabled is a boolean.
🪄 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: 59066a6f-eaab-45e9-91e4-3771481bd26a

📥 Commits

Reviewing files that changed from the base of the PR and between a47d0b1 and 22d3046.

📒 Files selected for processing (12)
  • .github/workflows/pages.yml
  • .github/workflows/pr-preview.yml
  • web/README.md
  • web/index.html
  • web/js/app.js
  • web/js/generate.js
  • web/js/generate.test.mjs
  • web/js/schema.js
  • web/js/schema.test.mjs
  • web/js/site.js
  • web/js/state.js
  • web/js/state.test.mjs
✅ Files skipped from review due to trivial changes (2)
  • web/js/state.test.mjs
  • web/js/schema.test.mjs
🚧 Files skipped from review as they are similar to previous changes (6)
  • .github/workflows/pages.yml
  • .github/workflows/pr-preview.yml
  • web/index.html
  • web/js/site.js
  • web/js/app.js
  • web/js/generate.js

Comment thread web/js/state.js Outdated
claude added 7 commits May 30, 2026 07:22
Follow-up to the restore-sanitisation: reject non-object (incl. array) fields
maps for ct/marstek/mqttInsights and coerce marstek/mqttInsights.enabled to a
boolean. Adds an asObject() guard (typeof [] === 'object', so arrays are now
rejected in cleanMeter's fields/tuning too). Covered by new state tests.
Apply the new palette and dark instrument-panel aesthetic across the website:
- Design tokens: void/slate neutrals, energy-flow gradient (emerald -> cyan ->
  astra purple), Astra spark (lemon/amber/bronze), cyan interactive accent + a
  cyan->violet primary-button gradient; status colors retuned to the palette
- Page background radial glow; hero with layered green/cyan/purple glows and an
  energy-arc underline; gradient step badges, how-it-works numbers, nav CTA and
  primary buttons; glassmorphism CTA box; recolored notes/badges/panels
- Brand mark rendered as the glowing four-pointed Astra star (placeholder until
  the logo SVGs land); add theme-color meta

CSS-only + brand glyph; no JS/behaviour changes (all tests still pass).
- web/assets/astrameter-icon.svg (gauge + Astra star mark) and
  astrameter-logo.svg (mark + AstraMeter wordmark), from the provided artwork
- Nav + footer brand use the icon; hero features the full lockup
- SVG favicon, plus PNG fallbacks (apple-touch-icon, og:image) rendered from
  the vectors for consistency; og:image + theme-color for link previews
- Replaces the ✦ placeholder glyph
HA shows ha_addon/icon.png (square, store + header) and ha_addon/logo.png
(info page). Rendered both from the vector brand assets so the add-on matches
the website identity. Transparent corners + the dark plate read well on light
or dark HA themes.
The <text>+system-ui wordmark relied on a font the rasteriser substituted with
a wider face, so text-anchor centering overflowed to the right in the PNG
exports (og:image, HA add-on logo). Outlining the wordmark to paths removes the
font dependency entirely: it now renders identically and perfectly centered in
every browser and renderer. Regenerated the PNG exports from the fixed SVG.
pages.yml now sets peaceiris destination_dir from the branch: main -> gh-pages
root, develop -> /develop/. keep_files keeps root, /develop/, and /pr-preview/
coexisting on the branch. Docs updated; the site uses relative URLs so it works
under the subpath unchanged.
Add .github/scripts/stamp-ref.sh, run by both Pages workflows before publish,
to rewrite the committed @develop / blob/develop links in web/ to the ref being
deployed: main -> @main, develop -> @develop, PR preview -> the PR branch. Covers
the footer docs, the per-meter reference link, and the ESPHome
external_components source in generated configs. Committed source stays on
@develop (valid where the code lives); only the ephemeral CI copy is rewritten.
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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/pages.yml:
- Around line 49-52: Replace direct inlining of `${{ github.ref_name }}` into
the run command by setting an environment variable (e.g., DEPLOY_REF: ${{
github.ref_name }}) and update the run invocation to call the script with that
env var (e.g., run: bash .github/scripts/stamp-ref.sh "$DEPLOY_REF");
specifically modify the Pages deploy step that currently inlines github.ref_name
and instead add an env: DEPLOY_REF and use "$DEPLOY_REF" in the call so
stamp-ref.sh receives safe, data-only input rather than a pre-parsed shell
string.

In @.github/workflows/pr-preview.yml:
- Around line 43-45: The workflow currently interpolates `${{ github.head_ref
}}` directly into the inline `run:` command; treat it as untrusted input by
passing it via `env` instead and reference it as a shell variable inside the
`run` step: add an environment variable (e.g., `PR_HEAD_REF: ${{ github.head_ref
}}`) to the step and change the invocation of the script `bash
.github/scripts/stamp-ref.sh` to use the shell variable (e.g., `"$PR_HEAD_REF"`)
so the head ref is not subject to shell interpolation before the script runs.
🪄 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: 87da72a4-28c8-4b8a-9d15-21f70b933794

📥 Commits

Reviewing files that changed from the base of the PR and between 32710c2 and 0c72d5e.

📒 Files selected for processing (4)
  • .github/scripts/stamp-ref.sh
  • .github/workflows/pages.yml
  • .github/workflows/pr-preview.yml
  • web/README.md
✅ Files skipped from review due to trivial changes (1)
  • .github/scripts/stamp-ref.sh

Comment thread .github/workflows/pages.yml Outdated
Comment thread .github/workflows/pr-preview.yml Outdated
claude added 2 commits May 30, 2026 10:10
The previous stamp only covered blob/develop and @develop. Extend
stamp-ref.sh to also rewrite bare repo-home links (.../astrameter -> /tree/<ref>)
and README section anchors (.../astrameter#anchor -> /tree/<ref>#anchor), so the
nav/hero 'GitHub' links and the install-card anchors track the deployed ref too.
/issues stays ref-agnostic. Verified for plain and slashy (PR) refs.
Replace the plain-JS site + deploy-time sed (stamp-ref.sh) with a TypeScript
build:
- ts/ sources bundled by esbuild into dist/, type-checked by tsc, tested by tsx
  (package.json scripts: typecheck/test/build). Schema is now fully typed.
- New ts/links.ts builds every GitHub URL (repo home, README anchors, doc files,
  per-meter reference, ESPHome external_components) from a single ref injected at
  build time via GH_REF -> __GH_REF__ (esbuild define). main->@main,
  develop->@develop, PR->head branch; /issues stays ref-agnostic. No post-build
  rewriting; remove .github/scripts/stamp-ref.sh.
- Static HTML links carry data-gh attributes that site.ts resolves at load
  (hardcoded href kept as a no-JS fallback).
- Workflows: install/typecheck/test/build, publish web/dist (main->root,
  develop->/develop/, PR->/pr-preview/). dist/ and node_modules gitignored.

No user-visible behaviour change; tsc clean, all suites pass, built bundle
verified to mount and render.
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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
web/ts/schema.ts (1)

659-668: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

modbus is listed as phase-capable but neither generator honors 3 phases for it.

In generate.ts, esphomeSensor hardcodes phases: 1 for the modbus kind, and meterSection has no per-phase handling for Modbus (no phaseListKeys, no phase: true field). So a user toggling 3-phase on a Modbus meter sees no effect on either the config.ini or the ESPHome YAML output. Consider removing modbus from PHASE_CAPABLE (or wiring per-phase register handling) so the UI toggle isn't misleading.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/ts/schema.ts` around lines 659 - 668, PHASE_CAPABLE currently includes
"modbus" but the generators don't support per-phase behavior; update
PHASE_CAPABLE to remove "modbus" (so the UI toggle is not misleading) or
implement per-phase support: in generate.ts update esphomeSensor to use phases
from meter config instead of hardcoding phases: 1 for Modbus and extend
meterSection to include Modbus in phaseListKeys and/or add phase: true handling
and per-phase register mappings for Modbus so the ESPHome YAML and config.ini
reflect 3-phase selection; key symbols to inspect/modify: PHASE_CAPABLE,
generate.ts::esphomeSensor, meterSection, phaseListKeys.
web/ts/generate.ts (1)

52-67: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Downstream crash risk from unvalidated general.

(...).join(",") (Line 55) and g.deviceIds.trim() (Line 57) assume deviceTypes is an array and deviceIds a string. With untrusted restored state these can be other types and throw. The fix belongs at the sanitization boundary — see the migrate() comment in web/ts/state.ts; no change needed here once that's addressed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/ts/generate.ts` around lines 52 - 67, generalSection reads properties
g.deviceTypes and g.deviceIds assuming types (array and string) which can crash
on untrusted state; instead add validation/coercion in the state sanitization
boundary (migrate() in web/ts/state.ts) so that State.general.deviceTypes is
always normalized to an array (e.g., empty or single-item array) and
State.general.deviceIds is coerced to a string (or undefined) before
generalSection is ever called; keep generalSection unchanged but rely on
migrate() to enforce these invariants for g.deviceTypes and g.deviceIds.
web/ts/state.ts (1)

109-124: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

migrate() doesn't validate general sub-fields, defeating its stated sanitization goal for untrusted input.

general: { ...d.general, ...(s.general || {}) } shallow-merges restored values without coercing them. A crafted share link / project file can set general.deviceTypes to a non-array (e.g. a string) or general.deviceIds to a non-string. That untrusted value then reaches generalSection() in generate.ts, where (...).join(",") (Line 55) and g.deviceIds.trim() (Line 57) throw a TypeError, breaking generation in the user's browser. Since migrate() is the designated boundary for untrusted state, coerce these fields here.

🛡️ Proposed fix
   const meters = Array.isArray(s.meters) && s.meters.length ? s.meters : d.meters;
+  const g = s.general && typeof s.general === "object" ? s.general : {};
+  const general = { ...d.general, ...g };
+  general.deviceTypes = Array.isArray(general.deviceTypes)
+    ? general.deviceTypes.filter((t: unknown): t is string => typeof t === "string")
+    : d.general.deviceTypes;
+  general.deviceIds = typeof general.deviceIds === "string" ? general.deviceIds : "";
   return {
     ...d,
     ...s,
     target: s.target === "esphome" ? "esphome" : "python",
-    general: { ...d.general, ...(s.general || {}) },
+    general,
     esphome: { ...d.esphome, ...(s.esphome || {}) },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/ts/state.ts` around lines 109 - 124, The migrate function currently
shallow-merges untrusted s.general into state without coercion; update migrate
(the migrate function in state.ts) to sanitize general sub-fields before
returning: ensure general.deviceTypes is coerced to an array (e.g., fallback to
d.general.deviceTypes when missing or not an array, and map/convert elements to
strings), ensure general.deviceIds is coerced to a string (fallback to
d.general.deviceIds when missing or not a string), and similarly coerce any
other expected general primitives to their proper types so downstream code like
generalSection() in generate.ts can safely call .join or .trim without
TypeErrors; keep the merge pattern using defaultState() as baseline (d.general)
but replace the direct spread of ...(s.general || {}) with a sanitized general
object constructed from d.general and validated/coerced values from s.general.
web/ts/app.ts (1)

425-431: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix XSS: render state.esphome.board without innerHTML

el() treats the html prop as innerHTML, but esphomeStepsCard() interpolates e.board into an html string (“Check the board matches…”). e.board comes from share-link/localStorage/project restore via migrate(), which does not constrain it to ESP_BOARDS, so a crafted value can inject HTML.

🔒 Proposed fix: render the board as text rather than HTML
-      li("<strong>Check the board matches.</strong> The file uses <code>board: " + (e.board || "esp32-s3-devkitc-1") + "</code>. This must be the board you actually bought — change it in step 2 above if not."),
+      el("li", {}, [
+        el("strong", { text: "Check the board matches." }),
+        document.createTextNode(" The file uses "),
+        el("code", { text: "board: " + (e.board || "esp32-s3-devkitc-1") }),
+        document.createTextNode(". This must be the board you actually bought — change it in step 2 above if not."),
+      ]),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/ts/app.ts` around lines 425 - 431, The string interpolation of e.board
inside the HTML passed to el()/li in esphomeStepsCard() can lead to XSS because
el() treats the html prop as innerHTML; instead render the board value as text
(not HTML) — e.g. stop concatenating e.board into the HTML string and inject it
as a text node or escaped content (use a separate text argument/child or a
dedicated text element) so state.esphome.board / e.board is inserted as plain
text rather than via innerHTML.
🧹 Nitpick comments (1)
.github/workflows/pages.yml (1)

32-34: Cross-workflow contention on gh-pages.

This workflow uses concurrency group gh-pages-deploy while pr-preview.yml uses pr-preview-${{ github.event.number }}. Since they don't share a group, a Pages deploy and a PR-preview deploy can run simultaneously and both push to gh-pages, occasionally causing non-fast-forward push failures. Consider a shared concurrency group (or rely on the publishing actions' built-in retry) if you observe intermittent push errors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pages.yml around lines 32 - 34, The concurrency block
currently sets group: gh-pages-deploy which doesn't match the PR preview
workflow's pr-preview-${{ github.event.number }} group, allowing simultaneous
pushes to gh-pages; change the concurrency group in this workflow to a shared
static name used by both (for example "gh-pages") so Pages deploys and
PR-preview deploys serialize, or alternatively coordinate both workflows to use
the exact same concurrency expression; update the concurrency group value
referenced in this file (group: gh-pages-deploy) to the agreed shared name to
prevent cross-workflow contention.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/README.md`:
- Around line 62-63: Update the README reference to the PR preview workflow so
it uses the full repository path for consistency: replace the standalone
"pr-preview.yml" mention with ".github/workflows/pr-preview.yml" (match the
style used for ".github/workflows/pages.yml") in the web/README.md line that
currently references "pr-preview.yml".

---

Outside diff comments:
In `@web/ts/app.ts`:
- Around line 425-431: The string interpolation of e.board inside the HTML
passed to el()/li in esphomeStepsCard() can lead to XSS because el() treats the
html prop as innerHTML; instead render the board value as text (not HTML) — e.g.
stop concatenating e.board into the HTML string and inject it as a text node or
escaped content (use a separate text argument/child or a dedicated text element)
so state.esphome.board / e.board is inserted as plain text rather than via
innerHTML.

In `@web/ts/generate.ts`:
- Around line 52-67: generalSection reads properties g.deviceTypes and
g.deviceIds assuming types (array and string) which can crash on untrusted
state; instead add validation/coercion in the state sanitization boundary
(migrate() in web/ts/state.ts) so that State.general.deviceTypes is always
normalized to an array (e.g., empty or single-item array) and
State.general.deviceIds is coerced to a string (or undefined) before
generalSection is ever called; keep generalSection unchanged but rely on
migrate() to enforce these invariants for g.deviceTypes and g.deviceIds.

In `@web/ts/schema.ts`:
- Around line 659-668: PHASE_CAPABLE currently includes "modbus" but the
generators don't support per-phase behavior; update PHASE_CAPABLE to remove
"modbus" (so the UI toggle is not misleading) or implement per-phase support: in
generate.ts update esphomeSensor to use phases from meter config instead of
hardcoding phases: 1 for Modbus and extend meterSection to include Modbus in
phaseListKeys and/or add phase: true handling and per-phase register mappings
for Modbus so the ESPHome YAML and config.ini reflect 3-phase selection; key
symbols to inspect/modify: PHASE_CAPABLE, generate.ts::esphomeSensor,
meterSection, phaseListKeys.

In `@web/ts/state.ts`:
- Around line 109-124: The migrate function currently shallow-merges untrusted
s.general into state without coercion; update migrate (the migrate function in
state.ts) to sanitize general sub-fields before returning: ensure
general.deviceTypes is coerced to an array (e.g., fallback to
d.general.deviceTypes when missing or not an array, and map/convert elements to
strings), ensure general.deviceIds is coerced to a string (fallback to
d.general.deviceIds when missing or not a string), and similarly coerce any
other expected general primitives to their proper types so downstream code like
generalSection() in generate.ts can safely call .join or .trim without
TypeErrors; keep the merge pattern using defaultState() as baseline (d.general)
but replace the direct spread of ...(s.general || {}) with a sanitized general
object constructed from d.general and validated/coerced values from s.general.

---

Nitpick comments:
In @.github/workflows/pages.yml:
- Around line 32-34: The concurrency block currently sets group: gh-pages-deploy
which doesn't match the PR preview workflow's pr-preview-${{ github.event.number
}} group, allowing simultaneous pushes to gh-pages; change the concurrency group
in this workflow to a shared static name used by both (for example "gh-pages")
so Pages deploys and PR-preview deploys serialize, or alternatively coordinate
both workflows to use the exact same concurrency expression; update the
concurrency group value referenced in this file (group: gh-pages-deploy) to the
agreed shared name to prevent cross-workflow contention.
🪄 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: 0e71864c-d00f-4365-94ce-8c806bd3aec5

📥 Commits

Reviewing files that changed from the base of the PR and between 32710c2 and d58de29.

⛔ Files ignored due to path filters (1)
  • web/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (19)
  • .dockerignore
  • .github/workflows/pages.yml
  • .github/workflows/pr-preview.yml
  • web/.gitignore
  • web/README.md
  • web/build.mjs
  • web/generator.html
  • web/index.html
  • web/package.json
  • web/ts/app.ts
  • web/ts/generate.test.ts
  • web/ts/generate.ts
  • web/ts/links.ts
  • web/ts/schema.test.ts
  • web/ts/schema.ts
  • web/ts/site.ts
  • web/ts/state.test.ts
  • web/ts/state.ts
  • web/tsconfig.json
💤 Files with no reviewable changes (3)
  • web/ts/state.test.ts
  • web/ts/schema.test.ts
  • web/ts/generate.test.ts
✅ Files skipped from review due to trivial changes (2)
  • web/.gitignore
  • .dockerignore
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/generator.html

Comment thread web/README.md Outdated
…ed CI lock

- app.ts: HTML-escape the (restorable) esphome.board before it enters an
  innerHTML string in the ESPHome steps card (escapeHtml helper)
- state.ts migrate(): coerce general sub-fields (deviceTypes->array, deviceIds
  & port/interval->string, flags->boolean) so generalSection can't crash on
  untrusted restored state; covered by new state.test assertions
- schema.ts: drop 'modbus' from PHASE_CAPABLE — the generators are single-phase
  for Modbus, so the 3-phase toggle was misleading
- pages.yml + pr-preview.yml: share one 'gh-pages' concurrency group (no cancel)
  so all gh-pages pushes serialise instead of racing
- README: full path for .github/workflows/pr-preview.yml
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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/pr-preview.yml:
- Around line 23-26: The workflow-level concurrency block (group: gh-pages /
cancel-in-progress: false) is serializing the entire PR-preview run; move this
concurrency configuration off the top-level and onto only the specific job(s)
that publish or clean up the gh-pages branch (the job that pushes/writes
gh-pages), so only those jobs use concurrency(group: gh-pages,
cancel-in-progress: false) while the rest of the workflow
(install/typecheck/test/build) runs normally.
🪄 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: 78c2fe9d-742d-41ec-aa0a-63aee78c5ef2

📥 Commits

Reviewing files that changed from the base of the PR and between d58de29 and 70be352.

📒 Files selected for processing (7)
  • .github/workflows/pages.yml
  • .github/workflows/pr-preview.yml
  • web/README.md
  • web/ts/app.ts
  • web/ts/schema.ts
  • web/ts/state.test.ts
  • web/ts/state.ts
💤 Files with no reviewable changes (1)
  • web/ts/schema.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • .github/workflows/pages.yml
  • web/ts/state.ts
  • web/ts/state.test.ts
  • web/ts/app.ts

Comment on lines +23 to +26
# Shared with pages.yml so all gh-pages pushes (root / develop / pr-preview)
# serialise instead of racing. No cancel: never abort an in-flight deploy.
group: gh-pages
cancel-in-progress: false
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 | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

In GitHub Actions, does a top-level concurrencyblock serialize the entire workflow run (including build/test jobs), and what is the recommended way to serialize only the deployment job that pushes togh-pages?

💡 Result:

Yes, a top-level concurrency block in GitHub Actions serializes the entire workflow run, including all jobs within it [1][2]. When this block is defined at the workflow level, GitHub Actions ensures that only one instance of that workflow with the specified concurrency group can run at a time [1][3]. If a new run is triggered while another is in progress, the default behavior is to cancel the in-progress run [1][2]. To serialize only a specific deployment job (such as one pushing to gh-pages) without affecting the execution of your build and test jobs, you should define the concurrency block at the job level rather than the workflow level [1][2]. Recommended approach: 1. Move the concurrency block from the top level of your workflow file to the specific job responsible for deployment [1][2]. 2. Use a unique group name that represents the deployment target (e.g., gh-pages or pages-deploy). 3. Set cancel-in-progress: true if you want to ensure that only the latest deployment proceeds, or omit it/set it to false if you prefer to queue them [1][3][4]. Example configuration: jobs: build: runs-on: ubuntu-latest steps: - run: echo "Building..." deploy: needs: build runs-on: ubuntu-latest concurrency: group: gh-pages-deployment cancel-in-progress: true steps: - name: Deploy to GitHub Pages run: echo "Deploying..." By isolating the concurrency block to the deploy job, your build and test jobs can continue to run in parallel for every push, while the deployment process remains serialized to prevent conflicting writes to your deployment branch [1][2][4].

Citations:


Scope gh-pages concurrency to deploy/cleanup, not the whole workflow.

Workflow-level concurrency in .github/workflows/pr-preview.yml queues/serializes the entire PR-preview run (including npm ci/typecheck/test/build), not just the gh-pages publish portion. If the intent is only to avoid races on the gh-pages branch, move the concurrency block to the specific job that pushes/writes gh-pages (and/or the corresponding cleanup), keeping a group scoped to that deployment.

# Shared with pages.yml so all gh-pages pushes (root / develop / pr-preview)
# serialise instead of racing. No cancel: never abort an in-flight deploy.
group: gh-pages
cancel-in-progress: false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pr-preview.yml around lines 23 - 26, The workflow-level
concurrency block (group: gh-pages / cancel-in-progress: false) is serializing
the entire PR-preview run; move this concurrency configuration off the top-level
and onto only the specific job(s) that publish or clean up the gh-pages branch
(the job that pushes/writes gh-pages), so only those jobs use concurrency(group:
gh-pages, cancel-in-progress: false) while the rest of the workflow
(install/typecheck/test/build) runs normally.

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