Task #517: App image upload, external links, built-in route lock

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 by the form when an external mode
  is chosen.
- Route field is locked (readOnly + lock hint) when editing a built-in
  app, since those slugs are hardcoded in the SPA router.

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, re-exported from lib/db.
- 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.*.
- New tests in apps-builtin-route-lock.test.mjs (4/4 pass) covering
  reject built-in route change, allow non-route built-in updates,
  allow non-builtin route changes, allow built-in same-route no-op.

Notes / drift:
- BUILTIN_APP_SLUGS is duplicated inline in admin.tsx
  (BUILTIN_APP_SLUGS_FE) because the browser bundle cannot import
  @workspace/db (pulls pg). Comment points at the canonical source;
  drift risk filed as a follow-up.
- Pre-existing failures unrelated to this task: 3 tests in
  executive-meetings-* and tsc errors in
  api-server/src/routes/executive-meetings.ts. Out of scope.
This commit is contained in:
riyadhafraa
2026-05-12 12:23:38 +00:00
parent d8baac9317
commit a794f92e61
14 changed files with 758 additions and 12 deletions
+34
View File
@@ -3626,8 +3626,27 @@ components:
type: ["string", "null"]
iconName:
type: string
imageUrl:
type: ["string", "null"]
description: |
Optional uploaded image used in place of the Lucide icon on the
launcher tile. Stored as the object-storage path returned by the
upload presigner (e.g. "/objects/<id>").
route:
type: string
externalUrl:
type: ["string", "null"]
description: |
For openMode "external_tab" or "external_iframe": the URL the
launcher should open. Ignored when openMode is "internal".
openMode:
type: string
enum: [internal, external_tab, external_iframe]
description: |
How the launcher opens this app: "internal" navigates to `route`
inside the SPA; "external_tab" opens externalUrl in a new tab;
"external_iframe" navigates to /embedded/:id, which renders
externalUrl inside an iframe shell.
color:
type: string
isActive:
@@ -3666,6 +3685,7 @@ components:
- nameEn
- iconName
- route
- openMode
- color
- isActive
- isSystem
@@ -3688,8 +3708,15 @@ components:
type: ["string", "null"]
iconName:
type: string
imageUrl:
type: ["string", "null"]
route:
type: string
externalUrl:
type: ["string", "null"]
openMode:
type: string
enum: [internal, external_tab, external_iframe]
color:
type: string
isActive:
@@ -3724,8 +3751,15 @@ components:
type: ["string", "null"]
iconName:
type: string
imageUrl:
type: ["string", "null"]
route:
type: string
externalUrl:
type: ["string", "null"]
openMode:
type: string
enum: [internal, external_tab, external_iframe]
color:
type: string
isActive: