atelier brief: port the ui, do not frame it
the first draft said follow gitea — sidecar serves its own ui, plugin proxies it, one panel renders an iframe. wrong for this one. atelier was a separate server because that was the quick answer at the time, and this extraction is what undoes that: it becomes part of officer, not something officer frames. and it is smaller than the repository suggests. 1,736 lines of react, but six of the twenty-five files are shadcn components officer already has, and the htmx half is eight files that get deleted rather than ported. what is left is generate, gallery, collections and providers — about 1,200 lines. providers is a settings screen, not a panel, so it moves to officer's settings surface. src/servers/providers/ is already adapter-shaped, so the point of moving it is that a SECOND provider becomes a new file rather than a new screen. the brief says explicitly not to add one in this run — leave the seam, say so in PLUGIN.md. still a sidecar, for the one reason that survives the ui moving: generation is long-running, capped by ATELIER_MAX_CONCURRENCY, and holds a credential the platform must not. the ui is officer's; the work is the sidecar's. added the trap that follows from all of this: "every image has a permanent URL" was true of a standalone server with its own PUBLIC_URL, and inside officer that url is behind the session. an <img> cannot carry a token — the core affordance says why — so anything needing an unauthenticated image is a blocker to record, not a route to open.
This commit is contained in:
@@ -34,8 +34,8 @@ Everything below happens in `~/officer-dev/atelier-work`.
|
|||||||
3. **`src/servers/plugins/`** — `discover.ts`, `manifest.ts`, `mount.ts`, `install.ts`, `ecosystem.ts`.
|
3. **`src/servers/plugins/`** — `discover.ts`, `manifest.ts`, `mount.ts`, `install.ts`, `ecosystem.ts`.
|
||||||
Where this prompt and that code disagree, **the code is right**.
|
Where this prompt and that code disagree, **the code is right**.
|
||||||
4. **`CLAUDE.md`** — house rules, the permission model, and why `assertPermissionTotality` refuses to boot.
|
4. **`CLAUDE.md`** — house rules, the permission model, and why `assertPermissionTotality` refuses to boot.
|
||||||
5. **`plugins/gitea/`** — because of the UI decision below. It solves the same problem.
|
5. **`docs/secret-store.md`** — Atelier holds an OpenAI key.
|
||||||
6. **`docs/secret-store.md`** — Atelier holds an OpenAI key.
|
6. **`src/workspaces/officerdev/APP_CONVENTIONS.md`** and `HOOK_CONVENTIONS.md` — you are writing panels.
|
||||||
|
|
||||||
## What Atelier is
|
## What Atelier is
|
||||||
|
|
||||||
@@ -52,8 +52,9 @@ env BUN_PORT, PUBLIC_URL, POSTGRES_URL, ATELIER_DATA_DIR,
|
|||||||
runs as pm2 `officer-atelier`, cwd ~/officer-dev/dockers/officer-atelier
|
runs as pm2 `officer-atelier`, cwd ~/officer-dev/dockers/officer-atelier
|
||||||
```
|
```
|
||||||
|
|
||||||
It is a **sidecar** plugin, not an API-only one, for the two reasons that always decide this: it does
|
It is a **sidecar** plugin for one reason that survives the UI moving into Officer: generation is
|
||||||
long-running work with a concurrency cap, and it holds a credential the platform must not.
|
long-running, capped by `ATELIER_MAX_CONCURRENCY`, and holds a credential the platform must not. The UI
|
||||||
|
is Officer's; the work is the sidecar's.
|
||||||
|
|
||||||
## Decisions already made — do not re-open these
|
## Decisions already made — do not re-open these
|
||||||
|
|
||||||
@@ -61,13 +62,35 @@ Each of these would normally be a question. They are answered so you can keep mo
|
|||||||
evidence that one is wrong, **write it in `BLOCKERS.md`, follow the decision anyway, and carry on** — a
|
evidence that one is wrong, **write it in `BLOCKERS.md`, follow the decision anyway, and carry on** — a
|
||||||
half-applied decision is worse than a wrong one consistently applied.
|
half-applied decision is worse than a wrong one consistently applied.
|
||||||
|
|
||||||
**1. The UI: follow gitea, do not port it.** Atelier ships htmx _and_ a React SPA with its own build
|
**1. The UI is PORTED into Officer. No proxy, no iframe, no sidecar-served UI.** Atelier was a separate
|
||||||
(`build-web.ts`, `gen-index.ts`). Rewriting 84 files into panels is not an overnight job and is not the
|
server because that was the quick answer at the time; this extraction is what undoes that. It becomes part
|
||||||
point of this extraction. Instead: the sidecar keeps serving its own UI, the plugin proxies it
|
of Officer, not something Officer frames.
|
||||||
same-origin with `createSidecarProxy`, and a single panel renders it in an iframe. `plugins/gitea` does
|
|
||||||
exactly this and its `sidecar/rewrite.ts` records the traps. **Read that file before writing any of it.**
|
|
||||||
|
|
||||||
**2. The database: check the rows, then decide.** Atelier uses a separate `atelier` Postgres database,
|
It is smaller than the repository suggests:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/apps/frontend 1,736 lines / 25 files
|
||||||
|
6 of those components/ui/* — shadcn Officer ALREADY HAS. Delete, import from @/components/ui.
|
||||||
|
real work Generate · Gallery (+ Lightbox) · Collections · Providers ≈1,200 lines
|
||||||
|
src/apps/web the htmx half — 8 files, DELETED, not ported
|
||||||
|
```
|
||||||
|
|
||||||
|
So: three panel apps — **Generate**, **Gallery**, **Collections** — under
|
||||||
|
`plugins/atelier/web/`, each exporting `appRegistryMetas`, with a `layout.ts` composing them. Follow the
|
||||||
|
frontend route conventions in `CLAUDE.md`: selection lives in the URL, rows are real links, no
|
||||||
|
`usePanelChannel` for "which thing is open".
|
||||||
|
|
||||||
|
The fourth feature, **Providers**, is not a panel — see the next decision.
|
||||||
|
|
||||||
|
**2. Providers become Officer settings, with more than one.** `features/Providers/` is a settings screen,
|
||||||
|
and Officer has a settings surface already. It moves there rather than becoming a panel.
|
||||||
|
|
||||||
|
`src/servers/providers/` is already adapter-shaped — `types.ts`, `openai.ts`, `credentials.ts`, an
|
||||||
|
`index.ts` that dispatches. Keep that shape and keep the seam: today OpenAI is the only adapter, and the
|
||||||
|
point of moving the settings is that a second one is a new file rather than a new screen. **Do not add a
|
||||||
|
second provider in this run** — leave the shape able to take one and say so in `PLUGIN.md`.
|
||||||
|
|
||||||
|
**3. The database: check the rows, then decide.** Atelier uses a separate `atelier` Postgres database,
|
||||||
deliberately — `src/databases/atelier_db/drizzle.config.ts` says why. Plugin tables normally live in
|
deliberately — `src/databases/atelier_db/drizzle.config.ts` says why. Plugin tables normally live in
|
||||||
`officer_db` with a prefix.
|
`officer_db` with a prefix.
|
||||||
|
|
||||||
@@ -78,17 +101,17 @@ deliberately — `src/databases/atelier_db/drizzle.config.ts` says why. Plugin t
|
|||||||
- **If either has rows**, keep the separate database for now and record the migration as a follow-up in
|
- **If either has rows**, keep the separate database for now and record the migration as a follow-up in
|
||||||
`BLOCKERS.md`. Losing someone's generated images to a schema decision at 4am is not a trade worth making.
|
`BLOCKERS.md`. Losing someone's generated images to a schema decision at 4am is not a trade worth making.
|
||||||
|
|
||||||
**3. Artefacts: `$OFFICER_ROOT/data/atelier/`.** Generated images are files, and `ATELIER_DATA_DIR`
|
**4. Artefacts: `$OFFICER_ROOT/data/atelier/`.** Generated images are files, and `ATELIER_DATA_DIR`
|
||||||
currently points wherever its `.env` says. A plugin directory is a git clone that an update replaces, so
|
currently points wherever its `.env` says. A plugin directory is a git clone that an update replaces, so
|
||||||
artefacts kept there vanish on upgrade with nothing logging it. `plugins/kiosk/artefacts.ts` has the same
|
artefacts kept there vanish on upgrade with nothing logging it. `plugins/kiosk/artefacts.ts` has the same
|
||||||
problem solved and the comment explaining it.
|
problem solved and the comment explaining it.
|
||||||
|
|
||||||
**4. `OPENAI_API_KEY` goes in the secret store**, not `.env`. `putSecret`/`getSecret` landed 2026-08-19;
|
**5. `OPENAI_API_KEY` goes in the secret store**, not `.env`. `putSecret`/`getSecret` landed 2026-08-19;
|
||||||
`bun run secret:set atelier-openai-key` puts it there. Read with `getSecret`, **never `getKey`** — `getKey`
|
`bun run secret:set atelier-openai-key` puts it there. Read with `getSecret`, **never `getKey`** — `getKey`
|
||||||
mints a random value for an absent purpose, which for a foreign credential means silently authenticating
|
mints a random value for an absent purpose, which for a foreign credential means silently authenticating
|
||||||
with a fake. Fall back to env when the store has nothing, so an install that predates this keeps working.
|
with a fake. Fall back to env when the store has nothing, so an install that predates this keeps working.
|
||||||
|
|
||||||
**5. One permission, `atelier`, `kind: 'app'`.** Image generation is not `execution` — it does not run as
|
**6. One permission, `atelier`, `kind: 'app'`.** Image generation is not `execution` — it does not run as
|
||||||
the owner's OS user in the owner's home.
|
the owner's OS user in the owner's home.
|
||||||
|
|
||||||
## How to work
|
## How to work
|
||||||
@@ -135,6 +158,11 @@ can be found later with a grep.
|
|||||||
- No unauthenticated routes. A plugin cannot serve one; `UNPROTECTED_API_PREFIXES` is hardcoded in the
|
- No unauthenticated routes. A plugin cannot serve one; `UNPROTECTED_API_PREFIXES` is hardcoded in the
|
||||||
platform. If Atelier needs one, that is a blocker, not something to work around. (`plugins/kiosk`
|
platform. If Atelier needs one, that is a blocker, not something to work around. (`plugins/kiosk`
|
||||||
is stuck on exactly this — read its README before concluding you have found a way.)
|
is stuck on exactly this — read its README before concluding you have found a way.)
|
||||||
|
- **Generated images are served through an authenticated route**, like every other plugin artefact. Every
|
||||||
|
image having "a permanent URL" was true of a standalone server with its own `PUBLIC_URL`; inside Officer
|
||||||
|
the URL is behind the session. If something needs an image without one — an `<img>` in an email, a
|
||||||
|
share link — that is a blocker to write down, not a route to open. See the image-rendering affordance
|
||||||
|
in `capabilities/affordances/officer/core/` for why an `<img>` cannot carry a token.
|
||||||
- No changes to `src/servers/plugins/` to make Atelier fit. If the contract does not accommodate it, that
|
- No changes to `src/servers/plugins/` to make Atelier fit. If the contract does not accommodate it, that
|
||||||
is a finding worth more than the workaround.
|
is a finding worth more than the workaround.
|
||||||
- No second agent, no second worktree, no background job that writes to this tree.
|
- No second agent, no second worktree, no background job that writes to this tree.
|
||||||
|
|||||||
Reference in New Issue
Block a user