main
166
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3c34b708b9 |
A mode that does not work stops being on the menu
The control-mode picker offered all five paths to everyone, always. When one of them is broken in a deployment — OCPP is, right now — there was nothing to do about it: the superadmin could pick a different mode for the global layer, but the option stayed in every organization's and every user's dropdown, waiting to be chosen. The cascade could impose a mode. It could not withdraw one. So each layer now carries a second, separate thing: a list of the modes it hides from the layers below it. controlModesDisabled sits beside controlMode, on the global layer as a plugin config field and on an organization as part of the same pluginSettings blob its credentials already live in. A superadmin ticking Own CSMS and Proxy CSMS takes both OCPP paths out of every picker underneath; an org admin ticking Modbus takes it out of their own users'. Three decisions are worth naming. A hide-list governs the layers below, not the layer holding it. The superadmin can keep running Proxy globally while hiding it from everyone else, which is what you want while a mode is being repaired rather than retired: the operator testing the fix is the one person who still needs to select it. The alternative, a list that also invalidates its own layer's choice, would have made the panel contradict itself — a mode chosen in one field and switched off in the one below it. But a hidden mode really is hidden, not merely absent from a dropdown. A user who had picked Proxy last month stops resolving to Proxy the moment the superadmin hides it, and falls back to monitoring only. Filtering the picker alone would have left every existing charger on the broken path and quietly disagreed with the list the operator had just filled in. Resolution now walks the layers accumulating what each hides from the next, so a stored value only takes effect if the layers above it still permit it. And off is never hideable. It is what a charger falls back to and what an empty cascade resolves to, so a layer that could take it away could leave the layer below with a picker holding no valid choice at all. It is not among the checkboxes in any of the three clients, and the parser drops it if it arrives anyway. The panel needed a field shape it did not have — several options, any number chosen — so ConfigField grows a "multiselect" type, stored as the comma-separated string that fits the flat map every other field already uses. That is generic: any plugin can declare one now, and the PUT body is unchanged. The phone's field specs grew the same way, a scopeOptions hook that narrows a declared option list to what the server still offers, rather than teaching the integration card about control modes specifically. Both clients clamp a stored mode that has since been hidden back to off before drawing the picker, so the box shows what will actually happen rather than a choice that would be dropped on save. Verified: Go tests pass, both frontends build, flutter analyze is clean, and the panel's new checkbox field was rendered against the real stylesheet. The end-to-end path — superadmin hides a mode, an org admin and then a user reload and find it gone — has not been walked on a live stack; the panel is embedded in the Go binary, so the remote deployment needs a rebuild before any of this is visible there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d4b9d0870e |
The server notices its own token has run out
Signing in worked and everything after it did not: the login screen took the password, PocketBase handed back the record and the token, and then /api/me answered "The requested resource wasn't found." The three symptoms underneath looked unrelated — a 404 on the profile, an organization whose name came back empty, a user list with nobody in it — and they are one thing. The API Server's superuser token had expired. PocketBase does not say so. A record call carrying a token it no longer accepts is not refused; the header is ignored and the request is served as a guest, and the collection rules answer in the transport's place. The organizations collection is superuser-only, so it 403s. A user record is guarded by a view rule, so it 404s — hidden rather than denied. The users list is rule-filtered, so it comes back 200 with an empty array. Not one of those is a 401, and a 401 was the only thing that made this client sign in again. So the token was acquired once at startup and then kept for the life of the container, and the retry meant to renew it could never fire. Uptime longer than the token's lifetime was all it took. Nothing had to change for it to break, which is why it broke on a stack nobody had touched. The client now reads the exp claim PocketBase stamps into the token and renews before spending it, a minute early so a call cannot land just after it lapses. A token with no readable expiry is still taken at face value, and the 401 retry stays where it is as the backstop for the case this does not cover. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
fe1e314df9 |
The store comes apart into the four roles it always had
`weed server -s3` was never one thing. Master, volume, filer and the S3 gateway ran as four goroutines under one process, on one volume, sharing one fate. Splitting them into four containers changes nothing a client can see — the same bucket answers on the same port — but it makes three things possible that were not: the SeaweedFS admin UI, which wants a cluster to look at; a restart or an upgrade of one role without the others; and, eventually, a second volume server somewhere else. A fifth container carries the panel itself. The single-process files stay exactly as they were. These are `.split.` twins beside them, four in all, one per folder per shape, each with the .env example of the same name that both READMEs already promise. Identities are the part that could not simply be copied across. SeaweedFS picks its credentials from one source, in order: an -s3.config file, the filer's IAM store, then AWS_ACCESS_KEY_ID and its secret — and a higher source replaces a lower one rather than adding to it. The existing files use the env pair, which is fine precisely because nothing else writes identities there. Hand somebody a panel that can, and the first user they create lands in the filer's store, the store outranks the environment, and PocketBase's key stops existing — with the first failed upload as the notification. So the gateway here is started with no config file and no AWS_* at all, and the init container seeds PocketBase's identity into the filer's store instead: the same store the panel writes. One source of truth, PocketBase's key sitting in Object Store → Users beside every other, keys minted there picked up without a restart, and a rotated PB_S3_SECRET re-applied in place on the next boot rather than added as a second identity. That seeding is now allowed to fail. The bucket-create it grew out of was best-effort — `|| true`, on the reasoning that the API Server's own S3 check would report a gateway that was genuinely unreachable. That reasoning does not survive the change: a gateway whose IAM store is empty does not refuse anyone, it serves everyone, and the bucket would be wide open rather than unreachable. So the step ends by grepping the configuration back for the access key, the gateway waits on it completing successfully, and a seed that did not land stops the stack instead of opening it. The prod files publish the gateway and the panel, both on loopback, and nothing else. Port 8080 on the volume server hands out file content by file id with no authentication of any kind — the S3 credentials have no bearing on it — so publishing it would publish every attachment in the stack, and the panel shows what that port and the master's would. The panel's own password is required rather than defaulted, because weed serves it with authentication switched off entirely when it is empty, and a page that mints bucket credentials is the bucket. It is passed as WEED_ADMIN_PASSWORD rather than a flag so it stays off the process command line, and SEAWEED_ADMIN_BIND is the knob a remote host needs, named after PB_BIND and API_BIND for the same reason. Master, volume and filer share one /data mount rather than taking three of their own. That is precisely the layout `weed server -dir=/data` writes — the master's raft state, the volume's .dat and .idx, the filer's filerldb2, no two of them naming the same file — so a stack can move between the single-process file and its twin in either direction with nothing to migrate. A second volume server would need its own, and the files say so where somebody would go looking. The dev files map the volume server to 8081 on the host: 8080 there is already the API Server, and in the all-in-one it is the API Server inside the image. Unexercised: written on a machine without Docker, so none of the four has been brought up. Every flag, health path and env name was read out of the pinned 4.45 source rather than recalled — -mdir, -volumeSizeLimitMB, -defaultStoreDir, -max, admin's -master and -dataDir and WEED_ADMIN_*, the filer's and gateway's /healthz, the panel's unauthenticated /health — and the four files were parsed, interpolated against their examples, and checked for duplicate host ports. A `docker compose config` on the target host is still the first thing to run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9a2a4ab72e |
The files leave the volume the database sits on
Every attachment — a document scan, a fuel receipt, a workshop invoice, a
photo of a part's box — has lived inside pb_data, in a directory beside the
SQLite file. One volume held both, so neither could be sized, backed up or
moved without the other. PocketBase can keep those bytes in an S3 bucket
instead, and now it is told to.
Nothing on the way to a client changes, because an attachment was never a
storage URL to begin with: it is fetched from GET /api/{records}/{id}/file,
which re-checks car access and asks PocketBase for the bytes as the service
account. PocketBase streams from the bucket through that same endpoint rather
than redirecting to it, so the web app, the phone and the plugin cannot tell
which side of the switch they are on.
The bootstrap that already creates the collections now writes PocketBase's
files-storage settings too, from PB_S3_*, on every boot and only when they
differ from what is already there — then asks PocketBase to prove it can reach
the bucket, and says so in the log either way. Two asymmetries are deliberate.
A read of the settings masks the stored secret, so a rotation of the secret
alone is invisible from here and needs another PB_S3_* to move with it. And it
never turns S3 back off: files already written to a bucket are reachable only
while PocketBase still points at it, so dropping the configuration would strand
them rather than undo anything.
Each deployment shape is one compose file with an .env example of the same
name, not a base plus an overlay to remember — six of each per folder, for
Docker and Docker-AIO alike: the plain one, .seaweedfs, .s3, and the three prod
twins. The SeaweedFS files run master, volume, filer and gateway as one process
and a one-shot init container beside it, because PocketBase never issues a
CreateBucket and SeaweedFS will not conjure one on first upload. The credentials
do double duty there — the gateway's only identity is also what PocketBase
authenticates with. In the all-in-one that gateway is a second container rather
than a fourth process under supervisord: keeping the object store inside the
image, on the volume the files are being moved off, would have defeated the
point and would have meant rebuilding.
Files uploaded before the switch are not carried across; PocketBase copies
nothing, and both READMEs say so where an operator will read it.
The TLS overlay and its Caddyfile go. The section they served stays, without
them: nothing in the stack terminates TLS any more, so it now names the four
variables to set in front of whichever proxy already does — TRUST_FORWARDED_PROTO
being the one that decides whether a charger is believed about how it arrived.
Unexercised: this was written on a machine without Docker, so the pinned
SeaweedFS image, the bucket-create and the settings write have not been run
against a live stack. The Go side builds, vets and tests clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
181f55a849 |
The phone catches up with the month the web had
Twenty-eight commits landed on the web app and the API since the phone was last touched, and the phone's own README opens by claiming full feature parity. It was not a small drift: a whole tab, two whole cards, and the two settings that decide how a time is read. The scheduler arrives as the third charging tab. One list of tasks covering every charger the account owns, where the charger's own cloud schedule is one window inside one box. A task is a flow — start at 23:00, cap to 10 A at 01:00, stop at 06:30 — on the days and the chargers it names, and naming no charger means all of them, including the ones imported later. The clock is the server's, so the tab only writes tasks and reads back how each one last went, and any step can be fired now to find out whether it will reach the charger before the night it matters. The RFID card comes with it: the list the account holds, a card added by its number or by holding it against the charger's own reader, and the charger's own list read back from the device. Both halves are written by every add and remove and they can still come apart, so when they disagree the card says which list each card is missing from — nothing else on the page would. The charger settings card the phone never had at all goes in whole rather than only its new half. Over Modbus that is the four writable registers; over the cloud it is the charger's whole settings group in sections, drawn from the same block table the web reads, one write per section because the charger takes a command whole and a schedule carrying only its switch is a schedule whose times have just been set to midnight. The clock and the week become settings. format.dart grows formatTime, the weekday order and the short names, with "auto" asking intl's own hour pattern and FIRSTDAYOFWEEK rather than a table here; Settings › Appearance asks both questions beneath the date. Flutter's own picker renders on the device locale, which nothing in this app steers, so TimeField types four digits on whichever clock is in force and keeps the meridiem as its own control — a box reading 13:45 beside a dial saying 01:45 PM is the disagreement the setting exists to end. The smaller ones travel too. The control card says which charger its buttons drive, picture and name, because it follows a serial and not the highlighted row; its two tiles take the names of the readings they actually hold; and the limit slider leaves it wherever a settings card now owns that value. The list's reachability re-asks every thirty seconds while the tab is in front, merged rather than replaced — "we could not ask" is not an answer, and it certainly is not "unknown". A settings frame that answers half a minute late is chased at widening gaps and then given up on. The information card names the fields the service sent under its own names and groups list records under their own, so list[0].* stops being read as one alphabetical run. An inherited integration field shows what it inherited rather than an example. The sign-in fields say nothing until you type. One gap stays open, and deliberately. The task form sends the phone's zone only when Dart reports an IANA name; Android usually answers with an abbreviation like CEST, which is not a zone, so it sends nothing and the server falls back to its own clock. A name the server would misread is worse than no name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2b4f4f034d |
A task holds the whole night, not one end of it
One command per task was the wrong unit. A charging window is two commands and reads as one intention, so it was two rows that had to be named twice, switched off twice, and kept in step by hand — and there was nowhere to put the third thing, the ease down to 10 A once the house is asleep. So a task holds a flow. Steps are rows in the editor: an action, a time, and the ceiling under the one action that takes one. The chargers and the days belong to the task, because they are the same for every step of a night, and the switch governs all of it. The steps keep the order they were written rather than being sorted by the clock. A night crosses midnight, and clock order files "start at 23:00" last, behind the stop that closes it — which is not the flow anybody described. Nothing about firing depends on the order: every step is timed on its own, and the sweep asks each one whether its minute has come. Run now moved onto the step. A flow is not a thing that can happen at once — firing a start and the stop that closes it back to back would leave the charger where it began and prove nothing — so the button fires the one line it sits on, and the outcome names the step by its time. The stored shape changes with it: action/amps/time give way to a steps list. The collection was a day old and empty, so this replaces them outright rather than carrying a compatibility path for a schema nothing has run on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0f48093d1a |
A week that starts where the person reading it starts theirs
The scheduler's day picker began on Sunday because that is where Intl numbers the days from, which is a fact about the API and not about anybody's week. Monday leads it across most of Europe. A row of seven buttons in the wrong order is not just odd to read — it is easy to misclick, and a misclicked day in a schedule is a car charging on the wrong night. So Settings › Appearance asks, beneath the date and the clock, as the third question a region gets: first day of the week, following the region unless it is answered outright. The same shape the time format already had, and the same "auto" default, so nothing changes for an account that never opens it. The rule lives in lib/format.js beside the clock's, with the ordering, the day names and the sort all coming from there. The two places that lay weekdays out — the picker and the line each task is summarised on — read it rather than each keeping an opinion, so a day set is written and read back in the same order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5a4515978f |
One schedule for every charger, and a clock on the server to keep it
The charger's own cloud schedule is one window inside one box: charge between these hours, every day, and that is the whole vocabulary. A third tab on Charging holds a list instead — each line an action, a time, the days it repeats on and the chargers it acts on — and one list covers the whole account rather than each charger hiding its own. The clock is the server's. A schedule that only fires while a tab is open is a reminder, so a ticker sweeps every enabled task and fires whichever minute has come. It sends by handing a synthesised request to the same control endpoint the page's buttons use, so a scheduled command goes through the same cascade, ownership gate, rate limit and audit trail — what the owner cannot press by hand, the scheduler cannot send for them. A task names its chargers, or names none, which means all of them and keeps meaning that for a charger imported next year. Times are stored as a wall clock plus the zone they were written in, so 23:00 stays 23:00 wherever the server sits. One action per task: a charging window is the two tasks that open and close it, which is how it is read back, edited and switched off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8e4c22cfcf |
The settings card takes a second look, because the charger answers late
Measured on an A5191 rather than guessed at. The settings frame is not lost and it is not missed: it lands about half a minute after the read that asked for it, by which time that read has long returned. The value goes into the server's state and stays there — six polls afterwards all carried the same settingsAt, served from cache in a dozen milliseconds. What was missing was the second look. Nothing on the page took one, so a card that came up before the answer arrived stayed empty until something else happened to refresh it. That is the whole of "it doesn't always load". So a refresh that comes back without the settings half queues another, at six seconds, twelve, twenty-four, forty-five, and then stops. Bounded because the message the server asks with is one the reference reads as carrying an Anker bug: a charger that never answers is a real possibility, and a page left open all day must not poll one for ever. The patience resets per charger, and the chase stops on an error and on unmount. The previous attempt at this treated it as a wait that was too short and lengthened it from four seconds to twelve. That was the wrong half of the problem — the delay is thirty-odd — and waiting it out inside a UI request would be a poor trade. The longer wait is left where it is; it was not wrong, only insufficient. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4bcf1305be |
The bolts in the list keep asking who is reachable
Two things kept the charger list showing whatever was true when the tab opened. The map of who is online was replaced wholesale on every read, so a service that would not answer took every charger it knows about grey with it — and then the read marked the question asked, and the guard above it meant nothing asked again. One failed call and the list sat colourless until somebody pressed Refresh. It merges now, and only when something actually answered: a provider that answered overwrites its own entries, a provider that could not be reached leaves its last word standing. "We could not ask" is not an answer, and it is not "unknown" either. And it only ever asked once. There is a thirty-second poll now, running only while the tab is being looked at — not on the public tab, not while the page is hidden, and asking straight away on the way back rather than waiting out an interval that was never going to fire. The poll takes the reachability half alone, which is what the second argument is for. That is one call per connected service, back in about a quarter of a second. The per-charger views behind it are a dozen cloud endpoints each, and a background poll has no business spending those. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
dd636bbe03 |
A time box that reads on the clock the user chose
The schedule windows met this account with "12:00 AM", clipped to "12:00 A!" by a box too narrow for it, while the card above them printed 00:00. Both halves of that are the native control: <input type="time"> renders in the browser's locale, and its am/pm did not fit. Nothing on the page moves it. lang= was measured rather than assumed — five inputs set to en-US, en-GB, da-DK, pl-PL and nothing came out identically wide, because the control follows navigator.languages and not the document. That is the wall DateField hit for dates, so the answer has its shape: the typing half is ours, the value stays 24-hour "HH:MM", and what the box shows follows the setting. Four digits, the colon inserted as they are typed, the meridiem its own control rather than something to spell. No picker button: a calendar earns one, four digits do not, and the browser's popup would have brought the 12-hour reading back in with it. A half-typed or impossible time emits nothing rather than the part of it that parses, so a block's Apply leaves that field out instead of writing a time nobody meant. format.js exports which clock is in force now, so what prints a time and what accepts one cannot disagree about it. The box is 80px because 12:30 measures 66 with its padding and the first attempt at 64 clipped — which was the complaint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4ff73e5110 |
Brightness and the solar floor become sliders too
Both are a place on a short, known range, which is what the slider row added for the current limit is for. Typing 70 into a box that only accepts tens was the worse way to say it. The solar minimum gets no floor note under it. The current limit's says that below six amps the charger pauses rather than charging slowly, which is a sentence about a ceiling; this is the least a solar charge will draw, and the same words would be wrong about it. Main breaker limit stays a box. Ten to five hundred amps is too wide a range to aim at with a slider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c1b76a801d |
An inherited field shows what it inherited, not an example
Country read "DE" under the words "inherited from your organization", while the organization it was inheriting from said DK. The DE was never a value at all — it was the example placeholder, left in place when the field locked, and an example in that position is not a hint. It is a wrong answer to the question the box is being asked: which country am I inheriting? The server had already settled what may be shown. It sends the secrets back as dots and everything else in the clear, country included, and only the panel was throwing that away. So a locked field now placeholders its effective value, and the example is kept for the case it was written for: an empty box waiting to be filled in. Applied to the non-secret cascading fields rather than to the one that was noticed — Green Cell's port, serial, timeout and command topic had the same example hardcoded a card further down, and would have told the same lie the moment an org set them. The dots are left alone. They are the panel's own masking, and rerouting them through the server's effective value would be the same result by a different path — not worth changing how a secret is displayed as a side effect of this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
830ef0cde5 |
The region says which clock, this app says how to punctuate it
Auto was the region's answer whole, dot and all, so Denmark got 13.45 while every explicit setting beside it wrote 13:45. One screen punctuating a time differently from the next is not local colour, it is an inconsistency, and it was ours to fix rather than the locale's. So the region is asked one question now — does this reader expect 13:45 or 01:45 pm, which is a real difference in how people tell the time — and the printing is the same two lines for all three settings. Denmark, Poland and Japan read 24-hour and get 13:45 from auto; the US reads 12-hour and gets 01:45 pm from it, with the same colon and the same marker as everywhere else. The marker reads in English wherever it appears. That is the trade the setting already made when it offered a 12-hour clock to regions that do not use one. This drops the formatToParts pass from the commit before it: once both halves are fully specified there is nothing left to ask the locale, and the answer is shorter written out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
263d35c688 |
Picking a clock picks its separator too
24-hour read as 13.45 in Denmark, because that is how Danish writes a clock and toLocaleTimeString was doing as it was told. But somebody who leaves "follow the region" and picks 24-hour has just said they want the region to stop deciding — and they mean 13:45. So the two explicit modes build the string from formatToParts and pin the mark between the hour and the minute. Only that one literal is replaced: everything else stays the locale's, which is why Japanese keeps 午後 in front of it and English keeps its lowercase pm after. 12-hour got the same for free — it had the identical 01.45 pm. Auto is left alone. A setting that says to follow the region has no business arguing with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4d51a34c44 |
The settings card stops emptying when one message is late
A snapshot has two halves and they travel separately: telemetry comes from the trigger, the settings only when the charger has something to say about itself. A read can land with the first and not the second — most often the first read after a reconnect — and the card was seeded from that answer alone, so it collapsed to the one control telemetry happens to carry. That is the state of one message, not the state of the charger. Three things, from the outside in. The card keeps what the charger has reported, per serial, across reads. A value stays until another replaces it. They are its own last word either way, and the same ones the server fills a grouped command's siblings from when a caller leaves them out. A charger that goes quiet is no longer written off for good. The miss counter decides whether a read waits for the settings frame at all, and it only ever rose: three unanswered requests early on and no later read waited again, however freely the charger answered afterwards. The comment said "recently enough"; the code said "ever". Answering clears it now. And the first settings are worth the wait a settings write already gives them. Stale settings and never-reported settings were both allowed four seconds. Stale has something to fall back on; never-reported is the empty card, so it gets the full wait — still bounded by the miss counter, so a charger that truly never answers costs it three times and no more. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c65ce0c081 |
Two tiles named for what they were, not for what they show
The control card's first two tiles were named when OCPP was the only thing they read. "Connector" is what an OCPP connector state is, and "Energy" is what a meter total is. Both tiles learned to read the charger's own snapshot instead — statusDesc and the session's own energy — and neither name followed. They take the readings card's names now, and only where they are showing the readings card's values: the same statusDesc it calls Charging status, the same session energy it calls Session energy. One value, one name, in both places it appears. OCPP keeps the old two, which are right for what it puts there. Both strings were already translated, so this adds none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7b339d3dac |
Tap the card, and that is the whole enrolment
The reader already filled the number in; adding it still meant walking back to the keyboard and pressing Add. That walk was the entire cost of the two-step version, and the card is in your hand at the charger. One press now opens the reader and writes whatever is held against it. The name is the server's own convention — RFID and the card's last four digits — because the name is left empty and rfidSaveCard fills it in. Deriving the same pattern here would have been a second place for it to drift; every card already on the account reads that way. A name typed into the box still wins, since throwing away what somebody typed is worse than the convention. The reader is a value now rather than a side effect on the form, and the write is shared with the typed path so the two cannot judge their answers differently. The caller holds the busy flag across both halves: nothing re-enables in the gap, where a second press would have opened a second twenty-second window. The number lands in the box on the way past, so a write that fails leaves something to retry rather than a card nobody can name. The old button stays for the times the number is wanted without the card being added. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e190364c77 |
One ceiling, one slider, in the card that owns it
The current limit sat in the control card and in the settings card at once. Over the cloud those were not two settings that happen to agree: the "limit" command builds its frame from the same table entry the maxCurrentA setting does, so it was one wire field with two controls. The control card gives it up wherever a settings card can take it — which, now that the cloud has one, is both transports that read the charger. OCPP keeps its slider. A charging profile is not a setting the charger reports, so there is no settings card to move it to, and clearing a limit is OCPP's alone: the cloud sets a ceiling and has no message for "no ceiling". Taking the control away there would have left those modes unable to set a limit at all. It arrives in the settings card as the slider it was, not as the number box the table gave it. Slider rows stack — label and the value it is at on one line, the track beneath, the floor hint under that — because a ceiling is a thing you slide between two known ends rather than type. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
dde5410788 |
The clock stops being a side effect of the region
Whether a time read as 13.45 or 01.45 pm was decided by the region picker, which also sets the decimal separator and the currency layout — so a Dane who wanted a 12-hour clock had to move their numbers to get one. Time format is its own setting now, beside the date format it is the other half of. It defaults to "auto", the region's own convention, which is what every timestamp in the app already said: nothing moves until somebody picks something. The 24-hour setting asks for hourCycle h23 rather than hour12:false, because with hour12 the en-US formatter prints midnight as 24:00. One helper, so it reaches everywhere at once: formatDateTime now calls formatTime, and every clock the app draws goes through it — the charger's telemetry and settings, a session's start, when a charger was linked, the provider panel's own timestamp. The users collection gains a time_format select in both places the schema is declared; it is in reconcileOrder, so a restart adds the field and nothing has to be migrated by hand. The native time inputs in the charger's settings card are left alone: the browser renders those in the OS convention whatever this says, and a text box that respected the setting would be the worse control. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3fddab6815 |
The control card says which charger the buttons move
The product shot was already in the account's charger list and already relayed; only the information card ever drew it. The card whose buttons start a session had no sign at all of which charger it meant — the name lives two cards further down, and two A5191s on one account look alike in a dropdown. Identified by the serial in force rather than by the record highlighted in the list beside it. They are usually the same charger, and when they are not, a picture of the other one is worse than no picture. Either source will do: the account's own list, or the live half held per provider, which carry the same image. A shot that will not load is remembered by its URL, so the picture stops being drawn and comes back on its own if the service starts answering for it again — nothing to reset when the serial changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9d4aecb668 |
The settings card stops being a Modbus card
Both transports that read the charger can also be told things, so both get the card now. Modbus has four writable registers and keeps the four controls it had. The cloud has the charger's whole settings group — two dozen of them, everything the Anker app sets short of the card list — and they were readable in the readings card and settable nowhere. Drawn from a table rather than written out one control at a time, because the charger's commands own sets of fields and take a command whole: a schedule frame carrying only its switch is a schedule whose times have just been set to midnight. So a section is one command, its fields are sent together, and Apply is per section. Only what the charger has reported gets a control — a blank box that writes whatever it was left at is worse than no box when the value travels as a sibling. The meter and monitor the balancing features watch stay read-only: there is no command for them, and what their modes select is undocumented. The phase select offers the automatic and single-phase this command carries, and shows a reported three-phase rather than reading as something the charger did not say. Nothing left the readings card. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3064bff8ad |
The charger's own card list gets a door, and says where it differs
0104 was already implemented and already in the action catalogue; nothing routed to it, so the only way to see the device's list was as a side effect of writing a card. It has an endpoint now, and the panel a button. The two lists are compared where they meet: a card the charger holds and the account has forgotten still opens it, and a card only the account holds will not, and neither shows anywhere else. The comparison is drawn only when they disagree, and the device's reading is dropped on a refresh rather than measured against an account list from a later moment. The new test asks all four card routes without a token: a capability the plugin implements and the catalogue advertises is still unusable if nothing routes to it, and no other test here would notice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7176867eb3 |
Tap the card on the charger and the number fills itself in
The enrolment the Anker app does, done here: 0108 a2=7 opens the reader, 0908 brings back the UID. The frames this sends are byte-for-byte the ones the app was captured sending — checksum included — which is what the new test asserts. Adding and removing now write the charger as well as the account: the device write is the app's own message, the account write is the inferred one that carries the name, and either may fail without the other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4c73d4ac05 |
0103 writes a card, 0104 asks for the list, 0108 a2=7 opens the reader
Caught on the charger's own command topic while a card was removed and added back in the Anker app. Enrolling at the charger is three MQTT messages and no REST call: open the reader, take the UID the tap publishes, write it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
62b0a29598 |
The reader speaks: 0904 is the card list, 0908 is the tap
Named from a live capture on an A5191: 0904 carried the nine UIDs the account's own card list answers with, and 0908 arrived the moment a card touched the reader, carrying its UID — and arrived without one when the window closed empty. 0911 names the OCPP backend the charger is pointed at. A UID is bytes, not a number, so type 0x04 now reads as hex. With the frame log on, the command topic is subscribed too: the app's own commands are the half no capture has seen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a7719fca6a |
A line per frame, for the frames nobody has named
ANKER_MQTT_FRAME_LOG logs every inbound cloud frame with its bytes, decoded or not — the ones this package drops are exactly the ones worth naming, so they are logged before the drop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
245870a96a |
The add and remove buttons, and the read that checks them
Anker documents neither rfid write, so the bodies are inferred from the field names get_device_cards answers with, and every write re-reads the list: what the card shows is what the account holds, never what an undocumented endpoint claimed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a5842201c0 |
The cards that open the charger get a card of their own
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4188d049cb |
An email is an email, whichever list it arrived in
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
928a39e03e |
Each RFID card as a card, not as list[3]
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
905eb24cc1 |
The service's own field names, said in the card's words
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0657decbd1 |
The sign-in fields say nothing until you type
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4ff6242c8f |
The last message in the map, and it reboots the charger
0108 was the one thing in the MQTT inventory nobody had wired: the device power mode, whose single documented value restarts the charger. It is the only way to reboot a charger that is on neither a CSMS nor the local network — which is most of them — so the cloud transport sends it now, and "reset" reaches it too, since that is what the OCPP path has always called the same act. Nothing waits for a confirmation: the device that would send it is the device rebooting, so the command answers at once and says the charger drops off the cloud for about a minute. The gate is unchanged and now covers both spellings — an explicit confirm plus a password step-up, audited either way. Modbus still refuses, because no register does this, but its refusal now names both transports that can rather than only the CSMS. Both clients already had the reset button and its password prompt; they were hidden in every mode that reads the device, which is why the cloud never showed one. Modbus is now the only mode without it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |