wire the trigger: connecting an admin server provisions everyone

the decision and the execution existed; nothing called them. connecting a
server would have given a working media connection and zero accounts.

the trigger is registering an ADMIN server, not installing the plugin. at
install there is no credential, so there is nothing to provision with — and
this is also the right moment for a re-run, since a revoked token or a moved
server both come back through the same door.

the sidecar now holds the pushed account list in memory. each users:list
REPLACES what was held rather than merging: it is the platform's current truth,
not a cache. persisting it would create a second copy of the platform's users
that could disagree with it, and the recovery from disagreement is re-reading
the platform anyway.

`hasProvisionableUsers()` distinguishes never-told from told-there-is-nobody.
an empty list means a server with only an owner; no list at all means the push
has not arrived or the plugin is not entitled. provisioning nothing is right
for the first and a silent failure for the second.

awaited, so the response says what happened instead of the owner finding it in
a log later. and it never throws: registering the server already succeeded by
then, and losing it to a provisioning error would leave them with no media
access AND no accounts, which is worse than the accounts alone.

mapping rows are written one at a time as each result is known, not batched at
the end. a create that succeeds upstream but whose row fails to write leaves an
orphan account officer will try to create again — the narrower that window, the
better.

found while wiring: the first edit landed the block in editServer as well as
addServer, where `auth` does not exist. caught by tsgo, removed.
This commit is contained in:
2026-08-20 20:20:56 +00:00
parent 470d4f83c3
commit d7af39f834
4 changed files with 147 additions and 1 deletions
+20 -1
View File
@@ -9,6 +9,7 @@ import {
updateJellyfinServer,
} from '../db';
import { UpstreamError, authenticate, invalidateConfig, normalizeBase, probe, publicInfo } from './upstream';
import { runProvisioning } from './provision-run';
// `/_config` — the Jellyfin server registry, driven from the app.
//
@@ -116,7 +117,25 @@ async function addServer(req: Request, userId: number): Promise<Response> {
activate,
});
invalidateConfig(userId);
return Response.json({ server });
// ── Provision, if this connection can ──
//
// The trigger is registering an ADMIN server, not installing the plugin: at install time there is no
// credential, so there is nothing to provision with. This is also the right moment for a re-run — a
// token revoked and re-issued, or the server moving, both come back through here.
//
// Awaited, so the response says what happened rather than the owner discovering it in a log. It can
// take a few seconds against a server with many accounts; that is the honest cost of telling the truth
// in the reply.
const provisioning = await runProvisioning({
userId,
serverId: server.id,
base: url,
token: auth.token,
jellyfinUserId: auth.userId,
deviceId,
});
return Response.json({ server, provisioning });
} catch (err) {
if (duplicateLabel(err)) return bad(`you already have a server called "${label}"`);
throw err;