Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ The tab bar is Start, Compare, Usage, Bill?, Free? and Solar+?. Hovering a tab s

### Bill?

- Upload an Amber bill (PDF or text). The app loads the bill period's meter data and checks every line against it: usage, peak demand and demand charges, daily supply, subscription, GST, the charges total, solar exports and export credits.
- Upload an Amber bill (PDF or text). The app loads the bill period's meter data for the check (leaving the dates at the top and the Compare results as they are) and checks every line against it: usage, peak demand and demand charges, daily supply, subscription, GST, the charges total, solar exports and export credits.

### Free?

Expand Down
2 changes: 1 addition & 1 deletion css/app.css

Large diffs are not rendered by default.

15 changes: 8 additions & 7 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,14 @@ Two checks keep this honest. ESLint's `no-undef` reports a call to another file'
| `solar.js` | ~220 | Towns, PVGIS weather loading, sun position, panel output per half hour (tilt, facing, shade, temperature, inverter clipping) |
| `planner.js` | ~1,480 | Solar+? engine: the half-hourly year, plan rate cards, exact costing, flows, existing-battery detection and unwinding, battery simulation, the search, finance and ratings |
| `planner-ui.js` | ~1,190 | Solar+? tab UI: inputs and settings, running the search, every results card |
| `app-core.js` | ~180 | What the page controller's files share: `state`, `Amber.app`, `$`, `storedJson`, the selected site and state, Amber's fixed charges (`loadAmberChargeFields`, `readAmberRates`), `fmtDate`, the message under the dates bar |
| `app-core.js` | ~200 | What the page controller's files share: `state`, `Amber.app`, `$`, `storedJson`, the selected site and state, Amber's fixed charges (`loadAmberChargeFields`, `readAmberRates`), `fmtDate`, the message under the dates bar, tables in their own sideways-scrolling box (`scrollBox`, `syncScrollHints`) |
| `app-plan-form.js` | ~620 | The plan form on Compare: TOU and feed-in windows, the plan picker (`updatePlanSelector`, `selectSupplierPlan`), `createPlanObjectFromForm`, `applyPlanTemplate`, saved settings (`saveAllSettings`, `loadAllSettings`), custom plans |
| `app-results.js` | ~680 | Showing results: the answer (`displayResults`), every plan (`renderAllPlansTable`), savings in the plan picker (`updatePlanSavings`), month by month (`loadMonthlyData`, `renderMonthly`), the Usage tab's daily graph, day picker and More Stats, `hideResults` |
| `app-results.js` | ~690 | Showing results: the answer (`displayResults`), every plan (`renderAllPlansTable`), savings in the plan picker (`updatePlanSavings`), month by month (`loadMonthlyData`, `renderMonthly`), the Usage tab's daily graph, day picker and More Stats, `hideResults` |
| `app-imports.js` | ~450 | Sites and imports: the site picker (`populateSites`, `syncSiteMode`), imported households (`loadLocalSites`, `renderLocalSites`), Amber CSV import (`handleUsageCsvFile`), NEM12 (`handleNem12File`, `renderNem12Options`, `buildNem12Estimate`) |
| `app-bill.js` | ~260 | An Amber bill: reading the file and filling Amber's charges from it (`handleAmberBillFile`, `applyAmberBillRates`), and the Bill? tab (`ensureBillLoaded`, `renderBillCheck`) |
| `app-bill.js` | ~300 | An Amber bill: reading the file and filling Amber's charges from it (`handleAmberBillFile`, `applyAmberBillRates`), and the Bill? tab, which loads the bill period's own readings (`ensureBillLoaded`, `billChannelData`, `renderBillCheck`) |
| `app-free.js` | ~250 | The Free? tab: its settings, `renderBatteryShift`, `drawBatteryShift` |
| `app-settings.js` | ~120 | The settings menu: `buildSettingsMenu`, `clearSavedReadings`, deleting custom plans |
| `app.js` | ~700 | The top of the controller, loaded last: tabs (`selectTab`), the Start tab's welcome and account fields, the dates bar and Download menu, `fetchAndCompare`, `recalculateAndShow`, `init()`, `Amber.appApi` |
| `app.js` | ~720 | The top of the controller, loaded last: tabs (`selectTab`), the Start tab's welcome and account fields, the dates bar and Download menu, `loadReadings` (the cache, then Amber), `fetchAndCompare`, `recalculateAndShow`, `init()`, `Amber.appApi` |

## Data model

Expand Down Expand Up @@ -162,7 +162,7 @@ Missing days (up to 31) are filled from the nearest whole day on the same weekda

### Compare (`app.js`: `fetchAndCompare`; `app-results.js`: `displayResults`, `renderAllPlansTable`, `renderMonthly`; `app-plan-form.js`: the plan form)

1. Read the cache for the dates, fetch what's missing (`api.js`) and write final days back.
1. Read the cache for the dates, fetch what's missing (`api.js`) and write final days back (`loadReadings` in `app.js`, shared with the bill check).
2. `processUsageData` → channel totals; `applyAmberFeedInSettlement` (Ausgrid EA029 export settlement, one calendar month at a time via `Amber.monthsInRange`; Bill? settles the bill's own period with `billPeriod`).
3. `calculateOtherSupplierCosts` (the chosen plan), `calculateDemandTariff` / `calculateOtherDemandTariff` (monthly peaks), `precalculateDailySummaries` (daily rows and calendar).
4. Render the answer, every plan for the network (`computePlanTotals` per template), then month by month (`monthly.js`, after the main results).
Expand All @@ -173,7 +173,7 @@ Uses the same channel totals and daily summaries. Charts use the interval's loca

### Bill? (`bill.js`, `app-bill.js`)

Reads the bill (pdf.js is loaded on demand), loads that period's meter data (from the cache or API), and `compareBill` checks each line. The last result is kept in localStorage `amberBillCheck`.
Reads the bill (pdf.js is loaded on demand), loads that period's meter data (from the cache or API), and `compareBill` checks each line. The load is the bill check's own (`ensureBillLoaded` → `loadReadings` → `state.billData`): the dates bar and the Compare results are left alone, and results already on show are used when they cover the bill. The last result is kept in localStorage `amberBillCheck`.

### Free? (`redispatch.js`, `app-free.js`)

Expand Down Expand Up @@ -234,14 +234,15 @@ Saved JSON is read through `storedJson` (`app-core.js`) and `storedCustomPlans`

- The CSP (`index.html` `<meta http-equiv="Content-Security-Policy">`) is `default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self' https://api.amber.com.au; img-src 'self' data:; …`. This means no inline scripts, no inline `<style>` and no `style="…"` attributes (setting `element.style.x` from JS is fine), and no third-party hosts.
- All data stays in the browser. The API key is obfuscated (XOR with a fixed key, then base64), not encrypted.
- Text is formatted in one place each: dollars with `Amber.money` (`costs.js`: "$1,234.56", a proper minus sign, whole dollars on Solar+?), dates with `Amber.formatDate` (`time.js`: "31 Oct 2026"), plan rates with `Amber.formatCentsPerKwh` ("27.11 c/kWh"), and percentages with no space ("20%").
- Dynamic HTML is built with template strings. Anything from data or user input goes through `Amber.escapeHTML` (aliased `esc` in `planner-ui.js`), which escapes `& < > " '`, so it is safe inside an element and inside a quoted attribute.

## Build, test and CI

- `npm run build:css` rebuilds `css/app.css`. Tailwind scans `index.html` and `js/**/*.js` for class names, and `tailwind.config.js` safelists classes that are only built dynamically. Commit `css/app.css` after changing classes.
- **Lint**: `npm run lint` runs ESLint 10 with `eslint.config.js` (a flat config). The files in `js/` are checked as classic browser scripts, and the unit checks, scripts and specs as CommonJS. On top of ESLint's recommended set it reports unused variables, shadowed names, `var`, `==` and a `let` that never changes, and holds the house style (4-space indent, single quotes, semicolons, from `@stylistic/eslint-plugin`). One rule is the project's own: `local/no-extra-arguments` (`eslint-rules/no-extra-arguments.js`) reports a call that passes more arguments than the function takes, for functions and `Amber.*` functions defined in the same file. Rules are tuned to the code rather than the code restyled: the tests keep their own alignment of long asserts, and `preserve-caught-error` is off because network errors are replaced on purpose by plain messages.
- **Unit checks**: `tests/unit-*.js`, run with plain `node` (`npm run test:unit`). Each prints `ok:` / `FAIL:` lines and exits non-zero on failure. `unit-syntax.js` runs first: it parses every `js/*.js` (no other unit script loads the DOM files such as `help.js` and `app.js`), checks that `index.html` loads each of them once, with `app.js` last, and checks the calls between the app files through `Amber.app`. `unit-planner.js` holds the parity checks: the planner's cost equals Compare's, battery energy balances, finance and ratings.
- **End-to-end**: `tests/*.spec.js` with Playwright (Chromium) against `scripts/static-server.js` on port 4173 (`playwright.config.js`, `reuseExistingServer: true`). Tests mock Amber's API with `page.route('**/api.amber.com.au/**', …)` and build NEM12 files in code. About 66 tests take about a minute with two workers.
- **End-to-end**: `tests/*.spec.js` with Playwright (Chromium) against `scripts/static-server.js` on port 4173 (`playwright.config.js`, `reuseExistingServer: true`). Tests mock Amber's API with `page.route('**/api.amber.com.au/**', …)` and build NEM12 files in code. About 69 tests take about a minute with two workers.
- **CI**: `.github/workflows/test.yml` runs `npm ci`, `npm run lint`, installs Chromium and runs `npm test -- --workers=2` on every pull request and on pushes to `main`. Failed runs upload `test-results/`.

## Releases
Expand Down
11 changes: 7 additions & 4 deletions docs/FRONTEND.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ main six <section role="tabpanel" id="panel-…" data-panel="…">, one
footer #disclaimerShort, <details id="disclaimer">, ko-fi link
```

The layout is a single centred column (`max-w-4xl`) of white cards (`bg-white rounded-lg shadow-md p-6`) on `bg-gray-100`. The accent colour is Tailwind indigo, with green/red for savings and costs, and amber/orange for warnings.
The layout is a single centred column (`max-w-5xl`, wide enough for the eleven-column month table on a computer) of white cards (`bg-white rounded-lg shadow-md p-6`) on `bg-gray-100`. The accent colour is Tailwind indigo, with green/red for savings and costs, and amber/orange for warnings.

### Tabs

Expand All @@ -49,6 +49,9 @@ Tab behaviour (`app.js` `selectTab`, `setupTabs`): `aria-selected`, roving `tabi
| `data-preset` | Date quick picks (`7d`, `thisMonth`, `lastMonth`, `3m`, `90d`, `12m`) |
| `data-jump`, `data-jump-nav` | In-page jump links |
| `data-results-placeholder` | "Nothing here yet" blocks |
| `data-scroll-box`, `data-scroll-hint` | A table's own sideways-scrolling box and the note under it, shown only while the table overflows (`Amber.scrollBox`, `Amber.syncScrollHints`) |
| `data-tight` | Set on a `data-scroll-box` whose table doesn't fit. Cells with a `group-data-[tight]:` class then give way first (the month table's Difference column wraps, a long plan name in "Every plan" may break), and the note shows only if the table is still too wide |
| `data-compact-only` | Lines shown only on a phone, repeating a column that is hidden there (left out of the table CSV) |
| `data-help`, `data-help-label` | Help bubbles (key into `Amber.HELP`, or plain text; label for screen readers) |
| `data-tou-period`, `data-add-tou-window`, `data-remove-window` | TOU plan form |
| `data-date`, `data-month-key` | Calendar days and months |
Expand Down Expand Up @@ -92,7 +95,7 @@ Text from data or the user goes through `Amber.escapeHTML` (`esc`). Keep that wh
## Responsive behaviour

- The tab bar stays on one line from 320 px up and scrolls sideways (tested at 320, 360 and 390 px), and the page itself never scrolls sideways (tested).
- Wide tables sit in cards with `overflow-x-auto` and scroll inside the card on a phone. The planner's options table (7 columns) and month table are the widest.
- On a computer (1024 px and wider) every Compare and Free? table fits its card. On a phone the answer table hides its When and kWh columns and shows them under What, so both prices show without swiping; "Every plan" fits too. How wide text is depends on the computer's or phone's own font (the app uses the system font), so a table that is a little too wide first lets one column wrap (`data-tight`) before it scrolls; the layout tests pass with fonts as wide as DejaVu Sans, the default on GitHub's Ubuntu runners. The eleven-column month table and the Free? tables scroll inside their own box (`data-scroll-box`) with the first column staying in view and a note saying there is more to the side. The Solar+? tables still scroll inside their cards (the options table has 7 columns).
- Planner inputs are grids that collapse to one column (`grid-cols-1 sm:grid-cols-2/3`).

## Accessibility today
Expand All @@ -110,14 +113,14 @@ The app is written for everyday Australian householders:
- Plain English with short sentences. Say what something means for the reader ("Pays back in 6.7 years"), not jargon ("ROI").
- Australian spelling and units: kWh, kW, c/kWh, $ with commas.
- Every input and table heading gets a help bubble. The jargon buster explains the terms that can't be avoided (feed-in, NEM12, controlled load, demand charge, TOU).
- **Known inconsistency**: percentages are "20%" in most of the app but "20 %" (with a space) in the Solar+? tab. Pick one in a refresh.

## Known UI issues and opportunities

- **Solar+? is long and text-heavy.** Results stack as: recommendation, rating, battery check, options, month table, breakdown, check the numbers, payback, assumptions. Many paragraphs are full sentences of figures. Candidates: a summary strip of 3–4 key numbers, progressive disclosure (`<details>`) for the breakdown, numbers and assumptions, and a sticky "option on show" header.
- **Inputs on Solar+?** are spread over several `<details>` groups. "Prices and rebates" has 18 numeric fields in one grid.
- **Compare's plan form** (flat / TOU / feed-in windows / demand) is dense and shown under the results, and custom plans are edited in place.
- **Tables** are styled inline in several places with small differences. A shared table component (CSS classes in `input.css`) would make them consistent.
- **Tables** are styled inline in several places with small differences. A shared table component (CSS classes in `input.css`) would make them consistent. The Solar+? tables don't use `Amber.scrollBox` yet.
- **Tab names** (Bill?, Free?, Solar+?) are short on purpose: longer names on a computer were tried and dropped in v1.53. The full name is the tab's hover text.
- **Charts**: the payback chart's x labels rotate on narrow screens. The Usage charts would benefit from a legend and colour review. Chart colours are hard-coded hex values in `charts.js` and `planner-ui.js`.
- **Visual hierarchy**: every card has the same weight; the main answer on each tab could stand out more.
- **Help bubbles** are everywhere, which helps beginners but adds visual noise. Consider showing them only on hover or focus on wider screens.
Expand Down
6 changes: 3 additions & 3 deletions docs/REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Notes for anyone reviewing this codebase: what it must keep doing, where mistake
npm install
npm run build:css
npm run lint # ESLint
npm test # ~11 unit scripts, then ~66 Playwright tests (~60 s with --workers=2)
npm test # ~11 unit scripts, then ~69 Playwright tests (~60 s with --workers=2)
npm run serve # http://127.0.0.1:4173
```

Expand Down Expand Up @@ -52,7 +52,6 @@ These are the properties the tests guard and that users rely on. A change that b
- **Vendored libraries** are pinned copies: Chart.js 4.4.2, chartjs-plugin-zoom 2.0.1, pdf.js 3.11.174. Updating them is manual.
- **The price-update workflow** uses Node 20 actions, which GitHub is deprecating (a warning only). `test.yml` already uses Node 22.
- **The API key** is obfuscated, not encrypted. That's fine for a browser-only tool, but it shouldn't be described as secure.
- **Number formatting** varies: percentages are "20%" in most places but "20 %" on Solar+?. Money uses `toLocaleString('en-AU')` in some places and `toFixed` in others.

## Questions worth a reviewer's opinion

Expand Down Expand Up @@ -80,6 +79,7 @@ These are the properties the tests guard and that users rely on. A change that b
| `unit-review.js` | Regression checks from an earlier review: household caps across channels, export settlement, missing vs zero days, CSV backup timestamps |
| `autoload`, `cache`, `import`, `review` specs | Opening state, the cache, CSV import and backup round trip |
| `ui`, `tabs`, `graphs`, `stats`, `performance` specs | Tabs, help bubbles, disclaimer, phone layout, charts, More Stats, fetch behaviour |
| `billcheck`, `shift`, `v140` specs | Bill?, Free?, financial-year rate switching |
| `billcheck`, `shift`, `v140` specs | Bill? (which loads its own period and leaves the dates alone), Free?, financial-year rate switching |
| `v165` spec | Tables fitting a computer screen, the phone layout of the answer, one money/date/rate format, the table CSV |
| `v162` spec | Saved plan prices refreshed when a built-in plan changes, the answer and month by month agreeing on Amber's total, clearing saved readings, no maintainer tools in the menu, plan names with quotes or tags, changing state (and a site outside NSW), the bill check's pointer to Start, damaged saved settings, a Compare when the readings can't be saved, Escape closing the menus |
| `nem12`, `planner` specs | The NEM12 path and every Solar+? flow |
Loading
Loading