fb4389cab96bf2fb2ac338b75d27228d055ce038
2247 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2fcaa27fbd |
Make admin recent-opens drill-in rows clickable
Task: #47 — Let admins jump from a recent open straight to that user. Changes (artifacts/teaboy-os/src/pages/admin.tsx): - AppOpensDrillIn: each user row in the per-app recent-opens popup is now a focusable button. Clicking jumps to that user's row in the Users list (closes the popup, then calls the existing handleSelectUserFromDashboard via a new onJumpUser prop), reusing the highlight behavior that already powers the leaderboard. - UserOpensDrillIn: each app row in the per-user recent-opens popup is now a focusable button. Clicking opens that app's edit modal (closes the popup, then calls the existing handleSelectAppFromDashboard via a new onJumpApp prop), mirroring the leaderboard click behavior. - Added type="button", hover/focus styles, and aria-labels reusing the existing admin.dashboard.viewUserDetails / admin.dashboard.viewAppDetails translation keys (already localized for both en and ar). The footer "Edit app" / "View user" buttons remain unchanged and keep working alongside the per-row clicks. No new translation keys, schema changes, or API changes were needed — AppOpenByAppEntry already exposes userId and AppOpenByUserEntry already exposes appId. Verification: tsc --noEmit on teaboy-os shows no new errors from these edits (only the same pre-existing errors that were present before this task). E2E was not run because the change is a thin wiring of existing handlers and admin login requires custom session setup beyond this task's scope. |
||
|
|
ce0819cfaa |
Make admin recent-opens drill-in rows clickable
Task: #47 — Let admins jump from a recent open straight to that user. Changes (artifacts/teaboy-os/src/pages/admin.tsx): - AppOpensDrillIn: each user row in the per-app recent-opens popup is now a focusable button. Clicking jumps to that user's row in the Users list (closes the popup, then calls the existing handleSelectUserFromDashboard via a new onJumpUser prop), reusing the highlight behavior that already powers the leaderboard. - UserOpensDrillIn: each app row in the per-user recent-opens popup is now a focusable button. Clicking opens that app's edit modal (closes the popup, then calls the existing handleSelectAppFromDashboard via a new onJumpApp prop), mirroring the leaderboard click behavior. - Added type="button", hover/focus styles, and aria-labels reusing the existing admin.dashboard.viewUserDetails / admin.dashboard.viewAppDetails translation keys (already localized for both en and ar). The footer "Edit app" / "View user" buttons remain unchanged and keep working alongside the per-row clicks. No new translation keys, schema changes, or API changes were needed — AppOpenByAppEntry already exposes userId and AppOpenByUserEntry already exposes appId. Verification: tsc --noEmit on teaboy-os shows no new errors from these edits (only the same pre-existing errors that were present before this task). E2E was not run because the change is a thin wiring of existing handlers and admin login requires custom session setup beyond this task's scope. Replit-Task-Id: 5db0964e-95fa-4199-9337-631cd1a790ec |
||
|
|
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.
|
||
|
|
0b7593bbc2 |
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.
Replit-Task-Id: c9065bc1-ab4e-4a3b-a865-81754f5c2e5a
|
||
|
|
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. |
||
|
|
a864b84567 |
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. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 4ab7f101-06a9-4dd7-94c8-617f1751327c Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/fYGOSe0 Replit-Helium-Checkpoint-Created: true |
||
|
|
75147c8f24 |
Task #45: Tapping a chat notification opens the matching conversation
- notifications.tsx: When a notification is clicked, if it has relatedType === "conversation" and a relatedId, navigate to /chat?c=<conversationId> in addition to marking it read. Other notification types continue to behave as before (just mark read). - chat.tsx: Read the optional `c` query parameter via wouter's useSearch hook. When present and valid, set it as the selectedConvId so the conversation opens automatically, then replace the URL back to /chat so refreshing or navigating away doesn't keep re-opening it. No API or schema changes required; notification records already carry relatedType/relatedId for chat notifications. |
||
|
|
8729cae01a |
Task #45: Tapping a chat notification opens the matching conversation
- notifications.tsx: When a notification is clicked, if it has relatedType === "conversation" and a relatedId, navigate to /chat?c=<conversationId> in addition to marking it read. Other notification types continue to behave as before (just mark read). - chat.tsx: Read the optional `c` query parameter via wouter's useSearch hook. When present and valid, set it as the selectedConvId so the conversation opens automatically, then replace the URL back to /chat so refreshing or navigating away doesn't keep re-opening it. No API or schema changes required; notification records already carry relatedType/relatedId for chat notifications. Replit-Task-Id: 2fda1864-99d6-4f07-a3d9-aa3021d5493f |
||
|
|
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.
|
||
|
|
1411486602 |
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.
Replit-Task-Id: ff3973f9-1c9d-4283-8091-bfeb3eb0d6a4
|
||
|
|
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. |
||
|
|
ce1fb84365 |
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. Replit-Task-Id: 3734d73b-2df7-4f32-9655-6c7a9e23536c |
||
|
|
e37f926581 |
Task #42: Tell members when someone leaves a group on their own
Original task: Post a bilingual system message into the group chat when a member leaves via POST /api/conversations/:id/leave, with real-time delivery via Socket.IO and rendering in the chat thread. Findings: - The leave handler in artifacts/api-server/src/routes/conversations.ts already inserts a `member_left` system message (and `admin_promoted` when a successor is auto-promoted) and emits via Socket.IO using insertAndEmitSystemMessage. Direct conversations and last-leaver cleanup paths are correctly skipped. - artifacts/teaboy-os/src/pages/chat.tsx renderSystemMessage already handles `member_left`, `admin_promoted`, and `adminPromotedAfterLeave`. - Bilingual locale keys exist in src/locales/en.json and ar.json under chat.system.memberLeft / adminPromoted / adminPromotedAfterLeave. Change made: - Extended the MessageWithSender.kind enum in lib/api-spec/openapi.yaml to include `member_left` and `admin_promoted` (previously only user/group_renamed/members_added/member_removed were declared). - Re-ran `pnpm --filter @workspace/api-spec run codegen` to regenerate api-zod and api-client-react schemas so the spec, runtime validation, and client types stay in sync. - Verified `pnpm -w run typecheck` passes across all packages. No behavioural code changes were necessary; the feature was already implemented end-to-end and only the API spec enum needed to catch up. |
||
|
|
62b79c7e7d |
Task #42: Tell members when someone leaves a group on their own
Original task: Post a bilingual system message into the group chat when a member leaves via POST /api/conversations/:id/leave, with real-time delivery via Socket.IO and rendering in the chat thread. Findings: - The leave handler in artifacts/api-server/src/routes/conversations.ts already inserts a `member_left` system message (and `admin_promoted` when a successor is auto-promoted) and emits via Socket.IO using insertAndEmitSystemMessage. Direct conversations and last-leaver cleanup paths are correctly skipped. - artifacts/teaboy-os/src/pages/chat.tsx renderSystemMessage already handles `member_left`, `admin_promoted`, and `adminPromotedAfterLeave`. - Bilingual locale keys exist in src/locales/en.json and ar.json under chat.system.memberLeft / adminPromoted / adminPromotedAfterLeave. Change made: - Extended the MessageWithSender.kind enum in lib/api-spec/openapi.yaml to include `member_left` and `admin_promoted` (previously only user/group_renamed/members_added/member_removed were declared). - Re-ran `pnpm --filter @workspace/api-spec run codegen` to regenerate api-zod and api-client-react schemas so the spec, runtime validation, and client types stay in sync. - Verified `pnpm -w run typecheck` passes across all packages. No behavioural code changes were necessary; the feature was already implemented end-to-end and only the API spec enum needed to catch up. Replit-Task-Id: 40fccb3a-4fc0-4ad7-a6e9-d1ac67ef7769 |
||
|
|
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). |
||
|
|
b6a0dc6ab9 |
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), Replit-Task-Id: f2fbe652-a788-4f25-9fb2-9ef2c535c8da #48 (paginate beyond 100 opens). |
||
|
|
732cb379ee |
Prevent database synchronization issues by ignoring specific tables
Configure Drizzle to exclude the `user_sessions` table from schema introspection. |
||
|
|
b910733adc |
Prevent database synchronization issues by ignoring specific tables
Configure Drizzle to exclude the `user_sessions` table from schema introspection. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 4e1b5887-b92a-4851-b4ff-a7278a110e95 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/YPEna1J Replit-Helium-Checkpoint-Created: true |
||
|
|
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. |
||
|
|
a52c1659fc |
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. Replit-Task-Id: 026e59a5-90c0-4faa-9080-aee0aee0a631 |
||
|
|
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.
|
||
|
|
5a0f6803b0 |
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.
Replit-Task-Id: 6b146a53-a575-4921-bd4f-3332247779df
|
||
|
|
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. |
||
|
|
db9cf7b315 |
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. Replit-Task-Id: 6dfa2b99-fbac-4146-b59b-8c04a14c9e96 |
||
|
|
00d07e02a8 |
Let group admins delete the group's picture
Task #37: Add a "Remove picture" action next to the camera icon in the group chat header so admins can clear a group's avatar back to the default Users icon, not just replace it. Implementation: - artifacts/teaboy-os/src/pages/chat.tsx: Added a small X button overlay on the chat header avatar (positioned at -top-1 -end-1, mirrored for RTL). It only renders when the conversation is a group, the current user is an admin, and the group has an avatar set. Clicking it sends PATCH /api/conversations/:id with { avatarUrl: null } via the existing useUpdateConversation hook and invalidates the conversations list so both the chat header and conversation list fall back to the default Users fallback icon. - Reused existing locale keys chat.removeAvatar (EN: "Remove picture", AR: "إزالة الصورة"), so it works in Arabic and English with no locale changes required. - Backend (artifacts/api-server/src/routes/conversations.ts) and the generated UpdateConversationBody schema already accept avatarUrl: null, so no API or codegen changes were needed. Notes / deviations: - Pre-existing TypeScript errors in chat.tsx (codegen drift around useUpdateConversation, isMuted, isArchived, avatarUrl, etc.) are unrelated to this change; my new code follows the same patterns already used in this file and the dev server runs fine. |
||
|
|
c1b53dea8d |
Let group admins delete the group's picture
Task #37: Add a "Remove picture" action next to the camera icon in the group chat header so admins can clear a group's avatar back to the default Users icon, not just replace it. Implementation: - artifacts/teaboy-os/src/pages/chat.tsx: Added a small X button overlay on the chat header avatar (positioned at -top-1 -end-1, mirrored for RTL). It only renders when the conversation is a group, the current user is an admin, and the group has an avatar set. Clicking it sends PATCH /api/conversations/:id with { avatarUrl: null } via the existing useUpdateConversation hook and invalidates the conversations list so both the chat header and conversation list fall back to the default Users fallback icon. - Reused existing locale keys chat.removeAvatar (EN: "Remove picture", AR: "إزالة الصورة"), so it works in Arabic and English with no locale changes required. - Backend (artifacts/api-server/src/routes/conversations.ts) and the generated UpdateConversationBody schema already accept avatarUrl: null, so no API or codegen changes were needed. Notes / deviations: - Pre-existing TypeScript errors in chat.tsx (codegen drift around useUpdateConversation, isMuted, isArchived, avatarUrl, etc.) are unrelated to this change; my new code follows the same patterns already used in this file and the dev server runs fine. Replit-Task-Id: 03db94ee-51ff-4695-8347-07fe7d962c5d |
||
|
|
4008bd4e17 |
Honor user's 12/24-hour clock preference for chat & notification timestamps
Original task: Chat message timestamps and notification list timestamps were always rendered in 24-hour format, ignoring the per-user clockHour12 preference that already controls the home-screen clock. Changes: - artifacts/teaboy-os/src/lib/i18n-format.ts: formatDateTime() now accepts an optional `hour12` boolean (defaults to false to preserve existing callers). formatTime() already passed hour12 through options. - artifacts/teaboy-os/src/pages/chat.tsx: Message bubble timestamp now passes `hour12: user?.clockHour12 ?? false` to formatTime(). The user is already pulled from useAuth() in this file. - artifacts/teaboy-os/src/pages/notifications.tsx: Imported useAuth, read user, and passed user?.clockHour12 ?? false to formatDateTime(). Locale handling: i18n-format already forces Latin digits via the `-u-nu-latn` locale extension and numberingSystem: "latn". With hour12 enabled, Intl.DateTimeFormat in the `ar` locale emits the localized ص/م markers automatically, satisfying the Arabic requirement. Verified: tsc --noEmit on the teaboy-os artifact reports no new errors introduced by these changes (pre-existing unrelated errors remain). No follow-up tasks proposed — task is self-contained and the project task list already covers adjacent chat/notification work. |
||
|
|
8e14846772 |
Honor user's 12/24-hour clock preference for chat & notification timestamps
Original task: Chat message timestamps and notification list timestamps were always rendered in 24-hour format, ignoring the per-user clockHour12 preference that already controls the home-screen clock. Changes: - artifacts/teaboy-os/src/lib/i18n-format.ts: formatDateTime() now accepts an optional `hour12` boolean (defaults to false to preserve existing callers). formatTime() already passed hour12 through options. - artifacts/teaboy-os/src/pages/chat.tsx: Message bubble timestamp now passes `hour12: user?.clockHour12 ?? false` to formatTime(). The user is already pulled from useAuth() in this file. - artifacts/teaboy-os/src/pages/notifications.tsx: Imported useAuth, read user, and passed user?.clockHour12 ?? false to formatDateTime(). Locale handling: i18n-format already forces Latin digits via the `-u-nu-latn` locale extension and numberingSystem: "latn". With hour12 enabled, Intl.DateTimeFormat in the `ar` locale emits the localized ص/م markers automatically, satisfying the Arabic requirement. Verified: tsc --noEmit on the teaboy-os artifact reports no new errors introduced by these changes (pre-existing unrelated errors remain). No follow-up tasks proposed — task is self-contained and the project task list already covers adjacent chat/notification work. Replit-Task-Id: 583b65bd-a229-46fb-92a6-bb0b095f4c93 |
||
|
|
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). |
||
|
|
36f2872dcd |
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). Replit-Task-Id: 965134ad-0d07-4cd2-a6ff-f60a50289d90 |
||
|
|
77843e3540 |
Show user avatars in the most-active-users leaderboard
Original task #33: The admin "Most active users" leaderboard rendered only a colored circle with the first letter of each user's name even though the API already returns each user's avatarUrl. Changes: - artifacts/teaboy-os/src/pages/admin.tsx - Added a small LeaderboardAvatar component that renders the avatar image when available and falls back to the initial when there is no avatarUrl or when the image fails to load (onError). - The avatar URL is resolved through the existing resolveServiceImageUrl helper (consistent with chat.tsx). - Kept the existing emerald circle styling as the fallback background; added overflow-hidden so the rounded image is clipped. - useEffect resets the error state when the src changes so a later valid URL still attempts to load. No API or schema changes were required (avatarUrl is already returned by GET /admin/stats and present in the generated client types). Pre-existing TypeScript errors in admin.tsx / chat.tsx / clock files are unrelated to this task and were not touched. |
||
|
|
1e6713f531 |
Show user avatars in the most-active-users leaderboard
Original task #33: The admin "Most active users" leaderboard rendered only a colored circle with the first letter of each user's name even though the API already returns each user's avatarUrl. Changes: - artifacts/teaboy-os/src/pages/admin.tsx - Added a small LeaderboardAvatar component that renders the avatar image when available and falls back to the initial when there is no avatarUrl or when the image fails to load (onError). - The avatar URL is resolved through the existing resolveServiceImageUrl helper (consistent with chat.tsx). - Kept the existing emerald circle styling as the fallback background; added overflow-hidden so the rounded image is clipped. - useEffect resets the error state when the src changes so a later valid URL still attempts to load. No API or schema changes were required (avatarUrl is already returned by GET /admin/stats and present in the generated client types). Pre-existing TypeScript errors in admin.tsx / chat.tsx / clock files are unrelated to this task and were not touched. Replit-Task-Id: a0fe21b3-7422-48ba-9731-ed8315eb9d68 |
||
|
|
a60ea914f4 |
Task #32: Let admins click a top app or active user to see details
Made the leaderboard rows on the admin dashboard interactive so admins can
drill in directly to investigate.
Changes:
- artifacts/teaboy-os/src/pages/admin.tsx
- DashboardSection: each "Top apps" and "Most active users" row is now a
real <button type="button"> with a descriptive aria-label
("View details for ..."), focus-visible ring, and hover styles.
- New onSelectApp / onSelectUser props passed from AdminPage:
* Top apps row -> switches to the Apps section AND opens the existing
Edit App modal prefilled with that app's data (uses the apps list
already loaded via useListApps to look up the full record).
* Most active users row -> switches to the Users section, scrolls the
matching user row into view, and applies a temporary primary-colored
ring highlight that fades after ~2.5s.
- Added userRowRefs (Map of id -> div) and a highlightedUserId state +
effect to drive the scroll/highlight behavior.
- artifacts/teaboy-os/src/locales/{en,ar}.json
- Added admin.dashboard.viewAppDetails / viewUserDetails strings used as
aria-labels.
Verification: e2e tested via the testing skill — login as admin, click a
top app row (modal opens), Cancel, click a most-active-users row (users
section opens with highlighted row), and Tab+Enter on a top apps row also
opens the modal (keyboard accessibility confirmed).
Notes / deviations: pre-existing TypeScript errors in the project's API
client types (topApps/mostActiveUsers/rangeFrom not yet on AdminStats,
and unrelated chat.tsx/home.tsx errors) are not introduced by this task
and were not addressed.
|
||
|
|
13040c8a93 |
Task #32: Let admins click a top app or active user to see details
Made the leaderboard rows on the admin dashboard interactive so admins can
drill in directly to investigate.
Changes:
- artifacts/teaboy-os/src/pages/admin.tsx
- DashboardSection: each "Top apps" and "Most active users" row is now a
real <button type="button"> with a descriptive aria-label
("View details for ..."), focus-visible ring, and hover styles.
- New onSelectApp / onSelectUser props passed from AdminPage:
* Top apps row -> switches to the Apps section AND opens the existing
Edit App modal prefilled with that app's data (uses the apps list
already loaded via useListApps to look up the full record).
* Most active users row -> switches to the Users section, scrolls the
matching user row into view, and applies a temporary primary-colored
ring highlight that fades after ~2.5s.
- Added userRowRefs (Map of id -> div) and a highlightedUserId state +
effect to drive the scroll/highlight behavior.
- artifacts/teaboy-os/src/locales/{en,ar}.json
- Added admin.dashboard.viewAppDetails / viewUserDetails strings used as
aria-labels.
Verification: e2e tested via the testing skill — login as admin, click a
top app row (modal opens), Cancel, click a most-active-users row (users
section opens with highlighted row), and Tab+Enter on a top apps row also
opens the modal (keyboard accessibility confirmed).
Notes / deviations: pre-existing TypeScript errors in the project's API
client types (topApps/mostActiveUsers/rangeFrom not yet on AdminStats,
and unrelated chat.tsx/home.tsx errors) are not introduced by this task
and were not addressed.
Replit-Task-Id: 95c90dfe-6761-4d04-8e71-f2aa14e590bb
|
||
|
|
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. |
||
|
|
7b77de107f |
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. Replit-Task-Id: faec58bb-12c6-4f6f-9ddb-f3f9f6c033f4 |
||
|
|
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.
|
||
|
|
f6a7bc294d |
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.
Replit-Task-Id: 9d1023cc-56de-45f2-9d73-4caafccc57b4
|
||
|
|
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.
|
||
|
|
55b39f7d6f |
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.
Replit-Task-Id: 34e8a2d2-621a-42a9-88ba-89652c6094dc
|
||
|
|
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.
|
||
|
|
bfdfffd8b5 |
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.
Replit-Task-Id: 03ea8cbe-ace7-4d36-afc5-49ebc9706c67
|
||
|
|
0fb4c80205 |
Persist admin dashboard time range per user
Task #24: Remember the admin's last-used dashboard time range. The admin dashboard's range selector (7d/30d/90d/custom) was resetting to "Last 7 days" on every visit. Now the selected preset range is persisted in localStorage under a per-user key (`admin.statsRange.<userId>`) so each admin sees their last choice on return. Implementation notes: - Edited only artifacts/teaboy-os/src/pages/admin.tsx. - Added two effects: one hydrates the stored value once `user.id` is available (auth loads asynchronously), the other writes to localStorage whenever the range changes after hydration. - Only the preset values (7d/30d/90d) are persisted; "custom" is intentionally not stored since the custom dates are session-scoped and would feel stale on return. After hydration, switching to "custom" leaves the previously stored preset untouched so the next visit still restores the last preset. - Used a hydration guard to avoid clobbering stored values with the default "7d" before the load effect runs. |
||
|
|
8d90771409 |
Persist admin dashboard time range per user
Task #24: Remember the admin's last-used dashboard time range. The admin dashboard's range selector (7d/30d/90d/custom) was resetting to "Last 7 days" on every visit. Now the selected preset range is persisted in localStorage under a per-user key (`admin.statsRange.<userId>`) so each admin sees their last choice on return. Implementation notes: - Edited only artifacts/teaboy-os/src/pages/admin.tsx. - Added two effects: one hydrates the stored value once `user.id` is available (auth loads asynchronously), the other writes to localStorage whenever the range changes after hydration. - Only the preset values (7d/30d/90d) are persisted; "custom" is intentionally not stored since the custom dates are session-scoped and would feel stale on return. After hydration, switching to "custom" leaves the previously stored preset untouched so the next visit still restores the last preset. - Used a hydration guard to avoid clobbering stored values with the default "7d" before the load effect runs. Replit-Task-Id: a29d7e30-8111-4edd-9307-3262c4dbc236 |
||
|
|
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.
|
||
|
|
1c82edf5a9 |
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.
Replit-Task-Id: a50d8a1e-60ad-43b2-b8ea-4eeae6ef5dd0
|
||
|
|
5dae7b3f74 |
Add a classic analog clock widget with customizable visibility
Adds a new `AnalogClockWidget` component with Roman numerals and a sweeping second hand, integrated into the home page. The widget's visibility can now be toggled via the `ClockStylePicker` and is persisted in `localStorage`. |
||
|
|
b6219e9bbe |
Add a classic analog clock widget with customizable visibility
Adds a new `AnalogClockWidget` component with Roman numerals and a sweeping second hand, integrated into the home page. The widget's visibility can now be toggled via the `ClockStylePicker` and is persisted in `localStorage`. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 0a1df300-b156-461b-90d5-e9874f25113f Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/YPEna1J Replit-Helium-Checkpoint-Created: true |
||
|
|
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.
|
||
|
|
72cd414208 |
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.
Replit-Task-Id: b7422abb-cc1b-4727-b70b-cde090f1a748
|