Entering a reading by hand previously meant POST /api/v1/readings with an
API key, or a one-row CSV through the import wizard. SourceType.Manual
existed in the enum but nothing was behind it. This adds the click path,
built for the case it is actually used in: walking to each manual meter
with a phone in hand.
"Add reading" on the Readings tab opens a dialog prefilled with the
meter's last register value and the current local time, both editable:
- An on-screen keypad, because a register is read standing at the meter.
It behaves like a calculator against the prefill - the first digit
replaces it (a fresh register), while backspace edits it in place,
which is the common case since only a register's last digits move.
- Typed input accepts both separators (last one wins), so a German and
an English phone keyboard both do the right thing. ReadingEntry owns
that rule and is unit-tested; it deliberately differs from
GermanNumber, where a lone dot really is a thousands separator.
- A live parsed-value echo plus delta-since-last, which is the net that
catches a mistyped digit before it is committed.
- Decrease / replaces-existing / future / backdated surfaced before
saving, and DST spring-forward gaps refused rather than shifted.
The verdict line sits in a fixed-height, no-wrap slot above the keypad.
That is load-bearing, not cosmetic: an alert that appears there when the
value dips below the last reading moves the keys out from under the
user's thumb mid-entry, which is a guaranteed mistype on a phone. The
long-form explanation goes below the keypad, where reflow is harmless.
Saving goes through IngestionService.IngestByMeterAsync, so the
monotonic-decrease guard and inline renormalization apply exactly as for
any other ingest. A new optional quality parameter stamps the row
ReadingQuality.Manual; null preserves today's behaviour, so a source
re-reporting the same timestamp updates the value without silently
relabelling a hand-entered or imported reading.
Also: the meter-detail tabs now render times in the instance timezone
per SDD section 10, instead of raw UTC. Without it a reading entered at
18:00 reads back as 16:00. Side effect is that historic imported monthly
rows show 01:00/02:00 rather than 00:00 - correct, if noisier.
Claude-Session: https://claude.ai/code/session_01D4x3JbNKCSV4cBR9s7bJmX