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>
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>
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>
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>
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>
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>