Web App: two sites, one tab — a server you can switch

Two locations means two full stacks, and until now the app could only ever
see the one that served it. The login screen's "Server settings" could point
it elsewhere, but as a global swap: it replaced the server rather than
adding one, and forgot the session you already had.

There is now a server picker at the bottom of the app rail, after signing
in rather than on the login screen. It names the server you are reading —
which matters most when both sites look identical — and switches in a click.

Each server is its own PocketBase with its own users, so nothing is
federated: you sign into each one once and a token is kept per server. The
home server is the one that served the app, reached same-origin through the
BFF proxy; any other is added by address and called straight from the
browser, which asks nothing new of the network — an API Server the phone app
can reach is internet-facing already.

Two things the switch turned out to need:

Views load on mount, so swapping the session alone left B's user looking at
A's cars. The RouterView is keyed on the active server and a switch returns
to the garage, since record ids belong to the server that issued them.

A request now carries the id of the server it went out to, and a 401 clears
that session rather than whatever is active when the answer lands. A page
fires half a dozen calls at once: the first rejection used to switch away
and the rest then cleared the session of the server it had switched to,
turning one stale remote token into a full sign-out.

A remote session expiring falls back to the home server with the entry left
in place to sign into again. Only Log out clears them all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 5 committed 2026-08-21 11:55:50 +02:00
1 parent 00d4c17392
commit f521d2b220
11 files changed
+757 -131

No files matched your search

+43 -7
View File
@@ -7,7 +7,9 @@ and all data access still flows through the API Server (never PocketBase directl
```
Browser ─► Web App BFF (:8090) ──/api/*──► API Server (:8080) ─► PocketBase
└── serves embedded Vue SPA
│ └── serves embedded Vue SPA
└──────────────────────────────────────► API Server at another site ─► its PocketBase
(added in the app, called straight from the browser)
```
## Layout
@@ -23,7 +25,8 @@ web/ Vue 3 + Vite + Tailwind v4 source
main.js app bootstrap
router.js /login, / (dashboard), /charging, /cars/:id, /settings
api.js the only place that calls the API Server (base URL resolution)
auth.js token/profile state, isAdmin
servers.js the server list + a session per server; which one is active
auth.js token/profile state for the active server, isAdmin
prefs.js theme/locale/date/font preferences -> <html>
i18n/ en / pl / da translation files + loader
lib/format.js date/km formatting + next-service status badges
@@ -34,7 +37,8 @@ web/ Vue 3 + Vite + Tailwind v4 source
TechnicalCheckFormModal, MaintenanceFormModal, FuelFormModal,
ChargingFormModal,
DocumentFormModal, ReminderFormModal, PartFormModal, ShareModal,
OrgManager, AdminUsers, Logo
OrgManager, AdminUsers, Logo, ServerSwitcher,
ServerConnectModal
views/ Login, Dashboard, CarDetail, Charging, Settings
```
@@ -62,9 +66,9 @@ override with `VITE_API_TARGET`), so the client uses same-origin relative URLs a
avoids CORS. It also listens on all interfaces (`host: true`) so it's reachable on
the LAN (e.g. `http://10.2.1.101:5173`).
At runtime, users can override the API base URL from the login screen's **Server
settings** (persisted in `localStorage` as `cc_server_url`); resolution order is
that override → `VITE_API_BASE` → `/api`.
The base URL each call goes to comes from the active server (see **More than one
server** below); with only the built-in one, that resolves to `VITE_API_BASE` →
`/api`.
## Build & run (production-style)
@@ -168,11 +172,43 @@ Config (`server/.env`, copy from `.env.example`):
- **Theming** — light/dark/system app-wide (Tailwind v4 class strategy); `prefs.js`
toggles `.dark` on `<html>` and applies the saved theme/locale/date/font.
## More than one server
Two sites, two full DriverVault stacks — and one browser tab. The server picker
sits at the bottom of the app rail (after signing in, not on the login screen):
it names the server you are reading right now, and switches between them in a
click.
- **The home server** is the one that served the app, reached same-origin through
the BFF's `/api` proxy. It is always in the list and can't be removed.
- **Any other server** is added by address — `https://garage.example.com`; the
`/api` is appended for you if you leave the path off — and is called **straight
from the browser**, not relayed through the BFF. That server therefore has to be
reachable from wherever the browser is, which it already is if the phone app
talks to it.
- **A session per server.** Each server is its own PocketBase with its own users,
so a token can't be carried across: you sign into each one once, and after that
switching needs no password. Sessions live in `localStorage` under
`cc_session_<id>`, the list under `cc_servers`, the active one under
`cc_active_server`.
- **Switching goes back to the garage**, because record ids belong to the server
that issued them — a car page can't survive the change.
- **An expiring remote session doesn't sign you out of the app**: that server's
token is dropped, the app falls back to the home server, and the entry stays in
the list to sign into again. Only *Log out* clears every server at once.
The remote server must allow the Web App's origin in **`CORS_ALLOW_ORIGINS`**
(API Server setting, `*` by default — so this works out of the box, and only
needs attention on a server whose list has been narrowed). Nothing needs to be
configured on the server you are browsing *from*.
## Auth & access
Login proxies to the API Server, which relays PocketBase's own token — there is
no JWT the server mints and no server-side session list. The token is stored
client-side and sent as `Authorization` on every call. `auth.js` exposes
client-side, per server, and sent as `Authorization` on every call; each request
is pinned to the server that was active when it went out, so one server's `401`
can never drop another's session. `auth.js` exposes
`isAdmin` and the current profile; the router guards `public` / `admin` routes.
Cars are per-user (owned + shared), and the UI mirrors the server's read / write
/ owner access levels.