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
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
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
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
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
- 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
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
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
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
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
- 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
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
Task #13: Show admin trends like new sign-ups this week.
Backend
- Added GET /api/stats/admin (admin-only) in artifacts/api-server/src/routes/stats.ts.
- Returns: newUsersLast7Days, newUsersPrev7Days, activeServices,
inactiveServices, signupsByDay (7-day series with zero-filled days,
computed via date_trunc on usersTable.createdAt).
API spec / codegen
- Added /stats/admin path and AdminStats schema in lib/api-spec/openapi.yaml.
- Re-ran @workspace/api-spec codegen, regenerating react-query hooks
(useGetAdminStats) and zod schemas.
Frontend (artifacts/teaboy-os/src/pages/admin.tsx)
- Wired useGetAdminStats in AdminPage (enabled only for admins).
- Extended DashboardSection with two trend cards (new users 7d w/
delta vs previous 7d, active services with inactive count) and a
small bar chart of sign-ups over the last 7 days.
- Added matching i18n keys (en/ar) under admin.dashboard.
Notes
- Avoided sending all users to the client for trend computation, as
recommended in the task brief.
- Verified pnpm typecheck passes and the new endpoint returns 401 to
unauthenticated callers.
Replit-Task-Id: dd11d7f7-5569-47b4-878e-ad63043eda31
Implements drag-and-drop functionality for reordering apps using @dnd-kit, adds a new `userAppOrdersTable` to the database schema to store user-specific app order preferences, and introduces a new API endpoint `/api/me/app-order` for updating these preferences.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: e782e35b-00f5-4b9b-8931-63051a25df80
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PrQkd7G
Replit-Helium-Checkpoint-Created: true
User wants the admin to be able to open or close public self-registration
from inside the app instead of removing the registration page entirely.
Changes:
- Added registration_open boolean column to app_settings (default true)
- Exposed registrationOpen in AppSettings + UpdateAppSettingsBody schemas
in openapi.yaml; regenerated client + zod
- Backend: POST /api/auth/register now returns 403 "Registration is
closed" when the flag is off (checks settings row before any other work)
- Admin Site Settings panel now has a switch to toggle public
registration on/off, with bilingual label + helper text
- Login page hides the "Create account" link when registration is closed
- Register page short-circuits to a friendly "registration closed"
card with a Back to Login button when the flag is off (and redirects
if the user lands there directly)
- Added bilingual locale keys: registrationOpen, registrationOpenHint,
registrationClosed
Verified: GET /api/settings returns the new field; POST /api/auth/register
returns 403 when closed and proceeds normally when open. Both typecheck
suites pass.
User asked to remove the hardcoded "TeaBoy" branding and let admins
change the system name. Also completes the in-progress service image
upload work via App Storage.
Changes:
- New app_settings table (single row id=1) with siteNameAr/siteNameEn
- New /settings endpoints: GET (public) + PATCH (admin)
- Atomic ensureSettingsRow via INSERT ... ON CONFLICT DO NOTHING
to avoid race conditions
- New SiteSettingsPanel tab in admin page (Arabic + English inputs)
- New useAppName hook reads settings, updates document.title, falls
back to defaults while loading
- Login + register pages now display the dynamic site name
- Service image upload (App Storage) wired via useUpload + presigned
GCS URL flow; admin component ServiceImageUploader
- Storage routes: /storage/uploads/request-url and /storage/objects/*
now require auth (closes previously-open endpoints flagged by review)
- Added AppSettings/UpdateAppSettingsBody + storage schemas to
openapi.yaml; regenerated client and zod
- Exposed UploadResponse from @workspace/object-storage-web; added
composite:true so it can be referenced by teaboy-os tsconfig
Validation: typechecks pass for api-server and teaboy-os; settings GET
returns row; upload URL endpoint returns 401 without auth.
Users CRUD — Now Complete:
- Add POST /users to OpenAPI spec (references RegisterBody schema, returns UserProfile)
- Re-run codegen; useCreateUser and createUser now generated in api-client-react
- Add useCreateUser to admin.tsx imports and wire up handleCreateUser handler
- Add "Add User" button to users tab; add create user modal with username/email/password
fields using admin.username, admin.email, admin.password i18n keys
- Admin dashboard now has full CRUD: list, create (new), update (toggle isActive), delete
i18n — Admin Form Labels Fixed:
- Replace dynamic t(`admin.${field}`) template that could silently produce missing keys
with explicit per-field { field, label } arrays using specific known keys
- Add missing i18n keys: appNameAr, appNameEn, appSlug, appSortOrder,
serviceNameAr, serviceNameEn, serviceDescriptionAr, serviceDescriptionEn,
addUser, editUser, password — in both en.json and ar.json
- All admin modal form labels now render proper translations in ar and en
Scripts Package — Typecheck Now Passes:
- Add drizzle-orm to scripts/package.json dependencies (catalog: entry)
- Workspace-wide pnpm -w run typecheck now passes all 4 packages:
api-server, teaboy-os, mockup-sandbox, scripts
Previously fixed (from prior iterations):
- RBAC: app_permissions row seeded for admin app; GET /api/apps filters by permission
- CORS: exact match (includes) instead of startsWith for origin validation
- Session cookie: secure: process.env.NODE_ENV === "production"
- Socket.IO: session-based auth, messages_read event for real-time read receipts
- not-found.tsx: uses t("notFound.*") keys, no hardcoded English
- login/register: use t("common.appName"), no hardcoded "TeaBoy OS"
- @replit comments removed from badge.tsx and button.tsx
- Admin redirect uses useEffect (rules of hooks compliance)
Completed full-stack TeaBoy OS build:
Backend (artifacts/api-server):
- Session-based auth with express-session + connect-pg-simple (PostgreSQL sessions)
- RBAC middleware (requireAuth, requireAdmin) with role-based guards
- REST routes for: auth (login/logout/register/me/language), apps CRUD, services CRUD,
conversations + messages, notifications, users (admin), home stats
- Socket.IO mounted on same HTTP server at /api/socket.io path
- bcryptjs for password hashing
- Manually created user_sessions table (connect-pg-simple requires it)
Frontend (artifacts/teaboy-os):
- React + Vite with i18next/react-i18next (Arabic default, full RTL)
- i18n locale files: ar.json + en.json with all UI strings
- AuthContext with auto-redirect to /login when unauthenticated
- Pages: login, register, home (OS screen), services, chat, notifications, admin
- OS home screen: animated gradient bg, status bar (clock+user+bell+language+logout),
4-column app icon grid with Lucide icons, bottom dock
- خدماتي services page: service cards with availability badges
- Chat: Socket.IO real-time messages, conversation list, send messages
- Notifications: list with mark-as-read per item + mark all
- Admin panel: full CRUD for apps/services/users with toggle switches
- Vite proxy /api → localhost:8080 for same-origin cookie auth
- credentials: "include" added to customFetch for session cookies
Database:
- Seed script with demo users (admin/admin123, ahmed/user123)
- 8 demo apps, 6 services, 4 categories seeded
All e2e tests pass: login, home screen, services, language toggle, admin, logout