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).
- 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.
Background:
Task #53 fixed the login/register session persistence race by explicitly
awaiting `req.session.save` before responding. Other session-mutating
endpoints (notably `/auth/logout`) still relied on express-session's
default end-hook, which can flush the response before the store write
finishes — producing intermittent "still logged in" / "logged out"
glitches on the immediate next request.
Changes:
- New shared helper `artifacts/api-server/src/lib/session.ts` exporting
`saveSession(req)` and `destroySession(req)` — promise wrappers around
`req.session.save` / `req.session.destroy` so handlers can `await`
store persistence before flushing the HTTP response. Documented the
rationale in the file so future session-mutating routes use the same
safe pattern.
- `artifacts/api-server/src/routes/auth.ts`:
- `/auth/register` and `/auth/login` now use `await saveSession(req)`
in place of the inline ad-hoc Promise wrapper.
- `/auth/logout` is now async and `await`s `destroySession(req)`
before responding, closing the same race for the destroy path.
- Audited remaining routes: only `auth.ts` mutates `req.session`; all
other handlers only read `req.session.userId`, so no further changes
are needed.
Notes / deviations:
- Pre-existing TypeScript errors in unrelated files (conversations,
notes, users, api-zod exports) were left untouched — out of scope.
- New test file `artifacts/api-server/tests/auth-session-persistence.test.mjs`
covers the acceptance criterion: login / register / logout each
followed by an immediate `/auth/me` probe to assert the session was
persisted (or destroyed) before the response was flushed. Modeled on
the existing leave-test pattern. Full suite: 18/18 passing.
Replit-Task-Id: 11b72d21-d7c2-42cb-a4d9-f1197cfad4c5
Adds artifacts/api-server/tests/conversations-leave.test.mjs, modeled on
the existing apps-open.test.mjs, covering POST /conversations/:id/leave:
- Solo member leaving deletes the conversation entirely.
- Sole admin leaving with no successor auto-promotes the
earliest-joined remaining member.
- Sole admin leaving with a chosen successorId promotes that user.
- Sole admin leaving with a non-member successorId returns 400 and
leaves the group untouched (leaver still admin, no promotion).
- Non-admin leaving a group removes them with no admin promotion.
Tests create their own users (with the standard user role) and groups
directly in Postgres so joined_at ordering is deterministic for the
auto-promotion case, then exercise the route through HTTP using a real
session cookie obtained from POST /api/auth/login. An after() hook
cleans up all created conversations, participants, messages, role
assignments, and users.
The optional e2e for the chooser dialog is intentionally deferred and
proposed as follow-up #51.
Verified by running `pnpm --filter @workspace/api-server test` three
times consecutively; all 15 tests pass on every run.
Replit-Task-Id: e31c169d-a4f5-4387-a642-b39a422c1408
Added offset/limit pagination to the two admin app-opens drill-in
endpoints so admins can investigate spikes that span more than the
default 100 most-recent opens.
Backend (artifacts/api-server/src/routes/stats.ts):
- New parsePaging() helper validates `limit` (1..200, default 100) and
`offset` (>=0, default 0); invalid values return 400.
- Both `/stats/admin/app-opens/by-app/:appId` and
`/stats/admin/app-opens/by-user/:userId` accept the new params, apply
`.limit(limit).offset(offset)`, and return `limit`, `offset`, and a
`nextOffset` (number | null) computed from `totalCount`.
- Added a stable secondary sort (`id desc`) so paged results don't
shuffle when timestamps tie.
Spec & client (lib/api-spec/openapi.yaml + regenerated clients):
- Added `limit`/`offset` query params and `limit`/`offset`/`nextOffset`
response fields to AdminAppOpensByApp/AdminAppOpensByUser.
- Re-ran `pnpm --filter @workspace/api-spec run codegen`.
Frontend (artifacts/teaboy-os/src/pages/admin.tsx + locales):
- AppOpensDrillIn / UserOpensDrillIn now accumulate extra pages in
local state and expose a "Load more" button via a shared
LoadMoreSection footer that also shows "Showing X of Y".
- Extra pages are fetched via the generated `getAdminAppOpensByApp` /
`getAdminAppOpensByUser` functions; accumulated state resets when
the appId/userId or stats query params change.
- Added en/ar translations for `loadMore`, `loadMoreError`, `shownOf`.
Tests:
- New artifacts/api-server/tests/admin-app-opens-pagination.test.mjs
covers happy-path paging for both endpoints, the default page size,
and 400 responses for invalid limit/offset.
- All 10 api-server tests pass; full workspace typecheck passes.
Replit-Task-Id: b6382efe-765f-4689-8c93-196fee253f63
Original task: verify POST /api/apps/:id/open behaves correctly under slow
networks (the keepalive POST must survive the user navigating away),
unauthenticated callers (401 with no row inserted), and unknown app ids
(404 with no row inserted).
Changes
- New committed test file artifacts/api-server/tests/apps-open.test.mjs
using Node's built-in node:test runner (no new test framework added):
* happy path: authenticated POST returns 204 and inserts an app_opens row
* unauthenticated POST returns 401 and inserts no row
* authenticated POST to a non-existent app id (max(id)+100000) returns 404
and inserts no row
* slow-network simulation: opens a raw http.request to /api/apps/:id/open,
aborts the socket ~50 ms after sending so the client never reads the
response (mimicking a navigation-aborted keepalive POST), then asserts
the server still inserted the row. This proves the route's
`await db.insert(...)` runs to completion independently of whether the
client is still around to read the 204.
- The tests create a dedicated test user with a precomputed bcrypt hash for
"TestPass123!", assign the standard "user" role, log in via
POST /api/auth/login to obtain a connect.sid cookie, run the four cases,
and clean up (app_opens / user_roles / users) in an `after` hook.
- Added `pg` as a devDependency on @workspace/api-server (used by the
tests for direct DB assertions) and a `test` script:
`node --test 'tests/**/*.test.mjs'`.
- Also ran in-browser end-to-end coverage via the testing skill that
exercised the keepalive + wouter navigation flow against a live home
page with a 3 s route delay; that run also passed.
Schema drift fixed during the run
- The dev DB was missing the `app_opens` table and the `users.clock_style`
column referenced by the running schema. `pnpm --filter @workspace/db
push` blocked on an interactive rename/create prompt that could not be
answered non-interactively, so I brought the dev DB in line with the
Drizzle schema using idempotent SQL (CREATE TABLE IF NOT EXISTS for
app_opens with its two indexes; ALTER TABLE users ADD COLUMN IF NOT
EXISTS clock_style varchar(30)). No schema files were modified.
No production code changes were required — the existing route already
returns 401/404/204 correctly and the tests now lock that behavior in.
Replit-Task-Id: b7422abb-cc1b-4727-b70b-cde090f1a748