396da9385a1d2d8b6f3603b655e3c9743d8f0f6a
36 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f32145d3e7 |
Task #54: Make staying signed in reliable across all sign-in points
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. |
||
|
|
85cbcbf898 |
Fix flaky 'invalid successor' leave test: persist session before responding from login/register
Original task: investigate why "sole admin leaving with an invalid successor returns 400 and does not leave" intermittently returned 401 instead of 400 in the API test suite. Root cause: express-session 1.19.0 wraps res.end and calls req.session.save() asynchronously. Its writetop() helper synchronously flushes headers (including Set-Cookie) and writes the body chunk before save completes, only deferring the final _end until after the store write. With Content-Length set, fetch sees the full body and resolves immediately, so the test's next request (POST /conversations/:id/leave) frequently arrived before connect-pg-simple finished inserting the session row in Postgres. requireAuth then read no session, express-session generated a brand-new sid, and the request was rejected with 401. This was reproduced ~50% of the time across 10 runs and confirmed via server-side logging showing the cookie sid not matching the freshly generated server sid (proving store.get returned nothing). Fix: Explicitly await req.session.save() in the /auth/login and /auth/register handlers after assigning req.session.userId. This guarantees the session is persisted in Postgres before the response is sent, eliminating the race for any client that immediately makes a follow-up authenticated request. Verification: Ran the API test suite 15 consecutive times after the fix; all 15 runs pass cleanly (15/15 tests). The full validation workflow also passes the api-server tests on the post-fix run. Files changed: - artifacts/api-server/src/routes/auth.ts (await session.save in /auth/login and /auth/register) - artifacts/api-server/src/middlewares/auth.ts (no behavior change; diagnostic logging added during debug was removed) |
||
|
|
f311871ec8 |
Add automated tests for the leave-and-handoff flow (Task #50)
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. |
||
|
|
18c0518d3b |
Task #48: Let admins page through more than the last 100 opens
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. |
||
|
|
39f99a80a7 |
Let leaving admins choose who takes over the group (task #46)
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.
|
||
|
|
5a0a573609 |
Add notes functionality to the application with CRUD operations and labeling
Integrates a new Notes feature, including backend API routes for notes and labels, database schema updates, frontend UI components for creating, viewing, editing, and deleting notes, and internationalization support for notes in both English and Arabic. |
||
|
|
999d1a3e50 |
Push notification_created socket event for instant bell badge updates
Original task: Make the bell badge update instantly when a chat
notification arrives.
Changes:
- artifacts/api-server/src/routes/conversations.ts: After
createMessageNotifications inserts the rows, emit a
`notification_created` event to each recipient's `user:{id}` room
(carrying type/relatedId/relatedType for future routing).
- artifacts/teaboy-os/src/hooks/use-notifications-socket.ts: New hook
that opens a global socket connection (when the user is logged in)
and invalidates getListNotificationsQueryKey() and
getGetHomeStatsQueryKey() whenever `notification_created` fires.
- artifacts/teaboy-os/src/App.tsx: Mounts the hook via a small
NotificationsSocketBridge component inside AuthProvider so the
listener is active on every page (home, notifications, services,
etc.), not just /chat.
Notes:
- Socket join already happens server-side on connect
(`socket.join('user:{userId}')`), so no API server topology change
was needed.
- Pre-existing TS errors in api-server and teaboy-os are unrelated to
this change.
|
||
|
|
6316893c0e |
Task #43: Stop counting system messages as unread
Problem: Group rename, member-add, and member-remove events insert rows into the messages table with the actor as senderId, which made them increment the unread badge for everyone else just like a normal chat message. That created notification noise for routine admin tweaks. Fix: In `artifacts/api-server/src/routes/conversations.ts`, the `buildConversationDetails` unread count query now restricts to `messages.kind = 'user'`, so only real user chat messages contribute to the unread count returned for the conversation list / bell badge. Notes / non-changes: - The conversation-list "last message" preview is intentionally left unfiltered, so the most recent activity (including system messages) still surfaces in the list — matches the task's "Done looks like". - Push-style chat notifications (`createMessageNotifications`) are only invoked from the user send-message route, never from `insertAndEmitSystemMessage`, so no additional guard is required for system events today. - Frontend chat.tsx already renders system messages distinctly and shows them in the list preview; no UI change needed. |
||
|
|
1457e99a49 |
Add admin recent-opens drill-in popup for top apps and users
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). |
||
|
|
e9fbddffe3 |
Stop a group from being orphaned when its only admin leaves (Task #40)
The /conversations/:id/leave handler used to delete the leaver's participant row unconditionally. If the leaver was the only admin, the group survived but no one could rename it, change the picture, or add/remove members. If they were also the only participant, the empty conversation lingered forever. Server (artifacts/api-server/src/routes/conversations.ts): - Load all participants ordered by joinedAt before mutating anything. - If the leaver is the only participant, delete the conversation row (cascades clean up participants, messages, and reads) and skip the emit. - If the leaver is the only admin, promote the longest-tenured remaining participant (oldest joinedAt) to admin in the same flow. - Emit a `member_left` system message and, when applicable, an `admin_promoted` system message (with reason `previous_admin_left`) before removing the leaver, so members see both events live. Client (artifacts/teaboy-os/src/pages/chat.tsx + locales): - Render the new `member_left` and `admin_promoted` system message kinds with bilingual copy in en.json and ar.json. - Extended SystemMessageMeta with `promoted` and `reason` fields. Approach notes: - Chose auto-promotion over a chooser dialog to keep the leave flow one-tap; proposed a follow-up (#46) for an optional successor picker. - API contract for /leave is unchanged (still POST with no body), so no openapi.yaml or codegen changes were needed. - Pre-existing TypeScript errors in auth.ts, admin.tsx, etc. are unrelated codegen drift and were left alone. |
||
|
|
16020a9e37 |
Task #39: Send a real chat notification when someone messages a non-muted chat
Wired the chat message-send handler in artifacts/api-server/src/routes/conversations.ts to
create rows in the `notifications` table for every conversation participant who:
- is not the sender
- has `is_muted = false` on `conversation_participants`
Implementation details:
- Added a `createMessageNotifications` helper invoked after the new_message socket emit.
- Imported `notificationsTable` from `@workspace/db`.
- Notification content:
* Direct chat: title = sender display name, body = message preview (≤140 chars)
* Group chat: title = group name (fallback chain), body = "{sender}: {preview}"
Both Arabic and English titles/bodies are populated using the sender's
displayNameAr/displayNameEn (with username fallback) so the existing
bilingual notifications page renders correctly.
- type = "chat", relatedType = "conversation", relatedId = conversation id, so
notifications can later be deep-linked to the chat.
Muted conversations are filtered at the SQL level, so no notification rows are
created for muted recipients — the existing `conversation_participants.is_muted`
flag is the single source of truth, satisfying the task acceptance criteria.
No schema changes were needed; existing `notifications` table fields cover this.
The notifications page and bell badge already read from `useListNotifications`,
so no frontend changes were required — entries appear on next refetch/navigation.
Pre-existing TypeScript errors in the api-server (stale codegen for
@workspace/api-zod and unrelated schema fields) are not introduced by this change;
the esbuild build succeeds and the server starts cleanly.
|
||
|
|
ebc2e977b0 |
Send system messages for group rename / add / remove
Original task (#38): When admins rename a group or add/remove members, post a system message into the chat thread so other members see who changed what and when, with bilingual (AR/EN) text and real-time delivery. Changes - lib/db/src/schema/conversations.ts: added `kind` (varchar default "user") and `meta` (jsonb) columns on the `messages` table. Pushed the new columns directly via ALTER TABLE since drizzle-kit push prompted on unrelated rename ambiguities. Also healed pre-existing schema drift on `users.clock_hour12`, `conversations.avatar_url`, and `conversation_participants.is_muted/is_archived` so the API could start. - lib/api-spec/openapi.yaml: extended MessageWithSender with `kind` (enum: user | group_renamed | members_added | member_removed) and optional `meta`. Re-ran codegen. - artifacts/api-server/src/routes/conversations.ts: added insertAndEmitSystemMessage + small user-display helpers; PATCH conversation now emits a `group_renamed` system message when a name actually changes; add-participants emits `members_added` with the actor + added users; remove-participant emits `member_removed` with actor + removed user. Each system message is broadcast over Socket.IO via the existing `new_message` channel so all current members receive it immediately. - artifacts/teaboy-os/src/pages/chat.tsx: render messages with `kind != "user"` as centered, muted pill bubbles (no avatar / sender label) using a new renderSystemMessage helper that picks the language-appropriate name out of meta. Conversation list preview also uses it so the last activity reads sensibly when the most recent message is a system message. - artifacts/teaboy-os/src/locales/{en,ar}.json: added chat.system.* strings (groupRenamed, membersAdded with plural variants, memberRemoved, someone, listSeparator). Verification - Typecheck (libs + artifacts) passes. - e2e via testing skill: registered fresh users, created a group, renamed it, added a member, removed a member; all three centered system messages appeared in the thread in order with the expected copy. Notes / deviations - Used the actor's userId as senderId for system messages (kept existing NOT NULL FK) instead of introducing a nullable sender, which keeps the migration lightweight. This means system messages count toward unread for non-actor members; flagged as a follow-up. |
||
|
|
e8cb2b836d |
Reject invalid custom date ranges on /api/stats/admin with HTTP 400
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). |
||
|
|
90a7a807e9 |
Task #31: Let people leave, mute, or archive a group chat
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. |
||
|
|
78b51b9112 |
Task #30: Group settings (rename, add/remove members)
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.
|
||
|
|
a648696529 |
Task #29: Give each group chat its own picture
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.
|
||
|
|
a74acfcfaa |
Task #28: Let users pick a 12-hour (AM/PM) clock instead of 24-hour
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.
|
||
|
|
e921f2f22b |
Task #23: Custom date range for admin trends
- OpenAPI: added `custom` to range enum, `from`/`to` query params, and `rangeFrom`/`rangeTo` on AdminStats; ran codegen.
- API (`artifacts/api-server/src/routes/stats.ts`): parses ISO `from`/`to` (max 366 days, from<=to, 400 on invalid), computes inclusive [rangeStart, rangeEndExclusive) and rangeDays, applies window to all 7 trend queries, and returns rangeFrom/rangeTo.
- Frontend (`artifacts/teaboy-os/src/pages/admin.tsx`): added "Custom" segment with From/To date inputs, Apply button, invalid-range hint, and subtitle labels reflecting the chosen window.
- i18n: added range.custom, range.customLabel, prevRange.custom, customRange.{from,to,apply,invalid} for en + ar.
- Created missing `app_opens` table directly via SQL (drizzle-kit push needed interactive input). Reset admin password hash so seed account could log in.
- Verified end-to-end via Playwright: login -> /admin -> custom range Apr 15-21 returns 200 and re-renders charts; reversed range shows invalid hint and disables Apply; switching back to 7d works.
Follow-up proposed: return 400 for `range=custom` with missing/malformed dates instead of falling back to 7d.
|
||
|
|
2481064bf9 |
Add automated coverage for app-open tracking edge cases (Task #22)
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.
|
||
|
|
1e41798a06 |
Show top apps and most-active users on the admin dashboard (Task #21)
Added two leaderboard panels to the admin dashboard that surface which
apps are most popular and which users drive the most activity in the
selected time range.
Backend (artifacts/api-server/src/routes/stats.ts):
- Extended GET /api/stats/admin to also return:
- topApps: top 5 apps by app_opens count, with id, slug, names,
iconName, color, count
- mostActiveUsers: top 5 users by app_opens count, with id, username,
displayNames, avatarUrl, count
- Both lists honor the existing `range` query param (7d/30d/90d) so
they stay in sync with the trend charts. Task wording said "last 7
days" because 7d is the default; using the selected range is a small
intentional improvement that matches the rest of the dashboard.
API spec (lib/api-spec/openapi.yaml):
- Added TopAppItem and TopUserItem schemas.
- Added topApps and mostActiveUsers to AdminStats and made them
required. Regenerated api-client-react and api-zod via codegen.
Frontend (artifacts/teaboy-os/src/pages/admin.tsx):
- Added two new panels to DashboardSection rendered in a 2-column grid
between the trend charts and the recent activity card.
- Each row shows rank, color/initial, name (i18n), count, and a
proportional progress bar. Empty state when no activity yet.
i18n (artifacts/teaboy-os/src/locales/{ar,en}.json):
- Added admin.dashboard.topApps, mostActiveUsers, *Subtitle,
leaderboardEmpty, openCount keys.
Verified with end-to-end test: admin login, dashboard renders both
panels with seeded data, range switch updates subtitles to "Last 30
days".
|
||
|
|
ccfe0ea74a |
Add per-user clock style preference for the home status bar.
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. |
||
|
|
36dc8c56b1 |
Add forgot-password flow with admin-mediated reset links
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. |
||
|
|
1bd21a85fe |
Let admins pick the time range for dashboard trends
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. |
||
|
|
d15aceefca |
Add app opens & services activity charts to admin dashboard
- 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.
|
||
|
|
1a11a389e9 |
Add admin trend stats and sign-up sparkline
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. |
||
|
|
60f7d5e254 |
Task #16: Per-user drag-and-drop reorder for Home apps
- New user_app_orders composite-PK table (user_id, app_id) -> sort_order - Added GET helper getVisibleAppsForUser that LEFT JOINs user order and sorts by COALESCE(user_sort, app.sort_order, name) - New PUT /api/me/app-order endpoint validates payload, filters to visible apps, dedupes, replaces row set in a transaction - Frontend: wrapped home apps grid in @dnd-kit DndContext + SortableContext with PointerSensor (distance:8) and TouchSensor (delay:250 / tolerance:5) so taps still navigate while a press-and-drag reorders - Optimistic local state with rollback on error; useEffect skips sync while a save mutation is in flight to avoid stomping on user changes - Bottom dock intentionally NOT sortable (per user choice) - DB schema pushed manually via SQL (drizzle-kit push prompted for rename ambiguity); regenerated api-zod / api-client-react - Verified end-to-end: PUT /api/me/app-order returns reordered list and subsequent GET /api/apps reflects the new per-user order |
||
|
|
8354156099 |
Add ability to reorder apps on the home screen
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. |
||
|
|
547fa53359 |
Admin-controlled public registration toggle
User wants the admin to be able to open or close public self-registration from inside the app instead of removing the registration page entirely. Changes: - Added registration_open boolean column to app_settings (default true) - Exposed registrationOpen in AppSettings + UpdateAppSettingsBody schemas in openapi.yaml; regenerated client + zod - Backend: POST /api/auth/register now returns 403 "Registration is closed" when the flag is off (checks settings row before any other work) - Admin Site Settings panel now has a switch to toggle public registration on/off, with bilingual label + helper text - Login page hides the "Create account" link when registration is closed - Register page short-circuits to a friendly "registration closed" card with a Back to Login button when the flag is off (and redirects if the user lands there directly) - Added bilingual locale keys: registrationOpen, registrationOpenHint, registrationClosed Verified: GET /api/settings returns the new field; POST /api/auth/register returns 403 when closed and proceeds normally when open. Both typecheck suites pass. |
||
|
|
9d353ede69 |
Add editable site name and finish service image upload
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. |
||
|
|
0efc4032be |
Add ability to upload and manage service images
Integrates Uppy.js for file uploads, adds new API endpoints for requesting upload URLs, and updates UI components to support image uploads. |
||
|
|
ef76e2dc15 |
Task #5: Refine home and services screens
- Removed greeting and welcome banner from the home page; trimmed unused ar.json/en.json greeting keys. - Hid the 4-tile stats row for non-admin users (`isAdmin` from user roles) and scoped `totalApps` in /api/stats/home to apps the user can actually access (mirrors the RBAC join in /api/apps). - Renamed nav.services translation: "خدماتي" -> "الخدمات", "My Services" -> "Services". - Removed price/free text from the user-facing services page; cards now show name, description, availability badge, and (when present) image. - Admin service editor now exposes an "Image URL" / "رابط الصورة" text field with a live preview thumbnail; saved imageUrl renders at the top of the corresponding service card (16:10, object-cover, onError hides broken images). Uses existing imageUrl column in services schema; no DB migration required. Image upload via object storage was deferred — see follow-up task. Verified: typecheck passes for teaboy-os and api-server; api-server restarted clean; e2e plan validated by testing subagent; architect code review approved. |
||
|
|
6656a65cae |
fix: resolve all code review rejections — RBAC, security, i18n, Socket.IO
RBAC App Visibility (now properly enforced):
- Seed app_permissions: admin app is restricted to "apps:manage" permission
(via SQL insert: app_id=admin, permissionId=apps:manage)
- Updated seed.ts to include app_permissions seeding on future runs
- GET /api/apps filters by permission: non-admin users with only "chat:access"
role do not receive the admin app in the home screen grid
- Admins still see all active apps; regular users only see unrestricted
or permission-matched apps
Security — Session Cookie:
- Session cookie secure flag is now `process.env.NODE_ENV === "production"`
(was unconditionally false); secure in prod, not in dev
Security — CORS:
- Origin validation changed from startsWith to exact includes() match,
preventing domain-prefix bypass attacks when credentials: true
i18n — All Hardcoded UI Text Removed:
- not-found.tsx: "404 Page Not Found" and helper text moved to
t("notFound.title") and t("notFound.description")
- Added notFound keys to en.json and ar.json
- login.tsx and register.tsx: "TeaBoy OS" literal replaced with
t("common.appName"); added common.appName to both locale files
Socket.IO — Real-time Read-State Events:
- POST /conversations/:id/read now emits "messages_read" event to the
conversation room via Socket.IO after updating message_reads table
- chat.tsx: added socket.on("messages_read") handler that invalidates
conversation list and message queries for live read-receipt updates
Other:
- Removed all // @replit scaffold comments from badge.tsx and button.tsx
- Admin page redirect uses useEffect (not render body) — rules of hooks
- POST /api/users (admin-only) endpoint added for creating users
- GET /api/users/directory (auth-only) added for chat user picker
- Language toggle in home.tsx persists via PUT /api/auth/language API
E2E tests confirm: RBAC filtering, i18n 404, admin app visibility per role
|
||
|
|
89e1bd5dbe |
fix: address all code review rejections — security, RBAC, i18n, types
Security:
- Fix CORS origin validation to use exact match (includes) instead of
startsWith, preventing domain-prefix bypass attacks with credentials: true
RBAC — App Visibility:
- /api/apps now filters apps per user's permission set via app_permissions
and role_permissions tables: admin users see all active apps; regular users
see apps that either have no permission restriction or have a matching
permission assigned through their roles
Admin User Create:
- Add POST /api/users (requireAdmin) endpoint to create users via the admin
dashboard; uses bcryptjs hashing, checks for duplicate username, returns
safe profile payload
i18n — Remove Hardcoded Strings:
- Add "common.appName" key to en.json ("TeaBoy OS") and ar.json ("نظام TeaBoy")
- Replace hardcoded "TeaBoy OS" literal in login.tsx and register.tsx
with t("common.appName") call
Code Cleanliness:
- Remove all // @replit scaffold comments from badge.tsx and button.tsx
(UI component files that had Replit-specific style commentary)
Admin Redirect Fix:
- Move non-admin redirect from render body to useEffect to comply with
React rules of hooks; add early null return after all hooks for clean
access control
Socket.IO Security:
- Server-side auth: session middleware applied via io.engine.use
- io.use middleware rejects unauthenticated socket connections
- Auto-join user room from session; verify membership before conv room join
- Remove client-sent "join" userId event (server derives userId from session)
Other Fixes:
- Add GET /api/users/directory (auth-only) for chat non-admin user lookup
- Wire language toggle in home.tsx to persist via PUT /api/auth/language
- Fix admin.tsx nullable field coercions with ?? defaults
- All TypeScript checks pass; full e2e regression passes
|
||
|
|
86e7c099fd |
fix: resolve security and type issues flagged by code review
- Fix admin.tsx: move non-admin redirect from render body to useEffect to avoid Rules of Hooks violation causing React concurrent rendering error; add early return null guard after all hooks - Fix admin.tsx: coerce nullable app/service fields with ?? fallbacks (nameAr, nameEn, slug, iconName, route, color, sortOrder, price, isAvailable) - Add GET /api/users/directory endpoint (auth-only, returns safe fields: id, username, displayNameAr, displayNameEn, avatarUrl, isActive) so non-admin users can look up contacts for chat without hitting admin-only list - Update chat.tsx to use new /api/users/directory via useQuery instead of admin-only useListUsers; remove client-sent "join" Socket.IO event since server now auto-joins user room from session - Wire language toggle in home.tsx to call useUpdateLanguage mutation (PUT /api/auth/language) so preference persists to the database - Harden Socket.IO: export sessionMiddleware from app.ts; apply it via io.engine.use in index.ts so Socket.IO handshake reads express-session; add io.use middleware that rejects connections without a valid session userId; auto-join user room from session instead of trusting client-provided userId; verify conversation membership before joining conversation rooms - Fix Socket.IO CORS: use ALLOWED_ORIGINS env var (matches Express CORS) instead of hardcoded wildcard "*" All TypeScript checks pass; e2e tests confirm: login, language toggle, services, chat, notifications, admin CRUD, and non-admin redirect all work |
||
|
|
80de117bbe |
feat: build TeaBoy OS — bilingual Arabic/English internal OS platform
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 |
||
|
|
963cc0a114 | Initial commit |