Admin Add/Edit App now supports:
- Custom image upload (or fall back to Lucide icon) via the existing
ServiceImageUploader; rendered on the home launcher when set.
- Open mode picker: internal (default), external_tab (window.open),
external_iframe (renders inside /embedded/:id). External URL input
shown conditionally and required when an external mode is chosen.
Internal route field is hidden entirely when an external mode is
selected, and locked (readOnly + lock hint) when editing a built-in
app whose path is hardcoded in the SPA.
Backend:
- apps schema gains image_url, external_url, open_mode (default
'internal'); drizzle-kit push applied.
- New lib/db/src/built-in-apps.ts exports BUILTIN_APP_SLUGS +
isBuiltinAppSlug. Exposed via subpath export
`@workspace/db/built-in-apps`; the file has zero imports so the
browser bundle can use it without pulling in `pg`. tx-os now imports
it directly — duplicate FE constant removed.
- Built-in slug list: services, notifications, admin, notes,
my-orders, orders-incoming, executive-meetings (everything with a
hardcoded <Route> in artifacts/tx-os/src/App.tsx). calendar /
documents are seeded but admin-defined and remain editable.
- PATCH /apps/:id rejects route changes whose previous slug is
built-in with 400 + code='builtin_route_locked'. Same-route no-op
is allowed; non-route updates on built-ins still work.
Other:
- New SPA route /embedded/:id and embedded-app page (iframe host with
back + open-in-new-tab + error/not-embeddable states).
- OpenAPI App / CreateAppBody / UpdateAppBody extended; codegen ran.
- en/ar locales: admin.appImage, appExternalUrl, appOpenMode.*,
builtinPathLocked, embeddedFrame.*.
- scripts/src/seed.ts: drift guard throws if a seeded built-in slug
uses a route that does not match the hardcoded SPA route.
Tests:
- New API test apps-builtin-route-lock.test.mjs (4/4 pass): reject
built-in route change, allow non-route built-in updates, allow
non-builtin route changes, allow built-in same-route no-op.
- New Playwright E2E admin-app-image-external-embedded.spec.mjs
(passes): launcher renders custom image_url, external_tab opens
external URL via window.open, external_iframe navigates to
/embedded/:id and renders <iframe src=externalUrl>.
Out of scope (pre-existing, not introduced here):
- 3 failing tests in executive-meetings-* and tsc errors in
api-server/src/routes/executive-meetings.ts.