first dated version. plugins were all sitting on 1.0.0, which said nothing about
when a build was made or which of two copies is newer — the platform already
uses YYYY.MM.DD.NN and this follows it.
the version lives in manifest.ts because a plugin has no package.json; that is
the field the marketplace publishes and the installer compares.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
opencode discovers AGENTS.md and nothing else; claude code discovers CLAUDE.md.
this repo had neither, so an agent sent to change something here arrived with no
guidance at all under either.
it deliberately does NOT repeat README.md. that file says what the plugin is,
for someone deciding whether to install it. this one answers the other question:
i have to change something in here, what will i break.
the facts in it are read from the repository rather than recalled — the panel
keys from web/panels.ts, the tables from db/schema.ts, the permission keys, the
host binaries and the route claims from manifest.ts. presence is the declaration
everywhere in this system, so the file says what the platform will actually
conclude from these files existing.
CLAUDE.md beside it is a pointer, not a copy: two copies drift and the stale one
is the one somebody reads.
tags are how the marketplace is browsed rather than scrolled: a list of them on
the left, the plugins that carry one on the right. a closed vocabulary
(PLUGIN_TAGS in the platform) because free text becomes homelab, home-lab, Home
Lab and selfhosted inside a month, and a filter list of synonyms is worse than
no filter at all.
declared here rather than curated in the catalogue, so a plugin can say what it
is and nobody edits a table when one ships. the marketplace caches it on sync,
exactly as it already does for label and summary.
Same gap as the photos plugin: users:list updated an in-memory array and
nothing acted on it, so a person added to Officer later reached Video only if
somebody re-ran provisioning by hand. The platform broadcasts on every create
and delete and re-sends on every registration — nothing was listening.
Safe to run unattended without changing what it does, because runProvisioning
already draws the line in the right place: it creates the missing accounts and
tokens them, and it RECORDS an adopted account without touching it. Minting for
an adopted account resets a password its owner knows, and that stays behind
includeAdopted, which only a person can ask for.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Provisioning already created each member's Jellyfin account and signed in as
them to get a token — its own header says the token exists so "the person's
Officer session can already read their library". It was then dropped, because
linkJellyfinAccount had nowhere to put it.
So every non-owner resolved to no config and got a 503 setup form, asking them
to connect a Jellyfin server with a password nobody knows. It reads as a
permissions fault and is not one: the grant, the role and the account list were
all correct.
jellyfin_accounts now carries access_token and device_id, getConfig falls back
to the link row when the caller has no registry of their own, and there is no
path from there to the owner's token — a member resolving to the owner's
credential would read the owner's watch state and every library.
POST /_config/:id/provision repairs accounts linked before the column existed.
It resets the password to mint, which is free on an account Officer created
(random, discarded, known to nobody) and destructive on an adopted one, so
adopted accounts are skipped unless includeAdopted says otherwise.
Applied on alpha: conceiba and guipinto minted and verified against the live
server; fedra and diogo skipped as adopted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pastilhas rendered on the Jellyfin side with nothing opposite it, labelled as belonging to nobody —
reported by the owner, about their own name, which is the only line on that screen that could not
possibly be true.
The cause is a correct exclusion used for the wrong question. `listProvisionableUsers` leaves the
owner out on purpose: they hold the admin account this flow authenticates as, and provisioning them
would be a second admin nobody asked for. But this view is not the provisioning list, it is "who
exists on each side", and an account absent from the input read as an account that does not exist.
So the viewer is looked up directly and joined onto the account the connection signs in as — known
exactly from `jellyfinUserId` on the server row, never matched by name. It gets its own state rather
than being called "linked", because there is no row in jellyfin_accounts and there should not be: it
is the credential, not a provisioned account. It is also excluded from the "Jellyfin only" count,
which was counting the one account both sides agree on as belonging to neither.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Provisioning was invisible. Connecting a server can create accounts, adopt accounts and leave
accounts alone, and the only record of which happened was a JSON field in a response nobody reads and
rows in a table nobody can see. "Did it work" could only be answered by opening Jellyfin and counting.
The nav entry is Settings rather than Connection, since it is now more than the connection.
The join is computed in the sidecar and shares `normalize` with the reconcile planner, so the screen
cannot disagree with the matching rule about whether Diogo and diogo are one person — a settings page
saying two would be lying about what the next connection will do. It joins by the link ROW first and
by folded name only where none exists: a recorded pairing is a fact, a matching name is a candidate.
Four states, because they mean different things — linked (adopted or created), names match but not
recorded yet, Officer account with no Jellyfin one, and a Jellyfin account with no Officer user. The
last is never touched, and a linked pairing whose Jellyfin account has vanished is shown as such
rather than silently re-matched by name.
"Not known" is kept distinct from "none": if the platform has not sent the account list, or the
server cannot be reached, it says so instead of rendering an empty table that would read as a fact
about the server.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A new Jellyfin user can see nothing: EnableAllFolders is false by default, so a provisioned account
signs in to an empty home screen and looks like a bug in Officer rather than an unset flag.
POST /Users/{id}/Policy replaces rather than merges, so the policy Jellyfin returned from /Users/New
goes straight back with one field flipped. A hand-built body would quietly reset the other 43
properties. EnabledFolders is left as-is — ignored while the blanket flag is on, and still there if
the owner ever turns it off.
Not fatal if it fails: the account exists and works, and failing the create here would leave an
orphan upstream with no mapping row, which is worse than a user who cannot see films yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
the platform pushes the account list (users:list, declared by userProvisioning
in the manifest); this decides what to do about each one and does it.
DECIDING IS SEPARATE FROM DOING. reconcile.ts is pure and has no idea what
jellyfin is — which is why it can be tested against the real populations on
alpha without creating anything. nine tests, built on the actual overlap:
diogo and fedra already have jellyfin accounts, conceiba and guipinto do not,
rezio and peter exist only on jellyfin.
that test found a real bug before it shipped. officer's `username` was unique
CASE-SENSITIVELY, so `diogo` and `DIOGO` could both exist here while colliding
on jellyfin's uppercased NormalizedUsername index — and the first version
handed both the same jellyfin account, because it only checked links that
already existed rather than ones claimed during the pass. the platform side is
now case-insensitive too, so the collision cannot be created in the first
place; the guard stays because the constraint and the guard protect different
moments.
three outcomes, and the third is the point: LINK an account that already
exists, CREATE one that does not, LEAVE ALONE a jellyfin account with no
officer user. never delete. an absence in officer is indistinguishable from a
list that has not been told about someone yet, and a wrong deletion costs
somebody their watch history.
`adopted` carries that distinction into the mapping table. false means officer
made the account and may remove it; true means it existed first and was merely
linked.
NOBODY IS EVER TOLD THEIR PASSWORD. jellyfin has no must-change flag — verified
against its openapi, UserPolicy has 44 properties and none of them is one — so
a handed-over password stays valid forever. instead the generated one is used
once, immediately, to mint that person's own access token, and discarded. the
account exists and their officer session can already read their library.
which also makes rule 4 unskippable: the sign-in that verifies the account is
the same call that obtains the token, so there is no version of this that
"verifies later".
one failure does not stop the rest. a partially provisioned server is fixed by
running again; a run that aborts on the first error leaves everyone after them
unprovisioned and no record of who.