Commit Graph

81 Commits

Author SHA1 Message Date
riyadhafraa cdc32061cb Add a Playwright UI test for the leave-group successor picker
Original task: Add a UI test for the leave-group successor chooser dialog
in artifacts/teaboy-os/src/pages/chat.tsx, mirroring the backend coverage
in artifacts/api-server/tests/conversations-leave.test.mjs.

What I added:
- artifacts/teaboy-os/tests/leave-group-successor.spec.mjs
  A real Playwright e2e test that drives the chat UI in a browser:
  * Cancel path: opens the leave dialog as the sole admin, verifies the
    successor chooser is visible with the auto option pre-selected and
    one option per remaining member, clicks Cancel, and asserts via DB
    that the conversation membership and admin flags are unchanged.
  * Chosen-successor path: re-opens the dialog, picks the *later-joined*
    member (so the choice differs from the auto pick), confirms the
    leave, asserts the POST /conversations/:id/leave call returns 200,
    and verifies via DB that the leaver was removed and the explicitly
    chosen member became the new admin.
  Both tests seed their own users + group via SQL and clean up after
  themselves so they can run repeatedly against the live dev DB.

- artifacts/teaboy-os/playwright.config.mjs
  Minimal Playwright config: testDir=tests, *.spec.mjs match, headless,
  baseURL defaults to http://localhost:80 (the workspace proxy), and
  is overridable via TEST_WEB_BASE.

- artifacts/teaboy-os/package.json
  Added @playwright/test and pg as devDependencies and a `test:e2e`
  script: `playwright test --config playwright.config.mjs`.

- .gitignore
  Ignored Playwright's test-results/ and playwright-report/ output dirs.

How to run:
  pnpm --filter @workspace/teaboy-os exec playwright install chromium
  DATABASE_URL=... pnpm --filter @workspace/teaboy-os run test:e2e

Verification:
  Both tests pass locally (2 passed in ~9s) against the running dev
  workspace. Required system libraries for headless Chromium were
  installed via the workspace's system-deps mechanism (glib, nss, nspr,
  atk, cups, dbus, libdrm, libxkbcommon, libgbm, alsa-lib, pango, cairo,
  and the relevant xorg libs), so `playwright test` works out of the
  box on this environment.

No application source files were modified.

Replit-Task-Id: d2f21eab-498e-4cc0-a913-6035b714b3da
2026-04-21 12:17:20 +00:00
riyadhafraa 1055866105 Git commit prior to merge 2026-04-21 12:16:59 +00:00
riyadhafraa 8fb6d54d78 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.

Replit-Task-Id: e31c169d-a4f5-4387-a642-b39a422c1408
2026-04-21 12:01:41 +00:00
riyadhafraa 42c2910d4b Show new admin clearly after sole admin leaves group chat
Task #49: When the sole admin leaves a group and a successor is auto- or
manually-promoted, surface the change prominently in addition to the existing
system messages.

Changes:
- artifacts/teaboy-os/src/pages/chat.tsx
  - New effect on conversations list: tracks the set of group conversations
    where the current user is currently an admin (from participant.isAdmin).
    On first load it silently records a baseline in localStorage
    (key teaboy:admin-known:<userId>); thereafter, any newly-admin group
    triggers a toast ("You are now the admin of <group>"). The set is pruned
    when the user is no longer admin so a future re-promotion toasts again.
    Using participant.isAdmin (rather than only lastMessage) ensures the cue
    fires even if newer messages have arrived in the conversation since the
    promotion.
  - Conversation list rows now render a small amber "New admin" / "You are now
    the admin" Crown badge whenever the most recent message is an
    admin_promoted system message. Variant differs for the promoted user vs
    other members. data-testid="badge-new-admin-<convId>" for tests.
  - Imported Crown from lucide-react.
- artifacts/teaboy-os/src/locales/en.json + ar.json
  - Added bilingual strings under chat.actions:
    youAreAdminTitle, youAreAdminDescription, newAdminBadge, newAdminLabel.

Implementation notes:
- lastMessage.meta is already returned by GET /conversations and includes the
  promoted user (id, displayNameAr/En, username), so no API changes were needed.
- Regenerated api-client-react via `pnpm --filter @workspace/api-spec run
  codegen` (pre-existing stale codegen was failing typecheck unrelated to this
  task, now clean for chat.tsx).
- Did not add automated tests; the existing follow-up task "Make sure the
  leave-and-handoff flow stays working with automated tests" already covers it.

Replit-Task-Id: 71495f44-8c6c-4f7f-8f48-6400630d1203
2026-04-21 11:53:59 +00:00
riyadhafraa 216ff65e04 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.

Replit-Task-Id: b6382efe-765f-4689-8c93-196fee253f63
2026-04-21 11:48:40 +00:00
riyadhafraa 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
2026-04-21 11:39:34 +00:00
riyadhafraa 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
2026-04-21 11:29:39 +00:00
riyadhafraa 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
2026-04-21 11:29:05 +00:00
riyadhafraa 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
2026-04-21 11:07:55 +00:00
riyadhafraa 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
2026-04-21 10:41:01 +00:00
riyadhafraa 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
2026-04-21 10:35:54 +00:00
riyadhafraa 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
2026-04-21 10:20:22 +00:00
riyadhafraa 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).
2026-04-21 10:17:41 +00:00
riyadhafraa 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
2026-04-21 09:53:06 +00:00
riyadhafraa 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
2026-04-21 09:44:00 +00:00
riyadhafraa 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
2026-04-21 09:38:44 +00:00
riyadhafraa 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
2026-04-21 09:35:32 +00:00
riyadhafraa 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
2026-04-21 09:09:18 +00:00
riyadhafraa 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
2026-04-21 09:06:19 +00:00
riyadhafraa 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
2026-04-21 09:01:03 +00:00
riyadhafraa 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
2026-04-21 08:54:14 +00:00
riyadhafraa 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
2026-04-21 08:50:25 +00:00
riyadhafraa 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
2026-04-21 08:29:01 +00:00
riyadhafraa 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
2026-04-21 08:07:15 +00:00
riyadhafraa 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
2026-04-21 07:46:16 +00:00
riyadhafraa 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
2026-04-21 07:16:50 +00:00
riyadhafraa 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
2026-04-21 07:04:30 +00:00
riyadhafraa 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
2026-04-21 07:01:43 +00:00
riyadhafraa 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
2026-04-21 06:57:47 +00:00
riyadhafraa 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
2026-04-21 06:38:55 +00:00
riyadhafraa ba199cac42 Add a comprehensive note-taking system similar to Google Keep
Adds three new tables (notes, note_items, note_shares) for storing note data, checklist items, and sharing permissions. Includes API endpoints and UI considerations for creating, editing, viewing, and managing notes, with features like pinning, color-coding, archiving, and user sharing.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: f9708c33-0313-4d52-9aaf-39f564d4af34
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/pCDTgLS
Replit-Helium-Checkpoint-Created: true
2026-04-21 06:35:39 +00:00
riyadhafraa 0d070ef71f Task #34: fix login 500s + harden post-merge schema sync
Problem
- Dev environment login returned 500: `column "clock_style" does not
  exist`. Task #20's schema change for the per-user clock style was
  never applied to the dev DB because `scripts/post-merge.sh` runs
  `drizzle-kit push` interactively, and drizzle-kit got stuck on a
  prompt asking whether `app_opens` was a rename of `user_sessions`.
  When the prompt couldn't be answered, the whole sync exited without
  applying any pending changes — including the new column.

Changes
- Applied the missing column directly:
  `ALTER TABLE users ADD COLUMN IF NOT EXISTS clock_style varchar(30);`
  (nullable, no default — matches `lib/db/src/schema/users.ts`).
  Verified column now present.
- scripts/post-merge.sh: switched
  `pnpm --filter db push` → `pnpm --filter db run push-force`.
  The `push-force` script (already defined in `lib/db/package.json`)
  passes `--force` to drizzle-kit, which auto-accepts safe operations
  and treats ambiguous renames as creates — which is the correct
  behavior for our case (we genuinely have new tables, not renames).
  This prevents the same class of failure from recurring after future
  merges.

Verification
- Restarted the API server.
- `POST /api/auth/login` with admin/admin → HTTP 200, returns AuthUser
  with `clockStyle: null` (frontend already falls back gracefully, no
  home.tsx change needed).
- No new 500s in the API logs.

Out of scope (as planned)
- Backfilling a default value for existing rows.
- Resolving the underlying drizzle-kit `app_opens` rename detection
  — `--force` sidesteps it correctly.
2026-04-21 06:27:28 +00:00
riyadhafraa b5a5f0b169 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 3e13d4ee-b711-46d2-8f01-475d2a345770
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/pCDTgLS
Replit-Helium-Checkpoint-Created: true
2026-04-21 06:26:50 +00:00
riyadhafraa 94a361032e 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".

Replit-Task-Id: c7b6aa4b-9242-443b-9802-a39ab0bc9547
2026-04-21 06:11:28 +00:00
riyadhafraa 284cb751ed 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.

Replit-Task-Id: 475a7439-b357-400d-805f-5f2fda20ed24
2026-04-20 16:40:49 +00:00
riyadhafraa 4a481261a2 Update chat to display group initials and relevant fallback
Adjust the AvatarFallback component in the chat page to correctly display initials for group conversations, using a Users icon for smaller groups and showing the first letter of the group name for larger ones.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: e1f2ec36-f2c2-4be6-ac4f-bab54bcfb2f0
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/pCDTgLS
Replit-Helium-Checkpoint-Created: true
2026-04-20 16:40:29 +00:00
riyadhafraa c898221604 Task #27: Let users create named group chats (UI only)
Original ask: in the chat page, give users a clear way to create a
group chat (the "+" button already opened a dialog, but it had no name
field, no Direct/Group distinction, and no way to tell groups apart in
the conversation list).

Backend, DB schema, and OpenAPI already supported groups
(isGroup/nameAr/nameEn/participantIds + isAdmin on creator), so this
task was UI-only.

Changes:
- artifacts/teaboy-os/src/pages/chat.tsx — rebuilt the "New
  conversation" dialog with:
    * Direct / Group segmented tabs (default Direct)
    * Bilingual group name fields (AR + EN, at least one required)
    * Search-filterable participant list (matches username,
      displayNameAr, displayNameEn)
    * Custom checkbox UI; direct mode is single-select (clicking
      another user replaces selection), group mode multi-select
    * Live participant counter and inline validation messages
      (pickOnePerson / minTwoMembers / needGroupName)
    * Submit button driven by validation; closes on backdrop click
      and resets state on open/close
    * Switching to Direct mode clears stale group-name input
- Conversation list rows and chat header now show a "Group" / "مجموعة"
  badge for is_group conversations; group avatar uses a Users icon
  instead of name initials. Direct chats unchanged.
- New convDisplayName helper: prefers active-language name, falls back
  to other language, then participant names, then "Direct Message".
- artifacts/teaboy-os/src/locales/{ar,en}.json — added all new strings
  under chat.* (modeDirect, modeGroup, groupNameAr/En, placeholders,
  searchUsersPlaceholder, participantsCount, validation.*, create,
  noUsersFound). Updated EN groupChat label to compact "Group".

No backend, schema, codegen, or OpenAPI changes. Regenerated api
client locally only because pre-existing pages had stale types from
prior merges.

Verification: e2e test passed — login, open dialog, verify Direct/Group
tabs, validation messages, group creation, header + list badge, and
search filtering all work.

Out of scope (proposed as follow-ups): group avatar upload, manage
members after creation, leave/mute/archive a group.
2026-04-20 16:39:33 +00:00
riyadhafraa 70acaed4c3 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 9866ad65-6c47-4aa8-8a07-f94d671f216e
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/pCDTgLS
Replit-Helium-Checkpoint-Created: true
2026-04-20 16:34:14 +00:00
riyadhafraa b915c01df7 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.

Replit-Task-Id: e7628acb-8901-4b62-a7ee-a1149d9e993f
2026-04-20 16:28:50 +00:00
riyadhafraa 4451877244 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.

Replit-Task-Id: 8066030b-b5c4-4b5c-a630-65616df5449e
2026-04-20 15:38:28 +00:00
riyadhafraa ea77b92120 Update services with new images and descriptions, allowing admin edits
Add new images and descriptions for six services, update image URL resolution logic, and register generated assets.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: ea511886-fccb-4ba5-92ab-56a4a5bb8193
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/pCDTgLS
Replit-Helium-Checkpoint-Created: true
2026-04-20 15:38:22 +00:00
riyadhafraa 18e99ec07f Make service cards smaller and more compact on the services page
Update the services page layout to display smaller service cards with a square aspect ratio and increased column count, along with adjustments to padding, image sizes, and text truncation for a more compact design.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: b2994de5-2015-4dec-9ec2-f273f1b2b8e7
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/pCDTgLS
Replit-Helium-Checkpoint-Created: true
2026-04-20 15:31:30 +00:00
riyadhafraa a5484283f4 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.

Replit-Task-Id: 776c14f7-4e5a-4bf6-80e2-a6c7586c1fcb
2026-04-20 15:16:19 +00:00
riyadhafraa 2c8394e7e3 Update chart tooltips to use the correct language formatting
Modify `chart.tsx` to import and utilize `useTranslation` and `formatNumber` for locale-aware tooltip value formatting, replacing hardcoded US English number formatting.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 4cdc0cd7-7821-4410-a349-dc57c6975820
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/WWIlFT7
Replit-Helium-Checkpoint-Created: true
2026-04-20 15:11:28 +00:00
riyadhafraa 610bc9e083 Task #19: Force Latin digits + redesign home screen
- New helper artifacts/teaboy-os/src/lib/i18n-format.ts wraps Intl with
  numberingSystem: "latn" so numerals always render 0-9 even in Arabic,
  while month/weekday names stay localized. Exports formatTime,
  formatDate, formatHijri, formatWeekday, formatDateTime, formatNumber
  and a greetingKey() helper for time-of-day greetings.

- Replaced every toLocaleTimeString / toLocaleDateString /
  toLocaleString / direct Intl.DateTimeFormat call in home, admin,
  chat and notifications with the new helpers. No remaining
  Arabic-Indic digits anywhere in the UI (verified e2e).

- Redesigned the home screen:
    * Tighter status bar with three balanced sections: identity (avatar
      + name + admin tag), centered clock + Hijri/Gregorian dates,
      grouped controls (language, bell, logout).
    * Personal greeting line ("Good morning / صباح الخير …") driven by
      local hour and the user's display name.
    * Admin-only 4-card stat row (Apps / Services / Messages / Alerts)
      with soft glass + small colored icon tiles.
    * Polished apps grid with section count badge and a friendly empty
      state. Drag-and-drop reorder behavior preserved.
    * Type-safe Lucide icon resolver (no unsafe casts).

- Added i18n keys home.greeting.{morning,afternoon,evening},
  home.noApps, home.noAppsHint, home.stats.* in ar.json and en.json.

- Cleaned up obsolete follow-up plan file now that this work shipped.

Verified via TypeScript + e2e Playwright run in both Arabic (RTL) and
English (LTR): login → home redesign → notifications → chat, all
numerals Latin in both languages.
2026-04-20 15:08:56 +00:00
riyadhafraa 04b0318aff Task #19: Force Latin digits + redesign home screen
- New helper artifacts/teaboy-os/src/lib/i18n-format.ts wraps Intl with
  numberingSystem: "latn" so numerals always render 0-9 even in Arabic,
  while month/weekday names stay localized. Exports formatTime,
  formatDate, formatHijri, formatWeekday, formatDateTime, formatNumber
  and a greetingKey() helper for time-of-day greetings.

- Replaced every toLocaleTimeString / toLocaleDateString /
  toLocaleString / direct Intl.DateTimeFormat call in home, admin,
  chat and notifications with the new helpers. No remaining
  Arabic-Indic digits anywhere in the UI (verified e2e).

- Redesigned the home screen:
    * Tighter status bar with three balanced sections: identity (avatar
      + name + admin tag), centered clock + Hijri/Gregorian dates,
      grouped controls (language, bell, logout).
    * Personal greeting line ("Good morning / صباح الخير …") driven by
      local hour and the user's display name.
    * Admin-only 4-card stat row (Apps / Services / Messages / Alerts)
      with soft glass + small colored icon tiles.
    * Polished apps grid with section count badge and a friendly empty
      state. Drag-and-drop reorder behavior preserved.
    * Type-safe Lucide icon resolver (no unsafe casts).

- Added i18n keys home.greeting.{morning,afternoon,evening},
  home.noApps, home.noAppsHint, home.stats.* in ar.json and en.json.

- Cleaned up obsolete follow-up plan file now that this work shipped.

Verified via TypeScript + e2e Playwright run in both Arabic (RTL) and
English (LTR): login → home redesign → notifications → chat, all
numerals Latin in both languages.
2026-04-20 15:07:09 +00:00
riyadhafraa c3f919785f Task #19: Force Latin digits + redesign home screen
- New helper artifacts/teaboy-os/src/lib/i18n-format.ts wraps Intl with
  numberingSystem: "latn" so numerals always render 0-9 even in Arabic,
  while month/weekday names stay localized. Exports formatTime,
  formatDate, formatHijri, formatWeekday, formatDateTime, formatNumber
  and a greetingKey() helper for time-of-day greetings.

- Replaced every toLocaleTimeString / toLocaleDateString /
  toLocaleString / direct Intl.DateTimeFormat call in home, admin,
  chat and notifications with the new helpers. No remaining
  Arabic-Indic digits anywhere in the UI (verified e2e).

- Redesigned the home screen:
    * Tighter status bar with three balanced sections: identity (avatar
      + name + admin tag), centered clock + Hijri/Gregorian dates,
      grouped controls (language, bell, logout).
    * Personal greeting line ("Good morning / صباح الخير …") driven by
      local hour and the user's display name.
    * Admin-only 4-card stat row (Apps / Services / Messages / Alerts)
      with soft glass + small colored icon tiles.
    * Polished apps grid with section count badge and a friendly empty
      state. Drag-and-drop reorder behavior preserved.
    * Type-safe Lucide icon resolver (no unsafe casts).

- Added i18n keys home.greeting.{morning,afternoon,evening},
  home.noApps, home.noAppsHint, home.stats.* in ar.json and en.json.

- Cleaned up obsolete follow-up plan file now that this work shipped.

Verified via TypeScript + e2e Playwright run in both Arabic (RTL) and
English (LTR): login → home redesign → notifications → chat, all
numerals Latin in both languages.
2026-04-20 15:05:11 +00:00
riyadhafraa c248c691ce Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: a762484e-ebdd-4e4a-987c-080da84cf213
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/YSMil5R
Replit-Helium-Checkpoint-Created: true
2026-04-20 14:57:24 +00:00
riyadhafraa ff58444796 Add animated background visuals and improve layout on the login page
Replace static login image with animated SVG art, introduce gradient orbs and particles, and refine element positioning for a cleaner interface.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: d05c5e2f-4508-444c-8333-9962b72fd989
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/YSMil5R
Replit-Helium-Checkpoint-Created: true
2026-04-20 12:33:56 +00:00
riyadhafraa ab2eac47c0 Update login screen and site settings with editable footer and AI art
Add editable footer text fields to site settings, update OpenAPI schema and API client, refactor the login page to display AI-generated artwork and use dynamic footer text, and remove the old logo and welcome heading.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: a650b496-7c3c-427e-bfbe-77bc8b9b5dd2
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/gdgNDb6
Replit-Helium-Checkpoint-Created: true
2026-04-20 12:28:25 +00:00