Landed a tight subset of the 13-item umbrella, mirroring the proven
narrow-then-defer pattern from #242:
- #195 — Plain DELETE /api/services/:id now writes a `service.delete`
audit row carrying nameEn + nameAr (force-with-deps still uses the
dedicated `service.force_delete`). Added matching `service.delete`
formatter case + EN/AR i18n keys, and surfaced nameAr on the existing
`service.force_delete` summary.
- #197 — `actorUserId` filter for `/admin/audit-logs` and CSV export.
openapi.yaml updated, codegen regenerated, server filter wired through
parseFilters/buildWhere with 400-on-invalid handling, AuditLogPanel UI
got an actor dropdown wired into params + export URL + reset, and a
new audit-logs-actor-filter API test (4 cases) covers list narrowing,
exclusion, invalid input, and CSV export.
- #178 — Formatter unit tests for user.delete (id-only, EN/AR display
name resolution, force flag, force + name) and the new service.delete
(id-only, EN/AR), 11 new cases (33/33 pass).
Skipped #194 — already implemented; users.ts DELETE persists displayName
fields and audit-summary already renders user.deleteWithName/forceDeleteWithName.
Deferred via follow-ups (no duplicate of existing #182/#183/#184):
- F1: #196 recent-activity endpoint + 5 admin panels
- F2: #205+#206+#208 permission history CSV/name resolution/timeline
- F3: #209+#210 cascade/bulk audit rows + e2e UI spec for History tabs
Validation: tx-os typecheck clean; pre-existing executive-meetings.ts
errors not regressed; all targeted server tests pass (delete-force-warnings 10,
audit-logs target-filter 7, forced-only 6, audit-log-coverage 27, new
actor-filter 4, broader audit/services sweep 40); e2e test verified actor
dropdown rendering, filter behavior, readable Arabic service.delete summary,
and CSV export honoring the filter.
T1 (audit-action rename in #93):
- Updated the user force-delete test to look up audit_logs by
action = ANY(['user.delete', 'user.force_delete']).
- Updated the app force-delete test to look up audit_logs by
action = ANY(['app.delete', 'app.force_delete']).
- Used array-of-actions matchers (forward-compatible with the old
name in case anything else still emits it).
- The service force-delete test was left untouched because
artifacts/api-server/src/routes/services.ts still emits
'service.force_delete' (only user/app/group were renamed in #93).
T2 (services per-run uniqueness + defensive sweep):
- Added module-level SVC_STAMP and SVC_PREFIX = `TestSvc_<stamp>_`.
- Renamed the three services-test name_en literals to
`${SVC_PREFIX}Clean`, `${SVC_PREFIX}Busy`, `${SVC_PREFIX}Force`
so re-running the file no longer collides on services_name_en_unique.
- Now pushes every created service id to createdServiceIds (clean +
busy + force), not just busy.
- Added a defensive sweep in after() that, after the tracked-id
cleanup, deletes any leftover services whose name_en starts with
SVC_PREFIX, plus their child rows in service_orders. This mirrors
the apps-side sweep added in Task #116.
Pre-existing pollution cleanup:
- Removed the orphan rows left over from the failed 2026-04-28
test runs (services ids 166/168 = 'Clean Svc'/'Force Svc',
user ids 4397/4889 = del_force_*) and their child rows. These
predate the fix; removing them now keeps the workspace clean.
Verification:
- pnpm --filter @workspace/api-server node --test
tests/delete-force-warnings.test.mjs: 9/9 PASS twice in a row.
- Post-run sweep query: 0 leftover apps, 0 leftover services with
the test prefixes, 0 leftover admin users.
Out of scope (not touched):
- Other test files in artifacts/api-server/tests/.
- The audit-action rename itself (already merged in #93).
- The wider failing `test` workflow — its other failures are
unrelated to this file.
Update delete-force-warnings.test.mjs to include user_app_orders in cleanup, add a defensive sweep for orphaned apps, and ensure app IDs are tracked for all app deletion test cases.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 5b25e846-3b26-42aa-9202-268dd811b0d2
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/UXp27BF
Replit-Helium-Checkpoint-Created: true
Apply the existing groups delete-warning pattern to user, app, and
service deletions so admins are warned (and have to confirm twice)
before destroying records that still own dependent data.
Backend
- openapi.yaml: added `force` query param to DELETE /users/{id},
/apps/{id}, /services/{id} plus three new conflict schemas
(UserDeletionConflict, AppDeletionConflict, ServiceDeletionConflict).
- routes/users.ts, apps.ts, services.ts: rewrote DELETE handlers to
count dependents (notes/orders/conversations/messages for users;
group_apps/restrictions/open events for apps; service_orders for
services), return 409 with counts when non-empty and force is not set,
and on `?force=true` perform the cascade in a transaction and write
an `*.force_delete` audit_logs row.
- Existing 204 success path preserved for empty deletes.
UI (artifacts/tx-os/src/pages/admin.tsx)
- New `DeletionWarningDialog` helper, plus app/service/user delete state
+ lazy 409 detection (first click probes; on 409 the dialog upgrades
to show counts + "Delete anyway"; second click sends ?force=true).
- Replaced the three plain `confirm(t("admin.deleteConfirm"))` callsites.
i18n
- Added admin.deleteApp/deleteService/deleteUser keys (title, warning,
forceHint, emptyBody, count keys, confirm, anyway) in en.json + ar.json.
Tests
- artifacts/api-server/tests/delete-force-warnings.test.mjs covers all
9 cases (clean delete, 409 with counts, force=true 204 + audit log)
for users, apps, and services. Existing groups-crud and service-orders
tests still pass.
Notes / drift
- Lazy detection (vs eager counts on the row like Groups already does)
was chosen because list endpoints don't return counts yet — proposed
follow-up #96 covers eager counts so the warning appears on first
click everywhere.
- E2E test for the UI flow flaked twice; backend integration tests
(9/9 pass), direct curl validation of force=true returning 204, and
typecheck across the monorepo all confirm correctness.
Replit-Task-Id: 91404d92-e74c-4720-8fc9-8eb772eefc33