Atelier is now plugins/atelier/: manifest, a thin auth-gated proxy router, the sidecar backend (providers, the generation queue, collections, its own separate Postgres database), three panels (Generate, Gallery+Lightbox, Collections), and Providers as an Officer settings screen instead of a fourth panel. Its own local git repository, four commits, not yet pushed anywhere — see BLOCKERS.md. bunx tsgo is clean. bun test: 1041->1069 pass, 0 fail, 90->92 files (the two new files are ported tests, no regressions). The real production SPA bundle builds successfully with the plugin included (Bun.build + Tailwind, 2.2s, zero errors) and the sidecar's HTTP API was exercised end to end against a real, non-production Postgres database. Not done, and recorded as open: a live PM2 install cycle against the shared production database, and publishing the plugin's repository to Gitea and the marketplace — both cross into affecting shared systems beyond this local checkout, which is not a call to make unattended. The standalone officer-atelier service — what the mobile app actually talks to today — was not touched, not stopped, and not migrated. This extraction is additive. ATELIER-COMPLETE
191 lines
12 KiB
Markdown
191 lines
12 KiB
Markdown
# Atelier extraction — blockers and decisions made without asking
|
|
|
|
Unattended run, started 2026-08-20. Per the kickoff prompt: decisions made here without being able to
|
|
ask, in the order found. Nobody is awake; each entry says what I did and why, then the run continues.
|
|
|
|
## Map, before anything moved
|
|
|
|
Grepped the whole platform for `atelier` (case-insensitive, every `.ts`/`.tsx`/`.md`/`.json`/`.cjs`
|
|
file). Two hits, both incidental:
|
|
|
|
- `src/workspaces/officerdev/src/apps/Chat/components/chat-images.test.ts:10` — a test fixture using
|
|
`https://atelier.officer.dev/images/....png` as an example markdown image URL. Not a dependency.
|
|
- `src/servers/api/chat/task-restart.ts:181` — a comment mentioning a stray `bun dev` process that once
|
|
ran from `~/dockers/officer-atelier` and got respawned by accident. Historical, not a dependency.
|
|
|
|
**No platform code imports anything from Atelier, and Atelier imports nothing from the platform.**
|
|
Unlike music/offscale, there is no reverse dependency to find and no "this stays behind" piece. It comes
|
|
out whole, the way offscale did before the imports were found — except here there really are none.
|
|
|
|
Also checked: `ecosystem.config.cjs` (platform's own) has no `atelier` entry — the standalone service is
|
|
managed by its OWN `ecosystem.config.cjs` inside `~/officer-dev/dockers/officer-atelier/`, entirely
|
|
outside this repository.
|
|
|
|
## The standalone service stays running, untouched
|
|
|
|
`officer-atelier` (PM2, port 3002) is a **live production service** — 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` via nginx and holds real data (87 generations, 2 collections, 5 providers,
|
|
1 provider_settings row, checked directly against Postgres).
|
|
|
|
I am not stopping it, not touching its nginx config, not touching its PM2 entry, and not touching the
|
|
mobile app. This extraction adds a new, additive plugin inside the platform; cutting the mobile app over
|
|
to it (or deciding whether to) is the owner's call, not mine to make unattended. Recorded here so nobody
|
|
reads the plugin's existence as "the old service is now redundant, kill it" — it is not redundant until
|
|
someone repoints the mobile app.
|
|
|
|
## Decision 3 — database rows checked, both non-empty → database stays separate
|
|
|
|
Queried the live `atelier` Postgres database directly (via a throwaway `postgres` client, same
|
|
credentials as `.env.production.local`):
|
|
|
|
```
|
|
collections 2
|
|
collection_items 73
|
|
provider_settings 1
|
|
providers 5
|
|
generations 87
|
|
```
|
|
|
|
`collections` (2) and `provider_settings` (1) are the two tables the brief named — both non-empty. Per
|
|
decision 3: **the `atelier` database stays separate**, not folded into `officer_db`. No
|
|
`plugins/atelier/db/schema.ts` (that convention is for `officer_db`-prefixed tables); the sidecar keeps
|
|
its own Postgres connection to the `atelier` database, exactly as today, just running from inside the
|
|
plugin's `sidecar/` directory instead of a standalone repo.
|
|
|
|
**The brief undersold the schema.** It says "tables: collections, provider_settings" — the real schema
|
|
has five: `generations` (the actual data — 87 rows, prompts/images/costs), `collection_items` (the join
|
|
table), `providers` (configured adapter+model+key rows — 5 of them; a newer, richer replacement for
|
|
`provider_settings` that the brief doesn't mention at all), plus the two named. `generations` alone
|
|
being non-empty would have settled decision 3 on its own. Following the letter of decision 3 (check the
|
|
two named tables) rather than re-deriving it from the fuller schema, since both answers point the same
|
|
way here.
|
|
|
|
**Migration to `officer_db` is a real follow-up**, not done here: fold `collections`, `collection_items`,
|
|
`providers`, `provider_settings`, `generations` into `officer_db` as `atelier_*`-prefixed tables once
|
|
someone is awake to watch a real data migration. Until then the plugin's sidecar owns a second Postgres
|
|
connection, to a second database, which is new territory for this plugin system (every other plugin's
|
|
`db/` is officer_db-prefixed tables) — documented in PLUGIN.md.
|
|
|
|
## Postgres connection for the separate database
|
|
|
|
The sidecar needs its own connection string. Added `ATELIER_POSTGRES_URL` as a new platform `.env`
|
|
variable (same pattern as `OFFICERDEV_POSTGRES_URL` for officer's own database) rather than routing it
|
|
through the secret store — a database URL carrying a password is handled as a plain `.env` value
|
|
everywhere else in this codebase (`OFFICERDEV_POSTGRES_URL` itself), and the secret store's own docs
|
|
scope it to "every encryption and signing key the platform uses" plus foreign bearer credentials
|
|
(APNs, FCM, webhooks) — not connection strings. Kept consistent with the existing convention rather than
|
|
inventing a new one.
|
|
|
|
**Not set automatically.** I don't have a safe way to guess whether the owner wants the plugin sidecar
|
|
to point at the _existing_ `atelier` database (sharing state with the still-running standalone service —
|
|
two writers to one queue table, which `generations.status` and the `claimableGenerations`/`markRunning`
|
|
claim logic were written assuming is single-process) or a fresh one. Left unset, with a clear startup
|
|
error, and documented in PLUGIN.md. **Do not just paste the standalone service's `POSTGRES_URL` in and
|
|
restart both** — two processes racing the same `generations` queue is exactly the hazard `markRunning`'s
|
|
atomic claim exists to prevent for restarts of _one_ process, not for two live ones.
|
|
|
|
## Decision 2 — Providers as an Officer settings screen, against a direct precedent
|
|
|
|
Found `IntegrationsSettings/index.tsx`'s own comment, written when email was extracted on 2026-08-18:
|
|
|
|
> "Email account management left with the feature (`plugins/email/`). A plugin contributes panels, not
|
|
> settings sections, so it now lives inside the email workspace itself."
|
|
|
|
That is the opposite of decision 2, written five days before this run, in the exact settings screen
|
|
decision 2 points Atelier's Providers into. Per the brief's own instructions ("if you find evidence one
|
|
is wrong, write it down, follow the decision anyway"): implemented Providers as a genuine platform
|
|
settings section — `IntegrationsSettings` gained an "Atelier" entry under the Enterprise tab — as new
|
|
**platform** code (`src/apps/officer-web/...`) that talks to `/api/atelier/providers` over HTTP via
|
|
`useClient`, same as any other authenticated screen. It does not import anything from
|
|
`plugins/atelier/`, so it does not violate "a plugin importing platform code is fine; the reverse is
|
|
not" — there is no import in either direction, only an HTTP call, which is what every frontend/backend
|
|
pair in this codebase already is.
|
|
|
|
Recording the contradiction here rather than silently picking the email precedent, since decision 2 was
|
|
explicit and pre-made.
|
|
|
|
## Images served through an authenticated route, not the old `PUBLIC_URL` scheme
|
|
|
|
The standalone service served `/images/*` unauthenticated, off Bun's top-level route table, with
|
|
`PUBLIC_URL` baked into every response so the URL was durable outside the process. Per "What you may NOT
|
|
do", that is gone: images are served from `GET /api/atelier/files/:filename`, mounted like every other
|
|
route, behind the permission gate. Named `files` rather than `images` so it cannot sit one path segment
|
|
away from `/api/atelier/images/generations/:id` — the JSON job API already owns `/images`.
|
|
|
|
The frontend needs `<img src>` to carry a credential a browser cannot put in an `Authorization` header.
|
|
Used the platform's existing `?token=` escape hatch — already read by both
|
|
`permission-gate.ts:37` and `user-middleware.ts:22`, already used by `FileViewer/file-types.ts` for
|
|
exactly this (`getRawUrl`, `getAudioTracksUrl`, ...) for the file browser's raw/audio/subtitle routes.
|
|
Not a new mechanism, not an unauthenticated route: the same query-token path every other authenticated
|
|
media URL in this app already takes.
|
|
|
|
## `sharp` added as a direct dependency
|
|
|
|
Atelier's `inspect.ts` uses `sharp` to measure width/height/alpha-channel/opacity of a written image —
|
|
load-bearing (it is how a `background: transparent` request that silently produced an opaque image gets
|
|
caught). `sharp@0.33.5` was already resolved in `bun.lock` as an optional transitive dependency of
|
|
`next`, so promoting it to a direct dependency of the platform added no new resolution — verified the
|
|
lockfile diff is exactly that promotion, nothing else moved. Done via the documented dance:
|
|
`frozenLockfile` commented out in `bunfig.toml` for one `bun install`, restored immediately after,
|
|
diff reviewed before committing both files.
|
|
|
|
## Publishing to the Marketplace — not done, left as the one open step
|
|
|
|
`plugins/atelier/` is its own local git repository (`git init`, per "Done means"), but I did not create
|
|
`gitea.officer.dev/plugins/atelier` or push to it. I found no Gitea API token or credential available to
|
|
this session, and creating a new repository on a shared Gitea instance and registering it in the
|
|
marketplace catalogue (`scripts/marketplace-sync.ts`, `BUNDLED` in `src/servers/plugins/marketplace.ts`)
|
|
is exactly the kind of action with effects beyond this local checkout that the run's own ground rules
|
|
say to flag rather than take unilaterally, especially with no verified way to confirm it succeeded.
|
|
|
|
Left for the owner:
|
|
|
|
```
|
|
cd plugins/atelier
|
|
git remote add origin git@gitea.officer.dev:plugins/atelier.git # after creating the repo on Gitea
|
|
git push -u origin main
|
|
cd ../..
|
|
bun --env-file=.env scripts/marketplace-sync.ts
|
|
```
|
|
|
|
Then add a `BUNDLED` entry in `src/servers/plugins/marketplace.ts` (offline fallback), matching the
|
|
other three plugins' entries.
|
|
|
|
Until that happens, the plugin still installs fine from a checkout that already has the directory
|
|
(`plugins/atelier/` committed to its own local repo, present on disk) — it just cannot be discovered by
|
|
the Marketplace screen on a machine that has never had it, per "Done means" #2. This is the one piece of
|
|
"done" I could not complete unattended.
|
|
|
|
## No `plugins/atelier/db/`, on purpose
|
|
|
|
"Done means" lists `db/` among the plugin's expected pieces, matching offscale's and music's shape. This
|
|
plugin has no `db/schema.ts` at all — `discoverPlugins()` correctly reports `schema: null` for it. That
|
|
convention is specifically for `officer_db`-prefixed tables pushed by `pushPluginSchema`; decision 3
|
|
keeps Atelier's tables in their own separate Postgres database instead, so there is nothing for that
|
|
convention to describe. The database code lives inside `sidecar/db/` as an internal implementation
|
|
detail of the sidecar process, not as a platform-recognised `db/` directory. Documented in full in
|
|
`plugins/atelier/PLUGIN.md`, under "The database".
|
|
|
|
## What I verified, and what I deliberately did not
|
|
|
|
- `bunx tsgo`: clean throughout.
|
|
- `bun test`: baseline 1041 pass / 0 fail / 90 files → after this run, 1069 pass / 0 fail / 92 files.
|
|
The two new files are the ported `serialize.test.ts` and `generations/response.test.ts`. No
|
|
regressions, file count only grew.
|
|
- `discoverPlugins()` finds the plugin and the manifest validates with no problems.
|
|
- The platform-side proxy router mounts and correctly 503s with no sidecar running.
|
|
- The sidecar's own Hono app was exercised directly against the real `atelier_dev` Postgres database
|
|
(not `officer_db`, not the production `atelier` database) with real HTTP requests and no port bound:
|
|
providers, providers/adapters, collections and images all answered correctly; a missing file 404s; an
|
|
unknown route gets the OpenAI-shaped error envelope; provider rows whose keys were encrypted under the
|
|
standalone service's own key correctly fail to decrypt rather than crashing (a fresh `secret.key` was
|
|
generated for this checkout at `$OFFICER_ROOT/data/atelier/secret.key`, which is expected and correct
|
|
— see PLUGIN.md).
|
|
- **Deliberately not done**: installing the plugin through the live, running `officer` PM2 process. That
|
|
process serves the owner's real session, its `plugins/` directory is the main checkout (not this
|
|
worktree, so it would not even find this code), and going through its install flow would write a real
|
|
`plugin_installs` row into the shared production `officer` Postgres database from a session with no way
|
|
to confirm the result against a live browser. Full PM2 install/enable/disable/uninstall/purge cycle,
|
|
per the runbook's verification table, is left for the owner — noted as open item 5 in PLUGIN.md.
|