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.
## Original task
`pnpm --filter @workspace/db run push` was failing with a duplicate-key
error on `app_permissions` (and a missing FK on
`executive_meeting_notifications`) because legacy data in the dev DB
violates constraints the schema now declares. Devs had to drop into psql
to fix it, which made bootstrapping painful and left
`role_permission_audit` reliant on hand-applied SQL.
## Changes
- Added `lib/db/scripts/pre-push-cleanup.ts`, an idempotent cleanup that:
- Collapses duplicate `app_permissions` rows to one per
`(app_id, permission_id)` so the composite PK can be added.
- Deletes orphan `executive_meeting_notifications` rows so the new
`ON DELETE CASCADE` FK can be added.
- Skips both checks when the tables don't exist yet (fresh DB
no-op).
- Wired the script into `lib/db/package.json` so both `push` and
`push-force` run cleanup first (`pnpm run pre-push-cleanup && drizzle-kit push ...`).
- Added `tsx` to `lib/db` devDependencies (catalog version) so the
package can run the cleanup without leaning on another workspace.
- Updated `replit.md` Deployment / Migration Runbook to reflect that
cleanup is now automatic — no manual SQL required in any environment.
## Verification
- `pnpm --filter @workspace/db run push` now collapses 2 duplicate
groups + removes 1978 orphan notifications, then succeeds with
`[✓] Changes applied`.
- Re-running push is idempotent: second run reports no duplicates and
no orphans, then succeeds.
- `pnpm --filter @workspace/db run push-force` (used by
`scripts/post-merge.sh`) was also verified end-to-end.
- Confirmed `app_permissions` now has the composite PK,
`executive_meeting_notifications` has the cascade FK, and
`role_permission_audit` matches the schema.
## Notes
- A pre-existing unrelated typecheck error in
`artifacts/api-server/src/routes/executive-meetings.ts` (font
settings `scope` overload) was confirmed to exist on `main` before
any changes here and is out of scope for this task.
- Proposed follow-up #213 to move from `push`/`push-force` to
versioned `drizzle-kit migrate` so legacy-data backfills are checked
in instead of living in a generic pre-push script.
Replit-Task-Id: 2efc4e22-0c65-48a1-b573-319474698b96