feat(ui): port the dev-platform operator SPA out of core web-ui (epic byte5ai/omadia#470 P2) - #2
Closed
Weegy wants to merge 3 commits into
Closed
feat(ui): port the dev-platform operator SPA out of core web-ui (epic byte5ai/omadia#470 P2)#2Weegy wants to merge 3 commits into
Weegy wants to merge 3 commits into
Conversation
… of core Epic byte5ai/omadia#470 P4. The dev-platform half of core's `middleware/sidecars/` (dev-runner, dev-runner-daemon, dev-dind), the runner shim, the compose overlay and the operator transcript CLI now live beside the plugin they serve. The other four sidecars in core (pii-detector, privacy-detector-presidio, skillspector, updater) are core's and stay there. What moved: sidecars/dev-runner/ 2 files (image that runs exactly one job) sidecars/dev-runner-daemon/ 30 files (control plane + egress proxy) sidecars/dev-dind/ 2 files (the one privileged service) packages/runner-shim/ 21 files (was middleware/packages/dev-runner-shim) docker-compose.dev-platform.yaml packages/plugin/scripts/dev-transcript.ts Paths rewritten for the new root: the dev-runner Dockerfile builds the shim from `packages/runner-shim/`, and the compose overlay's build contexts point at `sidecars/`. Compose resolves `build.context` relative to the file that declares it, so the overlay still composes against a core `docker-compose.yaml` living anywhere else. The daemon becomes a workspace member so the repo has ONE typescript, ONE @types/node and one lockfile. Its own `typecheck` script had never been wired into any CI, and turning it on found 38 errors — all pre-existing on core's pinned TypeScript 5.9, so none of them are the port's doing. Two are real defects rather than annotations: - `proxyClient.mjs` passed its abort callback as `withDeadline`'s THIRD argument, which is `label`. The timeout message read `() => controller.abort() exceeded 5000ms` and the abort never fired, so a hung egress proxy leaked its fetch until the process exited. - `buildEgressProxyClient` passed `tokens[0]` through unchecked. On an empty DEV_RUNNER_DAEMON_TOKEN the daemon would have authenticated to its own proxy as `Bearer undefined`; every job then sees 407 on every request, which inside a runner presents as a total network outage. Now a boot refusal. `PolicyClient.fetchJobPolicy`'s typedef had also drifted a whole parameter behind its implementation. The shim's suite is compiled and run as real files rather than bundled: `src/index.ts` self-invokes behind an `import.meta.url` main guard that a bundler silently defeats. 450/450 daemon tests and 76/76 shim tests green; both packages typecheck clean.
…ifacts Epic byte5ai/omadia#470 P4. SEAMS.md parked `daemonProtocol`, `composeTopology` and `goldenFixture.e2e` in P3 because each asserts against an artifact that had not moved yet. All three arrived in the previous commit, so all three come back. daemonProtocol 28 tests. The wire schema is DUPLICATED — once in the plugin, once in the daemon, because the daemon must not import host code — and this snapshots both to JSON Schema and deep-diffs them. It now reaches across packages, which is the point: the split is exactly when a duplicated schema starts to rot. composeTopology 26 tests. The overlay lives here now; the base docker-compose.yaml is core's and always will be, so it is read from OMADIA_CORE_DIR — the variable _helpers/coreSchema.ts already uses. Without a core checkout the cross-file assertions SKIP BY NAME with a warning rather than silently shrinking to the overlay-only subset: "only dev-dind is privileged" is worth nothing if it only ever read the file that declares dev-dind. goldenFixture 4 tests, pg + git gated. One job driven end to end through the real pieces with only the container and the LLM faked. Two traps the port had to clear, both the same shape — a bundler defeating an `import.meta.url` guard: - `yaml` is CommonJS. Bundled into an ESM test it becomes esbuild's "Dynamic require of process is not supported" and throws at import, before any assertion. Now external. - The shim's `src/index.ts` ends in `if (process.argv[1] && import.meta.url === \`file://${process.argv[1]}\`)`, the guard that lets one file be both the image entrypoint and an importable module. Bundled into a suite, `import.meta.url` becomes the bundle path — which under `node --test` is also `process.argv[1]`. The shim ran ITSELF at import and died reading a real runner's env. Now external and imported by package name, so the suite exercises the BUILT shim the dev-runner image copies in, not a re-transpilation of its sources. Also closes a gap of the same class as the daemon's: `scripts/dev-transcript.ts` sits outside the plugin's `tsconfig.json` (`rootDir: "src"`), so `npm run typecheck` had never seen the operator CLI at all, despite it importing three modules out of `src/`. `tsconfig.cli.json` covers it; `scripts/build-cli.mjs` emits the runnable artifact without disturbing `dist/`'s flat layout, which `manifest.yaml`'s `lifecycle.entry` depends on. The CLI also stops loading `dotenv/config`. Under core it ran inside the middleware checkout; here it is an operator tool pointed at a deployment, and silently sourcing whatever `.env` is in the working directory is how a purge gets run against the wrong database. 747/747 plugin tests green, 0 skipped, with Postgres and a core checkout.
…yte5ai/omadia#470 P2) Twenty-eight source files that lived in `web-ui/app/admin/dev-platform/**` and `web-ui/app/_components/devjobs/**` now build as a standalone Vite/React 19 bundle in `packages/ui`, ship inside the plugin ZIP as `ui/`, and are served by core at `/p/<pluginId>/ui/` behind web-ui's `/plugin-ui/<pluginId>` host page. `web-ui` is untouched. Four screens: hub, job detail, repo detail, add-repo wizard. What replaced what - `next-intl` -> `src/lib/i18n.tsx`. 300 keys per locale, plain `{name}` interpolation, no ICU parser. The three ICU plurals were de-sugared to `{ one, other }` at extraction; en and de share the `n === 1` rule. - `next/link`, `next/navigation` -> `src/lib/router.tsx`, a hash router. The static route serves only the bundle root and real files, so a client route in the PATH would 404 on reload; a fragment never reaches the server. It also avoids baking a plugin id into the build, since the id comes from the install. - core's 4,827-line `app/_lib/api.ts` -> `src/lib/apiError.ts`. Exactly one name was imported from it. - `framer-motion` and `lucide-react` -> dropped. Both animate or size with classes the served vocabulary does not contain. Tailwind, constrained to what core actually serves The pages carried 334 arbitrary values (`text-[color:var(--fg-muted)]` and friends); the ZIP allowlist rejects that shape and a class core never saw renders unstyled rather than erroring. All 334 are gone. `Button` and `ConfirmDialog` were rewritten rather than ported for the same reason — core expresses every Button variant as an arbitrary value. `scripts/check-ui-vocabulary.mjs` gates the build with three checks: no emitted stylesheet, core's two ingest regexes verbatim, and a whitelist diff against `vocabulary/classes.txt` — 690 classes extracted from the generated stylesheet itself, not transcribed from the spec table. The diff is the half core has no counterpart for: ingest cannot see `bg-blue-500`, which is not an arbitrary value, merely a class that does not exist. Packaging and nav `ui/` joins `dist` and `migrations` in build-zip's REQUIRED_DIRS, with assertions that `ui/index.html` exists and that no stylesheet is present. The nav entry moves from `/admin/dev-platform` — a page core deletes in this epic — to `/plugin-ui/<id>`, percent-encoded because this plugin's id is scoped. Tests: 50, covering the four screens against fixtures in both locales, the theme attribute crossing the iframe boundary, the router, the i18n runtime, and the vocabulary gate (fixtures for `w-[137px]`, `[&>tr]:`, `bg-blue-500`). Two mutations were run against the suite and both were killed. Three core defects found and NOT fixable here — see docs/iframe-credentials.md 1. The host page's `sandbox` omits `allow-same-origin`, so every authenticated call from the frame is cross-origin with `Origin: null`. All four screens are data-driven; none can load data in a real browser until this is decided. 2. That page's plugin-id regex rejects scoped ids, though `manifestLoader` blesses them and this plugin's id is `@omadia/dev-platform`. 3. `plugin-ui.source.css` lines 341/346/353 use a top-level comma where `@source inline()` expands only braces, so `border`, `divide-*` and `transition*` emit NOTHING. With the base reset at `border: 0 solid`, `class="border border-border"` renders invisible — the exact silent failure the contract exists to prevent. `src/lib/cx.ts` works around it with the four directional utilities and a test pins the broken state so the workaround cannot rot.
Merged
7 tasks
Contributor
Author
|
Superseded by #3 — a clean cherry-pick onto |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Ports the Dev Platform's four operator screens out of omadia core's
web-uiinto a standalone Vite/React 19 SPA inpackages/ui. The bundle builds intopackages/plugin/ui/, ships inside the plugin ZIP, and is served by core at/p/<pluginId>/ui/behind the/plugin-ui/<pluginId>host page that C8 added.web-uiis untouched. This is the epic's biggest single risk, isolated in its own repo and its own package.#/#/?tab=#/jobs/<id>#/repos/<id>#/repos/newWhat replaced what
next-intlsrc/lib/i18n.tsx{name}interpolation, no ICU parser.next/link,next/navigationsrc/lib/router.tsx@/app/_lib/api(4,827 lines)src/lib/apiError.tsApiError.framer-motion,lucide-reactButton,ConfirmDialogbg-[color:var(--accent)]). Ingest rejects that shape;bg-accentresolves to the same variable.DevJobChatCardis deliberately not ported — it renders inside core's chat transcript, andplan.md§4.3 excludes the chat surface (H3, still undecided). The one five-line functionJobDetailScreenneeded from its state module is insrc/lib/gates.ts.Tailwind, constrained to what core actually serves
The ported pages carried 334 arbitrary values. All are gone. A class core never saw does not error — it renders unstyled, on the operator's screen and nowhere else — so
scripts/check-ui-vocabulary.mjsgates the build with three checks:.cssis absent from the ZIP allowlist and the static router's Content-Type table; that absence is the enforcement.vocabulary/classes.txt— 690 classes extracted from the generated stylesheet, not transcribed from the spec table. This is the half core has no counterpart for: ingest cannot seebg-blue-500, which is not an arbitrary value, merely a class that does not exist.Packaging and nav
ui/joinsdistandmigrationsin build-zip'sREQUIRED_DIRS, with assertions thatui/index.htmlexists and no stylesheet is present. Optional would mean "a build that forgotvite buildships silently, installs, activates, adds a nav entry, and 404s when the operator clicks it".The nav entry moves from
/admin/dev-platform— a page core deletes in this epic — to/plugin-ui/<id>, percent-encoded because this plugin's id is scoped.Verification
npm run typecheck, exit 0vite build;find -name '*.css'= 0check-ui-vocabulary.mjs, exit 0w-[137px],[&>tr]:…,bg-blue-500test/screens.test.tsxnpm run package+unzip -lnpm cireproduces all of itnode_modules50 tests.
package-lock.jsonis regenerated — it did not contain the new workspace, which would have failednpm ciin CI.Full write-up in
docs/iframe-credentials.md. All three fail silently.1. The host page's sandbox makes every authenticated call cross-origin.
PluginUiFrame.tsxsetssandbox="allow-scripts allow-forms allow-popups"— noallow-same-origin— so the document has an opaque origin. Everyfetchleaves withOrigin: null,credentials: 'include'cannot attach the session cookie, andEventSource(withCredentials)has the same problem. All four screens are data-driven. The bundle is correct; the boundary is not, and no screen loads data in a real browser until this trust-model decision is made. Tests do not show it because they stubfetch, which has no origin.2. The host page rejects every scoped plugin id — including this one. Its regex is
/^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/and its comment claims it "mirrors the plugin-id charset gate inmanifestLoader". It does not:manifestLoader.ts:182is/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/, where the scope is optional but blessed. This plugin's id is@omadia/dev-platform. One-line fix in core.3.
borderrenders invisible.plugin-ui.source.csslines 341/346/353 use a top-level comma where@source inline()expands only braces:Verified against the committed artifact: no
.border,.divide-yor.transitionrule exists. Tailwind's base reset isborder: 0 solid, soclass="border border-border"— 27 occurrences in the ported pages — sets a colour on a zero-width border and renders invisible.plugin-ui-vocabulary.mdlists all three groups as available, so the doc and the artifact disagree.src/lib/cx.tsexportsBORDER = 'border-t border-r border-b border-l'(all four are emitted, 1px each) andtest/vocabulary.test.tspins the broken state, so regenerating the vocabulary after the core fix fails that test and prompts the workaround to collapse back to'border'rather than rotting.Fix in core:
Test plan
npm run typecheck— exit 0npm run build— exit 0, zero.cssemittednpm run test -w packages/ui— 50/50npm run lint:vocabulary— exit 0npm run package -w packages/plugin— ZIP containsui/index.html, zero stylesheetsdata-themeand fakingborderinto the vocabulary both turn the suite redNeed help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.