b60d929ed684604dada1dc18a9a190486e3efe97
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b60d929ed6 |
Service history: columns you can switch off and rearrange
A car's page has let you choose and arrange two things for a while - which tabs
it shows, and which rows the Information tab lists, both dragged into whatever
order you like. The Service history table was left out of that: nine columns,
hardcoded, in one order, for every car. An EV shows Oil & Oil filter and Engine
air filter on every row of a history that will never record either, and a reader
who mostly wants Notes has to look past four columns of dates and distances to
reach it.
It works the way the other two do, because a third mechanism for the same idea
would be one to keep in step. Both lists are properties of the car, so everyone
it is shared with sees the same table, and both need write access to set. The
columns are stored as the hidden set rather than the visible one, so a column
added in a later release is on by default. The arrangement covers the hidden
columns too, which is what makes a column switched back on return to where it
was instead of reappearing at the end - verified below, since that is the part
of this shape that is easy to get wrong and invisible until somebody hits it.
Date cannot be switched off. Every row of that table is work done on a day, and
a history with the day taken out stops being a history; it can still be dragged
anywhere, which is exactly the rule Information already follows in the tab bar.
That is a judgment call and the annotation that prompted this only circled the
other eight columns - moving "date" into hideableServiceColumns and dropping the
filter in the picker would reverse it in two lines if it turns out to be wrong.
Server: hidden_service_columns and service_column_order on the car, validated
against their own key sets by the endpoint that already does this for tabs,
fields and readings. The arrangeable set is derived from the hideable one plus
the date rather than written out again, so the two cannot drift as columns are
added. Bootstrap appends missing fields to existing collections, so the two
columns appear on the next server start with no migration to run.
Web: the table stopped being nine hardcoded th/td pairs and is now driven by one
list of columns, head and body from the same source, which is what stops a moved
or hidden column from shifting the headings out of line with the cells. The
cells are built a row at a time rather than a call per cell, so a long history
doesn't rebuild every cell three times to read its text, its classes and whether
it is the file column. The column headings kept their existing car.services.col*
translations - the keys are mapped rather than derived, because renaming a dozen
strings in three languages to save a lookup table would be the wrong trade. Four
new strings in all three languages.
Verified: go vet and go test ./... pass, with new tests covering both key sets -
that hiding the date is refused, that a field key is not a column key, and that
the arrangeable set is the hideable one plus the date. npm run build is clean.
The page itself was driven in a browser against a throwaway stub API: the
rewritten table renders identically to the hardcoded one, switching two columns
off removed exactly those two from head and body with the rest still aligned and
sent {"hiddenServiceColumns":["oil","engineFilter"]}, dragging Notes onto Km
reordered head and body live and saved an order with the hidden columns still
holding their places, switching Oil back on returned it between Next km and
Cabin air filter rather than to the end, and a read-only share gets no gear
button, no draggable headings and no drag hint.
Not verified: the drag was exercised by dispatching drag events at the
component's own handlers, not by a pointer - the browser pane was not
compositing, which rules out both screenshots and a real drag - so the native
drag image and cursor are unchecked. No automated test guards any of the web
behaviour; the web app still has no test runner. The API rejects unknown JSON
fields, so this web build against an older API Server would take a 400 when
saving the picker: they deploy together from this repo, but one must not ship
without the other. The phone app is deliberately untouched, having no column
table to arrange, and ignores both new fields.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
b4e99240c6 |
API panel: document the whole REST surface, not half of it
The API section's tables are hand-maintained and carry a comment saying they
mirror the routes in server.go. They had stopped: 60 rows against 141 routes,
so 81 endpoints were reachable and undocumented. Everything added since the
tables were written is in that gap - technical checks, fuel, charging,
maintenance, documents, reminders, attachments, the vehicle-provider and
integration surfaces, and the OCPP socket - along with the per-car
sub-resources (fuel-stats, charging-stats, the provider routes) and
PUT /api/cars/{id}/view. Ten new groups, 60 rows to 123, and nothing listed
that no longer exists.
Checked by extracting every mux.Handle route from server.go and diffing it
against every row in the tables rather than by reading both lists: the seven
attachmentRoutes calls expand to their 21 concrete routes, the three static
panel paths are excluded, and the diff is empty in both directions. That
script is not committed - it is a one-off, and a real guard belongs in the Go
tests where it can see the mux, which is worth doing if these tables drift
again.
The flat list endpoints require ?car={id} - they refuse a cross-car listing
so access can be enforced - so the ones that do now say it. Attachments are
documented once with {records} standing for the seven collections that take a
file, because all seven behave identically and seven copies of three rows
would bury that. EndpointTable grew an optional note line under the header to
say what {records} means.
Two things fixed while in there. PUT rendered uncolored - methodClass has had
no PUT entry since the superadmin group started using it - and the longest
paths, the per-charger OCPP control routes, overran the card and were clipped
by its overflow-hidden rather than scrolling; the table sits in an
overflow-x-auto wrapper now. Thirteen of the eighteen tables scroll at phone
width, four of them before this commit's rows were added.
Group titles are translated in all three languages, plus one new auth label
for the OCPP socket, which authenticates the charger with OCPP Basic auth
rather than a bearer token. The endpoint descriptions stay English:
TRANSLATIONS.md calls them developer reference documentation, and this commit
does not reopen that.
Verified by go build ./... and go vet ./internal/api, and by driving the
panel in a browser against a mock API Server standing in for the real one -
all eighteen tables render, the {records} note and the PUT color show, the
Polish titles resolve, and no table clips at 375px. dist is rebuilt so the
embedded panel matches the source.
Not verified: nothing ran against a live API Server, so the descriptions are
checked against the handlers' code and comments rather than against
responses. The mock only answers /api/identity and /api/status, which is
enough to get past the login gate and reach the section.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
215c027ada |
Panel: give the API Server a name, and a tab to set it in
The Web App can be pointed at more than one DriverVault, but a server it
adds is only ever identified by the URL that was typed into the connect
dialog. Nothing on the other end says what it is called, so the switcher
has no name to show that the operator did not invent locally.
So the server now carries one. SERVER_NAME joins the config, defaulting to
"DriverVault API Server" so /api/health always has something a client can
display rather than an empty string every caller has to special-case.
GET/PUT /api/admin/server-config follow the pb-config and webapp-config
shape exactly: superadmin only, applied at runtime and then persisted to
.env, with the same "applied but could not be saved" warning when the write
fails. There is no /test sibling, because a name is a label and not an
address - there is nothing to probe. The length cap counts runes rather
than bytes, so a 64-character Polish or Danish name is not cut off at the
halfway mark.
/api/health reports it, unauthenticated, which is the point of the whole
change: a client adding this server by URL can label it from the probe it
already makes, instead of needing a second and authenticated call before it
can draw the entry.
In the panel it is a new API Server tab, first in the superadmin group
since it is this server itself, ahead of the PocketBase and Web App tabs
that describe what it talks to. Strings in all three languages, and the
route table in the README and the API reference tab both grow the two new
endpoints.
Known gap, deliberately not closed here: the compose files do not pass
SERVER_NAME, so under Docker a rename from the panel writes the container's
.env and no volume keeps it - it reverts to the default the next time the
container is recreated. Wiring it as ${SERVER_NAME:-} would make the host
.env authoritative, at the cost of the other trap the previous commit
documented, where the environment silently overrides the panel on every
restart. That is a call about the deployment, not about this endpoint.
Verified by new tests over the handler: the rename applies at runtime,
lands in .env, reaches /api/health, is rejected without touching .env when
blank or over-long, and accepts a name of exactly the limit in multi-byte
runes. go build, go vet and go test ./... pass. Drove the built panel in a
browser against a stub backend - the tab renders, loads the current name,
saves, and reads correctly in Polish - and ran the rebuilt api-server.exe
and webapp.exe end to end, confirming the embedded bundle really contains
the new tab and that SERVER_NAME reaches /api/health through both the
server itself and the Web App's proxy.
Not verified: no Docker build, so the images still serve the old panel
until they are rebuilt and pushed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
cd16d4383f |
Orgs: let any user create an organization and become its admin
Organization writes were superadmin-only, so standing up a tenant needed an out-of-band superadmin. Creating one is now self-service, and an admin manages the org they belong to. - POST /api/orgs is open to any authenticated user. A creator who isn't a superadmin must have no organization yet (a single-valued membership relation means a second one would abandon the first), and is promoted to the new org's admin and first member in the same request. If that promotion fails the org is rolled back, so it is never left stranded with nobody able to administer it. Superadmins still create tenants without joining them. - PATCH/DELETE are manager-gated and scope an admin to their own org. An admin deletes theirs only as its sole member: they are detached and demoted to a plain user before the record goes, so the org is empty when it is removed. Other members still block deletion with a 409. - /api/me now carries organization + organizationName, which the clients need to tell "no org yet" from "org you administer". The panel, Web App (new OrgManager.vue in Settings) and Phone App (new _OrganizationSection) all mirror the server's gates rather than re-deciding them. The Phone App cached its role at login and gates the Users tab on it, so AuthService.adoptRole refreshes that from the profile instead of making a freshly promoted admin sign in again. Covered by orgs_test.go, which drives the real handler + middleware chain against a stand-in PocketBase: promotion, the already-a-member refusal, superadmin staying unattached, the rollback, own-org scoping, the detach-and-demote, and the blocking-member 409. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8656aaaee3 |
Split Web App Settings into tabs; allow unset plugin Control mode
Web App: Settings is now two tabs — Personal settings (account, appearance, profile, privacy, advanced/danger) and Integrations (Toyota, Anker Solix). Panels use v-show so loaded state and in-flight edits survive a tab switch. Adds settings.tabs.* labels (en/pl/da). API Server panel: non-required select config fields now render a leading "Not set" option, so a superadmin can leave the Anker Solix Control mode (and Toyota Brand) unset at the global layer. Previously every option was a real value, forcing a pick that won the cascade and locked organizations and users out of choosing their own mode. The server side already treated an empty value as "abstain"; this closes the UI gap. Adds plugins.notSet label (en/pl/da) and rebuilds the embedded panel dist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
367113f538 |
Default the Web App to :8090 (BFF) in the API Server
Point the API Server's Web App references at the production BFF port instead of the Vite dev port (5173): the WEBAPP_URL default probed by /api/status, and the panel's Web App URL / allowed-origins placeholders. Update .env.example and the README env table to match. Rebuild the embedded admin panel dist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
b6bb6b1df0 |
Add a language-switch system with per-language files
Introduce a hand-rolled i18n layer across all three UIs, each reading its
text from per-language JSON files (English base + Polish + Danish). Nothing
in the converted screens hardcodes English any more.
- Web App (Vue): src/i18n/{en,pl,da}.json + index.js exposing t()/tSplit(),
reactive to the signed-in profile locale. Every view, component, form and
the status labels in lib/format.js go through t().
- API Server panel (Vue): src/i18n/ with its own localStorage-persisted
language (the panel has no user profile) and a header language picker.
Chrome, cards, login and API section titles translated; endpoint reference
descriptions intentionally kept in English. Rebuilt embedded dist.
- Phone App (Flutter): assets/i18n/ + lib/i18n.dart loaded at startup,
driven by AppSettings.locale. Nav, login, lock, dashboard, the full
Settings panel (incl. language picker) and format.dart status labels
translated; remaining detail screens fall back to English.
Language = the language half of the existing BCP-47 locale; the region half
still drives date/number/currency formatting. Missing keys fall back to
English, and plurals use Intl.PluralRules / Intl.plural so Polish gets the
correct one/few/many forms. Settings flags languages without a translation.
Tests updated to assert the localized (Polish) status wording; all pass.
See TRANSLATIONS.md for the format and how to add a language.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
98acd7d121 |
Add Web App settings to the API Server panel
Mirror the PocketBase settings card with a superadmin-only Web App section: it edits the Web App base URL (the address /api/status already probed) and the CORS allowed origins, applying both at runtime and persisting them to .env so they survive a restart. The CORS middleware previously built its allow-list once, when Handler() was constructed, so an edited origin list would not have taken effect until a restart — which would have made the new field quietly lie. Move the lookup into a per-request originAllowed helper reading under the existing lock. Verified behaviour is unchanged: an allowed origin still gets the headers plus Vary: Origin, an unknown origin gets none, and preflight still returns 204. PUT rejects an empty origin list rather than silently keeping the old one, since an empty list would lock out every browser client. The authenticated round trip (loading and saving real settings) is not verified here — it needs a superadmin login. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
ae6ed4ac1e |
Rebuild API Server on the PilotVault structure
Mirror PilotVault's API Server layout and add the superadmin console,
plugin system, runtime PocketBase settings, and user/organization
management. The car domain (cars, service records, parts, sharing) is
carried over unchanged apart from the auth switch.
Layout: main.go -> cmd/server/main.go; module carcontrol/api ->
drivervault/apiserver. internal/api is split by concern (auth, users,
orgs, settings, plugins, status, health, respond).
Auth: replace the server-minted HS256 JWT and the sessions collection
with a PocketBase token proxy. /api/auth/login relays PocketBase's
{token, record}, and every protected request re-resolves that token
against PocketBase, so a role change or deletion takes effect at once
instead of waiting out a token. AUTH_SECRET is obsolete and internal/auth
is gone. Per-device session listing/revocation goes with it: PocketBase
tokens are stateless. Changing a password rotates the user's token key,
which invalidates every token already issued.
Roles: add superadmin alongside user/admin, plus an organizations
collection and users.organization. Admins are scoped to their own
organization; superadmins span all of them. Guards prevent changing your
own role, deleting your own account, an admin touching a superadmin, and
deleting an organization that still has members.
Plugins: new internal/plugins package with one contract over two kinds --
builtin (compiled in) and external (any HTTP service, registered at
runtime with no rebuild). State persists to plugins.json; secrets are
masked on read and preserved when saved back at the mask.
PocketBase settings: /api/admin/pb-config applies a new connection at
runtime and persists it to .env. It deliberately does not require a
working service account, so a wrong or unreachable connection can still
be fixed from the panel.
Panel: rebuilt as the superadmin console -- login gate, status, users,
organizations, PocketBase, plugins, and the endpoint reference.
Clients: update the Web App and Phone App for the PocketBase token shape,
the move of user management to /api/users ({users}/{user} envelopes, with
password resets folded into PATCH), and the removal of sessions. Both now
mirror the server's real guards rather than the old last-admin rule, and
parse PocketBase's field-level error shape.
Config: modern POCKETBASE_*/API_ADDR names with legacy PB_*/PORT
fallbacks, so existing .env files keep working. Also fixes /api/status
probing the Web App on 8090 instead of DriverVault's 5173.
Run scripts/setup-pocketbase.mjs to add the organizations collection and
grow users.role; every client must log in once more.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
ba3f227361 | Initial commit |