From fd10d9fbd28bb5939465a80aed3c246962617174 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 23:35:28 +0000 Subject: [PATCH] v1.61: documentation for code review and a frontend refresh - README restructured: getting started, what each tab does, where the numbers come from, privacy, developer commands and links to the new docs. - docs/ARCHITECTURE.md: script load order and the Amber namespace, a module map, the data model (sites, usage items, the cache, plan configs, the planner's year), how each tab works, the planner engine, storage keys, external data, CSP, build, tests, CI and releases. - docs/FRONTEND.md: what a redesign must keep (no JS build, CSP, prebuilt Tailwind, IDs and data attributes the JS and tests use, help keys, independence from Amber's branding), the page and tab inventory, styling, JS-built UI, responsive and accessibility notes, writing style and known UI issues. - docs/REVIEW.md: invariants, risk areas, known technical debt, questions for reviewers and a test map. - CLAUDE.md: short working rules for AI coding agents. - Header comments for the modules that had none (api, app, bill, calendar, charts, constants, costs, csv, monthly, stats, storage, templates, time). No behaviour changes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L7aZZe8i8qyWD6cwhnihsr --- CLAUDE.md | 44 +++++++++ README.md | 139 +++++++++++++++++--------- docs/ARCHITECTURE.md | 228 +++++++++++++++++++++++++++++++++++++++++++ docs/FRONTEND.md | 134 +++++++++++++++++++++++++ docs/REVIEW.md | 83 ++++++++++++++++ index.html | 48 ++++----- js/api.js | 7 ++ js/app.js | 10 ++ js/bill.js | 8 ++ js/calendar.js | 7 ++ js/charts.js | 8 ++ js/constants.js | 10 +- js/costs.js | 14 +++ js/csv.js | 5 + js/monthly.js | 6 ++ js/stats.js | 6 ++ js/storage.js | 8 ++ js/templates.js | 7 ++ js/time.js | 7 ++ tests/unit-time.js | 2 +- 20 files changed, 708 insertions(+), 73 deletions(-) create mode 100644 CLAUDE.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/FRONTEND.md create mode 100644 docs/REVIEW.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..13dd7c3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,44 @@ +# Working on Amber Compare & Export + +A browser-only tool for Australian electricity bills (Amber Electric vs other plans, bill checks, a solar and battery planner). Read these first: + +- [README.md](README.md): what the app does, for users +- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): modules, data model, each tab's flow, the planner engine, storage, releases +- [docs/FRONTEND.md](docs/FRONTEND.md): the UI contract (IDs, data attributes, help keys, CSP) and known UI issues +- [docs/REVIEW.md](docs/REVIEW.md): invariants, risk areas, technical debt, test map + +## Commands + +```bash +npm install +npm run build:css # after changing any Tailwind class in index.html or js/; commit css/app.css +npm run test:unit # Node unit checks (fast) +npm test # unit checks + Playwright (starts scripts/static-server.js on :4173) +npx playwright test tests/planner.spec.js --workers=2 # one spec +npm run serve # http://127.0.0.1:4173 +``` + +If Playwright can't find its browser, point it at an installed Chromium with `launchOptions.executablePath` in a local config, not by editing `playwright.config.js`. Don't leave your own server running on port 4173 during a test run: Playwright reuses it. + +## Rules of the codebase + +- **No build step for JS, no framework, no ES modules.** Each `js/*.js` is an IIFE adding to `window.Amber`; the load order in `index.html` matters (`app.js` is last). New files need a ` - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/js/api.js b/js/api.js index a97087d..69ee7de 100644 --- a/js/api.js +++ b/js/api.js @@ -1,6 +1,13 @@ (function (root) { const Amber = root.Amber = root.Amber || {}; + /* + * Amber API client (https://api.amber.com.au/v1): the site list, and usage for a date range + * fetched in chunks of Amber.FETCH_CHUNK_DAYS, a few at a time (mapPool). Every request has a + * timeout; 429s, server errors and network failures are retried after Amber.RETRY_DELAYS_MS. + * The caller (app.js fetchAndCompare) caches what comes back (storage.js). + */ + Amber.sleep = function (ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }; diff --git a/js/app.js b/js/app.js index 3148513..aa41b0c 100644 --- a/js/app.js +++ b/js/app.js @@ -1,6 +1,16 @@ (function (root) { const Amber = root.Amber = root.Amber || {}; + /* + * The page controller for everything except the Solar+? tab (planner-ui.js): start-up (init), + * tabs, the Start tab (API key, sites, Amber's fixed charges, Amber CSV and NEM12 imports), the + * dates bar, Compare (the plan form, fetchAndCompare, the answer, every plan, month by month), + * Usage (charts, calendar, More Stats), Bill?, Free? (the re-scheduling estimate), downloads and + * the settings menu. Pricing lives in costs.js / monthly.js; this file reads inputs, calls it + * and renders the results. Module state is in `state` (exposed as Amber._appState for tests); + * Amber.appApi is what planner-ui.js may use. See docs/ARCHITECTURE.md. + */ + const state = { templates: Amber.cloneTemplates(), originalTemplates: Amber.cloneTemplates(), diff --git a/js/bill.js b/js/bill.js index e8da3d1..b28bb93 100644 --- a/js/bill.js +++ b/js/bill.js @@ -1,6 +1,14 @@ (function (root) { const Amber = root.Amber = root.Amber || {}; + /* + * Amber bills: the text of a PDF (pdf.js, loaded on demand) or a text file is read into the bill + * period, Amber's fixed rates (connection, subscription, demand) and the line items + * (parseAmberBillText, parseBillCheck). compareBill then checks each line against the meter + * data for the same period. Used by the Bill? tab, and on Start to fill Amber's fixed charges. + * Layout-dependent: if a bill can't be read, the caller shows the text that was read. + */ + function roundCents(n) { if (!Number.isFinite(n)) return null; return Math.round(n * 1000) / 1000; diff --git a/js/calendar.js b/js/calendar.js index a5ab0fe..e7c74fc 100644 --- a/js/calendar.js +++ b/js/calendar.js @@ -1,6 +1,13 @@ (function (root) { const Amber = root.Amber = root.Amber || {}; + /* + * The Usage tab's calendar: a grid per month of the days in the selected period, with the + * day's cost on Amber and on the plan compared with in a tooltip (and Amber's demand peaks); + * clicking a day opens it on the daily graph. Days without readings say so rather than + * showing zero. + */ + Amber.hideTooltip = function () { const tooltip = document.getElementById('day-tooltip'); if (tooltip) tooltip.remove(); diff --git a/js/charts.js b/js/charts.js index a60321a..e07b2ce 100644 --- a/js/charts.js +++ b/js/charts.js @@ -1,6 +1,14 @@ (function (root) { const Amber = root.Amber = root.Amber || {}; + /* + * The Usage tab's Chart.js charts: the 24-hour average day (displayAverageGraph) and one day + * up close (displayDailyGraph), per channel (general, controlled load, feed-in) with Amber's + * price and the compared plan's c/kWh in the tooltip, on local time and the real interval + * length, with zoom and pan (chartjs-plugin-zoom). registerChartPlugins adds the cursor + * tooltip and the hover highlight once. + */ + Amber.registerChartPlugins = function () { if (!root.Chart || Amber._chartPluginsRegistered) return; root.Chart.Tooltip.positioners.cursor = function (items, eventPosition) { diff --git a/js/constants.js b/js/constants.js index ac244ae..bb1f1e0 100644 --- a/js/constants.js +++ b/js/constants.js @@ -1,6 +1,14 @@ (function (root) { const Amber = root.Amber = root.Amber || {}; + /* + * App-wide settings and reference data: Amber API limits, the app version (APP_VERSION, also + * in index.html and tests/unit-time.js), Amber's default fixed charges for this financial year + * and last (split at AMBER_RATES_CHANGE_DATE), state time zones, network names and matching + * plans to a site's network, and the company behind each plan (planCompany). Update each + * 1 July with templates.js and networks.js. + */ + Amber.API_URL = 'https://api.amber.com.au/v1'; Amber.FETCH_CHUNK_DAYS = 7; Amber.FETCH_CONCURRENCY = 3; @@ -12,7 +20,7 @@ // Compare "This month" when the page opens with a saved API key (a test can preset // window.Amber.AUTO_COMPARE = false to start idle). Amber.AUTO_COMPARE = Amber.AUTO_COMPARE !== false; - Amber.APP_VERSION = '1.60'; + Amber.APP_VERSION = '1.61'; Amber.TEMPLATES_AS_AT = '2026-07-01'; // All editable rates (plans, custom, Amber fixed charges) are GST-inclusive, // matching Energy Made Easy, DMO/VDO, retailer fact sheets and Amber perKwh. diff --git a/js/costs.js b/js/costs.js index f42d114..b4fbe2d 100644 --- a/js/costs.js +++ b/js/costs.js @@ -1,6 +1,20 @@ (function (root) { const Amber = root.Amber = root.Amber || {}; + /* + * Pricing a period of meter data. processUsageData turns API-shaped usage items into + * per-channel totals (with each interval kept for re-pricing); from those: + * - Amber: interval prices, the Ausgrid EA029 export settlement (applyAmberFeedInSettlement), + * demand per calendar month (calculateDemandTariff), daily connection and subscription + * for the year each day falls in (amberRatesOn); + * - another plan (calculateOtherSupplierCosts, computePlanTotals): flat or time-of-use rates, + * free windows with daily caps, controlled load, flat, windowed and capped feed-in (caps + * shared across channels: householdCappedSplits), demand (calculateOtherDemandTariff); + * - daily rows for the calendar and CSV (precalculateDailySummaries). + * Compare, month by month (monthly.js), Bill Check and Free? all price through here; the + * planner's half-hourly costing is checked against computePlanTotals (tests/unit-planner.js). + */ + Amber.absKwh = function (value) { const n = parseFloat(value); if (!Number.isFinite(n)) return 0; diff --git a/js/csv.js b/js/csv.js index a119585..98e1d78 100644 --- a/js/csv.js +++ b/js/csv.js @@ -1,6 +1,11 @@ (function (root) { const Amber = root.Amber = root.Amber || {}; + /* + * CSV downloads: every meter reading (a versioned file that the Amber CSV import on Start can + * restore as a backup), one row per day, or the comparison table as shown. + */ + Amber.downloadCSV = function (csvContent, fileName) { const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); diff --git a/js/monthly.js b/js/monthly.js index b7c7f16..d7894b1 100644 --- a/js/monthly.js +++ b/js/monthly.js @@ -1,6 +1,12 @@ (function (root) { const Amber = root.Amber = root.Amber || {}; + /* + * Month by month on Compare: Amber and the compared plan for each calendar month (and any + * period), worked out the same way as the main comparison (feed-in settlement, monthly demand, + * daily charges for every day). + */ + function daysInMonth(monthKey) { const y = parseInt(monthKey.substring(0, 4), 10); const m = parseInt(monthKey.substring(5, 7), 10); diff --git a/js/stats.js b/js/stats.js index d18da7c..f8c6a71 100644 --- a/js/stats.js +++ b/js/stats.js @@ -1,6 +1,12 @@ (function (root) { const Amber = root.Amber = root.Amber || {}; + /* + * More Stats on the Usage tab: average import and feed-in prices, power bought and sent out + * during price spikes (above STATS_DEFAULTS.spikeCents) and at negative prices, the evening + * share of imports, and days with no grid use. + */ + Amber.STATS_DEFAULTS = { spikeCents: 50 }; diff --git a/js/storage.js b/js/storage.js index 02b2252..2af8a66 100644 --- a/js/storage.js +++ b/js/storage.js @@ -1,6 +1,14 @@ (function (root) { const Amber = root.Amber = root.Amber || {}; + /* + * Browser storage. IndexedDB AmberDataCacheDB / dailyUsage holds the meter readings, one record + * per site and NEM date (id "_"). Only final whole days are cached (with + * old empty days remembered), and estimated readings stay eligible for a refetch + * (cachedDayUsable). Also the API key (obfuscated, not encrypted; localStorage, or + * sessionStorage when not remembered) and escapeHTML for everything put into innerHTML. + */ + const DB_NAME = 'AmberDataCacheDB'; const STORE_NAME = 'dailyUsage'; const DB_VERSION = 1; diff --git a/js/templates.js b/js/templates.js index 6a4ac00..7534962 100644 --- a/js/templates.js +++ b/js/templates.js @@ -1,6 +1,13 @@ (function (root) { const Amber = root.Amber = root.Amber || {}; + /* + * Built-in plans to compare with, by state: default offers (DMO, VDO), market offers, free-window + * plans and the Solar Sharer Offer, each with the date its prices are as at. The shape of a plan + * is in docs/ARCHITECTURE.md (Plan configs). Prices are typed in by hand from published fact + * sheets: update each 1 July (default offers) and when retailers change their offers. + */ + const ALL_DAYS = [1, 2, 3, 4, 5, 6, 0]; const WEEKDAYS = [1, 2, 3, 4, 5]; const AS_AT = '2026-07-01'; diff --git a/js/time.js b/js/time.js index 2ce6403..86e6ec2 100644 --- a/js/time.js +++ b/js/time.js @@ -1,6 +1,13 @@ (function (root) { const Amber = root.Amber = root.Amber || {}; + /* + * Dates and times. Amber's nemTime is an interval's end in NEM time (UTC+10, no daylight + * saving); plans' windows are local wall-clock time (or NEM time with clock: 'nem'). Helpers + * here convert between them (getClockParts, localWallToNemIso), match intervals to time-of-use + * windows (timeInWindow, windowMatches), and do day arithmetic on YYYY-MM-DD strings. + */ + function pad(n) { return String(n).padStart(2, '0'); } diff --git a/tests/unit-time.js b/tests/unit-time.js index f55b1f8..0fba87a 100644 --- a/tests/unit-time.js +++ b/tests/unit-time.js @@ -78,7 +78,7 @@ const clockA = Amber.clockPartsForItem(clockItem, vicLocal, 'VIC'); const clockB = Amber.clockPartsForItem(clockItem, vicLocal, 'VIC'); assert(clockA === clockB, 'clock parts are cached on the usage item'); assert(clockA.hours === 17, 'cached Melbourne DST hour is 17'); -assert(Amber.APP_VERSION === '1.60', 'app version is 1.60'); +assert(Amber.APP_VERSION === '1.61', 'app version is 1.61'); assert(Amber.DEFAULT_AMBER_CONNECTION_CENTS === 125.367 && Amber.PREVIOUS_AMBER_CONNECTION_CENTS === 116.787, 'Amber daily connection defaults (2026-27 and 2025-26)'); assert(Amber.DEFAULT_AMBER_SUBSCRIPTION_CENTS === 82.181, 'Amber subscription default'); assert(Amber.DEFAULT_AMBER_DEMAND_CENTS === 43.431 && Amber.PREVIOUS_AMBER_DEMAND_CENTS === 42.345, 'Amber demand defaults (2026-27 and 2025-26)');