docs: PLUGIN.md as the current truth, not the extraction
It described a separate atelier Postgres database, providers as a section in Officer's settings, and a BLOCKERS.md that was never committed here — none of which is true. It is now what the code does, and why: the three words that are not interchangeable, where keys live and why getSecret rather than getKey, the catalogue's four rules, why refresh is a button, what a refresh can and cannot learn, and the two ceilings the 240s poll sits under. Written so that someone arriving cold can carry on from here.
This commit is contained in:
@@ -1,204 +1,188 @@
|
||||
# Atelier — extracted from a standalone service into a plugin
|
||||
# Atelier — how it is built, and why
|
||||
|
||||
**Status: extracted 2026-08-20, unattended overnight run.** Read `BLOCKERS.md` alongside this file —
|
||||
it has the decisions made without being able to ask, in the order they were found. This file is what
|
||||
moved, what didn't, and why; that one is what was uncertain.
|
||||
Current as of **2026-08-20**. Rewritten that evening, because the previous version described an
|
||||
extraction rather than the thing that now exists and was wrong in three ways that mattered: it claimed a
|
||||
separate `atelier` Postgres database, providers as a screen in Officer's settings, and a `BLOCKERS.md`
|
||||
that was never committed to this repo.
|
||||
|
||||
Atelier is image generation as a service: a provider-shaped API over OpenAI's image endpoints, storing
|
||||
what it makes so every image has a durable reference. It came from
|
||||
`~/officer-dev/dockers/officer-atelier`, a standalone Bun/Hono/Postgres server predating the plugin
|
||||
system, still running in production as `officer-atelier` (PM2, port 3002, `atelier.officer.dev`) — see
|
||||
"What did not move" below before assuming it is safe to touch.
|
||||
**Code is the truth.** Where this file and the code disagree, fix this file.
|
||||
|
||||
---
|
||||
|
||||
## What moved, and what changed on the way
|
||||
## What it is
|
||||
|
||||
**Nothing pointed the other direction.** Unlike offscale and music, a grep of the whole platform for
|
||||
`atelier` found no reverse dependency — no widget, no relay, nothing importing from the standalone
|
||||
service. It came out whole with no seam to design around, which is the exception rather than the rule
|
||||
for this kind of extraction.
|
||||
Image generation as an Officer plugin: a prompt goes in, a job is queued, images come out and stay —
|
||||
with a gallery, collections, and an OpenAI-compatible API an agent can poll rather than block on.
|
||||
|
||||
### The backend — `sidecar/`
|
||||
|
||||
Providers (adapter-shaped: `types.ts`, `openai.ts`, an `index.ts` that dispatches — OpenAI is the only
|
||||
adapter today, and the point of the shape is that a second one is a new file, not a new screen), the
|
||||
generation queue (Postgres-backed, `MAX_CONCURRENT_GENERATIONS` workers), collections, and the database
|
||||
layer moved essentially unchanged. Three things did not:
|
||||
|
||||
1. **Images are served from `GET /files/:filename`, authenticated, not an unauthenticated `PUBLIC_URL`
|
||||
route.** The standalone service served `/images/*` off Bun's own route table with no session,
|
||||
because every image needed "a permanent URL" reachable by the mobile app and anything else on the
|
||||
tailnet. Inside Officer that is not an option — see "What you may NOT do" in the kickoff prompt. The
|
||||
platform's own `?token=` escape hatch (already used by `FileViewer/file-types.ts` and the music
|
||||
plugin's `MusicPlayerHost` for exactly this — a browser cannot put an `Authorization` header on an
|
||||
`<img src>`) covers it: the JSON API returns `/files/<name>` (relative, no host, no token — the
|
||||
sidecar cannot know the platform's public origin any more and does not need to), and the frontend
|
||||
appends its own token when rendering (`web/lib/api.ts`'s `fileUrl`).
|
||||
|
||||
2. **The OpenAI-key bootstrap fallback reads the secret store before `.env`.** `keyForProvider` still
|
||||
prefers a provider row's own encrypted key; the fallback that used to be `process.env.OPENAI_API_KEY`
|
||||
directly is now `getSecret('atelier-<adapter>-key')` first, `.env` second — `getSecret`, never
|
||||
`getKey`, because a foreign credential must not be silently substituted with a random one. Set it
|
||||
with `bun run secret:set atelier-openai-key`.
|
||||
|
||||
3. **`provider_settings` did not come along.** It is the one-row-per-adapter shape `providers` replaced
|
||||
(see the standalone repo's own `providers/schema.ts` comment and `scripts/migrate-providers.ts`), and
|
||||
grepping the server code found no caller anywhere except that migration script — which also did not
|
||||
move. Its table and its one row stay in the `atelier` database, untouched. If a machine's migration
|
||||
never ran, that is a real gap, but it is the standalone service's gap, not one this extraction
|
||||
introduced.
|
||||
|
||||
`ping`/`echo` (smoke-test routes, no caller) did not move either — the sidecar's `/_health` covers
|
||||
liveness, matching every other HTTP sidecar in this codebase.
|
||||
|
||||
### The database — stays separate, does not fold into officer_db
|
||||
|
||||
Decision 3 said to check whether `collections` and `provider_settings` were empty before folding them
|
||||
into `officer_db`. Both had rows (2 and 1), so the `atelier` Postgres database stays separate — the
|
||||
sidecar keeps its own connection (`sidecar/db/db.ts`), reading `OFFICERDEV_POSTGRES_URL` from the platform
|
||||
`.env`. **This is new territory for the plugin system**: `db/schema.ts` at a plugin's root is the
|
||||
`officer_db`-prefixed-tables convention (`pushPluginSchema`, `<appname>_` prefix), and this plugin has no
|
||||
such file — `discoverPlugins()` correctly reports `schema: null` for it, and the install flow's schema
|
||||
step is simply skipped. Nothing in `src/servers/plugins/` needed to change for this, because "no
|
||||
`db/schema.ts`" is already a valid state the discovery contract handles — a plugin with an internal,
|
||||
self-managed database is indistinguishable from a plugin with no tables at all, as far as the platform
|
||||
is concerned. Worth knowing before assuming every plugin's data lives in `officer_db`.
|
||||
|
||||
**Not set automatically.** `OFFICERDEV_POSTGRES_URL` has no default — see `BLOCKERS.md` for why (mainly:
|
||||
pointing it at the live production `atelier` database while that database's own standalone service is
|
||||
still running risks two processes racing the same generation queue). The owner has to add it before
|
||||
installing.
|
||||
|
||||
**Migrating into `officer_db` as `atelier_*`-prefixed tables is real follow-up work**, not done here —
|
||||
five tables (`generations`, `collections`, `collection_items`, `providers`, and the dead
|
||||
`provider_settings`, which should be dropped rather than migrated), real data, needs a human watching it.
|
||||
|
||||
### The frontend — three panels, plus a settings screen
|
||||
|
||||
`web/panels.ts` + `web/layout.ts`: Generate, Gallery (+ Lightbox), Collections — the ≈1,200 lines of
|
||||
real work the brief identified, ported with the shadcn components Officer already ships
|
||||
(`@/components/ui/*` — the standalone service's own `src/apps/frontend/components/ui/*` did not move,
|
||||
per decision 1). Providers did not become a fourth panel:
|
||||
|
||||
**Providers is a platform Settings screen** (`Settings → Integrations → Atelier`,
|
||||
`src/apps/officer-web/.../IntegrationsSettings/AtelierProviders.tsx`), per decision 2. Worth flagging
|
||||
loudly: **`IntegrationsSettings/index.tsx` itself carries a comment, written five days earlier when email
|
||||
was extracted, that says the opposite** — "a plugin contributes panels, not settings sections, so it
|
||||
now lives inside the email workspace itself." Decision 2 was explicit and pre-made, so it was followed
|
||||
anyway, and the contradiction is recorded in `BLOCKERS.md` rather than silently resolved either way.
|
||||
`AtelierProviders.tsx` calls `/api/atelier/providers` over plain HTTP via `useClient()` — it imports
|
||||
nothing from `plugins/atelier/`, so nothing here reaches back across the plugin boundary; it is exactly
|
||||
the same relationship any authenticated frontend has with any authenticated backend.
|
||||
|
||||
Panel coordination is URL + one channel, never props (a panel component only ever receives `panelId`):
|
||||
|
||||
- Gallery's lightbox is `?selected=<id>`; Collections' detail is `?collection=<id>` — two different
|
||||
names because both panels render on the same route at once and would otherwise collide.
|
||||
- Generate bumps `useAtelierRefresh` (added to `src/workspaces/officerdev/src/channels.ts`, same
|
||||
stable-bump shape as `useFilesRefresh`) when a job finishes; Gallery subscribes to it. This replaces
|
||||
the standalone app's `onFinished` callback prop, which only worked because `App.tsx` lifted the
|
||||
gallery's state above both tabs — not an option once they are independent panels.
|
||||
|
||||
The default layout is a fixed horizontal three-way split (Generate 26% / Gallery 48% / Collections
|
||||
26%) rather than the standalone app's hash-based tabs — the `LayoutNode` type has no tab-group concept,
|
||||
only `group` (split) and `panel`, so three panels means three panes. Users can resize or rearrange
|
||||
freely; this is only the starting arrangement.
|
||||
|
||||
### `sharp` — promoted to a direct dependency
|
||||
|
||||
`generations/inspect.ts` needs it to measure width/height/alpha-channel/opacity of a written image —
|
||||
load-bearing, not incidental: it is how a `background: transparent` request that silently produced an
|
||||
opaque image gets caught, which the standalone repo's own comments call "the single most consequential
|
||||
failure for an asset destined to be an app icon." It was already resolved in `bun.lock` as an optional
|
||||
transitive dependency of `next`; adding it as a direct dependency pulled in no new package versions —
|
||||
verified by reading the lockfile diff before committing it.
|
||||
```
|
||||
manifest.ts the plugin's declaration; `sharp` is declared here, shared across plugins
|
||||
api/router.ts a thin auth-gated proxy. No Atelier knowledge, and it must never grow any
|
||||
sidecar/ the whole contract: catalogue, providers, queue, database
|
||||
web/ three panels — Collections, Generate, Gallery
|
||||
db/schema.ts found by the platform's schema generator; re-exports what lives in sidecar/db
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What did not move, and must not be touched
|
||||
## Three words that are not interchangeable
|
||||
|
||||
**The standalone `officer-atelier` service keeps running, unchanged.** Its own `ecosystem.config.cjs`
|
||||
says plainly: "this is the instance the mobile app talks to, so it holds still." It serves
|
||||
`https://atelier.officer.dev`, holds real data (87 generations, 2 collections, 5 providers on the
|
||||
machine this was extracted from), and this extraction did not stop it, did not touch its nginx config,
|
||||
and did not touch the mobile app. **Cutting the mobile app over to the plugin's route — or deciding
|
||||
whether to at all — is the owner's call, not something this run made unilaterally.** Until that happens,
|
||||
having both installed means two independent Atelier deployments existing side by side, which is
|
||||
expected, not a bug.
|
||||
Getting these confused is what produced the UI we threw away on 2026-08-20, so they are fixed:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **provider** | A vendor. OpenAI. Curated by us, listed whether you have a key or not. |
|
||||
| **adapter** | Code that speaks a vendor's HTTP API. Written once, only developers say it. |
|
||||
| **catalogue** | The curated file: which providers exist, their models, and what each model can do. |
|
||||
|
||||
There is deliberately **no fourth concept**. There used to be — a "provider" was a database row of
|
||||
adapter + model + key, and choosing a model was therefore an act of configuration. That produced five
|
||||
rows nobody had created (a migration minted one per model from a hard-coded list), a key icon and a
|
||||
delete button inside a dropdown item, and no way to answer "which vendors could I use" without having
|
||||
already set one up.
|
||||
|
||||
**A provider is a vendor with a key or without one. The model is chosen per generation.**
|
||||
|
||||
---
|
||||
|
||||
## Where things live
|
||||
|
||||
**Tables — `officer_db`, `atelier_` prefixed, like every other plugin.** Two of them:
|
||||
`atelier_generations`, `atelier_collections` (+ `atelier_collection_items`). The extraction originally
|
||||
kept a separate `atelier` database; that was reversed the same week. A plugin that behaves differently
|
||||
from every other plugin is a plugin nobody can reason about.
|
||||
|
||||
**There is no providers table.** With the model off the row, all that was left of one was *the key for
|
||||
OpenAI* — and Officer already stores keys.
|
||||
|
||||
**Keys — the platform secret store**, `secrets/officer-keys.db`, purpose `atelier-<provider>-key`. One
|
||||
active value per purpose, `0600` inside a `0700` directory, deliberately outside `data/` so it does not
|
||||
travel in the same tarball as a database dump. Purposes are created on first use, so nothing is
|
||||
provisioned in advance and no plugin can read another's.
|
||||
|
||||
Read with **`getSecret`, never `getKey`**: `getKey` mints a random value for a purpose it cannot find,
|
||||
which is right for our own encryption keys and catastrophic for a foreign credential — it means
|
||||
authenticating with something invented. Falls back to `OPENAI_API_KEY` in the environment, which is how
|
||||
an install supplies a key without a UI.
|
||||
|
||||
**Images — `$OFFICER_ROOT/data/atelier/images/`,** named by generation id, so a row and its file find
|
||||
each other with no index between them. Not in the plugin directory: that is a git clone an update
|
||||
replaces, and artefacts kept there vanish on upgrade with nothing logging it.
|
||||
|
||||
**Catalogue — two files.** `sidecar/catalogue/shipped.ts` is the curated baseline, in git, reviewed;
|
||||
`$OFFICER_ROOT/data/atelier/catalogue.json` is what a refresh writes. The second merges over the first,
|
||||
and a failed refresh therefore degrades to what we shipped rather than to nothing.
|
||||
|
||||
---
|
||||
|
||||
## The catalogue's four rules
|
||||
|
||||
1. **Advisory, never authoritative.** It shapes what the UI *offers*; it never decides what the server
|
||||
*accepts*. `validate.ts` deliberately refuses to enforce per-model rules — a stale copy of them would
|
||||
reject valid combinations, and the vendor should be the one to answer. A model the catalogue has
|
||||
never heard of still generates.
|
||||
2. **Allowed to be incomplete.** A field a refresh cannot establish stays absent, and absence degrades:
|
||||
no price means a null cost, which is honest. An invented price sums into a total that is wrong.
|
||||
3. **Fields merge, not objects.** `GET /v1/models` knows ids and nothing else, so an entry built from it
|
||||
would otherwise wipe the sizes, qualities and prices that were curated by hand.
|
||||
4. **Retire, never delete.** A model the vendor stopped listing is marked `deprecated`: hidden from the
|
||||
dropdown, still resolving everywhere else. The gallery labels historical rows from this list, and 88
|
||||
images name the model that made them.
|
||||
|
||||
### Refresh is a button
|
||||
|
||||
There was a daily cron and it was removed the day it was written. A refresh matters at exactly one
|
||||
moment — you are choosing a provider and want to know its models are current — and a button answers that
|
||||
question, where a nightly pass can only have already answered it, invisibly, or not. A timer also buys a
|
||||
standing obligation to hit a vendor from every install forever, whether anyone opens Atelier or not.
|
||||
|
||||
`POST /providers/catalogue/refresh?provider=openai`, or without the parameter for all of them. A
|
||||
provider with no refresher answers 400 rather than a silent success: from a button, "nothing happened"
|
||||
and "nothing could happen" look identical.
|
||||
|
||||
### What a refresh can and cannot learn
|
||||
|
||||
`GET /v1/models` is authoritative and thin — ids, an owner, a timestamp. Which is why image models are
|
||||
matched by pattern (`/^(gpt-image|dall-e)/`) rather than a list: `gpt-image-2` appeared this way, on the
|
||||
day it shipped, with no release of this plugin.
|
||||
|
||||
**There is no pricing endpoint, on any plan.** Prices come off an HTML page, and `openai-refresh.ts`
|
||||
parses it in a way that gives up rather than guesses — a guess gets written to disk and then believed.
|
||||
As of 2026-08-20 it gives up: the page renders its prices client-side, and it says so in the log. **The
|
||||
replacement is not a better regex** — it is handing the page to a model and asking for structured
|
||||
output, which Officer can already do locally through its own Claude sidecar. That is the next piece of
|
||||
work here.
|
||||
|
||||
---
|
||||
|
||||
## The panels
|
||||
|
||||
**Collections** (left, top) — a filterable list, one row per collection: icon, name, count, rename and
|
||||
delete on hover. Create is a dialog. Deleting asks the one question that cannot be undone — keep the
|
||||
images, or delete them too — unchecked by default, so the recoverable answer is what you get by pressing
|
||||
Enter without reading. That needed `DELETE /collections/:id?images=delete`, which reads the members
|
||||
*before* deleting the collection, since dropping it takes the memberships with it.
|
||||
|
||||
**Generate** (left, bottom) — provider dropdown (the catalogue, one entry per vendor, "no key" shown
|
||||
inline), model dropdown (that provider's models), then Add/Change key, Update models, All providers, and
|
||||
the form.
|
||||
|
||||
**Gallery** (right) — every image, or one collection's images when `?collection=` is set. The collection
|
||||
view lives here rather than in the Collections panel: that panel is a narrow selector, and rendering a
|
||||
grid inside it replaced the list you were selecting from with the thing you selected.
|
||||
|
||||
The panels coordinate through the URL and one refresh channel, never props: `?selected=` is the
|
||||
lightbox, `?collection=` is what the gallery shows. Generate bumps `useAtelierRefresh` when a job
|
||||
finishes; Gallery subscribes.
|
||||
|
||||
**Removing a panel key rewrites saved layouts** that name it — see `dashboards/queries.ts`, which is a
|
||||
bug that cost an evening on offscale. The install flow clears this plugin's screen rows, so a panel
|
||||
removed between versions is safe **as long as the plugin is reinstalled rather than merely pulled**.
|
||||
|
||||
---
|
||||
|
||||
## The job model
|
||||
|
||||
Generation answers **202 with a job id**, never the image. `n: 10` is ten HTTP requests and holding a
|
||||
connection open for the length of them ties up a socket for minutes; the callers are agents that can
|
||||
poll. `GET /images/generations/:id?wait=` long-polls up to **240s**, which is bounded by two ceilings
|
||||
above it — Bun's `idleTimeout` at 255 (256 is rejected outright) and nginx at 320. The innermost has to
|
||||
be the one that fires, or the caller gets a dropped connection instead of a status.
|
||||
|
||||
Each image is its own row with its own cost and duration, so one failure out of four does not discard
|
||||
the three that worked.
|
||||
|
||||
The same model is what video will need when it arrives: a two-minute render is exactly the case 202 was
|
||||
built for. Video needs a longer poll ceiling, per-second pricing (the catalogue's `Pricing` is already a
|
||||
shape per unit for this reason), and duration/fps on the row — `has_alpha`/`opaque` go null for it, the
|
||||
way pricing already goes null when we do not know it.
|
||||
|
||||
---
|
||||
|
||||
## Serving images
|
||||
|
||||
`GET /files/:filename`, authenticated. The standalone service served them from an unauthenticated
|
||||
`PUBLIC_URL` route; inside Officer that is not an option. A browser cannot put an `Authorization` header
|
||||
on an `<img src>`, so the JSON API returns a relative `/files/<name>` and the frontend appends its own
|
||||
`?token=` — the same escape hatch `FileViewer/file-types.ts` and the music plugin already use.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
- `bunx tsgo`: clean.
|
||||
- `bun test`: baseline was 1041 pass / 0 fail across 90 files; after this extraction, 1069 pass / 0 fail
|
||||
across 92 files — the two new files are `sidecar/serialize.test.ts` and
|
||||
`sidecar/generations/response.test.ts`, both ported from the standalone repo (URLs updated for the new
|
||||
`/files/` scheme). No regressions, no file count drop.
|
||||
- `discoverPlugins()` finds the plugin, the manifest validates, `schema: null` as expected (separate
|
||||
database).
|
||||
- The platform-side proxy router (`api/router.ts`) mounts and correctly answers `503 atelier sidecar
|
||||
not available` with no sidecar running — same behaviour as offscale's.
|
||||
- The sidecar's internal Hono app was exercised directly (`buildHonoApp()`, real HTTP requests, no port
|
||||
bound) against the `atelier_dev` Postgres database: `/providers`, `/providers/adapters`,
|
||||
`/collections`, `/images` all answered `200` with real rows; `/files/<missing>` answered `404`; an
|
||||
unknown route answered the OpenAI-shaped `404` envelope. Old provider rows correctly failed to
|
||||
decrypt (a fresh `secret.key` was generated for this checkout, as intended — a key that will not
|
||||
decrypt is broken, not absent, and `keyForProvider` reports `configured: false` rather than crashing).
|
||||
- **The real production SPA bundle builds successfully with the plugin included.** Called
|
||||
`generatePluginsModule` and `rebuildFrontend` directly (the same functions `refreshPluginMounts` calls
|
||||
during a real install) against a synthetic "atelier is installed and enabled" state — no server
|
||||
running, no port bound. `Bun.build`, with the real Tailwind plugin, compiled `Plugins.gen.tsx`, all
|
||||
three panels, and the new `AtelierProviders` settings section in 2.2s with zero errors: 3 outputs, no
|
||||
warnings. This is the same build step an install performs and exercises everything `bunx tsgo` cannot
|
||||
— module resolution through Bun's own bundler, JSX compilation, Tailwind processing — without needing
|
||||
a running server. What it does not prove is that the result **renders correctly in a browser**; see
|
||||
below.
|
||||
- **Not done**: a live PM2 install/enable/disable/uninstall/purge cycle through `/marketplace`, per the
|
||||
runbook's own verification table. This machine's live `officer` process serves the owner's real
|
||||
session and its own `plugins/` directory is the main checkout, not this worktree — installing through
|
||||
it would write real rows into the shared `officer` Postgres database from code that only exists here.
|
||||
That crosses into "affects shared systems beyond the local environment," so it was not done
|
||||
unattended. The safer checks above cover the same ground read-only or against an isolated database.
|
||||
- `bunx tsgo` — clean, apart from `Cannot find module 'sharp'` in a checkout where the plugin's declared
|
||||
dependency has not been installed. That is expected outside a real install.
|
||||
- `bun test plugins/atelier` — 32 pass. `catalogue/merge.test.ts` covers the four merge rules above,
|
||||
which are the part where a mistake silently rewrites curated data.
|
||||
- The web bundle builds with the platform; the panels are typechecked against the real `@/components/ui`.
|
||||
|
||||
---
|
||||
|
||||
## Open, for whoever picks this up
|
||||
## Open
|
||||
|
||||
1. **Publish to `gitea.officer.dev/plugins/atelier` and register it in the marketplace.** Not done — see
|
||||
`BLOCKERS.md` for why and the exact commands.
|
||||
2. **`OFFICERDEV_POSTGRES_URL` has to be set before install.** No default, on purpose.
|
||||
3. **The `atelier` → `officer_db` migration**, described above, once someone can watch it happen.
|
||||
4. ~~`assets/icon.png` and `assets/splash.png` are `[placeholder]` flat swatches.~~ Done
|
||||
2026-08-20: both come from the suite's own masters in the OfficerIcons repo —
|
||||
`OffAtelierAppIcon` (trimmed, since that master is 1312x1199 rather than square, then centred on
|
||||
a transparent 256 canvas) and `OffAtelierSplashScreen` at 1024. Same shapes and same convention as
|
||||
plugins/music.
|
||||
5. **A real PM2 install/enable/disable/uninstall/purge cycle**, on a machine where doing so does not
|
||||
touch the owner's live session or shared database.
|
||||
6. **Deciding whether and when to cut the mobile app over** from `atelier.officer.dev` to the plugin's
|
||||
route, and what happens to the standalone service afterward.
|
||||
|
||||
## Reversed 2026-08-20: officer_db, not a separate database
|
||||
|
||||
The extraction kept Atelier's own `atelier` Postgres database, because `BLOCKERS.md` decision 3 said to
|
||||
when the tables already had rows. The owner reversed that afterwards, and the reasoning is worth keeping:
|
||||
|
||||
**A plugin that behaves differently from every other plugin is a plugin nobody can reason about.** The
|
||||
separate database meant a second connection string to set before install, a second thing to back up, a
|
||||
second thing to get wrong on a new machine, and failure modes no other plugin shares. None of that was
|
||||
bought by anything except avoiding a migration.
|
||||
|
||||
So: four `atelier_`-prefixed tables in `officer_db` — `atelier_collections`, `atelier_collection_items`,
|
||||
`atelier_providers`, `atelier_generations` — created empty by `bun db:push`, like every other plugin.
|
||||
Index and check constraint names carry the prefix too; those are global in Postgres and would otherwise
|
||||
be the one thing that collides.
|
||||
|
||||
`ATELIER_POSTGRES_URL` is gone. `sidecar/db/db.ts` reads `OFFICERDEV_POSTGRES_URL ?? POSTGRES_URL`, the
|
||||
house convention, which the platform already sets.
|
||||
|
||||
**The standalone service's data is NOT here.** It is still in the old `atelier` database, and the old
|
||||
service is still running against it, untouched. Migrating those rows — 2 collections, 73 items, 5
|
||||
providers, 87 generations — is a deliberate, watched step, done after the plugin is installed and proven
|
||||
on alpha. Nothing about this plugin depends on it happening.
|
||||
1. **The pricing extractor** — see above. Everything else in a refresh works; this is the one gap, and
|
||||
it fails loudly rather than silently.
|
||||
2. **Video.** Adapter, a longer poll, per-second pricing, duration/fps on the row.
|
||||
3. **`bun db:push`** is required to drop `atelier_providers` and `generations.provider_id` on an install
|
||||
that predates 2026-08-20.
|
||||
4. **The 88 historical images** were generated by the standalone service and predate all of this. Their
|
||||
prompts are the source for `OfficerIcons/ICON_GENERATION_RECIPE.md`.
|
||||
|
||||
Reference in New Issue
Block a user