Commit Graph
5 Commits
Author SHA1 Message Date
pastilhas 19e94129a0 refactor(providers): a provider is a vendor and a key, and the table is gone
The dropdown was listing "connections" — a row per adapter + model + key — which
made choosing a model an act of configuration, put a key icon and a bin inside a
menu item, and showed five accounts nobody had created.

A provider is now a vendor Officer curates. Every one is listed, set up or not,
because a vendor you have no key for is exactly what you need to see in order to
add one. The model is chosen per generation, from that provider's entry in the
catalogue, beside it.

With the model off the row, all that remained of a "provider" was the key for
OpenAI — and Officer already stores keys: one active value per purpose, 0600
inside a 0700 directory, outside the backup tarball, created on first use so
nothing needs provisioning. So `atelier_providers` is dropped, along with
`generations.provider_id` that pointed into it. `provider` and `model` stay as
text on each generation, which were always the historical fact rather than a
pointer to configuration.

`getSecret`, never `getKey`: this is a foreign credential, and getKey mints a
random value for a purpose it cannot find — which for someone else's API key
means authenticating with something invented.

Resolution follows: a request names a model, and the catalogue says whose it is,
rather than a database being asked which stored row served it. A model nobody
in the catalogue claims is still accepted — the catalogue is advisory, and a
model released today should work before we ship a file that mentions it.

Two routes replace five: PUT and DELETE on /providers/:name/key. Deleting a key
that came from the environment is refused rather than faked — it belongs to the
install, not to this screen.
2026-08-20 23:02:31 +01:00
pastilhas 3c59bf4d0c feat(catalogue): refresh is a button, not a schedule
The daily job is gone. A refresh only matters at one moment — you are picking a
provider and want to know its models are current — and at that moment a button
answers the question, where a nightly pass can only have already answered it,
invisibly, or not. It also bought a standing obligation: something hitting a
vendor from every install forever whether anyone opens Atelier or not.

So the add dialog is now the curated file made visible. Provider comes from the
catalogue, models come from that provider's entry in it, and two buttons re-read
it from the vendor: one for the provider in front of you, one for all of them.
`POST /providers/catalogue/refresh?provider=` is the only trigger there is.

The dialog also says how old the answer is — "as shipped" until a refresh has
ever run, which is worth showing, because it means the models listed are exactly
what the plugin was released with.

A provider with no refresher answers 400 rather than a silent success: from a
button, "nothing happened" and "nothing could happen" look identical.
2026-08-20 22:21:15 +01:00
pastilhas 0de38f0b13 feat(catalogue): a curated file of providers and models, refreshed daily
The five connections in the database were never configured by anyone — a
migration minted one per model from the adapter's own list, so the table held
five decisions nobody made. What that list actually is, is a CATALOGUE: what
you can choose from, which is not the same kind of thing as what you chose.

So it is a file. `sidecar/catalogue/shipped.ts` is the curated baseline, in
git, reviewed; `$OFFICER_ROOT/data/atelier/catalogue.json` is what a refresh
writes, and it wins where it has something to say. Data dir, not the plugin
dir — a plugin directory is a clone an update replaces, which is the rule
env.ts already states for images.

Refresh runs in the sidecar, daily, and only on boot when the file is actually
stale — this process has restarted twenty times in an afternoon and should not
hit OpenAI twenty times for it.

Two sources, unequal, and the code says so. `GET /v1/models` is authoritative
and thin: ids and nothing else, so a model matching /^(gpt-image|dall-e)/
appears the day it ships without a release. Prices come off an HTML page
because there is no pricing endpoint for any plan, and that parser is written
to give up rather than guess — a guess gets written to disk and then believed.
When it gives up it says so in the log; the durable replacement is handing the
page to a model for structured output, which Officer can already do locally.

Nothing a refresh cannot establish overwrites a curated value: fields merge
one by one, not objects wholesale, so an id-only listing cannot wipe the sizes
and prices that were written by hand. Retiring is decided by the merge, which
can tell a complete listing from a request that failed halfway — and it marks
`deprecated` rather than deleting, because the gallery labels 88 historical
rows from this list.

Per-model sizes, qualities and transparency now travel with each entry, which
is what will let the form stop offering dall-e-2 a transparent background it
cannot do. The catalogue is advisory throughout: validate.ts still refuses to
enforce per-model rules, exactly as before.
2026-08-20 22:15:35 +01:00
pastilhas 82ca696ccf refactor(web): two panels on the left, the work on the right
Four panels became three, and the arrangement carries the change: everything
that organises or configures is stacked in one narrow column, and the whole of
the rest is one surface for looking at images.

Collections is a LIST, not a grid of covers — it sits in a narrow column beside
the gallery, where two cards per row meant four collections visible. A row is a
name, a count, and rename/delete on hover. Create is a dialog rather than an
always-present input, and a filter field replaces it in that space, since
finding one is the frequent act and making one is not.

Selecting a collection now shows it in the GALLERY. It used to replace the list
inside its own panel, which meant the selector disappeared the moment it was
used, and both panels kept their own copy of one collection's images and
disagreed after any edit. `?collection=` is read by the gallery now; the
collections hook stopped fetching detail entirely.

Deleting asks the question that cannot be undone: keep the images, or delete
them too. That needed a server route — DELETE /collections/:id?images=delete —
which reads the members BEFORE the collection goes, since deleting it takes the
memberships with it. Unchecked by default: the recoverable answer is the one
you get by pressing Enter without reading.

Providers stopped being a panel. It was a card per provider, each with a key
field and three buttons, filling a quarter of the screen for something set once
— it is a dropdown at the top of Generate now, listing "adapter · model" with a
delete on each, and adding one opens a dialog. The form and the dropdown share
one provider list (useProviders); they were two fetches, so adding a provider
left the form showing the old models until a reload.
2026-08-20 21:34:34 +01:00
pastilhas f23ec1283b manifest, thin proxy router, and the sidecar backend
ported from the standalone officer-atelier service: providers (adapter-shaped, openai the only
adapter today), the generation queue, collections, and the database layer — kept on its own
Postgres database rather than folded into officer_db, since both collections and provider_settings
had rows on the machine this was extracted from.

what changed rather than just moved:
- images are served from an authenticated GET /files/:filename instead of an unauthenticated
  PUBLIC_URL-prefixed static route
- the OpenAI-key bootstrap fallback reads the secret store (getSecret) before .env, per-adapter
- provider_settings (the one-row-per-adapter shape providers replaced) did not come along — it has
  no callers left anywhere in the server code, only a migration script that also did not move

ping/echo (smoke-test routes with no caller) did not move either; the sidecar's own /_health covers
liveness.

not yet wired into the platform: sidecar/index.ts exists but nothing mounts it, and web/ has not
been written, so this is not yet a discoverable plugin.
2026-08-20 03:53:53 +00:00