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.
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.
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.
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.
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.
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.
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.
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 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.
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.
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.
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.
Context
- After Task #64 receivers can cancel an order they have already claimed.
- Previously the requester's cancellation notification body was the same regardless of who cancelled (admin vs receiver), giving them no signal that their order had been claimed and then dropped.
Changes
- artifacts/api-server/src/routes/service-orders.ts (PATCH /orders/:id/status, cancel branch):
- When the cancellation is initiated by the order's currently assigned receiver
(existing.assignedTo === userId && next === "cancelled"), the requester's
notification body now includes the bilingual phrase
"استلم طلبك ولكن تم إلغاؤه لاحقاً" / "Your order was claimed but later
cancelled by the receiver", appended to the service name for context.
- Admin / non-assignee cancellations keep the existing service-name body so
the two cases remain distinguishable in the requester's notifications list.
- The notification is created via the existing notifyUser() helper, which
persists to notifications and emits notification_created over Socket.IO,
so the bell badge updates in real time.
- artifacts/api-server/tests/service-orders.test.mjs:
- Added "requester gets a distinct notification when the receiver cancels a
claimed order" covering the full flow: place → confirm-receipt → cancel,
then asserts the owner has a cancellation notification whose body matches
the new bilingual receiver-cancel copy.
Verification
- All 7 tests in service-orders.test.mjs pass locally against the running API.
Notes / deviations
- Locale JSON files (ar.json/en.json) were inspected but not modified: notification
copy is stored server-side as titleAr/titleEn/bodyAr/bodyEn on the notifications
row; the client just renders those strings directly.
Replit-Task-Id: 5aa480a9-c6e0-4a06-8919-b2b6784d8a98
Backend (artifacts/api-server)
- Add admin-only role-toggle endpoints:
- POST /users/:id/roles { roleName } — idempotent, returns UserProfile
- DELETE /users/:id/roles/:roleName — idempotent, returns UserProfile
- Allow assigned receiver to cancel their own claimed order
(received/preparing) in PATCH /orders/:id/status. Owner & admin
rules unchanged.
- New backend test cases: receiver can cancel own assigned order;
another receiver gets 403.
API spec / codegen
- Add AddUserRoleBody schema and the two new role endpoints
under a new "roles" tag in lib/api-spec/openapi.yaml.
- Regenerated api-zod and api-client-react.
Frontend (artifacts/teaboy-os)
- New page src/pages/orders-incoming.tsx at /orders/incoming:
RBAC-gated (admin || order_receiver), shows "My active orders"
and "Awaiting receiver" sections, with claim, mark preparing,
mark completed and cancel buttons. Handles 409 already_claimed.
- Add Inbox button to home top bar, conditional on the same roles.
- Admin users table now has an "Order Receiver" Switch wired to
the new role-toggle hooks.
- Extend use-notifications-socket to invalidate the incoming-orders
query on order_incoming_changed and on notification_created with
type === "order".
- Bilingual locale keys (ar/en) for the new page and admin label.
Tests
- All 25 api-server tests pass (24 existing + 1 new receiver-cancel
case). All 3 teaboy-os e2e tests pass.
Follow-up filed: #67 (notify requester when receiver cancels).
Backend (artifacts/api-server)
- Add admin-only role-toggle endpoints:
- POST /users/:id/roles { roleName } — idempotent, returns UserProfile
- DELETE /users/:id/roles/:roleName — idempotent, returns UserProfile
- Allow assigned receiver to cancel their own claimed order
(received/preparing) in PATCH /orders/:id/status. Owner & admin
rules unchanged.
- New backend test cases: receiver can cancel own assigned order;
another receiver gets 403.
API spec / codegen
- Add AddUserRoleBody schema and the two new role endpoints
under a new "roles" tag in lib/api-spec/openapi.yaml.
- Regenerated api-zod and api-client-react.
Frontend (artifacts/teaboy-os)
- New page src/pages/orders-incoming.tsx at /orders/incoming:
RBAC-gated (admin || order_receiver), shows "My active orders"
and "Awaiting receiver" sections, with claim, mark preparing,
mark completed and cancel buttons. Handles 409 already_claimed.
- Add Inbox button to home top bar, conditional on the same roles.
- Admin users table now has an "Order Receiver" Switch wired to
the new role-toggle hooks.
- Extend use-notifications-socket to invalidate the incoming-orders
query on order_incoming_changed and on notification_created with
type === "order".
- Bilingual locale keys (ar/en) for the new page and admin label.
Tests
- All 25 api-server tests pass (24 existing + 1 new receiver-cancel
case). All 3 teaboy-os e2e tests pass.
Follow-up filed: #67 (notify requester when receiver cancels).
Adds the user-facing half of the service ordering workflow on top of the
backend foundation merged in Task #62.
What's new:
- Order button on each available service card (hidden when service is
unavailable) opens a focused OrderServiceModal showing only the
service name + image with a multi-line notes textarea (500-char cap
with live counter). Submitting calls POST /orders, shows a toast, and
invalidates the My Orders cache.
- New /my-orders page lists the user's own orders newest-first with a
status pill, a horizontal 4-step timeline (pending → received →
preparing → completed), and a red terminal pill for cancelled orders.
- Cancel action with confirm dialog is shown only while the order is
pending or received; it calls PATCH /orders/:id/status with
cancelled.
- "My Orders" link added to the services page header.
- Realtime: the existing notifications socket also invalidates the
my-orders query on notification_created (when type === 'order') and
on a new order_updated event.
- Locale keys mirrored in ar.json and en.json for services.order/*,
myOrders.*, and orderStatus.*. RTL/LTR handled.
- Friendly load-error state with retry, plus client-side sort by
createdAt desc.
Verification: typecheck clean, all 3 e2e tests pass, manual end-to-end
UI flow (place + cancel + locale switch) verified via testing skill,
backend smoke test confirmed POST /orders + GET /orders/my wiring.
Adds the user-facing half of the service ordering workflow on top of the
backend foundation merged in Task #62.
What's new:
- Order button on each available service card (hidden when service is
unavailable) opens a focused OrderServiceModal showing only the
service name + image with a multi-line notes textarea (500-char cap
with live counter). Submitting calls POST /orders, shows a toast, and
invalidates the My Orders cache.
- New /my-orders page lists the user's own orders newest-first with a
status pill, a horizontal 4-step timeline (pending → received →
preparing → completed), and a red terminal pill for cancelled orders.
- Cancel action with confirm dialog is shown only while the order is
pending or received; it calls PATCH /orders/:id/status with
cancelled.
- "My Orders" link added to the services page header.
- Realtime: the existing notifications socket also invalidates the
my-orders query on notification_created (when type === 'order') and
on a new order_updated event.
- Locale keys mirrored in ar.json and en.json for services.order/*,
myOrders.*, and orderStatus.*. RTL/LTR handled.
- Friendly load-error state with retry, plus client-side sort by
createdAt desc.
Verification: typecheck clean, all 3 e2e tests pass, manual end-to-end
UI flow (place + cancel + locale switch) verified via testing skill,
backend smoke test confirmed POST /orders + GET /orders/my wiring.
- New `service_orders` table (status pending|claimed|delivered|received|cancelled)
- New `orders:receive` permission + `order_receiver` role; admins implicitly allowed
- Added `requirePermission(name)` and `userHasPermission(userId, name)` middleware helpers
- New routes:
- POST /api/orders place order (authenticated)
- GET /api/orders/my list current user's orders
- GET /api/orders/incoming receivers see pending+active visible orders
- PATCH /api/orders/:id/status claim (atomic), deliver, cancel
- PATCH /api/orders/:id/confirm-receipt requester confirms a delivered order
- Atomic claim via UPDATE ... WHERE status='pending' AND assigned_to IS NULL
(returns 409 already_claimed on race)
- Realtime: emits `notification_created` to receivers/requester,
`order_incoming_changed` to all receivers, `order_updated` to requester
- Service shape in order responses limited to id/nameAr/nameEn/imageUrl
(no description fields), per spec
- OpenAPI updated with new paths and schemas; codegen run
- Seed updated idempotently (permission, role, role_permissions)
- New tests in artifacts/api-server/tests/service-orders.test.mjs
(full lifecycle, atomic claim race, unauth rejection) — all 21 api tests pass
No deviations from the planned scope. Tasks #63 (client UI) and #64
(receiver page + admin role toggle) remain blocked-by #62 and are next.
Integrates the top bar clock's visibility toggle into the ClockStylePicker component and removes the standalone eye button. Updates locale files for Arabic and English to reflect the new string for showing the top bar clock.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 89fb8094-c538-4564-8b9b-29c0f4236486
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PwtbShH
Replit-Helium-Checkpoint-Created: true
Introduced a new hook `useTopbarClockVisibility` in `artifacts/teaboy-os/src/components/clock.tsx` to manage the visibility of the top bar clock, separate from the home clock widget. Updated `artifacts/teaboy-os/src/pages/home.tsx` to utilize this new hook, allowing the eye icon to toggle the top bar clock's visibility.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 08cff793-79ae-45ad-9958-d029baf1737d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PwtbShH
Replit-Helium-Checkpoint-Created: true
Added a dedicated Eye/EyeOff icon button next to the existing
ClockStylePicker in the home top bar. Clicking it instantly hides or
shows the home wall-clock widget without opening any popover.
- Wired to the existing useHomeClockVisibility hook so it stays in
sync with the toggle inside the clock-style popover (same storage
key + custom event).
- Reuses existing locale keys home.clockStyle.hideWidget /
home.clockStyle.showWidget for aria-label and title (flips with
state).
- aria-pressed reflects hidden state for accessibility.
- No new locale keys, no changes to default position behavior, and
the popover toggle is left intact as requested.
Files:
- artifacts/teaboy-os/src/pages/home.tsx (added Eye/EyeOff imports
and the new icon button between ClockStylePicker and the language
toggle).
Added a dedicated Eye/EyeOff icon button next to the existing
ClockStylePicker in the home top bar. Clicking it instantly hides or
shows the home wall-clock widget without opening any popover.
- Wired to the existing useHomeClockVisibility hook so it stays in
sync with the toggle inside the clock-style popover (same storage
key + custom event).
- Reuses existing locale keys home.clockStyle.hideWidget /
home.clockStyle.showWidget for aria-label and title (flips with
state).
- aria-pressed reflects hidden state for accessibility.
- No new locale keys, no changes to default position behavior, and
the popover toggle is left intact as requested.
Files:
- artifacts/teaboy-os/src/pages/home.tsx (added Eye/EyeOff imports
and the new icon button between ClockStylePicker and the language
toggle).
Update home component logic to correctly pin the clock to the far-left position in RTL layouts when it's unset or dragged to the end of the app list, ensuring it occupies the first two columns.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: a6f7b52f-d48d-41b0-9435-36a5f4524394
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PwtbShH
Replit-Helium-Checkpoint-Created: true
Per user request, the home page no longer shows the time-of-day greeting
("مساء الخير، {name}") nor the day-name + Gregorian date subtitle. The
apps grid (and the admin stats row when applicable) now sits directly
under the top bar.
Changes (artifacts/teaboy-os/src/pages/home.tsx):
- Removed the entire greeting block (the wrapper div, the <h1> greeting
and the date <p>) from the Main Content section.
- Removed the now-unused `dayName`, `gregorianDate` and `greeting`
locals plus the `formatWeekday` and `greetingKey` imports.
- Kept `displayName` (still used in the avatar header) and the
`home.greeting.*` translation keys (left in place; cheap to keep and
avoids touching unrelated locale files).
Mid-task fix:
- First pass also dropped `displayName` which broke the avatar
(`displayName is not defined`). Restored it; `displayName` is needed
by the user-card initials and name labels.
Validation:
- `pnpm --filter @workspace/api-server test` — 18/18 pass
- `pnpm --filter @workspace/teaboy-os test:e2e` — 3/3 pass
Notes:
- Pre-existing TypeScript errors in admin.tsx / clock-style-picker.tsx /
clock.tsx are unrelated to this task (codegen drift from earlier
tasks).
Per user request, the home page no longer shows the time-of-day greeting
("مساء الخير، {name}") nor the day-name + Gregorian date subtitle. The
apps grid (and the admin stats row when applicable) now sits directly
under the top bar.
Changes (artifacts/teaboy-os/src/pages/home.tsx):
- Removed the entire greeting block (the wrapper div, the <h1> greeting
and the date <p>) from the Main Content section.
- Removed the now-unused `dayName`, `gregorianDate` and `greeting`
locals plus the `formatWeekday` and `greetingKey` imports.
- Kept `displayName` (still used in the avatar header) and the
`home.greeting.*` translation keys (left in place; cheap to keep and
avoids touching unrelated locale files).
Mid-task fix:
- First pass also dropped `displayName` which broke the avatar
(`displayName is not defined`). Restored it; `displayName` is needed
by the user-card initials and name labels.
Validation:
- `pnpm --filter @workspace/api-server test` — 18/18 pass
- `pnpm --filter @workspace/teaboy-os test:e2e` — 3/3 pass
Notes:
- Pre-existing TypeScript errors in admin.tsx / clock-style-picker.tsx /
clock.tsx are unrelated to this task (codegen drift from earlier
tasks).
Original task #58 was to add a dismissible hint when the home clock is
hidden. The hint was implemented and verified end-to-end (EN + AR/RTL),
but the user then asked to remove it. This commit removes the hint and
also removes the "تطبيقاتي · N" / "My Apps · N" header above the apps
grid per the user's request to show the apps directly.
Changes:
- artifacts/teaboy-os/src/pages/home.tsx
- Removed the hidden-clock hint UI block, its session-storage state
(clockHintDismissed) and the related useEffect/dismiss helper.
- Removed the unused Clock-icon lucide import.
- Removed the "My Apps" heading row (h3 + count) above the apps grid.
- artifacts/teaboy-os/src/locales/en.json, ar.json
- Removed the new keys: home.clockStyle.hiddenHint,
hiddenHintAction, dismissHint.
Rebase notes:
- Rebased onto main (a05249f). Two conflicts:
* artifacts/teaboy-os/public/opengraph.jpg — binary asset unrelated
to this task; took main's version (--ours during rebase).
* artifacts/teaboy-os/src/pages/home.tsx — main re-styled the apps
grid header (flex/gap layout with inline " · N" count) and added
a useGridCols() hook. My commit removes that header entirely per
user request, so kept the user-requested removal while preserving
main's useGridCols() addition. No semantic divergence — just the
deletion still applies cleanly to the new header markup.
Notes:
- Clock can still be hidden via the X on the clock tile and shown
again via the clock-style popover in the top bar (existing behavior).
- Translation key home.myApps is still defined but no longer used on
the home page; left in place to avoid touching other consumers.
- Pre-existing TypeScript errors on AuthUser.clockStyle/clockHour12
in chat.tsx/home.tsx are unrelated to this task.
Replit-Task-Id: 14544719-933c-49f8-84a0-6577d06fbc14