Commit Graph
132 Commits
Author SHA1 Message Date
tajniak81andClaude Opus 5 b2d333a63f The charger was never asked what it is set to
The trigger buys telemetry and only telemetry, so a charger that has been
read a hundred times and commanded none reports amps, volts and nothing
else: no schedule, no balancing, no Modbus server, not even its firmware.
The message that asks for that half is 0040, and the reference keeps it
commented out because the app sends its timestamp without a value type.
The app is what the charger answers, so the oddity is reproduced rather
than corrected — sent when the settings half is missing or older than ten
minutes, waited four seconds for, and after three unanswered requests
still sent but no longer waited on.

The three settings the panel has and the writer did not — swipe up, swipe
down, smart touch — are writable now, which is all eleven of the 0100
commands. Nothing else in the map was missing: every named field of every
message was already decoded, and the raw keys the card shows are fields
the reference does not name either.

Both cards drop a row with nothing in it, which turned a charger that
reports only its ceiling into a charger that reports no current range at
all. Half a range is still a bound.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 23:05:40 +02:00
tajniak81andClaude Opus 5 3f9d5b943f Four questions asked of an account that answers twenty-one
The connector called ten endpoints of the read surface the map lists, and
the charger card showed four views. Everything else an EV charger can
reach is now a capability too: the sessions and the history, the savings,
the sharing, the binding, the group, the Wi-Fi, the firmware and its
update log, the tamper records, the site's own detail, price, networks
and energy — plus the vehicle catalogue, dynamic pricing, the currencies
and the notification views. Thirty-eight endpoints, one action each. The
two message views are GET, so the request path grew a GET half that shares
the login retry with the POST one.

The per-charger fan-out asks all of them, six at a time rather than one
after another, and a charger that belongs to a site brings that site's
four views with it once the by-serial lookup has found it. A view that
answers with nothing now says so instead of vanishing: the station record
is empty for a standalone charger because it has no station, which is an
answer worth reading. And "source 0" in the OCPP box carries the address
the account's endpoint list gives it.

Anker's account-level writes stay out, as do the endpoints whose payloads
were only ever read out of the app package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 22:53:36 +02:00
tajniak81andClaude Opus 5 fb42791f8d The card beside it had boxes, so this one gets boxes
Charger information was one long list of everything the service knows;
the readings card next to it had been splitting its fields into a box
per group all along. Same treatment here: Device, Status, Network and
On the account, plus the service's own fields and the per-charger views,
each in its own sunken section under a heading. Both clients, since the
web and the phone draw the same card.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 22:18:45 +02:00
tajniak81andClaude Opus 5 1a7f04cba0 A dash printed above the value it was missing
The Charger information card read "—" beside State and OCPP status while the raw
block three rows below it printed chargerStatus 1 and ocpp_connect_status 2. The
account had answered both. The merge asked for the state as evChargerStatus,
operating_state or status, which is how the standalone and station views spell
it, and the bound-device view — the one this account actually answers from —
spells it chargerStatus. The OCPP state it never asked that view for at all. All
three views now read through one fillDevice, which tries every spelling a view is
known to use, so a value any of them sends reaches the row that was drawing a
dash for want of it.

The same views were carrying the whole box-on-the-wall half unread: the Wi-Fi
network and its MAC, the signal strength, the Bluetooth MAC, the time zone, when
the account bound the charger, how the app can reach it — BLE, Wi-Fi — and the
product shot for the model, which now sits beside the charger's name in both
apps. Named rows, in three languages, the way the register map's readings are
named.

One field wanted the opposite treatment. The device record carries blue_password,
the charger's own Bluetooth pairing password, and the card was printing it in
clear into every screenshot anyone takes of that page. Any leaf key holding a
password, secret, token, private key or certificate is now masked in the raw
block: that the field exists is worth reporting, its value is not.

Four endpoints answer only when a serial is named, so none of them could belong
to the list the card is drawn from, and nothing had ever called them. The station
record, the charging totals, the OCPP backend and the RFID cards now arrive
through a charger-details capability behind
GET …/anker-solix/chargers/{sn}/details, asked for the charger being looked at,
best effort, each view reporting its own failure — an account that is not the
owner cannot read the cards, which is a fact about the account rather than an
error in the read.

Those four are shown under the cloud's own keys, and that is not an oversight.
The REST map documents which endpoints exist and what each is for; it does not
document a single one of their payloads. Naming those fields is the next commit,
made from what actually comes back, now that there is somewhere to see it.

Not touched: the endpoints the map marks ready but unwired — session history,
site price, OTA, sharing, notifications — each a feature rather than a row on this
card; and the unmapped ones, which the map warns delete sessions and unbind
devices with payloads nobody has ever seen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 21:45:08 +02:00
tajniak81andClaude Opus 5 8e2073fc4c The map knew the names the card was showing as hex
Every field in the cloud MQTT map that has a documented meaning now reads as a
named row, on the same labels the register map uses for the same quantities. The
raw block stays, and shrinks to what genuinely nobody has identified — which is
the only honest reason for a key like 0410.b9 to be on screen at all.

Three fields the reference decodes for nobody are decoded here. ac is where the
charge is coming from — off or paused, grid, solar — and it is called
chargingSource rather than chargingMode, because that name already belongs to a
Modbus register and the last time a cloud field borrowed one, d9 spent a release
reporting the wrong thing under the right name. b6 is the session's order id.
f1, f2 and f3 are the identity fields the reference marks multi-value: four bytes
read as the parts of a version, in the order they arrive, which is what the
account view's own firmware string looks like. If the panel shows those parts
reversed, the order is the thing to flip — it is the one assumption here that the
wire has not yet confirmed.

The rest was already decoded and simply never drawn. The readings card now shows
the session's start, its id, the charging source, whether a cable is in, the
charging window, and — since a reading is worth what its age is — the live-stream
flag and both stream clocks, because telemetry and settings arrive on different
messages with different triggers. Per-phase session energy joins the phase matrix
as a fourth column, appearing on the transport that counts a session and staying
away from the one that does not, exactly as the reactive and apparent pair does.
The settings block gains the fourteen the register map has no address for: plug
lock, auto restart, random delay, the schedule and its mode, the weekend window
and how the weekend is handled, the light-off schedule and window, the breaker
limit, the solar mode and its minimum current, automatic phase switching, the
three panel gestures, and what the two balancing features are watching — the
meter and monitor serials by name, their two unpinned numbers as the numbers they
are. A local network block says whether the charger's own Modbus server is on and
where, which is the answer the Modbus mode's setup screen otherwise has to be
given by hand. The device block gains the controller version.

A test now holds the line the projection quietly drew: every name in the message
maps must reach a snapshot field. A name added to a map without a field to land
in would otherwise surface in the raw block looking like something we understood.

Left raw: a1, the frame opener the charger echoes back; b7, which the map itself
calls unidentified; b9, bc and bd, which appear in no map; the five-minute 0400;
and 0857 — a message type the reference's closed inventory of fourteen does not
contain and this charger publishes anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 21:31:43 +02:00
tajniak81andClaude Opus 5 197ff73a39 The one card with a button is the one with a crooked arrow
Four of the charging cards fold from a header that is a single button: the title
at the left, the arrow hard against the right edge. The fifth has a refresh
button in its header, and that button was placed after the toggle — so the arrow
ended up a button's width in from the edge, alone among the five, and the eye
finds it by searching rather than by knowing where it is.

The header now spends its width the way the others do. The heading keeps the
title and stays the drag handle, the refresh button takes the place beside the
edge, and the arrow is its own control at the end of the row. It folds the card
exactly as the heading does, so nothing that worked before stops working; only
the order changed.

The same layout lives in the Phone App's _FoldCard, with the same fault, so the
arrow moves past the action slot there too. All five cards share that widget and
only this one passes an action, which is why the other four look identical
before and after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 21:01:24 +02:00
tajniak81andClaude Opus 5 2425a8d3d6 A field we have no name for is still a field it sent
Two cards on the Charging page were answering with a fraction of what the charger
and the account actually report, and in both the losses happened quietly, in a
parse that kept the fields it recognised and dropped the rest on the floor.

Charger information asked three account-wide views and kept fourteen fields.
A charger registered on its own is absent from the site view, which is the only
one of the three carrying state, charge power and OCPP status — so exactly the
charger that stands alone got the column of dashes, and nothing said why. The
per-charger station record, get_evcharger_station_info, is what the mobile app
opens when you tap a charger, and it is the one view that answers for a charger
outside a station; it is now the fourth view, asked per charger, a failure there
costing that charger's row and no more. Alongside it, every field each of the
four views sent is kept as attrs, under the cloud's own key, nested objects
joined with a dot and arrays carrying their index. First view to answer a key
wins, which is the rule the named fields already merged by. Two hundred keys and
two hundred and forty runes per value keep a station record with a session list
from becoming the whole card.

Charger readings lost data twice over. The frame decoder skipped any field byte
its per-message map could not name, and a message type with no map decoded to
nothing at all; those fields are now kept under the message and the byte they
arrived in — 0410.c9 — decoded but unscaled, because a factor is half of a
meaning and we do not have the other half. Then the projection read forty-odd
names into typed fields and dropped the remainder: sessionStartedAt, the
per-phase session energies, the three touch modes, the load-balance monitor and
its meter flag, the solar monitor. Those land in extra, and the list maintains
itself — the four accessors note every key they read, extra is what is left, and
a field modelled later stops appearing there without anyone remembering to
remove it.

Keeping unnamed fields had one consequence worth guarding. An unmapped message
now decodes to something rather than nothing, and ingest stamped settingsAt for
anything that was not telemetry — the timestamp a control command waits on to
say the charger acknowledged it. A frame we cannot read is not an
acknowledgement, so the stamp is now conditional on the message type being one
we map, while its fields are kept either way.

Both cards show the remainder as what it is: the service's own key, no unit, no
translation, no renaming, under a heading that says whose words these are. The
blocks appear only when there is something in them, so a Modbus charger's
readings card and a charger the cloud says nothing more about are unchanged.
Naming one of these fields is a later commit, made from evidence; inventing a
label for it today would only make a guess look settled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 20:58:15 +02:00
tajniak81andClaude Opus 5 1dc20461de A raw key, a bare number, and two columns of dashes
Switch the control mode to the Anker cloud and the charging cards still read as
though they were built for Modbus. Three separate causes, and none of them was
the shared layout — the cards already branch on the control mode in sixteen
places, which is why the address fields, the reset button and the skip-delay
button each appear only under the transport that has them.

The loudest was a missing string. The cloud transport arrived with three keys the
templates call and the language files never got: cloudNote, cloudLocalFound and
skipDelay. A key missing from English returns itself, deliberately, so that a gap
shows up in the UI instead of rendering as a blank — and it did, as
"charging.control.cloudNote" sitting in the connection card where a sentence
belongs. All three are added, in both surfaces and all three languages, so the
Phone App is not left showing the same raw key.

The second was an enum wearing one name over two transports. Modbus register
20087 reports the phase mode as 1 single-phase or 3 three-phase; the cloud's own
field reports 0 automatic or 1 single-phase. Only phaseMode1 and phaseMode3 had
labels, so a cloud charger sitting on automatic rendered "Running on 0" — the
enum fell back to printing the number, which is the right fallback and the wrong
answer. phaseMode0 is added. Worth naming the shape of this one: it is the same
collision that made the cloud's d9 field wrong when it was called chargingMode
after the register at 20088, and a third transport reporting a 3 that means
something else would break it again.

The third was honest but useless. Reactive and apparent power are registers of
their own and the cloud has no message carrying either, so over that transport
those two columns could only ever be three dashes each. They now appear when the
charger actually reports them, which also tidies up a Modbus charger whose
firmware leaves them out.

The layout stays shared. A value both transports report should keep one name and
one row, and what each transport can be told still branches where it has to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 20:22:50 +02:00
tajniak81andClaude Opus 5 90558d60b2 What the app can set, the cloud connection can set
The broker transport could move a session along — start, stop, boost, skip the
delay, cap the current — and nothing else. Everything the charger is actually
configured with sat one field away in the same messages we were already
decoding: the schedule it charges on, the plug lock, auto-start, the LED, load
balancing, solar charging, and the Modbus server the local transport depends on.
Readable, and unreachable.

The obstacle was never the cloud, it was the shape of the protocol. A setting is
not a register write. It is a *command*, and a command owns a set of fields
inside a message type — mostly one, but five own several, and the charger reads
the whole command as the new truth. A light-off schedule sent carrying only its
switch is a schedule whose start and end have just been set to midnight. So a
grouped write resends the siblings the caller did not name, using the values the
charger itself last reported, and refuses when it has never reported them. That
last part is not caution for its own sake: load balancing and solar charging
carry the serial of the meter they watch, and nothing outside the charger knows
it. An empty one would be adopted.

Those values do not arrive with the telemetry, either. The fast 0410 stream a
realtime trigger turns on carries none of them — the settings come on 0405, 0840
and 0900, which the charger sends when it has something to acknowledge. So a
grouped write may have to send a trigger first purely to make the charger talk
about itself, and says so plainly when even that produces nothing.

Everything a caller supplies is encoded before the cloud is touched at all. A
request naming one bad value changes nothing rather than half of what it asked
for, and a mistyped setting costs a validation error instead of a sign-in, a
certificate fetch and a broker connection to be told no.

mqttsettings.go holds one table and it is the only place a setting is defined:
the wire field, the name a caller uses, the state key its current value comes
from, and how a value becomes bytes. The names are the snapshot's own, so a
caller can read a status, change one entry and send it back. The existing limit
command now builds its frame from that table too rather than encoding field a8 a
second time.

Reading grew to match. The frame decoder gains the fields the grouped writes must
carry back — the two load-balance settings, both monitor serials, the solar
monitoring mode — plus the swipe gestures, and the snapshot exposes the rest of
what is now writable. One name was wrong and is corrected: field d9 was called
chargingMode after the Modbus register at 20088, but the reference has it as the
solar charging mode, so it becomes solarChargeMode and moves in beside the solar
settings. A mislabelled reading is bad; a mislabelled writable field is worse.

Over HTTP it is one action rather than a dozen, because the charger groups the
fields anyway: POST .../settings with a settings object, and settings sharing a
command travel in one frame instead of overwriting each other. The other two
transports refuse it by name and say which one has it, the way they already
refuse each other's commands. The audit trail records the values, not just that
a write happened — a setting that changes what the charger will draw, or whether
it answers on the LAN at all, is worth being able to trace afterwards.

Two things worth saying plainly. This is built from the reference project's
message maps and checked against its own frame layout, not against hardware —
there is no charger on this end to point it at. And modbusEnabled is a loaded
gun: writing it off stops the charger serving the register map, and the way back
is this transport, or the app.

The ignore rule for the local Modbus map artifact widens to the protocol maps
that now sit beside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 18:28:45 +02:00
tajniak81andClaude Opus 5 576df58776 Go the way the owner's phone already goes
Control had two transports and neither fitted the ordinary customer. OCPP waits
for the charger to dial in, which needs a public endpoint it can reach, a
certificate, and a firmware willing to talk to our CSMS. Modbus TCP dials the
charger, which needs the server on the charger's own network. Between them they
cover a charger we host and a charger we stand next to; the common case is a
charger behind someone else's router, and that had nothing.

It was never unreachable, though. The charger holds a connection open to Anker's
own broker — it is how the mobile app drives it from anywhere, and it is the
mqttStatus register the Modbus snapshot has been reporting all along. So a third
control mode joins that broker as the account: get_user_mqtt_info issues a client
certificate, mTLS to aiot-mqtt-eu.anker.com:8883, and commands go out on the same
topics the app publishes on. Nothing on the customer's side has to be forwarded,
addressed or certificated.

What travels is not an API call. The payload is a JSON envelope around a base64
binary frame the device itself speaks — marker, little-endian length, message
type, name/length/type/value fields, XOR checksum — so mqttframe.go is a codec
rather than a client, written from the message maps in anker-solix-api and
anchored on the one frame that project documents byte for byte. A frame whose
fields do not tile exactly up to the checksum is refused rather than half-read:
these arrive over a link we do not control, and a truncated frame must not read
as a charger reporting zeros.

Two of the charger's habits shape the rest. It publishes nothing unless asked, so
a status read arms a telemetry trigger and waits for the next frame, and a poll
inside that window answers from what has since arrived. And a broker connection
costs a fetched certificate and a TLS handshake while the plugin manager builds a
throwaway instance per request — so the connection lives on the account's shared
session beside the auth token, for exactly the reason the token lives there, and
closes itself after five idle minutes.

The transport also sees two signals no other one does: the boost flag, and the
plug and start countdowns. The package doc has said since the first commit that
they are never set and the derived mode must do without them. Here they are set,
so a charger that has been told to start and is counting down a delay says so
rather than sitting in "preparing", and "skip the delay" is offered only while
there is a delay to skip.

The clients generalise instead of growing a second layout. Both snapshots name
the same quantities the same way, so what was Modbus-only in the readouts is now
whichever transport read the charger — ModbusStatus becomes ChargerStatus on the
phone, mb becomes dev on the web. What each transport can be *told* still
differs, and the buttons branch on that: reset and clear-limit stay with OCPP,
the timeout and phase registers with Modbus, skip-delay with the cloud. A command
a transport has no equivalent for is refused by name, saying which one has it.

The cost is worth saying plainly. This leans on Anker's cloud being up and on an
unofficial protocol the app may change under us, where Modbus leans on nothing
but the LAN. And it is checked against the reference implementation's own worked
example rather than against hardware — there is no charger on this end to point
it at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 16:47:10 +02:00
tajniak81andClaude Opus 5 b27ca3ee19 The two cards stop keeping numbers from each other
Charger control and Charger readings each held a reading the other did not.
Control's tiles showed the charger's operating state and the session's energy,
which appeared nowhere in the readout that claims to be the whole snapshot;
readings had the total power and the session length, which are the two numbers
you actually look at after pressing start to see whether anything happened.

Both gaps close. The control card grows two tiles — total power (20068) and
session length (20082) — laid out two by two beside the connector state and the
energy, and they use the readings card's own labels and formatting so the
wording cannot drift apart. They appear only when the charger reports them: the
OCPP status carries neither, so on that path the card keeps its original two
tiles rather than showing a pair of dashes.

The readings card grows the other two: the charging status (20097) leads the
live block, since what the charger is doing is what the block is about, and the
session energy (20084) follows the session length it belongs beside.

Nothing is exclusive to one card any more, which is the point — a number that
can only be read in the card that acts on it is a number you have to go looking
for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 10:50:00 +02:00
tajniak81andClaude Opus 5 e02855173a One register, one slider
The settings card took over the charger's current ceiling last commit and the
control card kept its own slider for it, so the same register had two controls a
card apart, each showing whatever it was last dragged to rather than what the
charger holds.

The control card gives it up on the local path. What is left there is what the
card is named for: start, stop, boost — things done to the session in front of
you, not settings the charger keeps.

It stays on the OCPP path, where there is nothing to hand it to. A charging
profile is not a setting the charger reports back, so there is no settings card
on that side of the page, and the clear button belongs to it — clearing is an
OCPP command the register map has no equivalent for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 10:28:41 +02:00
tajniak81andClaude Opus 5 65a5c67afc The settings card sets things
The card added last commit showed the charger's settings and did nothing with
them, which for a page whose whole point is acting on the charger is half a
card. It also took the settings block out of the readings card to do it, so
reading the charger top to bottom now had a hole in it.

The readings card is whole again — phases, live data, settings, device, alarms,
exactly as before. What the settings card holds is the same values with controls
on them.

Which values get a control is the register map's decision, not a design one. Six
holding registers are writable, and four of them are settings: the current
ceiling, boost, the timeout and the phase count. Charging mode, the two
balancing flags and the LED brightness sit in the measurement block, which the
charger reports over FC04 and does not accept writes on — they are set in the
Anker app. So the card is in two halves and says which is which, rather than
offering a control that would quietly do nothing.

The limits come from the same places the server's own checks do: the slider
floors at 6 A because below that the charger pauses instead of charging slowly
and ModbusSetMaxCurrent refuses it, its ceiling is the charger's reported rating,
and the timeout floors just above the spec's "more than five seconds". Phase and
boost write on the change itself, having one value each; current and timeout are
typed, so they wait for Apply.

Every write goes through the existing control action, so it is gated, rate
limited and audited like the buttons in the control card, and the card reseeds
from the snapshot afterwards — the charger clamps what it is given, and the form
should show what it took rather than what was asked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 10:00:54 +02:00
tajniak81andClaude Opus 5 e1c063d4b1 What the charger is set to, where the setting is made
The nine values the charger reports back about itself — the current limit, the
timeout, the phase setting, whether boost took, the last command it accepted,
the charging mode, the two balancing flags, the LED — sat fourth in the readings
card, under a phase table and a live-data block. They are the readback of what
the control card's buttons just wrote, so they were being read straight after
pressing something, at the bottom of the longest card on the page.

They get their own card, directly under control in the default order, and the
readings card keeps what it is for: what the charger is doing right now, what it
is, and any alarm. Nothing is shown twice.

The card folds and drags by its heading like the other four, on the same
chargerCardOrder. A column somebody has already arranged doesn't mention this
key, so there it arrives at the end rather than in the middle of a layout that
was chosen — the rule a tab added in a later release already follows — and one
drag settles it.

It appears on the Modbus path only, because that is the only transport that
reports a settings snapshot at all; the OCPP one has nothing to put in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 09:11:54 +02:00
tajniak81andClaude Opus 5 6877d311ea One word was answering two questions
The charging page carries two badges about the same charger, a card apart. The
connection card asks whether this server can reach the charger to control it —
in Modbus mode a live dial, every status call. The information card asks what
the connected service says about it. Both said "Offline" for no, in all three
languages, so a charger the service can see while its saved local address has
gone stale reads as a page contradicting itself. It isn't: the charger talks to
the vendor's cloud over its own uplink, and Modbus is a local path that answers
only from the network the charger is actually on.

The connection badge now says "Not connected", which is what it was measuring
all along and pairs with the "Connected" it already used. Online/Offline stays
with the service, where it is the service's word.

Both apps show this badge from the same key, so both files change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 09:04:04 +02:00
tajniak81andClaude Opus 5 f3235c403c A page opens where you put its tabs
Every tab bar in the app drags into the order you want, and then all three of
them opened on a tab picked in the source anyway: "public" on Charging, "info"
on a car, "personal" in Settings. Dragging Home chargers to the front of the
charging bar rearranged the bar and changed nothing about where the page landed,
which is the opposite of what dragging it there says.

So the front of the bar is now the landing tab, everywhere. An arrangement is
already the statement of what you want to see first; it just wasn't being read
as one. Settings > Appearance overrides it per page for the case where reading
order and landing tab are two different wishes, with "First in the bar" as the
default and the meaning of no override at all.

The rule lives in one place, lib/tabs.js, because it is one rule and three
pages: the saved choice if that tab is actually on the bar, otherwise whatever
leads it. The bar it is given is the one that will really render, hidden tabs
and inapplicable ones already dropped, so a default that no longer has a button
- a tab switched off for that car, Users on a non-admin - falls back to the
front instead of opening nothing. The tab key lists moved there too, since the
picker needs all three and would otherwise have copied them.

Each page starts on no tab and keeps following the profile until the user says
otherwise, rather than guessing and then correcting itself: the arrangement and
the default both arrive with /api/me, which on a hard refresh lands after the
view has mounted. A click ends the following, and so does the start of a drag -
rearranging a bar must not pull the content out from under the pointer. In
Settings ?tab= still wins over both, since that is what /admin redirects to.

Stored as defaultTabs on the profile, one page->tab map validated per page: a
tab that exists but on another page is an error, and an empty value is stored
as an absent key so "no default" has a single representation.

Also adds charger_tab_order and charger_card_order to the PocketBase setup
script. They were never there - the arrangements of the last two commits had no
column to persist into on a freshly set-up server - and default_tabs would have
gone the same way beside them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 08:52:20 +02:00
tajniak81andClaude Opus 5 641f427db4 The chargers you own, on the phone as well
The web's Charging page grew a real home half while the phone kept a demo one.
There, a charger is a record imported from a connected service; here it was a
hardcoded row called "Home charger", and the only real thing on the tab was a
single OCPP control card. Modbus had been a working transport for a while, and
the phone had no way to give it the address it needs.

The home tab is now the four cards the web shows, about whichever charger is
picked, and the list of the ones you have imported. Control acts on the charger
and offers what the transport actually has — boost on Modbus, clear-limit and
reset on OCPP. Connection asks for a serial or an address depending on which,
and falls back to a text box for a serial the account does not list. Readings
render the Modbus snapshot the way it gets asked about: the per-phase matrix,
what the charger is doing, what it is set to, what it is, and any alarm word.
Information stands without a control mode at all, because what a charger is is
known either way; beside it the service's own view of whether it is reachable,
asked for when the tab is opened rather than on every build.

Rearranging is the one place the two apps differ, for the reason the car's view
picker already differs: the web drags the tab bar and the card headings, and on
a touch screen the bar owns that gesture and a heading is the fold toggle. Both
arrangements are made in a sheet with handles instead, and still saved to
chargerTabOrder / chargerCardOrder on the profile — so an arrangement made in
either app shows up in the other. Which cards are folded stays on the device.

Settings groups its integrations into the same categories the API Server panel
does, says Online as well as Offline, and shows firmware in a charger's line.
Shared strings are copied out of the web's i18n files rather than retyped, per
TRANSLATIONS.md; only the arrange sheet's own three are written here.

The readings and information cards look up some sixty keys by name at render
time, so models_format_test now guards those the way it guards the car's — an
enum value is deliberately left out, since a charger may report a number this
release has never heard of and falling back to it is the point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 08:27:16 +02:00
tajniak81andClaude Opus 5 3c64d6e84c The cards drag too, held by their headings
Same arrangement the provider panel gives its readings, applied to the four
charging cards: drag one and the column reorders live under the pointer, the
card being dragged goes half-transparent, the one it is over takes a ring, and
the rail's lock holds the lot still.

Two things differ from the readings row, both because these are four different
things rather than four of one. A card is placed with the CSS order property
instead of by moving markup, so each keeps its own template and its own v-if.
And the handle is the card's heading rather than the whole card — a card that
was draggable everywhere would fight the current-limit slider and the address
fields for the pointer.

Saved on the profile as charger_card_order, beside the tab order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 21:24:04 +02:00
tajniak81andClaude Opus 5 1418a566fd The charging tabs drag, like a car's do
Reordering the bar meant editing the template, which is a poor way to ask for
Home chargers first. The tabs now drag into either order on the same native
drag events as a car's tabs and the garage, down to the live reorder as the
pointer crosses a tab, the grab cursor, and the rail's lock holding the bar
still for anyone who would rather not nudge it on the way to a tab.

The arrangement is saved on the profile as charger_tab_order, beside the garage
order and for the same reason: it is a layout choice that should follow the
account rather than the browser, unlike which cards are folded. The field is
reconciled onto the users collection at boot, so no migration step. Its
normalizer is the garage's, which now takes the field name and cap as
arguments instead of being copied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 21:10:52 +02:00
tajniak81andClaude Opus 5 2a5d21d328 Controls first, because that is what the page is opened for
The column opened with the charger picker and its address — the two things
touched once and then never again — and put the start button below them. Order
reversed: control, then connection, then readings, then information.

The comments on both cards described the old order, so they move with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 21:06:02 +02:00
tajniak81andClaude Opus 5 b48019b08f Four cards is three too many to scroll past
The charging column grew a card at a time and now runs well past a screen, so
each of the four folds away: connection, control, readings, information. The
mechanism is the provider panel's, down to the chevron and the localStorage
key — folding is a reading habit of this browser, not an account setting, and
one learned gesture should work in both places.

The connected badge stays in the connection header while it is folded, since
whether the charger is reachable is the thing worth seeing without opening
anything. Charger information keeps its Refresh button beside the toggle
rather than inside it, a button being no place for another button.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 21:04:55 +02:00
tajniak81andClaude Opus 5 bcc44da735 Setting up the connection is not the same as using it
The control card opened with the parts that get touched once — which charger,
and where it lives on the network — and only then reached what gets used every
day. Split in two: a connection card holding the picker, the address and the
connected badge, then a control card holding the tiles and the buttons.

The not-connected hint and the error line move up with the connection, since
that is what they are about, and the control card simply does not appear until
there is a connection to control over. That also retires the inner template
that repeated the card's own condition.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 20:12:05 +02:00
tajniak81andClaude Opus 5 5547daa689 The readings get their own card
The register data was appended to the control card, which turned a card for
acting on the charger into a long scroll with the buttons at the top of it.
It is a separate concern and now a separate card: control above, readings
below, alongside the charger information card that was already there.

Its title says readings rather than information, since the card below it holds
what the record knows about the charger while this one holds what the charger
itself just said.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 20:10:13 +02:00
tajniak81andClaude Opus 5 05f8bd07ed The register map, kept locally without keeping it in the repo
The Anker Modbus reference page is published as an artifact and lives in the
project root as a working copy. It is generated rather than authored, and the
published page is the one that gets updated, so tracking the local copy would
only invite the two to drift apart in review.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 19:49:39 +02:00
tajniak81andClaude Opus 5 62cb691f98 Everything the register map carries, sorted the way it gets asked about
The Modbus snapshot reported about half of what one poll already brings back.
The rest was read into the block and thrown away: line-to-line voltages,
reactive and apparent power per phase, the PWM flag, the control-pilot voltage,
and the identity block's product number, rated power and current range. All of
it now decodes — no extra requests, the registers were in hand already.

Added alongside it: the control block, read back over FC03. It answers a
question the live registers cannot, which is what the charger is *set* to as
opposed to what it is doing — a boost that was asked for reads there while the
live block still reports none running. Best effort, so a charger that refuses
it still reports its state.

Two registers the spec leaves blank are decoded on the hardware's evidence. The
control-pilot voltage reads 11873 while the CP signal register reports state A,
which that enum names as 12 V, so the register is millivolts. The identity
block's current range is in amps, whatever its unit column says about watts and
kVA.

The charging card lays this out in sections rather than a wall of forty numbers:
per-phase measurements as the matrix they are, then live state, then settings,
then the device itself, with alarms surfacing only when a word is non-zero.
Strings in en/da/pl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 19:37:22 +02:00
tajniak81andClaude Opus 5 aaa89dfe10 Relays that run at 33 degrees, not 331
The two relay temperatures came back as 331 and 319 from a charger sitting idle
with nothing plugged in. The spec's gain column says 1 for both, so we reported
them as 331 °C and 319 °C — a reading that would have meant a fire rather than a
wallbox at room temperature.

The gain is 10. The same table hands the maximum current setting a unit of watts
and the timeout a unit of amps, so its unit and gain columns are not load-bearing
here; what settles the alignment is the LED brightness two registers earlier,
which reads exactly 100 at gain 1, and the fact that the neighbouring registers
all decode as tabulated. Read back from the charger afterwards: 33.1 °C and
31.9 °C.

The field becomes a float, as the voltages and currents beside it already are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 19:01:29 +02:00
tajniak81andClaude Opus 5 cf4fd14b56 The table the charger actually keeps its measurements in
Modbus mode never returned a reading: every status poll came back as "the
charger did not answer", though the charger was answering all along. It was
refusing the question. The A5191 splits its map across two tables where the
spec's single 2xxxx column suggests one — 20000-20100 are input registers and
reject FC03 with an illegal-address exception at every address in the range,
while 21000-21005 really are holding registers and read back over FC03. We
inferred one space from the spec's layout and asked for all of it with FC03.

The client learns FC04, sharing a body with FC03 since the two differ only in
which table the server consults, and the plugin's two measurement reads move to
it. Writes stay on FC06, where the controls already live.

Confirmed against an A5191 on firmware 1.0.6.1: identity, live block and the
control registers all decode as the spec tabulates them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 18:42:39 +02:00
tajniak81andClaude Opus 5 f025dc100f The local mode, offered by the menu that picks the mode
Modbus TCP has been a working control path since the server learned to dial
the charger, and the Charging page has asked for the address it needs — but
the Control mode selector never listed it. The mode could be reached only by
writing it through the API, which is to say not at all.

Both selectors now offer it, and both stop offering what it does not use: the
OCPP provisioning card and its "Use this one" button are gated on the modes
where the charger dials us, and Modbus gets a line saying where its address is
asked for instead. On the phone that gate is more than tidiness — the field is
a DropdownButtonFormField, so a mode saved from the web left it holding a value
none of its items matched.

Strings added in en/da/pl for both apps. The mode hint no longer says control
happens "over OCPP", because it no longer always does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 18:26:21 +02:00
tajniak81andClaude Opus 5 1aedc0ddc7 A serial the account does not list is still a serial worth showing
The dropdown could only show a serial it had an option for. A remembered one the
account does not report — a charger imported before the account was linked, one
the cloud is quiet about today — selected nothing, so the field sat blank while
that serial was the one every command went to. Nothing on screen said which
charger was being driven, or let it be corrected.

The field now falls back to the text box in that case, the way it already does
when the account lists no chargers at all, and shows the serial actually in
force. One link switches between picking and typing, so a serial off the list is
not a dead end and the list is not the only way in; coming back to it lands on a
charger the list holds rather than blanking the dropdown again.

Which of the two is showing is decided when the list arrives, not on every
keystroke — recomputing it as the serial is typed would turn the text box into a
dropdown mid-word, the moment what had been typed happened to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 17:54:54 +02:00
tajniak81andClaude Opus 5 7ccd785742 The charger's address, asked for where control is set up
Modbus mode had no way in from the app: the mode could be picked in Settings,
but the address the server dials had to be PUT by hand. The control card now
asks for it in the place the charger is already being controlled from.

Which provisioning the card shows follows the mode, because the two are not
alternatives to each other — OCPP installs a token into the charger, Modbus
records where the charger is. So does what the card offers: boost is a Modbus
command and appears there, clear-limit and reset are OCPP ones the register map
has no equivalent for and stay behind.

The status tiles read whichever snapshot arrived. An OCPP session counts a meter
in Wh and names a connector state; a Modbus snapshot counts the session's own
energy and names the charger's. Both land in the same two tiles.

When nothing answers, the server has already tried the address and says what it
found, so the card shows that rather than a hint written in advance.

The hint naming Own and Proxy CSMS as the way to control a charger was true
until there was a third mode; it now names all three.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 17:27:06 +02:00
tajniak81andClaude Opus 5 f7472bada3 Reach the charger where it is, instead of waiting for it to call
OCPP asks the charger to dial us: a public endpoint, a TLS certificate, and a
route in through the customer's router. Our own handler then demanded two more
things the V1 does not offer — TLS on a charger that connects over ws://, and
Basic auth credentials the Anker app has no field for — so every connection was
turned away before the upgrade.

Anker publishes a Modbus TCP register map for this charger, and it inverts the
problem: we dial the charger, on its own network, with no inbound reachability
to arrange. That works for a charger behind a router that OCPP cannot reach at
all.

internal/modbus is the protocol, hand-rolled against the spec like the MQTT and
WebSocket clients beside it. The plugin's modbus.go is the V1's map: the same
0-8 status enum the cloud already reports, per-phase measurements, and the
writable registers behind start, stop, current limit, boost and phase mode. A
new "modbus" control mode routes the existing control endpoints down it, so the
REST surface, the rate limit, the confirmation step and the audit trail are the
ones already there.

The commands the register map has no equivalent for say so by name rather than
failing as unknown, and a current below the charger's 6 A floor is refused
because it pauses the charge rather than slowing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 17:03:27 +02:00
tajniak81andClaude Opus 5 e9a82a1cca One hostname a charger can be told about and actually reach
Charger control needs TLS, and the stack speaks plain HTTP, so the README
said "terminate TLS in a reverse proxy" and left the operator to work out
which four settings have to agree. This adds the proxy: a Caddy overlay
that fronts the Web App BFF — which already carries /api/ and /ocpp/ — so
browsers and chargers arrive at the same name and the certificate is issued
on first boot.

The four settings are derived from DV_DOMAIN, since one value getting typed
right is better odds than four: OCPP_PUBLIC_URL, OCPP_REQUIRE_TLS back on,
CORS, and TRUST_FORWARDED_PROTO on the Web App. That last one is the
non-obvious one — without it the BFF overwrites Caddy's X-Forwarded-Proto
with its own plaintext hop and the API Server rejects the charger it just
told to connect over wss.

The README's OCPP section was stale besides: it still sent chargers to the
API Server port alone, from before the BFF carried that path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 15:36:56 +02:00
tajniak81andClaude Opus 5 9f5c8dc49a The address chargers are given is now an address that answers
The API Server tells a charger to dial the host it was itself asked on.
The panel asks through the Web App, so the address handed out is the Web
App's — which proxied /api/ and nothing else, and answered the WebSocket
handshake at /ocpp/ with index.html. A charger pointed at the endpoint the
screen showed could never connect to it, and the screen went on saying
"Not connected" without a hint as to why.

Both front doors now carry /ocpp/ through to the API Server: the BFF via
the same reverse proxy, which relays the 101 by hijacking, and the
all-in-one image's nginx via a location of its own, with timeouts long
enough for a session that is idle between heartbeats.

The proxied hop also has to say how the client arrived, since the API
Server reads X-Forwarded-Proto to decide a charger reached it over TLS.
That header is set from this server's own connection and overwrites
whatever came in: believing a client on that point would let a plaintext
charger claim wss and walk past OCPP_REQUIRE_TLS. TRUST_FORWARDED_PROTO
opts into the inbound value for the one deployment where it is true — TLS
ending at a proxy in front of the stack.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 14:31:24 +02:00
tajniak81andClaude Opus 5 019c28db85 The card is about the charger you picked, not all of them
Charger information stacked every imported charger, so the list beside it
selected one and the card ignored the choice — with two chargers the page
said everything twice and the picking meant nothing. It now shows the
selected one alone, and falls back to the first until something is picked:
a card that stays blank until clicked is a worse greeting than the charger
most people have only one of.

The per-block ring goes with it. Highlighting the selection inside a card
that shows nothing else is a distinction without a second thing to draw.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 13:01:09 +02:00
tajniak81andClaude Opus 5 9686cae8b9 The charger row asks the same way everything else does
Removing a home charger kept its own inline prompt, written before there
was an app-wide one. Two mechanisms for one question is one too many: it
now calls askConfirm() like every other destructive action, and the panel,
its pending-removal state and the ask/cancel pair go with it. The button
still disables while the delete is in flight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 12:09:59 +02:00
tajniak81andClaude Opus 5 c9ffb9c698 One prompt for the whole app, asked where the browser cannot refuse it
Fifteen destructive actions were still gated behind window.confirm(), the
same call that made removing a home charger look broken: a browser that
suppresses native dialogs never shows it and returns false, so deleting a
service, a part, a user or an organization would quietly not happen and say
nothing about why.

askConfirm() puts the question in the page and resolves to what was
actually pressed, so each call site changed by one line and reads the way
it did before. ConfirmDialog is mounted once in App.vue and draws over
everything, including a modal — removing a server is asked from inside one,
which is what Modal's new zIndex is for.

The home charger keeps its own inline prompt: that one belongs to its row
rather than to the middle of the screen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 12:02:38 +02:00
tajniak81andClaude Opus 5 749f42f481 Remove asks in the page, not in a dialog the browser may refuse to show
Removing a charger was gated behind window.confirm(). A browser that
suppresses native dialogs — an embedded webview, a blocked-dialogs setting
— does not show it and hands back false, so the click answered "no" on the
user's behalf: no request, no error, nothing. The button looked broken and
the endpoint was never reached. It always had been fine; a delete with an
unknown id still answers 404 and the ownership path still resolves.

The prompt is part of the row now — the warning, Cancel, Remove — the way
the charger reset in the same view already asks. Remove disables while the
delete is in flight, and a failure lands in the error line the list
already has.

Fifteen other confirm() call sites share the flaw and are left for their
own change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 11:56:04 +02:00
tajniak81andClaude Opus 5 21a54c4cfa The bolt in the list takes the colour the badge already had
Every charger in "Your chargers" drew its bolt in the same green, state or
no state — the list's one visual signal said nothing. It now takes the
green and amber the information card's badges use, off the same live map,
so the list answers "which one is up?" without opening anything.

A charger the service is silent about draws muted rather than green: a
green bolt for an unknown charger is a claim. The icon carries a title as
well, so the state is not left to colour alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 11:52:04 +02:00
tajniak81andClaude Opus 5 7cbc81778c Every row, every time, dashes included
The information card drew a row only when it had a value, so two chargers
side by side had two different shapes and neither could be read against the
other — and a field the service is silent about looked the same as a field
the card never offers. Every row is drawn now, with a dash where there is
nothing to say, which is itself worth seeing.

Service id loses the rule that hid it when it matched the serial. Kept, it
would have printed a dash for a charger that does have one, and a dash that
means "no" where the answer is "the same as above" is worse than the
repetition.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 11:47:23 +02:00
tajniak81andClaude Opus 5 ad785ee9f8 The fields the cloud sends, kept all the way to the card
Normalizing a provider's charger list threw most of the answer away:
firmware, the site id, how the charger is registered, the charge power and
the cloud's own OCPP reading all arrived from Anker and none of them got
past providerCharger, which carried seven fields and dropped the rest. The
information card could not show what it was never handed.

It carries them now, and the card lays them out: firmware beside the model,
site and site id where the charger lives, "Registered as" for standalone /
in a system / bound, and — when the service knows — state, charge power and
OCPP status. Charge power is relayed exactly as worded upstream, since the
unit is theirs and putting one on it here would be inventing it. Settings'
own list gains the firmware in its subtitle. A field no view supplied still
leaves no row, so an account whose chargers stand outside a system reads
shorter rather than emptier.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 11:43:23 +02:00
tajniak81andClaude Opus 5 86ea97b414 Online said out loud, not left as the absence of "Offline"
Settings listed a charger's reachability with one badge, drawn only when
the cloud said online:false. A reachable charger got nothing — the same
nothing a charger the cloud declined to report on gets — so the account's
two chargers read as one offline and one unremarkable. Online now has a
badge of its own, and the silent case stays silent: not saying is not the
same as saying no.

The Charging page said nothing at all, having only the record, which knows
what a charger is and not whether it is answering. So it asks: each
connected service's charger list is read into a live map keyed by the
provider's own id, and the information card carries the badge and, when
the service reports one, the operating state. Asking costs a round trip
per service, so it happens when the home tab is first opened — the moment
the question is being asked — and after that only when the user presses
Refresh or imports a charger. A service that will not answer leaves its
chargers unbadged rather than offline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 11:22:02 +02:00
tajniak81andClaude Opus 5 4203ca93db What a charger is, readable without a CSMS to control it with
The Home chargers tab put a card where the control panel goes and, with
neither Own nor Proxy CSMS turned on, filled it with a single sentence
pointing at Settings — an empty box beside a list of two real chargers the
app already knew the vendor, model and serial of.

That knowledge now has somewhere to be. Charger information lists every
imported charger with the fields its record actually holds; a field the
service never sent is left out rather than shown as a dash, and the
provider's own id appears only when it is something other than the serial.
The selected charger carries the same accent ring the list beside it does,
so picking one still reads as picking the one control drives. The card
stands on its own either way: with control on it sits beneath it, and with
control off the Settings hint becomes a footnote under something worth
reading instead of the whole card.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 11:12:17 +02:00
tajniak81andClaude Opus 5 d71c1b4691 The panel header stops folding "Sign out" onto two lines
The console shell was capped at max-w-4xl (896px), narrow enough that the
header row ran out of room and the last button wrapped mid-word. It now
uses the same 1368px cap as the Web App shell, so the header has the space
it always assumed it had. Rebuilt the embedded dist to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 10:47:10 +02:00
tajniak81andClaude Opus 5 190ae923a6 The Anker token outlives the request that fetched it
The manager builds a throwaway plugin instance for every per-user call —
HealthCheckWith, InvokeWith, InvokeBatchWith each construct, Init, probe and
Shutdown. The auth token lived on that instance, so it died with the HTTP request
that fetched it: opening the Anker panel signed in once for the health probe and
again for the charger list, and a page that also asked for OCPP info signed in a
third time. Every refresh, a fresh login.

Anker throttles passport/login per IP per minute and answers code 26161 ("Failed
to request.") once tripped, so this is the shape of the failure the panel has been
reporting; the cloud has also historically kept one token per account, so each of
those logins could evict the one the mobile app was holding.

Tokens and the login backoff now live in a package-level session keyed by the
account signing in, so every instance configured for that account shares one
login. Re-configuring the same credentials keeps the token; a different account,
or the same account on the other regional server, gets its own session. Sessions
unused for a fortnight are pruned, so an edited password does not leave its entry
behind for the life of the process.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 23:11:52 +02:00
tajniak81andClaude Opus 5 c1aee0fac1 One refused sign-in, not five: the chargers poll no longer locks the account
A chargers poll asks four cloud views. Each called apiRequest, each found no
token, and each ran its own login — so a login Anker refuses was offered four
times in one poll, and the next poll spent the fifth. Five is what disables the
account for ten minutes, which is how "code 26161: Failed to request." turned
into "your account has been disabled" on the very next attempt.

The plugin now remembers a refused login instead of repeating it: the failure is
cached and replayed to every caller until a backoff window passes — a minute at
first, doubling to fifteen, or the full ten minutes when Anker says it has
already locked the account (code 10019). New credentials clear it, so a fixed
password is tried at once.

chargerInventory signs in once up front. A login the cloud refuses is not four
views failing, so it is reported as itself rather than as three warnings with the
lockout notice buried in the last one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 22:54:24 +02:00
tajniak81andClaude Opus 5 423bc2ab16 The charger list sorted by a field the collection never had
Opening Home chargers answered {"status":400,"message":"Something went wrong
while processing your request."} — PocketBase's generic refusal, here for an
unknown sort field. home_chargers declares its own fields and nothing else:
PocketBase adds no created field to a collection defined through the API, which
is exactly why control_audit and organizations declare theirs. The list handler
sorted by created anyway. It was the only handler in the server that sorts by
created — every other one sorts by name, km or date, fields their collections
actually declare — so the gap had never had a chance to show.

The field is now declared, and reconcile adds it to the collection already
standing on the next boot, since home_chargers is in reconcileOrder. Import order
is the only order a charger has: it carries no date of its own, and a wallbox
bolted to a wall does not accumulate events the way a car does.

The list also stops depending on that. A rejected sort now falls back to the
unsorted query rather than failing the request: the order is a nicety, the list
is not, and an owner reading a database error about a field they cannot see is
the worst of both. It also makes the deploy order stop mattering — the page works
before the bootstrap has run, and the sorted query wins once it has.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:34:48 +02:00
tajniak81andClaude Opus 5 06f91578da Integrations, sorted into the categories the panel already sorts them into
The API Server panel has grouped plugins by category since the plugin list was
drawn — car manufacturers, EV chargers, notifications, each tab carrying its
count. The Settings page had the same integrations in one flat stack, so the two
screens described the same set of things in two different shapes. Now they agree:
the same categories, the same counted tabs, the same accent border on the active
one. The category ids come from the Category* constants both sides already read,
so nothing here invents a third vocabulary.

Only a category holding something gets a tab, which is the panel's rule too: two
tabs today rather than six empty ones. Below one category the bar hides itself
entirely — with nothing to switch between, a single tab is a label pretending to
be a control.

The active group falls back to the first rather than to a fixed id, because the
three integration views load independently and a tab can be momentarily empty on
first paint; falling back keeps the section populated instead of blanking it.
The cards moved into a space-y-4 wrapper and lost their per-card mt-4, so the
first card sits the same distance under the bar whichever tab is showing —
previously Toyota was the only one that could ever be first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:20:08 +02:00
tajniak81andClaude Opus 5 a3f69fa5ef Home chargers: your own wallbox as a record, imported the way a car is
The Home chargers tab has been showing a hardcoded "Home charger · 11 kW · NACS"
since it was drawn, and the control card asked for a serial as free text — a
number printed on a box hanging in a garage, typed in by hand while the connected
account already knew it. The garage solved the same problem for cars a while ago,
so this is that solution aimed at the wall: pick the charger off a service you
have connected, press Import, and it becomes a record of yours.

A charger is a record rather than a live listing because it has to outlive the
account it came from. Disconnect Anker and the wallbox is still on the wall; the
integration is how the charger was found, not what it is. Hence home_chargers,
owned by a person and not related to any car — it charges whichever car is
plugged into it, and it outlives all of them — and hence no sharing: a charger is
one household's business in a way a car shared with a partner is not.

The provider layer is vehicleproviders.go's shape on purpose, down to the soft
gate: a listing answers 200 with an empty list and the sentence that says what to
do about a closed gate, a write answers 400, because there the caller asked for
something that did not happen. Anker and Greencell are two adapters over plugins
that already exist, so the next charger service is an adapter appended to
chargerSources() and nothing else. What is deliberately absent is the car
import's checkbox panel: a charger is a name, a serial and the hardware behind
it, all of which the list already carries, so there is nothing to choose and the
whole screen is pick one, press Import.

Only the name is editable afterwards. The rest describes hardware and came from
the service, and the provider link is written by the import endpoint alone, so
renaming a charger cannot quietly orphan it from the account it tracks. Deleting
one says as much in its confirmation: the charger is untouched, and importing it
again brings the record straight back.

The Anker gate moved into ankerGate() beside greencellGate(), because the same
four-case switch was about to exist in a third place. Behaviour is unchanged —
the same sentences, and the probe still skips the personal opt-in, since checking
credentials is what you do before switching the integration on.

The phone app still has the old tab; parity there is a separate change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:07:21 +02:00
tajniak81andClaude Opus 5 a809980d8b Anker health: count the chargers the panel lists, not the ones one endpoint admits to
The probe still asked get_user_bind_and_not_in_station_evchargers and read its
userBindEvChargersCount, so it reported "0 EV charger(s) bound to account" for an
account whose two chargers the panel was listing directly underneath — the same
blind spot the capability was just moved off, left behind in the health check. It
now takes the same inventory the chargers capability returns and counts that.

Authenticated with nothing on the account is degraded rather than ok, following
Greencell's rule: the half we address answers, and the empty half is the account
or the country that picks the regional server, so the message says so instead of
reporting a healthy connection to nothing. A count reached with some view missing
says how many views stayed silent, because the number is then a floor rather than
a total. The web panel colours degraded amber, as it already did for Greencell.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 20:46:02 +02:00
tajniak81andClaude Opus 5 e138fad3f4 Anker: every charger on the account, not just the ones outside a station
get_user_bind_and_not_in_station_evchargers is the only list the connector ever
asked for, and its name says exactly what it withholds. A charger that belongs to
a system is not in it. Its userBindEvChargersCount, though, counts every charger
bound to the account — so an owner with two chargers in a system got "authenticated;
2 EV charger(s) bound to account" from the health probe and an empty list from the
capability that is supposed to show them. A working login that finds nothing.

So the capability now asks every view the cloud has and merges them by serial. The
standalone list still answers for chargers standing on their own; get_site_list
walks the systems and reads each one through get_scen_info, falling back to
get_system_running_info where that is silent — the power-service / HES split
charger-state already knows; and get_relate_and_bind_devices contributes model,
firmware and the Wi-Fi flag, and discovers anything in the A519 family that the
first two missed. Whichever way a charger was registered, one of the three has it.

The merge is first-writer-wins per field rather than last view overwriting: the
standalone record knows the name, the site record knows the live state, and neither
should blank what the other established. A view that fails is a warning on the
document instead of an error on the call, because one dead endpoint should not
cost the chargers the other two found. Only losing all three is a failure. When
nothing comes back at all the response says so in its own words and names the
remaining suspect — country picks the regional server, and the wrong one
authenticates happily and shows an empty account.

The other half of "not showing any chargers" was that neither client ever showed a
list. The serial was a text box, and the number is printed on a charger hanging on
a wall. Both apps now list what the account holds — name, serial, model, site,
state, an offline badge — and hand the serial to the OCPP control card instead of
asking anyone to go and read it. Where control is off the list still stands on its
own, as the answer to the first question an owner has after entering credentials.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 20:13:36 +02:00