Water Softener is configured as a submeter (parent_meter_id) of Main Hotel
Water, but estimates.js, reports.js, and internal.js's cost + estimate
routes summed every active meter flatly regardless of parent/child, double-
counting the softener's consumption on top of the main meter's own reading.
Sub-meter rows are still shown individually (useful for diagnostics), just
excluded from the category subtotal/total sums, with a 'submeter' badge on
the row explaining why. Fixes the water figures on Estimates, Reports, and
downstream consumers of internal.js (Directors report, Weekly Actuals).
iOS Safari has no beforeinstallprompt API, so the portal's Install
button (?install=1) silently did nothing there. IosInstallHint now
shows Share -> Add to Home Screen steps on Safari, or a prompt to
switch to Safari first if opened in another iOS browser/in-app
webview (those can't install PWAs on iOS at all).
Also added apple-mobile-web-app-title + apple-touch-icon so the
home screen icon isn't a page screenshot, and added the
beforeinstallprompt handler that was missing entirely (Android
Install button did nothing either).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Consumes the existing /history endpoint (raw/hourly/daily-reading
telemetry) to show a consumption chart with a range picker, for
meters that have an mqtt_topic set.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The water-softener totaliser publishes litres but the water category is
m3 — without a conversion factor its readings would land in the DB
1000x too high. Also, mqtt_topic was only settable via direct SQL since
the Meters form never exposed it.
Reports and Estimates now group meters by category with a per-category
subtotal (in that category's own unit) instead of one flat list — the
grand-total row no longer sums consumption across categories, since
mixing kWh/m3/L is meaningless; money totals still sum fine.
On Reports: CCL, RAB levy and fixed extras collapse into one "Extras"
column/stat (still separately editable/stored, just merged for
display). New "Basis" column classifies how each meter's figure was
derived — Complete, Distributed (interpolated across a sparse-reading
gap), Up to date / As of [date] (real data short of period end), or
No data — via a new classifyBasis() in cost-calc.js.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Gas meters are read in raw volume (ft3 on our imperial meter, m3 on
newer metric ones), never kWh — but tariff rates and CCL/RAB levy are
all p/kWh. Add gas_volume_unit + gas_calorific_value to meters, and
convert reading deltas to kWh in cost-calc.js using the standard
UK gas-billing formula (matches the exact calculation printed on
supplier invoices) before any cost math runs.
Also fixes meter/readings pages showing raw gas dial readings
mislabeled as "kWh" — they now show the actual physical unit
(ft³/m³) via a new reading_unit_label, while computed consumption
stays correctly labeled kWh.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RAB levy funds nuclear electricity generation, so it can't apply to
gas/oil/water tariffs. Hide the field for non-electric categories and
clear any stray value if a tariff's category changes or is saved as
non-electric.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The June electricity bill showed £252.72 (7.5% of the bill) in charges the
cost model didn't account for: Nuclear RAB Levy, Metering Charge, and DUoS
Availability/Excess/Reactive. RAB levy and metering charge are modeled
directly (same shape as CCL / standing charge); the DUoS charges need kVA
capacity and kVArh reactive energy that only the supplier's own meter reads,
so they're covered by one adjustable "other network charges" £/month field
to update manually from each bill.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rate windows were matched by substring (label.includes(key)), so
overlapping labels like "Day"/"Weekday" could silently cross-match to
the wrong rate. Switched to an exact match.
The "split must total 100%" check was advisory-only in the UI and
never validated server-side, letting a tariff save with a split that
doesn't sum to 100% and permanently mis-cost that meter's usage.
Enforced on both the API (POST/PATCH /api/tariffs) and the Save
button.
Also aligned the app's portal category to 'Hotel' (already correct in
auth/src/db.js) here and in stack-init/install-stack.sh, which had
both drifted to 'Operations'.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rate changes are often scheduled ahead of time or confirmed by finance
after the fact and backdated — assign-tariff already supported any
effective_from date, but cost-calc previously priced a whole period at
a single tariff (whichever was in force on the period-end date),
silently mispricing the days on the other side of a change.
getTariffSegments() finds every tariff assignment overlapping a date
range; getMeterCostForPeriod/getMeterEstimateForPeriod now split
consumption pro-rata by day count across segments and price each
against its own tariff, summing to the period total. Estimates project
the remaining days across a future-scheduled change the same way,
rather than assuming today's tariff holds for the rest of the period.
Also consolidates internal.js's cost/estimate routes (previously their
own duplicate calc) onto the same shared functions reports.js and
estimates.js use, so the Directors' report can't drift from the in-app
numbers.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>