## Task #80 — Tell receivers when an order is no longer available to claim
### Problem
When a receiver tried to act on an order that had already been cancelled, completed,
or claimed by someone else, the UI showed a generic "Could not complete the action"
toast. The stale card also stayed in the list, requiring a manual refresh.
### Changes
**artifacts/api-server/src/routes/service-orders.ts**
- PATCH /orders/:id/confirm-receipt: after a failed atomic claim, query the current
order status; return 409 `order_unavailable` if it's cancelled/completed (vs the
existing 409 `already_claimed` for when it was grabbed by someone else first)
- PATCH /orders/:id/status (preparing/completed branch): terminal-state check runs
BEFORE permission gating — returns 409 `order_unavailable` when order is already
cancelled/completed instead of ever hitting 403 Forbidden
- PATCH /orders/:id/status (cancel branch): same — terminal-state check moved to
the top of the branch so non-admin receivers trying to cancel an already-terminal
order get 409 `order_unavailable` (not 403 Forbidden which would never let the UI
remove the stale card)
**artifacts/tx-os/src/pages/orders-incoming.tsx**
- handleConfirmReceipt onError: distinguish 409 `order_unavailable` from
`already_claimed`, show specific toast, remove stale card via invalidate()
- handleSetStatus onError: same pattern — specific toast + invalidate() on
`order_unavailable`
- handleCancel onError: same pattern
**artifacts/tx-os/src/locales/en.json + ar.json**
- Added `incomingOrders.orderUnavailable` ("This order is no longer available" /
"هذا الطلب لم يعد متاحاً")
## Bonus fix — Disabled app disappears from admin panel (user-reported)
### Problem
When an admin disabled an app via the toggle, the app disappeared from the admin
panel too (isActive filter applied to everyone), making it impossible to re-enable
without direct DB access.
### Changes
**artifacts/api-server/src/routes/apps.ts**
- getVisibleAppsForUser: admins now receive ALL apps including inactive ones via
conditional $dynamic() Drizzle query; non-admins still only see active apps
**artifacts/tx-os/src/pages/home.tsx**
- Filter orderedApps to only active apps before rendering so inactive apps don't
appear on the home screen even for admins
**artifacts/tx-os/src/pages/admin.tsx**
- Inactive app cards show reduced opacity + a "Disabled/معطّل" badge
**artifacts/tx-os/src/locales/en.json + ar.json**
- Added `admin.appDisabled` ("Disabled" / "معطّل")
## Task #80 — Tell receivers when an order is no longer available to claim
### Problem
When a receiver tried to act on an order that had already been cancelled, completed,
or claimed by someone else, the UI showed a generic "Could not complete the action"
toast. The stale card also stayed in the list, requiring a manual refresh.
### Changes
**artifacts/api-server/src/routes/service-orders.ts**
- PATCH /orders/:id/confirm-receipt: after a failed atomic claim, query the current
order status; return 409 `order_unavailable` if it's cancelled/completed (vs the
existing 409 `already_claimed` for when it was grabbed by someone else first)
- PATCH /orders/:id/status (preparing/completed branch): terminal-state check runs
BEFORE permission gating — returns 409 `order_unavailable` when order is already
cancelled/completed instead of ever hitting 403 Forbidden
- PATCH /orders/:id/status (cancel branch): same — terminal-state check moved to
the top of the branch so non-admin receivers trying to cancel an already-terminal
order get 409 `order_unavailable` (not 403 Forbidden which would never let the UI
remove the stale card)
**artifacts/tx-os/src/pages/orders-incoming.tsx**
- handleConfirmReceipt onError: distinguish 409 `order_unavailable` from
`already_claimed`, show specific toast, remove stale card via invalidate()
- handleSetStatus onError: same pattern — specific toast + invalidate() on
`order_unavailable`
- handleCancel onError: same pattern
**artifacts/tx-os/src/locales/en.json + ar.json**
- Added `incomingOrders.orderUnavailable` ("This order is no longer available" /
"هذا الطلب لم يعد متاحاً")
## Bonus fix — Disabled app disappears from admin panel (user-reported)
### Problem
When an admin disabled an app via the toggle, the app disappeared from the admin
panel too (isActive filter applied to everyone), making it impossible to re-enable
without direct DB access.
### Changes
**artifacts/api-server/src/routes/apps.ts**
- getVisibleAppsForUser: admins now receive ALL apps including inactive ones via
conditional $dynamic() Drizzle query; non-admins still only see active apps
**artifacts/tx-os/src/pages/home.tsx**
- Filter orderedApps to only active apps before rendering so inactive apps don't
appear on the home screen even for admins
**artifacts/tx-os/src/pages/admin.tsx**
- Inactive app cards show reduced opacity + a "Disabled/معطّل" badge
**artifacts/tx-os/src/locales/en.json + ar.json**
- Added `admin.appDisabled` ("Disabled" / "معطّل")
Replit-Task-Id: 8d6fade8-e76c-4d3c-864b-59007688c12c
Original task: Task #79 — Make DELETE /api/groups/:id refuse to wipe a non-empty
group unless explicitly forced, surface the impacted counts in the admin UI,
and log forced deletions to an audit trail.
Changes:
- API: DELETE /api/groups/:id now computes member/app/role counts. If any are
non-zero and `?force=true` is not passed, it returns 409 with
{ error, memberCount, appCount, roleCount }. With `force=true` it deletes
and writes an entry to the new `audit_logs` table
(action='group.force_delete', target_type='group', target_id, metadata).
System group guard and 404 behaviour preserved.
- DB: Added `audit_logs` table (lib/db/src/schema/audit-logs.ts) with actor,
action, target type/id, jsonb metadata, timestamp. Pushed via drizzle-kit.
- OpenAPI: Added `force` query param, 409 response, and `GroupDeletionConflict`
schema. Re-ran orval codegen for api-client-react and api-zod.
- api-client-react: Re-exported `ApiError` and `ErrorType` so the UI can
inspect 409 responses.
- Admin UI (artifacts/teaboy-os/src/pages/admin.tsx GroupsPanel):
Replaced the bare `confirm()` with a confirmation dialog that shows the
group name and impacted member/app/role counts. Empty groups get a simple
"Delete" confirmation; non-empty groups get a warning + "Delete anyway"
button which sends `force=true`. If the server returns 409 (race), the
dialog updates to show the server-reported counts.
- Locales: Added en/ar strings for the new dialog (deleteTitle, deleteWarning,
deleteForceHint, deleteEmptyBody, deleteConfirm, deleteAnyway).
Verified end-to-end: 409 on first DELETE, dialog warning visible, force
deletion succeeds, single audit_logs row recorded with correct metadata.
Original task: Task #79 — Make DELETE /api/groups/:id refuse to wipe a non-empty
group unless explicitly forced, surface the impacted counts in the admin UI,
and log forced deletions to an audit trail.
Changes:
- API: DELETE /api/groups/:id now computes member/app/role counts. If any are
non-zero and `?force=true` is not passed, it returns 409 with
{ error, memberCount, appCount, roleCount }. With `force=true` it deletes
and writes an entry to the new `audit_logs` table
(action='group.force_delete', target_type='group', target_id, metadata).
System group guard and 404 behaviour preserved.
- DB: Added `audit_logs` table (lib/db/src/schema/audit-logs.ts) with actor,
action, target type/id, jsonb metadata, timestamp. Pushed via drizzle-kit.
- OpenAPI: Added `force` query param, 409 response, and `GroupDeletionConflict`
schema. Re-ran orval codegen for api-client-react and api-zod.
- api-client-react: Re-exported `ApiError` and `ErrorType` so the UI can
inspect 409 responses.
- Admin UI (artifacts/teaboy-os/src/pages/admin.tsx GroupsPanel):
Replaced the bare `confirm()` with a confirmation dialog that shows the
group name and impacted member/app/role counts. Empty groups get a simple
"Delete" confirmation; non-empty groups get a warning + "Delete anyway"
button which sends `force=true`. If the server returns 409 (race), the
dialog updates to show the server-reported counts.
- Locales: Added en/ar strings for the new dialog (deleteTitle, deleteWarning,
deleteForceHint, deleteEmptyBody, deleteConfirm, deleteAnyway).
Verified end-to-end: 409 on first DELETE, dialog warning visible, force
deletion succeeds, single audit_logs row recorded with correct metadata.
Replit-Task-Id: 61624ce4-83b3-43be-b22b-e261662301f1
Removes Arabic and English description fields from the admin form, the services display component, and the seed data in `scripts/src/seed.ts`, and removes the corresponding columns from the database.
Removes Arabic and English description fields from the admin form, the services display component, and the seed data in `scripts/src/seed.ts`, and removes the corresponding columns from the database.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 5f1c43b0-7465-4e56-bb0e-896a4df38886
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/dnVFHsG
Replit-Helium-Checkpoint-Created: true
Replaces all user-facing instances of "TeaBoy" with "Tx" across the application, including titles, locale files, database settings, seed data, and API documentation. Also updates internal storage keys and session secrets to remove the old branding.
Replaces all user-facing instances of "TeaBoy" with "Tx" across the application, including titles, locale files, database settings, seed data, and API documentation. Also updates internal storage keys and session secrets to remove the old branding.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 07b19cb0-11b5-4be9-8932-ae4820eb73b8
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/HxTkDPZ
Replit-Helium-Checkpoint-Created: true
Group/membership changes now push to affected users over Socket.IO so
their app dock updates without a manual refresh.
Server:
- New helper `artifacts/api-server/src/lib/realtime.ts` exposing
`emitAppsChangedToUsers(userIds)` which lazy-imports the io instance
(matching the pattern used in conversations/service-orders) and emits
`apps_changed` to each `user:<id>` room.
- `routes/groups.ts`:
- PATCH /groups/🆔 snapshot members before the write, union with
members after, and emit to that set when membership changes; emit to
current members when only apps/roles change.
- POST/DELETE /groups/:id/users/:targetId emit to the target user.
- POST/DELETE /groups/:id/apps|roles/:targetId emit to all current
members of the group.
- DELETE /groups/:id emits to all (former) members.
- `routes/users.ts` PATCH /users/:id emits to the affected user when
groupIds is changed.
Client:
- `artifacts/teaboy-os/src/hooks/use-notifications-socket.ts` now also
listens for `apps_changed` and invalidates `getListAppsQueryKey()` and
`getGetMeQueryKey()`, refreshing the dock and AuthUser context.
Validation:
- API tests pass for all groups/membership cases. One pre-existing
flaky service-orders parallel-receipt test failed; unrelated to this
change.
Group/membership changes now push to affected users over Socket.IO so
their app dock updates without a manual refresh.
Server:
- New helper `artifacts/api-server/src/lib/realtime.ts` exposing
`emitAppsChangedToUsers(userIds)` which lazy-imports the io instance
(matching the pattern used in conversations/service-orders) and emits
`apps_changed` to each `user:<id>` room.
- `routes/groups.ts`:
- PATCH /groups/🆔 snapshot members before the write, union with
members after, and emit to that set when membership changes; emit to
current members when only apps/roles change.
- POST/DELETE /groups/:id/users/:targetId emit to the target user.
- POST/DELETE /groups/:id/apps|roles/:targetId emit to all current
members of the group.
- DELETE /groups/:id emits to all (former) members.
- `routes/users.ts` PATCH /users/:id emits to the affected user when
groupIds is changed.
Client:
- `artifacts/teaboy-os/src/hooks/use-notifications-socket.ts` now also
listens for `apps_changed` and invalidates `getListAppsQueryKey()` and
`getGetMeQueryKey()`, refreshing the dock and AuthUser context.
Validation:
- API tests pass for all groups/membership cases. One pre-existing
flaky service-orders parallel-receipt test failed; unrelated to this
change.
Replit-Task-Id: 3e5ae44b-9dcf-42f4-8d5d-a0f2b05b6065
Adds a "Roles" tab to the GroupDetailEditor in the admin Groups screen so
admins can manage which roles are auto-granted to members of a group.
Changes:
- OpenAPI: added `GET /roles` (operationId `listRoles`) and a `Role` schema
in `lib/api-spec/openapi.yaml`. Ran `pnpm --filter @workspace/api-spec
run codegen` to regenerate `lib/api-client-react` and `lib/api-zod`.
- API server: added `GET /api/roles` (admin-only) in
`artifacts/api-server/src/routes/groups.ts`, returning all roles
ordered by name.
- Frontend (`artifacts/teaboy-os/src/pages/admin.tsx`):
- Imported `useListRoles` / `getListRolesQueryKey`.
- Added `roleIds` state and a fourth `"roles"` tab to GroupDetailEditor
with a checkbox list backed by the new endpoint, hydrated from
`group.roleIds`.
- Save now sends `roleIds: Array.from(roleIds)` via the existing
`PATCH /api/groups/:id` mutation.
- On save, also invalidate `getGetGroupQueryKey(editingGroupId)` so the
detail cache (30s staleTime) reflects the new role list immediately
when the editor is reopened.
- Translations: added `admin.groups.tab.roles`, `admin.groups.rolesHint`,
and `admin.groups.rolesEmpty` in both `en.json` and `ar.json`.
Verification:
- `pnpm -w run typecheck` passes.
- `pnpm --filter @workspace/api-server test` — 42/42 pass.
- e2e (testing skill): logged in as admin, opened TeaBoy group, toggled
the `user` role on the new Roles tab, saved, reopened — toggle
persisted.
No deviations from the task description.
Adds a "Roles" tab to the GroupDetailEditor in the admin Groups screen so
admins can manage which roles are auto-granted to members of a group.
Changes:
- OpenAPI: added `GET /roles` (operationId `listRoles`) and a `Role` schema
in `lib/api-spec/openapi.yaml`. Ran `pnpm --filter @workspace/api-spec
run codegen` to regenerate `lib/api-client-react` and `lib/api-zod`.
- API server: added `GET /api/roles` (admin-only) in
`artifacts/api-server/src/routes/groups.ts`, returning all roles
ordered by name.
- Frontend (`artifacts/teaboy-os/src/pages/admin.tsx`):
- Imported `useListRoles` / `getListRolesQueryKey`.
- Added `roleIds` state and a fourth `"roles"` tab to GroupDetailEditor
with a checkbox list backed by the new endpoint, hydrated from
`group.roleIds`.
- Save now sends `roleIds: Array.from(roleIds)` via the existing
`PATCH /api/groups/:id` mutation.
- On save, also invalidate `getGetGroupQueryKey(editingGroupId)` so the
detail cache (30s staleTime) reflects the new role list immediately
when the editor is reopened.
- Translations: added `admin.groups.tab.roles`, `admin.groups.rolesHint`,
and `admin.groups.rolesEmpty` in both `en.json` and `ar.json`.
Verification:
- `pnpm -w run typecheck` passes.
- `pnpm --filter @workspace/api-server test` — 42/42 pass.
- e2e (testing skill): logged in as admin, opened TeaBoy group, toggled
the `user` role on the new Roles tab, saved, reopened — toggle
persisted.
No deviations from the task description.
Replit-Task-Id: 42808400-9209-469b-b526-37c776ffb25d
Original task (#73): Mirror the existing 7s Undo toast (already in place
for owner-side cancels in My Orders) on the receiver-side cancel action
in the Incoming Orders page.
Implementation:
- artifacts/teaboy-os/src/pages/orders-incoming.tsx
- Captures the order's previous status (received | preparing) before
issuing the cancel PATCH.
- On success, shows a toast with title "Order cancelled" and an
"Undo" ToastAction. Clicking Undo PATCHes the order status back to
the previous value, then shows a "Restored" confirmation toast.
- UNDO_WINDOW_MS = 7000 to match the owner-side flow.
- artifacts/api-server/src/routes/service-orders.ts
- Refactored the PATCH /orders/:id/status authorization. Detects
"restore from cancelled" up front (existing.status === cancelled
and next !== cancelled) and grants permission to:
- owner/admin: may restore to pending or received (existing rule)
- original assignee: may restore to received or preparing (NEW)
- The same 15s server-side undo window applies.
- artifacts/teaboy-os/src/locales/{en,ar}.json
- Added incomingOrders.cancelled / undo / undone / restoreFailed
strings in English and Arabic.
- Regenerated lib/api-zod and lib/api-client-react via the api-spec
codegen script (the previously-cached output was missing pending /
received / preparing from the request body enum, which now matches
the OpenAPI spec).
Verification:
- Typecheck passes for api-server and teaboy-os.
- e2e flow verified: cancelling a received order shows the Undo toast;
clicking Undo restores the order to "received" (DB confirmed);
letting the window expire leaves the order permanently "cancelled"
(DB confirmed).
Deviations: None.
Original task (#73): Mirror the existing 7s Undo toast (already in place
for owner-side cancels in My Orders) on the receiver-side cancel action
in the Incoming Orders page.
Implementation:
- artifacts/teaboy-os/src/pages/orders-incoming.tsx
- Captures the order's previous status (received | preparing) before
issuing the cancel PATCH.
- On success, shows a toast with title "Order cancelled" and an
"Undo" ToastAction. Clicking Undo PATCHes the order status back to
the previous value, then shows a "Restored" confirmation toast.
- UNDO_WINDOW_MS = 7000 to match the owner-side flow.
- artifacts/api-server/src/routes/service-orders.ts
- Refactored the PATCH /orders/:id/status authorization. Detects
"restore from cancelled" up front (existing.status === cancelled
and next !== cancelled) and grants permission to:
- owner/admin: may restore to pending or received (existing rule)
- original assignee: may restore to received or preparing (NEW)
- The same 15s server-side undo window applies.
- artifacts/teaboy-os/src/locales/{en,ar}.json
- Added incomingOrders.cancelled / undo / undone / restoreFailed
strings in English and Arabic.
- Regenerated lib/api-zod and lib/api-client-react via the api-spec
codegen script (the previously-cached output was missing pending /
received / preparing from the request body enum, which now matches
the OpenAPI spec).
Verification:
- Typecheck passes for api-server and teaboy-os.
- e2e flow verified: cancelling a received order shows the Undo toast;
clicking Undo restores the order to "received" (DB confirmed);
letting the window expire leaves the order permanently "cancelled"
(DB confirmed).
Deviations: None.
Replit-Task-Id: 65c4b053-613a-4898-a0f7-b6fb2d680301
Original task: Task #75 — add automated tests for the groups system
(CRUD + group-driven app visibility + auto-Everyone) and stabilize the
flaky pagination test.
Changes:
- Extended artifacts/api-server/tests/groups-crud.test.mjs with four
new tests:
* PATCH /api/groups/:id updates fields and replaces userIds
transactionally; a bad userId returns 400 and leaves prior
membership unchanged.
* Creating a group with invalid roleIds returns 400 and creates no
rows.
* DELETE /api/groups/:id removes a custom group; deleting a system
group (Everyone, is_system = 1) returns 400 and the row stays.
* /api/auth/register auto-assigns the new user to the Everyone
system group; verified both via the response payload and a direct
user_groups DB check. The test temporarily flips
app_settings.registration_open to true and restores the prior
value on completion.
- The existing apps-group-visibility.test.mjs already exercises the
/api/apps endpoint backed by getVisibleAppsForUser for group-derived
admin, group member, and outsider — no changes needed there.
- The previously flaky tests/admin-app-opens-pagination.test.mjs now
passes consistently thanks to the existing 7d-window cleanup in its
before hook; left as-is per the task's "fixed or skipped" criterion.
Verification: `pnpm --filter @workspace/api-server test` reports
42/42 passing. The downstream Playwright suite (4 tests) also passes.
Original task: Task #75 — add automated tests for the groups system
(CRUD + group-driven app visibility + auto-Everyone) and stabilize the
flaky pagination test.
Changes:
- Extended artifacts/api-server/tests/groups-crud.test.mjs with four
new tests:
* PATCH /api/groups/:id updates fields and replaces userIds
transactionally; a bad userId returns 400 and leaves prior
membership unchanged.
* Creating a group with invalid roleIds returns 400 and creates no
rows.
* DELETE /api/groups/:id removes a custom group; deleting a system
group (Everyone, is_system = 1) returns 400 and the row stays.
* /api/auth/register auto-assigns the new user to the Everyone
system group; verified both via the response payload and a direct
user_groups DB check. The test temporarily flips
app_settings.registration_open to true and restores the prior
value on completion.
- The existing apps-group-visibility.test.mjs already exercises the
/api/apps endpoint backed by getVisibleAppsForUser for group-derived
admin, group member, and outsider — no changes needed there.
- The previously flaky tests/admin-app-opens-pagination.test.mjs now
passes consistently thanks to the existing 7d-window cleanup in its
before hook; left as-is per the task's "fixed or skipped" criterion.
Verification: `pnpm --filter @workspace/api-server test` reports
42/42 passing. The downstream Playwright suite (4 tests) also passes.
Replit-Task-Id: 188075ef-382c-4af1-8a9a-90a0563c92c2
Original task (#72): Mirror the recently-added delete Undo toast for
the cancel action so accidental cancellations can be reverted within
~7s.
Changes
- Frontend (artifacts/teaboy-os/src/pages/my-orders.tsx):
OrderCard.handleCancel captures the order's previous status, and on
successful cancel shows a toast with an "Undo" action (duration
matches the existing UNDO_WINDOW_MS = 7000ms). Clicking Undo issues
a PATCH to restore the order to its previous status (pending or
received) and emits a "Restored" toast. Errors surface a
"Could not restore the order" toast.
- Cancel confirmation copy was softened (no longer says "can't be
reopened") and a new restoreFailed string was added in both en.json
and ar.json.
- Backend (artifacts/api-server/src/routes/service-orders.ts):
PATCH /orders/:id/status accepts pending and received as targets
for the owner (or admin) when the existing status is cancelled,
serving as the restore-from-cancelled transition. We require
assignedTo to be null for pending and non-null for received so we
always restore to the actually-prior state. The existing logic
already broadcasts order_incoming_changed to receivers and emits
order_updated to the owner, so receivers' incoming queues refresh
automatically when an order is restored. notifyUser titleMap was
extended with "Your order was restored" entries.
- Time-bound restore: server enforces a RESTORE_WINDOW_MS of 15s
(slightly larger than the 7s client window to absorb network/clock
skew). After it expires, restore is rejected with
`undo_window_expired`, so cancellation truly becomes permanent —
this applies to admin too, so restore is strictly Undo and not an
arbitrary reopen.
- API spec (lib/api-spec/openapi.yaml):
UpdateServiceOrderStatusBody enum extended to include pending and
received; endpoint summary updated. Regenerated api-zod and
api-client-react.
Tests
- Added a new test case in artifacts/api-server/tests/service-orders.test.mjs
that covers: restore from pending, restore from received,
pending<->received validation against assignedTo, stranger forbidden,
and undo window expiry (by backdating updated_at).
Verification
- pnpm typecheck passes across libs and artifacts.
- New api-server test "owner can restore (undo) a freshly cancelled
order..." passes; all pre-existing service-order tests still pass.
- e2e tested: login -> place order -> cancel -> Undo restores to
Pending; cancel again -> let window expire -> order stays Cancelled
with no Undo available.
Notes / unrelated
- The unrelated `admin-app-opens-pagination` test
(`by-app: paginating with limit returns nextOffset until exhausted`)
fails in the dev environment because the dev DB has > 100 app_opens
rows for the chosen app and the test only walks up to 50 pages of
size 2. This is a pre-existing flaky test unrelated to this task
and was not modified.
Original task (#72): Mirror the recently-added delete Undo toast for
the cancel action so accidental cancellations can be reverted within
~7s.
Changes
- Frontend (artifacts/teaboy-os/src/pages/my-orders.tsx):
OrderCard.handleCancel captures the order's previous status, and on
successful cancel shows a toast with an "Undo" action (duration
matches the existing UNDO_WINDOW_MS = 7000ms). Clicking Undo issues
a PATCH to restore the order to its previous status (pending or
received) and emits a "Restored" toast. Errors surface a
"Could not restore the order" toast.
- Cancel confirmation copy was softened (no longer says "can't be
reopened") and a new restoreFailed string was added in both en.json
and ar.json.
- Backend (artifacts/api-server/src/routes/service-orders.ts):
PATCH /orders/:id/status accepts pending and received as targets
for the owner (or admin) when the existing status is cancelled,
serving as the restore-from-cancelled transition. We require
assignedTo to be null for pending and non-null for received so we
always restore to the actually-prior state. The existing logic
already broadcasts order_incoming_changed to receivers and emits
order_updated to the owner, so receivers' incoming queues refresh
automatically when an order is restored. notifyUser titleMap was
extended with "Your order was restored" entries.
- Time-bound restore: server enforces a RESTORE_WINDOW_MS of 15s
(slightly larger than the 7s client window to absorb network/clock
skew). After it expires, restore is rejected with
`undo_window_expired`, so cancellation truly becomes permanent —
this applies to admin too, so restore is strictly Undo and not an
arbitrary reopen.
- API spec (lib/api-spec/openapi.yaml):
UpdateServiceOrderStatusBody enum extended to include pending and
received; endpoint summary updated. Regenerated api-zod and
api-client-react.
Tests
- Added a new test case in artifacts/api-server/tests/service-orders.test.mjs
that covers: restore from pending, restore from received,
pending<->received validation against assignedTo, stranger forbidden,
and undo window expiry (by backdating updated_at).
Verification
- pnpm typecheck passes across libs and artifacts.
- New api-server test "owner can restore (undo) a freshly cancelled
order..." passes; all pre-existing service-order tests still pass.
- e2e tested: login -> place order -> cancel -> Undo restores to
Pending; cancel again -> let window expire -> order stays Cancelled
with no Undo available.
Notes / unrelated
- The unrelated `admin-app-opens-pagination` test
(`by-app: paginating with limit returns nextOffset until exhausted`)
fails in the dev environment because the dev DB has > 100 app_opens
rows for the chosen app and the test only walks up to 50 pages of
size 2. This is a pre-existing flaky test unrelated to this task
and was not modified.
Replit-Task-Id: beca78bc-32f3-4cdc-8440-9a661b48363b
Round 3 rejection items closed:
- UI smoke test added: artifacts/teaboy-os/tests/admin-create-group-
app-visibility.spec.mjs. Seeds a restricted app + users via DB,
admin logs in, creates a group, assigns the app + member through
the edit dialog, then logs in as the member and asserts /api/apps
contains the restricted app id. Cleans up after itself. Passes.
- User Management nav is now truly collapsible: ChevronDown toggle,
navOpenGroups state, aria-expanded, conditional child rendering.
Defaults to expanded only when a child is the active section.
- Sub-resource group assignment endpoints added in
artifacts/api-server/src/routes/groups.ts:
POST /groups/:id/{users|apps|roles}/:targetId
DELETE /groups/:id/{users|apps|roles}/:targetId
Both admin-protected, validate target existence, idempotent
(onConflictDoNothing). Aggregate PATCH /groups/:id remains.
Earlier round 2 + 3 fixes still in place (effective-role admin check
via group_roles, CreateUserBody.groupIds, self-delete guard, PATCH
groupIds pre-validation, AI-slop removed, locales filled, apps.ts
inArray fix).
Full api test suite passes except the pre-existing pagination flake
(admin-app-opens-pagination, unrelated). Architect: PASS.
Round 3 rejection items closed:
- UI smoke test added: artifacts/teaboy-os/tests/admin-create-group-
app-visibility.spec.mjs. Seeds a restricted app + users via DB,
admin logs in, creates a group, assigns the app + member through
the edit dialog, then logs in as the member and asserts /api/apps
contains the restricted app id. Cleans up after itself. Passes.
- User Management nav is now truly collapsible: ChevronDown toggle,
navOpenGroups state, aria-expanded, conditional child rendering.
Defaults to expanded only when a child is the active section.
- Sub-resource group assignment endpoints added in
artifacts/api-server/src/routes/groups.ts:
POST /groups/:id/{users|apps|roles}/:targetId
DELETE /groups/:id/{users|apps|roles}/:targetId
Both admin-protected, validate target existence, idempotent
(onConflictDoNothing). Aggregate PATCH /groups/:id remains.
Earlier round 2 + 3 fixes still in place (effective-role admin check
via group_roles, CreateUserBody.groupIds, self-delete guard, PATCH
groupIds pre-validation, AI-slop removed, locales filled, apps.ts
inArray fix).
Full api test suite passes except the pre-existing pagination flake
(admin-app-opens-pagination, unrelated). Architect: PASS.
Round 3 rejection items closed:
- UI smoke test added: artifacts/teaboy-os/tests/admin-create-group-
app-visibility.spec.mjs. Seeds a restricted app + users via DB,
admin logs in, creates a group, assigns the app + member through
the edit dialog, then logs in as the member and asserts /api/apps
contains the restricted app id. Cleans up after itself. Passes.
- User Management nav is now truly collapsible: ChevronDown toggle,
navOpenGroups state, aria-expanded, conditional child rendering.
Defaults to expanded only when a child is the active section.
- Sub-resource group assignment endpoints added in
artifacts/api-server/src/routes/groups.ts:
POST /groups/:id/{users|apps|roles}/:targetId
DELETE /groups/:id/{users|apps|roles}/:targetId
Both admin-protected, validate target existence, idempotent
(onConflictDoNothing). Aggregate PATCH /groups/:id remains.
Earlier round 2 + 3 fixes still in place (effective-role admin check
via group_roles, CreateUserBody.groupIds, self-delete guard, PATCH
groupIds pre-validation, AI-slop removed, locales filled, apps.ts
inArray fix).
Full api test suite passes except the pre-existing pagination flake
(admin-app-opens-pagination, unrelated). Architect: PASS.
Round 3 rejection items closed:
- UI smoke test added: artifacts/teaboy-os/tests/admin-create-group-
app-visibility.spec.mjs. Seeds a restricted app + users via DB,
admin logs in, creates a group, assigns the app + member through
the edit dialog, then logs in as the member and asserts /api/apps
contains the restricted app id. Cleans up after itself. Passes.
- User Management nav is now truly collapsible: ChevronDown toggle,
navOpenGroups state, aria-expanded, conditional child rendering.
Defaults to expanded only when a child is the active section.
- Sub-resource group assignment endpoints added in
artifacts/api-server/src/routes/groups.ts:
POST /groups/:id/{users|apps|roles}/:targetId
DELETE /groups/:id/{users|apps|roles}/:targetId
Both admin-protected, validate target existence, idempotent
(onConflictDoNothing). Aggregate PATCH /groups/:id remains.
Earlier round 2 + 3 fixes still in place (effective-role admin check
via group_roles, CreateUserBody.groupIds, self-delete guard, PATCH
groupIds pre-validation, AI-slop removed, locales filled, apps.ts
inArray fix).
Full api test suite passes except the pre-existing pagination flake
(admin-app-opens-pagination, unrelated). Architect: PASS.
Round 2 fixes:
- apps.ts getVisibleAppsForUser uses getEffectiveRoleIds() so admin
detection honors group_roles inheritance (closes group-only-admin
bypass). Exported helper from middlewares/auth.ts.
- POST /users accepts optional groupIds[] via new CreateUserBody
schema. Validates ids exist; user create + auto-Everyone + requested
groups happen in one transaction. OpenAPI extended; api-zod and
api-client-react regenerated.
- DELETE /users/:id 400s on self-delete.
- scripts/src/seed.ts: removed `void inArray;` slop and unused import.
- Locales (ar+en): added admin.users.col.{displayNameAr,displayNameEn,
language} and admin.groups.searchPlaceholder.
Round 3 fixes:
- Admin Users "Add User" dialog gets a groupIds checkbox multi-select
populated from useListGroups; createUser sends groupIds when set.
- PATCH /users/:id validates ALL groupIds before deleting memberships
(returns 400 on any invalid id); replacement wrapped in transaction
to avoid partial-loss windows.
- Apps admin SQL bug: `${arr} = ANY(...)` produced bad params; now uses
`and(inArray(rolesTable.id, ids), eq(name,'admin'))`.
- New tests (artifacts/api-server/tests/apps-group-visibility.test.mjs):
group-derived admin sees all; group member sees group-granted +
unrestricted; outsider sees only unrestricted. All 3 pass.
Typecheck clean; full api test suite green except pre-existing
admin-app-opens-pagination flake (unrelated). Architect: PASS.
Round 2 fixes:
- apps.ts getVisibleAppsForUser uses getEffectiveRoleIds() so admin
detection honors group_roles inheritance (closes group-only-admin
bypass). Exported helper from middlewares/auth.ts.
- POST /users accepts optional groupIds[] via new CreateUserBody
schema. Validates ids exist; user create + auto-Everyone + requested
groups happen in one transaction. OpenAPI extended; api-zod and
api-client-react regenerated.
- DELETE /users/:id 400s on self-delete.
- scripts/src/seed.ts: removed `void inArray;` slop and unused import.
- Locales (ar+en): added admin.users.col.{displayNameAr,displayNameEn,
language} and admin.groups.searchPlaceholder.
Round 3 fixes:
- Admin Users "Add User" dialog gets a groupIds checkbox multi-select
populated from useListGroups; createUser sends groupIds when set.
- PATCH /users/:id validates ALL groupIds before deleting memberships
(returns 400 on any invalid id); replacement wrapped in transaction
to avoid partial-loss windows.
- Apps admin SQL bug: `${arr} = ANY(...)` produced bad params; now uses
`and(inArray(rolesTable.id, ids), eq(name,'admin'))`.
- New tests (artifacts/api-server/tests/apps-group-visibility.test.mjs):
group-derived admin sees all; group member sees group-granted +
unrestricted; outsider sees only unrestricted. All 3 pass.
Typecheck clean; full api test suite green except pre-existing
admin-app-opens-pagination flake (unrelated). Architect: PASS.
Auth middleware:
- Add getEffectiveRoleIds() that UNIONs user_roles with group_roles via
group memberships, used by requireAdmin / requirePermission /
userHasPermission / getUserRoles.
- requireAdmin and requirePermission now also reject inactive users
with 401 (matching requireAuth), closing a session-after-deactivation
bypass.
Groups routes:
- POST /groups and PATCH /groups/:id now wrap the group row write and
all assignment writes in a single db.transaction via
applyGroupAssignmentsTx(tx, ...), so partial state cannot leak.
- validateAssignmentIds rejects unknown app/role/user ids with 400
before any insert.
- Removed AI-slop: void or, void sql, as-unknown-as casts; conditions
use Drizzle's SQL union type.
Users route:
- /api/users supports q, groupId, status filters (server-side).
Admin UI (teaboy-os/admin.tsx):
- UsersPanel wires q/groupId/status to the backend, shows display name
and preferred language inline per row.
- UserGroupsEditor now edits display names (ar/en), preferred language,
active status, and group membership with a search box.
- GroupsPanel adds a top-level group search box.
- GroupDetailEditor Users tab adds a user search box.
Infra:
- scripts/post-merge.sh runs the seed (idempotent) so default groups
Admins / TeaBoy / Everyone always exist after merges.
Tests (artifacts/api-server/tests/groups-crud.test.mjs, all passing):
- Admin-only access (regular user gets 403).
- Default seed groups exist.
- Create group + member assignment.
- Bad userIds yields 400 with no leaked group row.
- Admin role inherited via group_roles grants admin access.
- Deactivated admin session is rejected with 401.
- Group create rolls back atomically when assignment fails.
- /api/users q + groupId + status filters return correct rows.
Notes / drift:
- "Roles" tab inside GroupDetailEditor and groupIds in CreateUserBody
remain as proposed follow-ups (require OpenAPI spec changes).
- Pre-existing pagination-test flake unrelated to this work.
Auth middleware:
- Add getEffectiveRoleIds() that UNIONs user_roles with group_roles via
group memberships, used by requireAdmin / requirePermission /
userHasPermission / getUserRoles.
- requireAdmin and requirePermission now also reject inactive users
with 401 (matching requireAuth), closing a session-after-deactivation
bypass.
Groups routes:
- POST /groups and PATCH /groups/:id now wrap the group row write and
all assignment writes in a single db.transaction via
applyGroupAssignmentsTx(tx, ...), so partial state cannot leak.
- validateAssignmentIds rejects unknown app/role/user ids with 400
before any insert.
- Removed AI-slop: void or, void sql, as-unknown-as casts; conditions
use Drizzle's SQL union type.
Users route:
- /api/users supports q, groupId, status filters (server-side).
Admin UI (teaboy-os/admin.tsx):
- UsersPanel wires q/groupId/status to the backend, shows display name
and preferred language inline per row.
- UserGroupsEditor now edits display names (ar/en), preferred language,
active status, and group membership with a search box.
- GroupsPanel adds a top-level group search box.
- GroupDetailEditor Users tab adds a user search box.
Infra:
- scripts/post-merge.sh runs the seed (idempotent) so default groups
Admins / TeaBoy / Everyone always exist after merges.
Tests (artifacts/api-server/tests/groups-crud.test.mjs, all passing):
- Admin-only access (regular user gets 403).
- Default seed groups exist.
- Create group + member assignment.
- Bad userIds yields 400 with no leaked group row.
- Admin role inherited via group_roles grants admin access.
- Deactivated admin session is rejected with 401.
- Group create rolls back atomically when assignment fails.
- /api/users q + groupId + status filters return correct rows.
Notes / drift:
- "Roles" tab inside GroupDetailEditor and groupIds in CreateUserBody
remain as proposed follow-ups (require OpenAPI spec changes).
- Pre-existing pagination-test flake unrelated to this work.
Backend:
- New schema: groups, user_groups, group_apps, group_roles (lib/db/src/schema/groups.ts)
- Seeds Admins, TeaBoy, Everyone system groups idempotently and maps existing users
- /api/groups CRUD with admin guard, batch counts, system-group delete protection
- Validates appIds/roleIds/userIds (400 on missing) and wraps assignment writes in
a single DB transaction (no partial state on failure)
- /api/users gains q/groupId/status filters, batch role+group loading, groupIds
replacement on PATCH, auto-assigns Everyone on admin-create
- /auth/register also auto-assigns Everyone for consistent default linkage
- buildAuthUser now returns groups (matches updated AuthUser OpenAPI schema)
- App visibility (getVisibleAppsForUser) unions group-granted apps via
group_apps + user_groups in addition to existing permission gating
Frontend (admin.tsx):
- Nav restructured: User Management section with Users + Groups children
- Section deep-linked via #section=… URL hash
- Users page rebuilt: search, group filter, status filter, sortable table,
groups column, edit-groups dialog, mobile cards
- New Groups page: cards with member/app/role counts, create dialog,
detail editor with Info/Apps/Users tabs and system-group guard
- ar/en translations added for all new keys
Testing:
- pnpm typecheck clean (api + web)
- 25/26 api tests pass; the only failure is pre-existing flaky pagination test
(admin-app-opens-pagination) — left as-is per scratchpad note
- Code review feedback addressed (validation, transactions, register auto-assign)
Backend:
- New schema: groups, user_groups, group_apps, group_roles (lib/db/src/schema/groups.ts)
- Seeds Admins, TeaBoy, Everyone system groups idempotently and maps existing users
- /api/groups CRUD with admin guard, batch counts, system-group delete protection
- Validates appIds/roleIds/userIds (400 on missing) and wraps assignment writes in
a single DB transaction (no partial state on failure)
- /api/users gains q/groupId/status filters, batch role+group loading, groupIds
replacement on PATCH, auto-assigns Everyone on admin-create
- /auth/register also auto-assigns Everyone for consistent default linkage
- buildAuthUser now returns groups (matches updated AuthUser OpenAPI schema)
- App visibility (getVisibleAppsForUser) unions group-granted apps via
group_apps + user_groups in addition to existing permission gating
Frontend (admin.tsx):
- Nav restructured: User Management section with Users + Groups children
- Section deep-linked via #section=… URL hash
- Users page rebuilt: search, group filter, status filter, sortable table,
groups column, edit-groups dialog, mobile cards
- New Groups page: cards with member/app/role counts, create dialog,
detail editor with Info/Apps/Users tabs and system-group guard
- ar/en translations added for all new keys
Testing:
- pnpm typecheck clean (api + web)
- 25/26 api tests pass; the only failure is pre-existing flaky pagination test
(admin-app-opens-pagination) — left as-is per scratchpad note
- Code review feedback addressed (validation, transactions, register auto-assign)
Task: Audit notification rules in service-orders.ts so users never
receive notifications about actions they themselves initiated. The
specific scenario: a user with the `orders.receive` permission who
cancels their own assigned order would notify themselves under the
old logic.
Approach:
- Added an optional `actorId` parameter to `notifyUser`. When the
notification recipient equals the actor, the function returns
early (no DB insert, no socket emit).
- Threaded `actorId` through the three notification call sites:
1. POST /orders — placing an order no longer notifies the placer
even if they also hold the receiver role.
2. PATCH /orders/:id/confirm-receipt — a receiver claiming their
own order no longer notifies themselves.
3. PATCH /orders/:id/status — replaced the narrow
`!initiatedByOwner` guard with the generic `actorId === userId`
check inside `notifyUser`. This now covers owner-cancel as
before AND owner-as-assignee transitions (preparing/completed/
cancelled) where the owner happens to also be the assignee.
Notes / deviations:
- Kept the existing distinguishing wording for receiver-initiated
cancels ("claimed but later cancelled by the receiver"); it's
simply suppressed when the receiver is also the owner.
- No schema or API contract changes; purely server-side notification
filtering. Typecheck passes.
- No follow-ups proposed: existing project tasks already cover
automated tests for order/cancel flow, re-order from history, and
an undo window for cancellations.
Task: Audit notification rules in service-orders.ts so users never
receive notifications about actions they themselves initiated. The
specific scenario: a user with the `orders.receive` permission who
cancels their own assigned order would notify themselves under the
old logic.
Approach:
- Added an optional `actorId` parameter to `notifyUser`. When the
notification recipient equals the actor, the function returns
early (no DB insert, no socket emit).
- Threaded `actorId` through the three notification call sites:
1. POST /orders — placing an order no longer notifies the placer
even if they also hold the receiver role.
2. PATCH /orders/:id/confirm-receipt — a receiver claiming their
own order no longer notifies themselves.
3. PATCH /orders/:id/status — replaced the narrow
`!initiatedByOwner` guard with the generic `actorId === userId`
check inside `notifyUser`. This now covers owner-cancel as
before AND owner-as-assignee transitions (preparing/completed/
cancelled) where the owner happens to also be the assignee.
Notes / deviations:
- Kept the existing distinguishing wording for receiver-initiated
cancels ("claimed but later cancelled by the receiver"); it's
simply suppressed when the receiver is also the owner.
- No schema or API contract changes; purely server-side notification
filtering. Typecheck passes.
- No follow-ups proposed: existing project tasks already cover
automated tests for order/cancel flow, re-order from history, and
an undo window for cancellations.
Replit-Task-Id: 65b6d89a-6882-42f8-adb7-6ac1a2560748
Task #70: After deleting a finished order from My Orders, users had no way
to recover it. This change introduces a short undo window so accidental
deletes can be reversed while keeping the cleanup workflow snappy.
Approach:
- Implemented as a client-side deferred-delete in
artifacts/teaboy-os/src/pages/my-orders.tsx. No backend changes.
- On delete (single trash icon or bulk "Clear N finished"):
* The order(s) are added to a local pendingDeleteIds set so they
immediately disappear from the list.
* A toast is shown with a localised "Undo" action and a 7s duration.
* A setTimeout is scheduled to call DELETE /api/orders/:id for the
affected ids and then invalidate the my-orders query.
- Clicking "Undo" cancels the timer, removes the ids from
pendingDeleteIds (so the cards reappear), dismisses the toast, and
shows a "Restored" confirmation toast.
- On unmount, any still-pending deletions are flushed so they aren't
silently lost if the user navigates away mid-window.
- If the deferred DELETE later fails, the ids are restored to the list
and a "Could not delete the order" destructive toast is shown.
OrderCard now delegates the actual delete to a new onDelete callback
from the page (the confirmation dialog stays in the card). Bulk clear
no longer fires N sequential mutations from inside a confirm dialog
spinner — it just schedules them.
i18n: added myOrders.undo / myOrders.undone strings to en.json and
ar.json.
Verification:
- pnpm tsc --noEmit passes for teaboy-os.
- e2e (runTest) covered: single delete + undo restores; single delete
with no undo becomes permanent after 7s; bulk clear + undo restores
all. DB state was asserted in each case.
Follow-up proposed: #72 — apply the same undo pattern to cancelled
orders.
Task #70: After deleting a finished order from My Orders, users had no way
to recover it. This change introduces a short undo window so accidental
deletes can be reversed while keeping the cleanup workflow snappy.
Approach:
- Implemented as a client-side deferred-delete in
artifacts/teaboy-os/src/pages/my-orders.tsx. No backend changes.
- On delete (single trash icon or bulk "Clear N finished"):
* The order(s) are added to a local pendingDeleteIds set so they
immediately disappear from the list.
* A toast is shown with a localised "Undo" action and a 7s duration.
* A setTimeout is scheduled to call DELETE /api/orders/:id for the
affected ids and then invalidate the my-orders query.
- Clicking "Undo" cancels the timer, removes the ids from
pendingDeleteIds (so the cards reappear), dismisses the toast, and
shows a "Restored" confirmation toast.
- On unmount, any still-pending deletions are flushed so they aren't
silently lost if the user navigates away mid-window.
- If the deferred DELETE later fails, the ids are restored to the list
and a "Could not delete the order" destructive toast is shown.
OrderCard now delegates the actual delete to a new onDelete callback
from the page (the confirmation dialog stays in the card). Bulk clear
no longer fires N sequential mutations from inside a confirm dialog
spinner — it just schedules them.
i18n: added myOrders.undo / myOrders.undone strings to en.json and
ar.json.
Verification:
- pnpm tsc --noEmit passes for teaboy-os.
- e2e (runTest) covered: single delete + undo restores; single delete
with no undo becomes permanent after 7s; bulk clear + undo restores
all. DB state was asserted in each case.
Follow-up proposed: #72 — apply the same undo pattern to cancelled
orders.
Replit-Task-Id: 1854a0b1-f6d3-4148-b2c2-b0940202d0df
- Add DELETE /api/orders/:id endpoint: owner can delete completed/cancelled
orders; admin can delete any. 401/403/404/204 responses. Emits
order_deleted to owner and assignee for live invalidation.
- Update OpenAPI spec with deleteServiceOrder operation; regenerate
api-client-react + api-zod.
- Frontend: per-order trash button on completed/cancelled cards with
confirm dialog; bulk "Clear finished (N)" button at top of list with
confirm dialog and toast summary (handles partial failures).
- Add ar/en locale keys under myOrders for delete + clear flows
(singular/plural variants).
- Backend tests cover all paths: pending owner=403, stranger=403,
owner-cancelled=204, owner-completed=204, admin-pending=204, 404, 401.
All 25 service-order tests pass; the 1 failing test (admin-app-opens-
pagination) is unrelated/pre-existing.
Follow-ups proposed: #70 undo for deleted orders, #71 audit
self-notification on receiver-cancel-own-order.
- Add DELETE /api/orders/:id endpoint: owner can delete completed/cancelled
orders; admin can delete any. 401/403/404/204 responses. Emits
order_deleted to owner and assignee for live invalidation.
- Update OpenAPI spec with deleteServiceOrder operation; regenerate
api-client-react + api-zod.
- Frontend: per-order trash button on completed/cancelled cards with
confirm dialog; bulk "Clear finished (N)" button at top of list with
confirm dialog and toast summary (handles partial failures).
- Add ar/en locale keys under myOrders for delete + clear flows
(singular/plural variants).
- Backend tests cover all paths: pending owner=403, stranger=403,
owner-cancelled=204, owner-completed=204, admin-pending=204, 404, 401.
All 25 service-order tests pass; the 1 failing test (admin-app-opens-
pagination) is unrelated/pre-existing.
Follow-ups proposed: #70 undo for deleted orders, #71 audit
self-notification on receiver-cancel-own-order.
- Add DELETE /api/orders/:id endpoint: owner can delete completed/cancelled
orders; admin can delete any. 401/403/404/204 responses. Emits
order_deleted to owner and assignee for live invalidation.
- Update OpenAPI spec with deleteServiceOrder operation; regenerate
api-client-react + api-zod.
- Frontend: per-order trash button on completed/cancelled cards with
confirm dialog; bulk "Clear finished (N)" button at top of list with
confirm dialog and toast summary (handles partial failures).
- Add ar/en locale keys under myOrders for delete + clear flows
(singular/plural variants).
- Backend tests cover all paths: pending owner=403, stranger=403,
owner-cancelled=204, owner-completed=204, admin-pending=204, 404, 401.
All 25 service-order tests pass; the 1 failing test (admin-app-opens-
pagination) is unrelated/pre-existing.
Follow-ups proposed: #70 undo for deleted orders, #71 audit
self-notification on receiver-cancel-own-order.
- Add DELETE /api/orders/:id endpoint: owner can delete completed/cancelled
orders; admin can delete any. 401/403/404/204 responses. Emits
order_deleted to owner and assignee for live invalidation.
- Update OpenAPI spec with deleteServiceOrder operation; regenerate
api-client-react + api-zod.
- Frontend: per-order trash button on completed/cancelled cards with
confirm dialog; bulk "Clear finished (N)" button at top of list with
confirm dialog and toast summary (handles partial failures).
- Add ar/en locale keys under myOrders for delete + clear flows
(singular/plural variants).
- Backend tests cover all paths: pending owner=403, stranger=403,
owner-cancelled=204, owner-completed=204, admin-pending=204, 404, 401.
All 25 service-order tests pass; the 1 failing test (admin-app-opens-
pagination) is unrelated/pre-existing.
Follow-ups proposed: #70 undo for deleted orders, #71 audit
self-notification on receiver-cancel-own-order.