- 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.
Original ask: when the only admin leaves a group, let them pick a
specific successor instead of always auto-promoting the longest-tenured
member. Keep "auto" available, with bilingual labels.
Changes:
- lib/api-spec/openapi.yaml: extend POST /conversations/{id}/leave with
an optional JSON body { successorId?: number | null }.
- Regenerated lib/api-client-react and lib/api-zod via api-spec codegen.
- artifacts/api-server/src/routes/conversations.ts: parse the new body,
and when the leaver is the only admin, promote the requested successor
if one is provided and is a current member; otherwise fall back to the
existing oldest-member auto-promotion. Returns 400 if successorId is
not a remaining member.
- artifacts/teaboy-os/src/pages/chat.tsx:
- Track a successorChoice state ("auto" | userId).
- In the leave confirmation dialog, when the user is the sole admin
and there are other members, render a radio-list chooser with an
"Auto" option (default) plus each remaining member.
- Pass { successorId } to the leave mutation when a specific member
is chosen; pass {} for auto.
- Reset choice on success.
- artifacts/teaboy-os/src/locales/{en,ar}.json: added successorTitle,
successorHelp, successorAuto strings.
Verification:
- pnpm -w run typecheck passes for libs and all artifacts.
- Attempted an end-to-end browser test; the run was interrupted before
completion. Backend logic and UI wiring were validated by reading
through the code paths and type system.
Task #41: Show a usage history popup when admins click a top
apps row or a most-active users row on the admin dashboard.
Changes:
- API: extracted the existing range/from/to parser in
artifacts/api-server/src/routes/stats.ts into a shared
parseRangeWindow() helper so the same window logic powers
the existing /stats/admin endpoint and two new ones.
- API: added GET /stats/admin/app-opens/by-app/:appId returning
the last 100 opens for an app inside the selected window,
including the user (id, username, displayName, avatarUrl)
and a totalCount. requireAuth + requireAdmin guarded.
- API: added GET /stats/admin/app-opens/by-user/:userId with
the same shape but per-app metadata for each open.
- Spec: added matching paths and AdminAppOpensByApp /
AdminAppOpensByUser schemas in lib/api-spec/openapi.yaml,
reran orval codegen for api-client-react and api-zod.
- UI: in artifacts/teaboy-os/src/pages/admin.tsx the Top apps
and Most active users leaderboard rows now open a drill-in
modal instead of immediately navigating away. The modal
shows a scrollable timeline of recent opens (timestamp +
user/app), respects the dashboard's current range selector
(7d/30d/90d/custom), and exposes an "Edit app" / "View user"
footer button that preserves the previous jump-to behaviour.
Loading, error, and empty states are handled.
- i18n: added admin.dashboard.drillIn keys in en.json and
ar.json (titles, subtitle, empty/error, footer buttons).
Verification: pnpm run typecheck passes.
Follow-ups proposed: #47 (clickable rows inside the popup),
#48 (paginate beyond 100 opens).
Original task (Task #35): When the admin dashboard's custom range
receives a malformed or missing date (e.g. ?range=custom&from=foo),
the API silently fell back to the 7-day window instead of returning
an error. This masked client bugs and confused admins.
Changes:
- artifacts/api-server/src/routes/stats.ts:
- Refactored the custom-range branch so range=custom now always
validates from/to. Returns 400 with a helpful, specific message
when:
* from or to is missing
* from or to is not a valid YYYY-MM-DD UTC date
* from is after to (existing behavior, message clarified)
- Removed the silent `if (range === "custom") range = "7d"` fallback.
- lib/api-spec/openapi.yaml:
- Documented the 400 ErrorResponse on getAdminStats so the generated
clients know about this failure mode.
- Regenerated @workspace/api-client-react and @workspace/api-zod via
`pnpm --filter @workspace/api-spec run codegen`.
- artifacts/teaboy-os/src/pages/admin.tsx:
- Captured `error` from useGetAdminStats (with retry: false) and
surface a translated, role="alert" panel beneath the range
controls when the API returns an error, including the server's
error message. The frontend already guards against client-side
invalid input via isCustomValid, so this primarily covers any
remaining edge cases (stale querystring, race conditions).
- Added admin.dashboard.customRange.loadError translations in
en.json and ar.json.
Verified with `pnpm -w run typecheck` (passes for libs and all
artifacts). Manual curl confirmed the route is wired (auth gate
returns 401 first, as expected).
Adds per-user mute / archive / leave actions for chats.
Schema:
- `lib/db/src/schema/conversations.ts`: added `is_muted` and
`is_archived` boolean columns on `conversation_participants`.
- Columns applied directly via SQL (drizzle push wanted to make
unrelated app_opens decisions; force-applied is_muted/is_archived
with `ALTER TABLE ... ADD COLUMN IF NOT EXISTS`).
API (`artifacts/api-server/src/routes/conversations.ts`):
- New endpoint `PATCH /conversations/:id/state` — current user
toggles their own `isMuted` / `isArchived`.
- New endpoint `POST /conversations/:id/leave` — removes the
caller from a group conversation; rejects DMs.
- `buildConversationDetails` now returns `isMuted` and
`isArchived` for the requesting user.
- `sendMessage` auto-clears `isArchived` for all participants so
archived chats reappear when a new message arrives.
OpenAPI (`lib/api-spec/openapi.yaml`):
- Added the two new operations and `UpdateConversationStateBody`.
- Added `isMuted` / `isArchived` to `ConversationWithDetails`.
- Re-ran codegen for `@workspace/api-zod` and
`@workspace/api-client-react`.
UI (`artifacts/teaboy-os/src/pages/chat.tsx`):
- Header gets a kebab "chat actions" button visible for both DMs
and groups when a conversation is open.
- Action sheet offers Mute/Unmute, Archive/Unarchive, and
(groups only) Leave with a confirmation dialog.
- Conversation list now has Active / Archived tabs and a
bell-off indicator + dimmed unread badge for muted chats.
- Bilingual strings added to en.json and ar.json.
Side fixes (unrelated pre-existing schema drift discovered while
testing): added missing `users.clock_hour12` and
`conversations.avatar_url` columns directly so login and the
conversations list work; the schema files already declared them.
Verified end-to-end with the testing tool: mute, archive,
unarchive, and leave-group flows all pass.
Lets group admins manage their groups after creation.
API changes (lib/api-spec/openapi.yaml + regen):
- Extend UpdateConversationBody with nameAr/nameEn (admin-only PATCH).
- Add POST /conversations/{id}/participants (add members).
- Add DELETE /conversations/{id}/participants/{userId} (remove member).
Backend (artifacts/api-server/src/routes/conversations.ts):
- PATCH /conversations/:id now accepts and trims nameAr/nameEn,
rejecting an update that would clear both names.
- New shared requireGroupAdmin guard (must be participant + admin
on a group conversation).
- Add-participants validates user IDs exist and skips duplicates.
- Remove-participants forbids the admin from removing themselves.
- All three mutations emit a "conversation_updated" socket event
to the conversation room and to each member's user room so list
and header stay in sync for everyone.
Frontend (artifacts/teaboy-os/src/pages/chat.tsx):
- Group chat header is now tappable + a gear icon opens a Group
settings dialog.
- Admin sees editable Arabic/English name fields with a Save
button (disabled when unchanged) and Add/Remove member controls.
- Non-admins see a read-only members list.
- Add Members reuses /users/directory and excludes existing members.
- Removes own row's trash button so admin can't remove themselves.
- Subscribes to "conversation_updated" socket event to refresh list.
- Bilingual strings added (chat.settings.*) in en.json and ar.json
with full Arabic plural forms.
Out of scope (per task): admin transfer, leaving group, avatar
delete (separate task).
Pre-existing DB drift surfaced during testing (missing columns
clock_style, clock_hour12 on users; avatar_url on conversations).
Added with non-destructive ALTER TABLE ... ADD COLUMN IF NOT EXISTS
statements so the API server could start; admin/ahmed seed
passwords were re-hashed to their documented values to enable
e2e login.
Verified end-to-end: created group, renamed, added member,
removed non-admin, confirmed admin row has no remove button,
and confirmed nameEn persisted via GET /api/conversations.
Adds upload + display of a custom avatar for group conversations.
Changes:
- DB: Added `avatar_url text` (nullable) to `conversations` table.
Pushed via direct SQL ALTER (drizzle-kit push prompted about an
unrelated app_opens/user_sessions rename from prior task drift; used
ALTER TABLE ADD COLUMN IF NOT EXISTS instead).
- OpenAPI (`lib/api-spec/openapi.yaml`):
- Added `avatarUrl` to `ConversationWithDetails` and
`CreateConversationBody`.
- Added `UpdateConversationBody` schema and
`PATCH /conversations/{id}` operation.
- Regenerated `@workspace/api-zod` and `@workspace/api-client-react`.
- API (`artifacts/api-server/src/routes/conversations.ts`):
- Persist `avatarUrl` on create.
- New `PATCH /conversations/:id` (admin-only) to update `avatarUrl`.
- Web (`artifacts/teaboy-os/src/pages/chat.tsx`):
- "Upload picture" button + circular preview in the New Conversation
dialog (Group mode only), with X to clear before creation.
- Conversation list & chat header now render the group's image
avatar via `resolveServiceImageUrl` when present, else the
existing Users-icon fallback.
- Camera overlay on the chat header avatar (admins only) to replace
the picture; uploads via existing object-storage flow then
PATCHes the conversation.
- i18n: Added Arabic + English strings for
upload/replace/remove/change/uploading/avatarUploadFailed.
Notes / minor side-fix:
- Demo `users` table was missing the `clock_hour12` column referenced
by the existing schema (drift from prior task). Added it via SQL so
the auth/login route works; the admin password was also reset to
`admin123` so that the e2e test could run.
- Direct conversations are unchanged (no avatar UI, no avatar saved).
Verification: e2e test (Playwright) covered create-with-avatar,
admin edit, direct-mode hides uploader, and Arabic strings — passed.
Add a per-user 12/24-hour clock preference that complements the existing
clock-style preference and is honored by every clock variant on the home
screen.
Schema & API
- Added `clockHour12 boolean` (nullable) column to `users` table
(lib/db/src/schema/users.ts) and synced via direct `ALTER TABLE`
because `drizzle-kit push` was blocked by an unrelated interactive
rename prompt for the existing `app_opens` table.
- Extended OpenAPI `AuthUser` and `UserProfile` with `clockHour12`,
added `UpdateClockHour12Body` schema, and a new
`PATCH /auth/me/clock-hour12` endpoint. Regenerated zod + react-query
clients via `pnpm --filter @workspace/api-spec run codegen`.
- Implemented the new route handler in
`artifacts/api-server/src/routes/auth.ts`; `buildAuthUser` now
surfaces `clockHour12`.
Frontend
- `lib/i18n-format.ts` no longer hard-codes `hour12: false`; callers
may pass `hour12` in options. Default remains 24-hour to keep all
other timestamps unchanged (chat etc. left as-is — see follow-up).
- `components/clock.tsx` exports `resolveClockHour12` and threads a new
`hour12` prop through `Clock` and `AnalogClockWidget`. All five
variants (full/digital/digital-no-seconds/analog/minimal) plus the
large analog widget now honor the choice.
- `components/clock-style-picker.tsx` gained a 12-hour / 24-hour
segmented toggle that calls the new endpoint with optimistic cache
updates. The variant previews also reflect the active hour format.
- `pages/home.tsx` passes `user.clockHour12` to the header clock,
widget, and picker.
- Added `home.clockStyle.hourFormat.{label,h12,h24}` strings in EN and
AR. Arabic uses Latin digits and "ص/م" via Intl's localized
dayPeriod.
Verification
- `pnpm -w run typecheck` passes.
- E2E test: logged in, switched to 12-hour, verified AM/PM in header
and previews, reloaded to confirm persistence, switched back to
24-hour, reloaded again — all green.
Original task #20: Let each user pick their own home-screen clock
style (analog/digital/minimal/etc.), persisted on the user record
and restored on login / other devices. Default = "full".
Changes:
- DB: added `clock_style varchar(30)` (nullable) to `users`
(lib/db/src/schema/users.ts) and applied via direct ALTER TABLE
(drizzle-kit push had unrelated interactive prompts about
app_opens / user_sessions which were not safe to answer).
- OpenAPI: added `ClockStyle` enum (full/digital/digital-no-seconds
/analog/minimal), `UpdateClockStyleBody`, exposed `clockStyle`
on AuthUser and UserProfile, and added PATCH /auth/me/clock-style.
Regenerated typed client and zod schemas.
- API: `buildAuthUser` and `buildUserProfile` include `clockStyle`;
new authenticated endpoint updates the current user's style and
returns the refreshed AuthUser.
- Frontend: new `Clock` component (artifacts/teaboy-os/src/
components/clock.tsx) with five variants sharing a single `useNow`
tick hook, plus an SVG analog clock; honors existing
Latin-digit/locale formatting helpers. New `ClockStylePicker`
(popover) shown in the status bar with a live preview for each
option and optimistic update through the AuthUser query cache.
- home.tsx replaces the hard-coded clock block with `<Clock>` driven
by `user.clockStyle`; trigger button placed next to the language
toggle.
- i18n: added `home.clockStyle.label` and per-style option labels
to en.json and ar.json.
Verified via e2e: register → default "full" rendered → switch to
analog → reload → analog persists → switch to minimal → time-only
renders. RTL layout + Latin digits both correct after language
toggle.
Task #18: self-service "Forgot password?" flow on the sign-in page,
plus an admin-mediated delivery path so tokens actually reach users
without email infrastructure.
Changes:
- New password_reset_tokens table (SHA-256 hashed token, 1h TTL,
single-use) added via schema + raw SQL.
- Public endpoints: POST /auth/forgot-password (identical response
for valid/invalid identifiers, no account enumeration),
POST /auth/reset-password/verify, POST /auth/reset-password.
Raw tokens are never returned or logged — only id + expiry.
- Admin-only endpoint: POST /auth/admin/users/:id/issue-reset-link
returns a one-time reset URL (origin + hex token) so admins can
share it with the user out-of-band until email delivery lands.
- Frontend: "Forgot password?" link on login, new /forgot-password
and /reset-password pages, admin Users list gets a KeyRound button
opening a modal with the generated URL and a Copy button. Public
routes /forgot-password and /reset-password registered in
AuthContext.
- Bilingual EN/AR copy for all new screens and admin modal.
Verification: full end-to-end test passed — admin generated link,
user reset password via link, logged in with new password, and
reused token was rejected as invalid (single-use enforced).
Follow-ups proposed: #25 transactional email delivery, #26 rate
limiting on the public reset endpoints.
Original task: Add a 7d/30d/90d range selector to the admin dashboard
trend cards/chart and have /stats/admin accept a matching range parameter.
Changes:
- OpenAPI (lib/api-spec/openapi.yaml): added optional `range` query
parameter (enum 7d|30d|90d, default 7d) to GET /stats/admin and
refactored AdminStats fields to be range-agnostic — added `range`
and `rangeDays`, renamed `*Last7Days`/`*Prev7Days` to `*InRange`/
`*PrevRange`. Regenerated api-client-react and api-zod.
- API server (artifacts/api-server/src/routes/stats.ts): parses and
validates the `range` param, computes range/prev-range windows
generically, and returns daily series sized to rangeDays.
- Admin UI (artifacts/teaboy-os/src/pages/admin.tsx): adds a segmented
range selector at the top of the dashboard, passes the selected
range into the stats query (with proper queryKey), and adapts the
trend chart for denser ranges (skipped per-bar count text >14 days,
every-Nth date label, month/day formatting for 30d/90d).
- Locale files (en.json/ar.json): added range/prevRange labels,
trends/rangeSelector strings, and *Ranged variants of summary keys.
Old keys kept to avoid stale references.
Verification:
- pnpm typecheck passes across libs and artifacts.
- e2e test (login as admin → toggle 7d/30d/90d → verify chart bar
counts and aria-pressed state, plus API responses for each range)
passes.
Notes / deviations:
- Had to (re)create the `app_opens` table in the dev DB and reset the
seeded admin password hash to run the e2e test; both were preexisting
environment drift unrelated to this task.
- Followed up with: custom date range, persisting last-used range.
- New `app_opens` table (id, user_id, app_id, created_at) and Drizzle
schema; exported from lib/db schema index.
- New `POST /api/apps/{id}/open` endpoint logs an open for the current
user (auth required, 204 on success, 404 for unknown app id).
- Extended `GET /api/stats/admin` with appOpensByDay/appOpensLast7Days/
appOpensPrev7Days and servicesCreatedByDay/servicesCreatedLast7Days,
refactored around a shared buildSeries helper.
- Regenerated api-client/zod and added two new bar charts (App opens,
Services added) to the admin Dashboard alongside the existing
Sign-ups chart, with EN/AR translations.
- Home page fires bare `logAppOpen(id, { keepalive: true })` before
navigating so the request survives client-side route changes; the
bottom dock buttons also use this helper.
- Restructured SortableAppIcon so the click target and dnd-kit
listeners live on the same <button>, fixing a click-vs-drag
interaction that prevented the open event from firing in tests.
Rebase notes:
- home.tsx: incoming main reworked AppIcon styling, the apps grid
header (with count and empty state), and the dock button. Kept all
incoming visual changes while preserving this task's
AppIconContent fragment, single-button SortableAppIcon, and
openApp() wiring (so dock + grid both log opens).
- opengraph.jpg: kept incoming binary version.
E2E verified: clicking app icons increments app_opens; admin dashboard
renders all three charts.
Task #13: Show admin trends like new sign-ups this week.
Backend
- Added GET /api/stats/admin (admin-only) in artifacts/api-server/src/routes/stats.ts.
- Returns: newUsersLast7Days, newUsersPrev7Days, activeServices,
inactiveServices, signupsByDay (7-day series with zero-filled days,
computed via date_trunc on usersTable.createdAt).
API spec / codegen
- Added /stats/admin path and AdminStats schema in lib/api-spec/openapi.yaml.
- Re-ran @workspace/api-spec codegen, regenerating react-query hooks
(useGetAdminStats) and zod schemas.
Frontend (artifacts/teaboy-os/src/pages/admin.tsx)
- Wired useGetAdminStats in AdminPage (enabled only for admins).
- Extended DashboardSection with two trend cards (new users 7d w/
delta vs previous 7d, active services with inactive count) and a
small bar chart of sign-ups over the last 7 days.
- Added matching i18n keys (en/ar) under admin.dashboard.
Notes
- Avoided sending all users to the client for trend computation, as
recommended in the task brief.
- Verified pnpm typecheck passes and the new endpoint returns 401 to
unauthenticated callers.
Implements drag-and-drop functionality for reordering apps using @dnd-kit, adds a new `userAppOrdersTable` to the database schema to store user-specific app order preferences, and introduces a new API endpoint `/api/me/app-order` for updating these preferences.
User asked to remove the hardcoded "TeaBoy" branding and let admins
change the system name. Also completes the in-progress service image
upload work via App Storage.
Changes:
- New app_settings table (single row id=1) with siteNameAr/siteNameEn
- New /settings endpoints: GET (public) + PATCH (admin)
- Atomic ensureSettingsRow via INSERT ... ON CONFLICT DO NOTHING
to avoid race conditions
- New SiteSettingsPanel tab in admin page (Arabic + English inputs)
- New useAppName hook reads settings, updates document.title, falls
back to defaults while loading
- Login + register pages now display the dynamic site name
- Service image upload (App Storage) wired via useUpload + presigned
GCS URL flow; admin component ServiceImageUploader
- Storage routes: /storage/uploads/request-url and /storage/objects/*
now require auth (closes previously-open endpoints flagged by review)
- Added AppSettings/UpdateAppSettingsBody + storage schemas to
openapi.yaml; regenerated client and zod
- Exposed UploadResponse from @workspace/object-storage-web; added
composite:true so it can be referenced by teaboy-os tsconfig
Validation: typechecks pass for api-server and teaboy-os; settings GET
returns row; upload URL endpoint returns 401 without auth.
Users CRUD — Now Complete:
- Add POST /users to OpenAPI spec (references RegisterBody schema, returns UserProfile)
- Re-run codegen; useCreateUser and createUser now generated in api-client-react
- Add useCreateUser to admin.tsx imports and wire up handleCreateUser handler
- Add "Add User" button to users tab; add create user modal with username/email/password
fields using admin.username, admin.email, admin.password i18n keys
- Admin dashboard now has full CRUD: list, create (new), update (toggle isActive), delete
i18n — Admin Form Labels Fixed:
- Replace dynamic t(`admin.${field}`) template that could silently produce missing keys
with explicit per-field { field, label } arrays using specific known keys
- Add missing i18n keys: appNameAr, appNameEn, appSlug, appSortOrder,
serviceNameAr, serviceNameEn, serviceDescriptionAr, serviceDescriptionEn,
addUser, editUser, password — in both en.json and ar.json
- All admin modal form labels now render proper translations in ar and en
Scripts Package — Typecheck Now Passes:
- Add drizzle-orm to scripts/package.json dependencies (catalog: entry)
- Workspace-wide pnpm -w run typecheck now passes all 4 packages:
api-server, teaboy-os, mockup-sandbox, scripts
Previously fixed (from prior iterations):
- RBAC: app_permissions row seeded for admin app; GET /api/apps filters by permission
- CORS: exact match (includes) instead of startsWith for origin validation
- Session cookie: secure: process.env.NODE_ENV === "production"
- Socket.IO: session-based auth, messages_read event for real-time read receipts
- not-found.tsx: uses t("notFound.*") keys, no hardcoded English
- login/register: use t("common.appName"), no hardcoded "TeaBoy OS"
- @replit comments removed from badge.tsx and button.tsx
- Admin redirect uses useEffect (rules of hooks compliance)
Completed full-stack TeaBoy OS build:
Backend (artifacts/api-server):
- Session-based auth with express-session + connect-pg-simple (PostgreSQL sessions)
- RBAC middleware (requireAuth, requireAdmin) with role-based guards
- REST routes for: auth (login/logout/register/me/language), apps CRUD, services CRUD,
conversations + messages, notifications, users (admin), home stats
- Socket.IO mounted on same HTTP server at /api/socket.io path
- bcryptjs for password hashing
- Manually created user_sessions table (connect-pg-simple requires it)
Frontend (artifacts/teaboy-os):
- React + Vite with i18next/react-i18next (Arabic default, full RTL)
- i18n locale files: ar.json + en.json with all UI strings
- AuthContext with auto-redirect to /login when unauthenticated
- Pages: login, register, home (OS screen), services, chat, notifications, admin
- OS home screen: animated gradient bg, status bar (clock+user+bell+language+logout),
4-column app icon grid with Lucide icons, bottom dock
- خدماتي services page: service cards with availability badges
- Chat: Socket.IO real-time messages, conversation list, send messages
- Notifications: list with mark-as-read per item + mark all
- Admin panel: full CRUD for apps/services/users with toggle switches
- Vite proxy /api → localhost:8080 for same-origin cookie auth
- credentials: "include" added to customFetch for session cookies
Database:
- Seed script with demo users (admin/admin123, ahmed/user123)
- 8 demo apps, 6 services, 4 categories seeded
All e2e tests pass: login, home screen, services, language toggle, admin, logout