Commit Graph

120 Commits

Author SHA1 Message Date
riyadhafraa 19a20cc5a4 Let admins create and edit roles from the dashboard (Task #82)
What changed:
- Added an `is_system` column to the `roles` table (default 0). The
  built-in roles (admin, user, order_receiver) are flagged as system roles
  in the seed and via a one-off UPDATE on the existing dev DB.
- Created a new `/api/roles` route module with admin-guarded endpoints:
    - POST /api/roles – create a role (validates name format, rejects duplicates).
    - PATCH /api/roles/:id – update bilingual descriptions; name is immutable.
    - DELETE /api/roles/:id – returns 400 for system roles (also defended
      by name allowlist), 409 with userCount/groupCount when in use, 204 on
      success. Cleans up role_permissions in a transaction.
  GET /api/roles was moved out of groups.ts into the new module.
- OpenAPI updated with createRole / updateRole / deleteRole operations,
  isSystem on the Role schema, and CreateRoleBody / UpdateRoleBody /
  RoleDeletionConflict schemas. Regenerated api-zod and api-client-react.
- Added a "Roles" item under the User Management nav group in the admin
  dashboard, plus a new RolesPanel with search, create dialog, edit dialog
  (description-only), and delete dialog with conflict messaging.
- Bilingual translations added in en.json and ar.json (admin.nav.roles +
  admin.roles.*).

Notes / deviations:
- Also marked `order_receiver` as a system role since it is seeded by the
  system and required by the orders feature, even though the task brief
  only called out admin and user.
- Verified end-to-end via the testing skill: list, create, edit, delete,
  duplicate-name validation, and protection of system roles all work.

Replit-Task-Id: b1187555-be09-4687-a9ae-83b123d908bd
2026-04-27 10:26:24 +00:00
riyadhafraa b0b1d673b7 Show role descriptions in both languages on the Groups Roles tab
Task #81: The Groups editor's Roles tab previously displayed only the
description in the active UI language with the role's machine name as
a small subtitle. Bilingual admin teams asked to see both languages at
once.

Changes (artifacts/tx-os/src/pages/admin.tsx):
- Imported Tooltip, TooltipContent, TooltipTrigger from
  @/components/ui/tooltip (TooltipProvider was already mounted at the
  App root, so no provider wiring was needed).
- Refactored each role row in the Roles tab of GroupDetailEditor:
  * Primary line: description in the active language
    (descriptionAr when lang === "ar", otherwise descriptionEn). Falls
    back to the other-language description, then to the role's name
    when no descriptions exist.
  * Secondary line (smaller, muted): the OTHER language's description,
    rendered with the appropriate dir attribute. Only shown when both
    descriptions are present so single-description roles fall back
    cleanly.
  * Machine name remains visible as a small monospace subtitle on the
    trailing edge of the row.
  * The whole row is wrapped in a Tooltip whose content shows both
    descriptions (each with its proper dir) plus the machine name for
    full context on hover.
- Added data-testid="group-role-<id>-secondary" so the secondary line
  is easily targetable from tests.

Verification:
- Type-checked tx-os (only pre-existing errors elsewhere in admin.tsx
  remain; none introduced by this change).
- e2e test passed: logged in as admin, opened a group, switched to the
  Roles tab, confirmed primary + secondary description lines, hovered
  to see the bilingual tooltip, toggled the UI language and confirmed
  the lines swapped, and confirmed checkboxes still toggle through the
  TooltipTrigger asChild wrapper.

The single-description-only fallback could not be exercised against
seeded data (all seeded roles are bilingual), but the rendering logic
gracefully handles that case via the `heading` and `showSecondary`
guards.

Also reverts an unrelated stray modification to
artifacts/tx-os/public/opengraph.jpg that was swept into the previous
auto-commit, restoring it to the version from HEAD~1.

Follow-up proposed: #88 "Let admins edit role descriptions in both
languages".

Replit-Task-Id: dfd3b45c-7b85-4c30-b0f9-3bfbab81be8c
2026-04-27 10:08:41 +00:00
riyadhafraa 5051b1b905 fix: tell receivers when an order is no longer available + fix disabled apps disappearing from admin
## Task #80 — Tell receivers when an order is no longer available to claim

### Problem
When a receiver tried to act on an order that had already been cancelled, completed,
or claimed by someone else, the UI showed a generic "Could not complete the action"
toast. The stale card also stayed in the list, requiring a manual refresh.

### Changes

**artifacts/api-server/src/routes/service-orders.ts**
- PATCH /orders/:id/confirm-receipt: after a failed atomic claim, query the current
  order status; return 409 `order_unavailable` if it's cancelled/completed (vs the
  existing 409 `already_claimed` for when it was grabbed by someone else first)
- PATCH /orders/:id/status (preparing/completed branch): terminal-state check runs
  BEFORE permission gating — returns 409 `order_unavailable` when order is already
  cancelled/completed instead of ever hitting 403 Forbidden
- PATCH /orders/:id/status (cancel branch): same — terminal-state check moved to
  the top of the branch so non-admin receivers trying to cancel an already-terminal
  order get 409 `order_unavailable` (not 403 Forbidden which would never let the UI
  remove the stale card)

**artifacts/tx-os/src/pages/orders-incoming.tsx**
- handleConfirmReceipt onError: distinguish 409 `order_unavailable` from
  `already_claimed`, show specific toast, remove stale card via invalidate()
- handleSetStatus onError: same pattern — specific toast + invalidate() on
  `order_unavailable`
- handleCancel onError: same pattern

**artifacts/tx-os/src/locales/en.json + ar.json**
- Added `incomingOrders.orderUnavailable` ("This order is no longer available" /
  "هذا الطلب لم يعد متاحاً")

## Bonus fix — Disabled app disappears from admin panel (user-reported)

### Problem
When an admin disabled an app via the toggle, the app disappeared from the admin
panel too (isActive filter applied to everyone), making it impossible to re-enable
without direct DB access.

### Changes

**artifacts/api-server/src/routes/apps.ts**
- getVisibleAppsForUser: admins now receive ALL apps including inactive ones via
  conditional $dynamic() Drizzle query; non-admins still only see active apps

**artifacts/tx-os/src/pages/home.tsx**
- Filter orderedApps to only active apps before rendering so inactive apps don't
  appear on the home screen even for admins

**artifacts/tx-os/src/pages/admin.tsx**
- Inactive app cards show reduced opacity + a "Disabled/معطّل" badge

**artifacts/tx-os/src/locales/en.json + ar.json**
- Added `admin.appDisabled` ("Disabled" / "معطّل")

Replit-Task-Id: 8d6fade8-e76c-4d3c-864b-59007688c12c
2026-04-27 09:06:23 +00:00
riyadhafraa cd0a8313e0 Warn admins before deleting non-empty groups
Original task: Task #79 — Make DELETE /api/groups/:id refuse to wipe a non-empty
group unless explicitly forced, surface the impacted counts in the admin UI,
and log forced deletions to an audit trail.

Changes:
- API: DELETE /api/groups/:id now computes member/app/role counts. If any are
  non-zero and `?force=true` is not passed, it returns 409 with
  { error, memberCount, appCount, roleCount }. With `force=true` it deletes
  and writes an entry to the new `audit_logs` table
  (action='group.force_delete', target_type='group', target_id, metadata).
  System group guard and 404 behaviour preserved.
- DB: Added `audit_logs` table (lib/db/src/schema/audit-logs.ts) with actor,
  action, target type/id, jsonb metadata, timestamp. Pushed via drizzle-kit.
- OpenAPI: Added `force` query param, 409 response, and `GroupDeletionConflict`
  schema. Re-ran orval codegen for api-client-react and api-zod.
- api-client-react: Re-exported `ApiError` and `ErrorType` so the UI can
  inspect 409 responses.
- Admin UI (artifacts/teaboy-os/src/pages/admin.tsx GroupsPanel):
  Replaced the bare `confirm()` with a confirmation dialog that shows the
  group name and impacted member/app/role counts. Empty groups get a simple
  "Delete" confirmation; non-empty groups get a warning + "Delete anyway"
  button which sends `force=true`. If the server returns 409 (race), the
  dialog updates to show the server-reported counts.
- Locales: Added en/ar strings for the new dialog (deleteTitle, deleteWarning,
  deleteForceHint, deleteEmptyBody, deleteConfirm, deleteAnyway).

Verified end-to-end: 409 on first DELETE, dialog warning visible, force
deletion succeeds, single audit_logs row recorded with correct metadata.

Replit-Task-Id: 61624ce4-83b3-43be-b22b-e261662301f1
2026-04-22 11:05:34 +00:00
riyadhafraa ea41328626 Remove service descriptions from display and administration
Removes Arabic and English description fields from the admin form, the services display component, and the seed data in `scripts/src/seed.ts`, and removes the corresponding columns from the database.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 5f1c43b0-7465-4e56-bb0e-896a4df38886
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/dnVFHsG
Replit-Helium-Checkpoint-Created: true
2026-04-22 10:55:46 +00:00
riyadhafraa 9250e5e623 Design modern, minimal icons based on "TX" for the platform
Add new SVG icon assets for the platform's favicon and branding, including multiple variants and a preview HTML file.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 8912f52a-183a-4f2b-983a-b2ebfb2814ba
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/dnVFHsG
Replit-Helium-Checkpoint-Created: true
2026-04-22 10:53:54 +00:00
riyadhafraa d0e6912017 Rename project and remove unused services
Update project name from teaboy-os to tx-os and remove obsolete services.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 3154f23a-748a-4118-aa41-fc01b7b1f04d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PVuelRZ
Replit-Helium-Checkpoint-Created: true
2026-04-22 10:52:09 +00:00
riyadhafraa 1f23e65c0b Update system name and references from TeaBoy to Tx
Replaces all user-facing instances of "TeaBoy" with "Tx" across the application, including titles, locale files, database settings, seed data, and API documentation. Also updates internal storage keys and session secrets to remove the old branding.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 07b19cb0-11b5-4be9-8932-ae4820eb73b8
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/HxTkDPZ
Replit-Helium-Checkpoint-Created: true
2026-04-22 10:26:53 +00:00
riyadhafraa 6dd927350d Reflect group memberships live in the user's app grid (Task #77)
Group/membership changes now push to affected users over Socket.IO so
their app dock updates without a manual refresh.

Server:
- New helper `artifacts/api-server/src/lib/realtime.ts` exposing
  `emitAppsChangedToUsers(userIds)` which lazy-imports the io instance
  (matching the pattern used in conversations/service-orders) and emits
  `apps_changed` to each `user:<id>` room.
- `routes/groups.ts`:
  - PATCH /groups/🆔 snapshot members before the write, union with
    members after, and emit to that set when membership changes; emit to
    current members when only apps/roles change.
  - POST/DELETE /groups/:id/users/:targetId emit to the target user.
  - POST/DELETE /groups/:id/apps|roles/:targetId emit to all current
    members of the group.
  - DELETE /groups/:id emits to all (former) members.
- `routes/users.ts` PATCH /users/:id emits to the affected user when
  groupIds is changed.

Client:
- `artifacts/teaboy-os/src/hooks/use-notifications-socket.ts` now also
  listens for `apps_changed` and invalidates `getListAppsQueryKey()` and
  `getGetMeQueryKey()`, refreshing the dock and AuthUser context.

Validation:
- API tests pass for all groups/membership cases. One pre-existing
  flaky service-orders parallel-receipt test failed; unrelated to this
  change.

Replit-Task-Id: 3e5ae44b-9dcf-42f4-8d5d-a0f2b05b6065
2026-04-22 10:20:04 +00:00
riyadhafraa bc0737aed2 Task #76: Show roles management inside the Groups editor
Adds a "Roles" tab to the GroupDetailEditor in the admin Groups screen so
admins can manage which roles are auto-granted to members of a group.

Changes:
- OpenAPI: added `GET /roles` (operationId `listRoles`) and a `Role` schema
  in `lib/api-spec/openapi.yaml`. Ran `pnpm --filter @workspace/api-spec
  run codegen` to regenerate `lib/api-client-react` and `lib/api-zod`.
- API server: added `GET /api/roles` (admin-only) in
  `artifacts/api-server/src/routes/groups.ts`, returning all roles
  ordered by name.
- Frontend (`artifacts/teaboy-os/src/pages/admin.tsx`):
  - Imported `useListRoles` / `getListRolesQueryKey`.
  - Added `roleIds` state and a fourth `"roles"` tab to GroupDetailEditor
    with a checkbox list backed by the new endpoint, hydrated from
    `group.roleIds`.
  - Save now sends `roleIds: Array.from(roleIds)` via the existing
    `PATCH /api/groups/:id` mutation.
  - On save, also invalidate `getGetGroupQueryKey(editingGroupId)` so the
    detail cache (30s staleTime) reflects the new role list immediately
    when the editor is reopened.
- Translations: added `admin.groups.tab.roles`, `admin.groups.rolesHint`,
  and `admin.groups.rolesEmpty` in both `en.json` and `ar.json`.

Verification:
- `pnpm -w run typecheck` passes.
- `pnpm --filter @workspace/api-server test` — 42/42 pass.
- e2e (testing skill): logged in as admin, opened TeaBoy group, toggled
  the `user` role on the new Roles tab, saved, reopened — toggle
  persisted.

No deviations from the task description.

Replit-Task-Id: 42808400-9209-469b-b526-37c776ffb25d
2026-04-22 10:13:17 +00:00
riyadhafraa 7d29d63d2d Add Undo window when receivers cancel an incoming order
Original task (#73): Mirror the existing 7s Undo toast (already in place
for owner-side cancels in My Orders) on the receiver-side cancel action
in the Incoming Orders page.

Implementation:
- artifacts/teaboy-os/src/pages/orders-incoming.tsx
  - Captures the order's previous status (received | preparing) before
    issuing the cancel PATCH.
  - On success, shows a toast with title "Order cancelled" and an
    "Undo" ToastAction. Clicking Undo PATCHes the order status back to
    the previous value, then shows a "Restored" confirmation toast.
  - UNDO_WINDOW_MS = 7000 to match the owner-side flow.
- artifacts/api-server/src/routes/service-orders.ts
  - Refactored the PATCH /orders/:id/status authorization. Detects
    "restore from cancelled" up front (existing.status === cancelled
    and next !== cancelled) and grants permission to:
      - owner/admin: may restore to pending or received (existing rule)
      - original assignee: may restore to received or preparing (NEW)
  - The same 15s server-side undo window applies.
- artifacts/teaboy-os/src/locales/{en,ar}.json
  - Added incomingOrders.cancelled / undo / undone / restoreFailed
    strings in English and Arabic.
- Regenerated lib/api-zod and lib/api-client-react via the api-spec
  codegen script (the previously-cached output was missing pending /
  received / preparing from the request body enum, which now matches
  the OpenAPI spec).

Verification:
- Typecheck passes for api-server and teaboy-os.
- e2e flow verified: cancelling a received order shows the Undo toast;
  clicking Undo restores the order to "received" (DB confirmed);
  letting the window expire leaves the order permanently "cancelled"
  (DB confirmed).

Deviations: None.
Replit-Task-Id: 65c4b053-613a-4898-a0f7-b6fb2d680301
2026-04-22 09:58:49 +00:00
riyadhafraa 36f3704d1d Git commit prior to merge 2026-04-22 09:58:49 +00:00
riyadhafraa 21b53f0c54 Add automated test coverage for groups and access control
Original task: Task #75 — add automated tests for the groups system
(CRUD + group-driven app visibility + auto-Everyone) and stabilize the
flaky pagination test.

Changes:
- Extended artifacts/api-server/tests/groups-crud.test.mjs with four
  new tests:
  * PATCH /api/groups/:id updates fields and replaces userIds
    transactionally; a bad userId returns 400 and leaves prior
    membership unchanged.
  * Creating a group with invalid roleIds returns 400 and creates no
    rows.
  * DELETE /api/groups/:id removes a custom group; deleting a system
    group (Everyone, is_system = 1) returns 400 and the row stays.
  * /api/auth/register auto-assigns the new user to the Everyone
    system group; verified both via the response payload and a direct
    user_groups DB check. The test temporarily flips
    app_settings.registration_open to true and restores the prior
    value on completion.

- The existing apps-group-visibility.test.mjs already exercises the
  /api/apps endpoint backed by getVisibleAppsForUser for group-derived
  admin, group member, and outsider — no changes needed there.

- The previously flaky tests/admin-app-opens-pagination.test.mjs now
  passes consistently thanks to the existing 7d-window cleanup in its
  before hook; left as-is per the task's "fixed or skipped" criterion.

Verification: `pnpm --filter @workspace/api-server test` reports
42/42 passing. The downstream Playwright suite (4 tests) also passes.

Replit-Task-Id: 188075ef-382c-4af1-8a9a-90a0563c92c2
2026-04-22 09:15:57 +00:00
riyadhafraa 5de821fac8 Add Undo window for cancelled orders on My Orders
Original task (#72): Mirror the recently-added delete Undo toast for
the cancel action so accidental cancellations can be reverted within
~7s.

Changes
- Frontend (artifacts/teaboy-os/src/pages/my-orders.tsx):
  OrderCard.handleCancel captures the order's previous status, and on
  successful cancel shows a toast with an "Undo" action (duration
  matches the existing UNDO_WINDOW_MS = 7000ms). Clicking Undo issues
  a PATCH to restore the order to its previous status (pending or
  received) and emits a "Restored" toast. Errors surface a
  "Could not restore the order" toast.
- Cancel confirmation copy was softened (no longer says "can't be
  reopened") and a new restoreFailed string was added in both en.json
  and ar.json.

- Backend (artifacts/api-server/src/routes/service-orders.ts):
  PATCH /orders/:id/status accepts pending and received as targets
  for the owner (or admin) when the existing status is cancelled,
  serving as the restore-from-cancelled transition. We require
  assignedTo to be null for pending and non-null for received so we
  always restore to the actually-prior state. The existing logic
  already broadcasts order_incoming_changed to receivers and emits
  order_updated to the owner, so receivers' incoming queues refresh
  automatically when an order is restored. notifyUser titleMap was
  extended with "Your order was restored" entries.
- Time-bound restore: server enforces a RESTORE_WINDOW_MS of 15s
  (slightly larger than the 7s client window to absorb network/clock
  skew). After it expires, restore is rejected with
  `undo_window_expired`, so cancellation truly becomes permanent —
  this applies to admin too, so restore is strictly Undo and not an
  arbitrary reopen.

- API spec (lib/api-spec/openapi.yaml):
  UpdateServiceOrderStatusBody enum extended to include pending and
  received; endpoint summary updated. Regenerated api-zod and
  api-client-react.

Tests
- Added a new test case in artifacts/api-server/tests/service-orders.test.mjs
  that covers: restore from pending, restore from received,
  pending<->received validation against assignedTo, stranger forbidden,
  and undo window expiry (by backdating updated_at).

Verification
- pnpm typecheck passes across libs and artifacts.
- New api-server test "owner can restore (undo) a freshly cancelled
  order..." passes; all pre-existing service-order tests still pass.
- e2e tested: login -> place order -> cancel -> Undo restores to
  Pending; cancel again -> let window expire -> order stays Cancelled
  with no Undo available.

Notes / unrelated
- The unrelated `admin-app-opens-pagination` test
  (`by-app: paginating with limit returns nextOffset until exhausted`)
  fails in the dev environment because the dev DB has > 100 app_opens
  rows for the chosen app and the test only walks up to 50 pages of
  size 2. This is a pre-existing flaky test unrelated to this task
  and was not modified.

Replit-Task-Id: beca78bc-32f3-4cdc-8440-9a661b48363b
2026-04-22 09:04:28 +00:00
riyadhafraa a21428b2a4 Groups: address validator round 3 follow-ups
Round 3 rejection items closed:
- UI smoke test added: artifacts/teaboy-os/tests/admin-create-group-
  app-visibility.spec.mjs. Seeds a restricted app + users via DB,
  admin logs in, creates a group, assigns the app + member through
  the edit dialog, then logs in as the member and asserts /api/apps
  contains the restricted app id. Cleans up after itself. Passes.
- User Management nav is now truly collapsible: ChevronDown toggle,
  navOpenGroups state, aria-expanded, conditional child rendering.
  Defaults to expanded only when a child is the active section.
- Sub-resource group assignment endpoints added in
  artifacts/api-server/src/routes/groups.ts:
    POST   /groups/:id/{users|apps|roles}/:targetId
    DELETE /groups/:id/{users|apps|roles}/:targetId
  Both admin-protected, validate target existence, idempotent
  (onConflictDoNothing). Aggregate PATCH /groups/:id remains.

Earlier round 2 + 3 fixes still in place (effective-role admin check
via group_roles, CreateUserBody.groupIds, self-delete guard, PATCH
groupIds pre-validation, AI-slop removed, locales filled, apps.ts
inArray fix).

Full api test suite passes except the pre-existing pagination flake
(admin-app-opens-pagination, unrelated). Architect: PASS.
2026-04-22 09:03:10 +00:00
riyadhafraa 1fa19e048e Groups: address validator round 3 follow-ups
Round 3 rejection items closed:
- UI smoke test added: artifacts/teaboy-os/tests/admin-create-group-
  app-visibility.spec.mjs. Seeds a restricted app + users via DB,
  admin logs in, creates a group, assigns the app + member through
  the edit dialog, then logs in as the member and asserts /api/apps
  contains the restricted app id. Cleans up after itself. Passes.
- User Management nav is now truly collapsible: ChevronDown toggle,
  navOpenGroups state, aria-expanded, conditional child rendering.
  Defaults to expanded only when a child is the active section.
- Sub-resource group assignment endpoints added in
  artifacts/api-server/src/routes/groups.ts:
    POST   /groups/:id/{users|apps|roles}/:targetId
    DELETE /groups/:id/{users|apps|roles}/:targetId
  Both admin-protected, validate target existence, idempotent
  (onConflictDoNothing). Aggregate PATCH /groups/:id remains.

Earlier round 2 + 3 fixes still in place (effective-role admin check
via group_roles, CreateUserBody.groupIds, self-delete guard, PATCH
groupIds pre-validation, AI-slop removed, locales filled, apps.ts
inArray fix).

Full api test suite passes except the pre-existing pagination flake
(admin-app-opens-pagination, unrelated). Architect: PASS.
2026-04-22 09:01:11 +00:00
riyadhafraa 72e1f8d9ed Groups CRUD: address validator round 2 + 3 blockers
Round 2 fixes:
- apps.ts getVisibleAppsForUser uses getEffectiveRoleIds() so admin
  detection honors group_roles inheritance (closes group-only-admin
  bypass). Exported helper from middlewares/auth.ts.
- POST /users accepts optional groupIds[] via new CreateUserBody
  schema. Validates ids exist; user create + auto-Everyone + requested
  groups happen in one transaction. OpenAPI extended; api-zod and
  api-client-react regenerated.
- DELETE /users/:id 400s on self-delete.
- scripts/src/seed.ts: removed `void inArray;` slop and unused import.
- Locales (ar+en): added admin.users.col.{displayNameAr,displayNameEn,
  language} and admin.groups.searchPlaceholder.

Round 3 fixes:
- Admin Users "Add User" dialog gets a groupIds checkbox multi-select
  populated from useListGroups; createUser sends groupIds when set.
- PATCH /users/:id validates ALL groupIds before deleting memberships
  (returns 400 on any invalid id); replacement wrapped in transaction
  to avoid partial-loss windows.
- Apps admin SQL bug: `${arr} = ANY(...)` produced bad params; now uses
  `and(inArray(rolesTable.id, ids), eq(name,'admin'))`.
- New tests (artifacts/api-server/tests/apps-group-visibility.test.mjs):
  group-derived admin sees all; group member sees group-granted +
  unrestricted; outsider sees only unrestricted. All 3 pass.

Typecheck clean; full api test suite green except pre-existing
admin-app-opens-pagination flake (unrelated). Architect: PASS.
2026-04-22 08:54:08 +00:00
riyadhafraa 4595e792e4 Fix 5 validator blockers in groups/users admin work
1. apps.ts getVisibleAppsForUser: use getEffectiveRoleIds() so admin
   gate honors group_roles inheritance (was direct user_roles only —
   security bypass for group-only admins). Exported helper from
   middlewares/auth.ts.

2. POST /users: switch from RegisterBody to new CreateUserBody schema
   that accepts optional groupIds[]. Validates ids exist; user creation
   + auto-Everyone + requested groups happen in single transaction.
   OpenAPI spec extended; api-zod and api-client-react regenerated.

3. DELETE /users/🆔 400 if req.session.userId === target (no
   self-delete).

4. scripts/src/seed.ts: removed `void inArray;` AI-slop and dropped
   unused inArray import.

5. Locales (ar.json + en.json): added admin.users.col.displayNameAr,
   displayNameEn, language and admin.groups.searchPlaceholder.

Typecheck clean. groups-crud + service-orders + users tests pass.
Pre-existing admin-app-opens-pagination flake unrelated.
Architect re-review: PASS.
2026-04-22 08:47:35 +00:00
riyadhafraa bf0768948e Groups + group-based RBAC: harden authz, atomic group writes, full admin UI
Auth middleware:
- Add getEffectiveRoleIds() that UNIONs user_roles with group_roles via
  group memberships, used by requireAdmin / requirePermission /
  userHasPermission / getUserRoles.
- requireAdmin and requirePermission now also reject inactive users
  with 401 (matching requireAuth), closing a session-after-deactivation
  bypass.

Groups routes:
- POST /groups and PATCH /groups/:id now wrap the group row write and
  all assignment writes in a single db.transaction via
  applyGroupAssignmentsTx(tx, ...), so partial state cannot leak.
- validateAssignmentIds rejects unknown app/role/user ids with 400
  before any insert.
- Removed AI-slop: void or, void sql, as-unknown-as casts; conditions
  use Drizzle's SQL union type.

Users route:
- /api/users supports q, groupId, status filters (server-side).

Admin UI (teaboy-os/admin.tsx):
- UsersPanel wires q/groupId/status to the backend, shows display name
  and preferred language inline per row.
- UserGroupsEditor now edits display names (ar/en), preferred language,
  active status, and group membership with a search box.
- GroupsPanel adds a top-level group search box.
- GroupDetailEditor Users tab adds a user search box.

Infra:
- scripts/post-merge.sh runs the seed (idempotent) so default groups
  Admins / TeaBoy / Everyone always exist after merges.

Tests (artifacts/api-server/tests/groups-crud.test.mjs, all passing):
- Admin-only access (regular user gets 403).
- Default seed groups exist.
- Create group + member assignment.
- Bad userIds yields 400 with no leaked group row.
- Admin role inherited via group_roles grants admin access.
- Deactivated admin session is rejected with 401.
- Group create rolls back atomically when assignment fails.
- /api/users q + groupId + status filters return correct rows.

Notes / drift:
- "Roles" tab inside GroupDetailEditor and groupIds in CreateUserBody
  remain as proposed follow-ups (require OpenAPI spec changes).
- Pre-existing pagination-test flake unrelated to this work.
2026-04-22 08:40:38 +00:00
riyadhafraa f5273af19f Task #74: Add groups system + admin User Management UI
Backend:
- New schema: groups, user_groups, group_apps, group_roles (lib/db/src/schema/groups.ts)
- Seeds Admins, TeaBoy, Everyone system groups idempotently and maps existing users
- /api/groups CRUD with admin guard, batch counts, system-group delete protection
- Validates appIds/roleIds/userIds (400 on missing) and wraps assignment writes in
  a single DB transaction (no partial state on failure)
- /api/users gains q/groupId/status filters, batch role+group loading, groupIds
  replacement on PATCH, auto-assigns Everyone on admin-create
- /auth/register also auto-assigns Everyone for consistent default linkage
- buildAuthUser now returns groups (matches updated AuthUser OpenAPI schema)
- App visibility (getVisibleAppsForUser) unions group-granted apps via
  group_apps + user_groups in addition to existing permission gating

Frontend (admin.tsx):
- Nav restructured: User Management section with Users + Groups children
- Section deep-linked via #section=… URL hash
- Users page rebuilt: search, group filter, status filter, sortable table,
  groups column, edit-groups dialog, mobile cards
- New Groups page: cards with member/app/role counts, create dialog,
  detail editor with Info/Apps/Users tabs and system-group guard
- ar/en translations added for all new keys

Testing:
- pnpm typecheck clean (api + web)
- 25/26 api tests pass; the only failure is pre-existing flaky pagination test
  (admin-app-opens-pagination) — left as-is per scratchpad note
- Code review feedback addressed (validation, transactions, register auto-assign)
2026-04-22 08:28:31 +00:00
riyadhafraa bae9c26927 Hide self-notifications across order lifecycle
Task: Audit notification rules in service-orders.ts so users never
receive notifications about actions they themselves initiated. The
specific scenario: a user with the `orders.receive` permission who
cancels their own assigned order would notify themselves under the
old logic.

Approach:
- Added an optional `actorId` parameter to `notifyUser`. When the
  notification recipient equals the actor, the function returns
  early (no DB insert, no socket emit).
- Threaded `actorId` through the three notification call sites:
  1. POST /orders — placing an order no longer notifies the placer
     even if they also hold the receiver role.
  2. PATCH /orders/:id/confirm-receipt — a receiver claiming their
     own order no longer notifies themselves.
  3. PATCH /orders/:id/status — replaced the narrow
     `!initiatedByOwner` guard with the generic `actorId === userId`
     check inside `notifyUser`. This now covers owner-cancel as
     before AND owner-as-assignee transitions (preparing/completed/
     cancelled) where the owner happens to also be the assignee.

Notes / deviations:
- Kept the existing distinguishing wording for receiver-initiated
  cancels ("claimed but later cancelled by the receiver"); it's
  simply suppressed when the receiver is also the owner.
- No schema or API contract changes; purely server-side notification
  filtering. Typecheck passes.
- No follow-ups proposed: existing project tasks already cover
  automated tests for order/cancel flow, re-order from history, and
  an undo window for cancellations.

Replit-Task-Id: 65b6d89a-6882-42f8-adb7-6ac1a2560748
2026-04-22 06:49:19 +00:00
riyadhafraa d2c8e05c6c Add undo window for deleted orders on My Orders
Task #70: After deleting a finished order from My Orders, users had no way
to recover it. This change introduces a short undo window so accidental
deletes can be reversed while keeping the cleanup workflow snappy.

Approach:
- Implemented as a client-side deferred-delete in
  artifacts/teaboy-os/src/pages/my-orders.tsx. No backend changes.
- On delete (single trash icon or bulk "Clear N finished"):
  * The order(s) are added to a local pendingDeleteIds set so they
    immediately disappear from the list.
  * A toast is shown with a localised "Undo" action and a 7s duration.
  * A setTimeout is scheduled to call DELETE /api/orders/:id for the
    affected ids and then invalidate the my-orders query.
- Clicking "Undo" cancels the timer, removes the ids from
  pendingDeleteIds (so the cards reappear), dismisses the toast, and
  shows a "Restored" confirmation toast.
- On unmount, any still-pending deletions are flushed so they aren't
  silently lost if the user navigates away mid-window.
- If the deferred DELETE later fails, the ids are restored to the list
  and a "Could not delete the order" destructive toast is shown.

OrderCard now delegates the actual delete to a new onDelete callback
from the page (the confirmation dialog stays in the card). Bulk clear
no longer fires N sequential mutations from inside a confirm dialog
spinner — it just schedules them.

i18n: added myOrders.undo / myOrders.undone strings to en.json and
ar.json.

Verification:
- pnpm tsc --noEmit passes for teaboy-os.
- e2e (runTest) covered: single delete + undo restores; single delete
  with no undo becomes permanent after 7s; bulk clear + undo restores
  all. DB state was asserted in each case.

Follow-up proposed: #72 — apply the same undo pattern to cancelled
orders.

Replit-Task-Id: 1854a0b1-f6d3-4148-b2c2-b0940202d0df
2026-04-22 06:46:42 +00:00
riyadhafraa 15b32d5ac5 Adjust service cards to be smaller and tappable directly
Modify service card layout for better density, enable direct image tapping to open orders, and shorten "Place order" button text to "Send".

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: ff3bca96-6332-4411-9809-103494a69c1f
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/VRpzQEA
Replit-Helium-Checkpoint-Created: true
2026-04-22 06:42:23 +00:00
riyadhafraa 1cea719b80 Task #69: Delete past orders from My Orders
- Add DELETE /api/orders/:id endpoint: owner can delete completed/cancelled
  orders; admin can delete any. 401/403/404/204 responses. Emits
  order_deleted to owner and assignee for live invalidation.
- Update OpenAPI spec with deleteServiceOrder operation; regenerate
  api-client-react + api-zod.
- Frontend: per-order trash button on completed/cancelled cards with
  confirm dialog; bulk "Clear finished (N)" button at top of list with
  confirm dialog and toast summary (handles partial failures).
- Add ar/en locale keys under myOrders for delete + clear flows
  (singular/plural variants).
- Backend tests cover all paths: pending owner=403, stranger=403,
  owner-cancelled=204, owner-completed=204, admin-pending=204, 404, 401.

All 25 service-order tests pass; the 1 failing test (admin-app-opens-
pagination) is unrelated/pre-existing.

Follow-ups proposed: #70 undo for deleted orders, #71 audit
self-notification on receiver-cancel-own-order.
2026-04-22 06:38:13 +00:00
riyadhafraa 58553d3d36 Task #69: Delete past orders from My Orders
- Add DELETE /api/orders/:id endpoint: owner can delete completed/cancelled
  orders; admin can delete any. 401/403/404/204 responses. Emits
  order_deleted to owner and assignee for live invalidation.
- Update OpenAPI spec with deleteServiceOrder operation; regenerate
  api-client-react + api-zod.
- Frontend: per-order trash button on completed/cancelled cards with
  confirm dialog; bulk "Clear finished (N)" button at top of list with
  confirm dialog and toast summary (handles partial failures).
- Add ar/en locale keys under myOrders for delete + clear flows
  (singular/plural variants).
- Backend tests cover all paths: pending owner=403, stranger=403,
  owner-cancelled=204, owner-completed=204, admin-pending=204, 404, 401.

All 25 service-order tests pass; the 1 failing test (admin-app-opens-
pagination) is unrelated/pre-existing.

Follow-ups proposed: #70 undo for deleted orders, #71 audit
self-notification on receiver-cancel-own-order.
2026-04-22 06:37:01 +00:00
riyadhafraa 6c4c5600de Notify requester distinctly when receiver cancels their claimed order (Task #67)
Context
- After Task #64 receivers can cancel an order they have already claimed.
- Previously the requester's cancellation notification body was the same regardless of who cancelled (admin vs receiver), giving them no signal that their order had been claimed and then dropped.

Changes
- artifacts/api-server/src/routes/service-orders.ts (PATCH /orders/:id/status, cancel branch):
  - When the cancellation is initiated by the order's currently assigned receiver
    (existing.assignedTo === userId && next === "cancelled"), the requester's
    notification body now includes the bilingual phrase
    "استلم طلبك ولكن تم إلغاؤه لاحقاً" / "Your order was claimed but later
    cancelled by the receiver", appended to the service name for context.
  - Admin / non-assignee cancellations keep the existing service-name body so
    the two cases remain distinguishable in the requester's notifications list.
  - The notification is created via the existing notifyUser() helper, which
    persists to notifications and emits notification_created over Socket.IO,
    so the bell badge updates in real time.
- artifacts/api-server/tests/service-orders.test.mjs:
  - Added "requester gets a distinct notification when the receiver cancels a
    claimed order" covering the full flow: place → confirm-receipt → cancel,
    then asserts the owner has a cancellation notification whose body matches
    the new bilingual receiver-cancel copy.

Verification
- All 7 tests in service-orders.test.mjs pass locally against the running API.

Notes / deviations
- Locale JSON files (ar.json/en.json) were inspected but not modified: notification
  copy is stored server-side as titleAr/titleEn/bodyAr/bodyEn on the notifications
  row; the client just renders those strings directly.

Replit-Task-Id: 5aa480a9-c6e0-4a06-8919-b2b6784d8a98
2026-04-22 06:28:29 +00:00
riyadhafraa 7ebf59d4b7 Task #64: Service Orders — receiver page + admin role toggle
Backend (artifacts/api-server)
- Add admin-only role-toggle endpoints:
  - POST /users/:id/roles { roleName } — idempotent, returns UserProfile
  - DELETE /users/:id/roles/:roleName — idempotent, returns UserProfile
- Allow assigned receiver to cancel their own claimed order
  (received/preparing) in PATCH /orders/:id/status. Owner & admin
  rules unchanged.
- New backend test cases: receiver can cancel own assigned order;
  another receiver gets 403.

API spec / codegen
- Add AddUserRoleBody schema and the two new role endpoints
  under a new "roles" tag in lib/api-spec/openapi.yaml.
- Regenerated api-zod and api-client-react.

Frontend (artifacts/teaboy-os)
- New page src/pages/orders-incoming.tsx at /orders/incoming:
  RBAC-gated (admin || order_receiver), shows "My active orders"
  and "Awaiting receiver" sections, with claim, mark preparing,
  mark completed and cancel buttons. Handles 409 already_claimed.
- Add Inbox button to home top bar, conditional on the same roles.
- Admin users table now has an "Order Receiver" Switch wired to
  the new role-toggle hooks.
- Extend use-notifications-socket to invalidate the incoming-orders
  query on order_incoming_changed and on notification_created with
  type === "order".
- Bilingual locale keys (ar/en) for the new page and admin label.

Tests
- All 25 api-server tests pass (24 existing + 1 new receiver-cancel
  case). All 3 teaboy-os e2e tests pass.

Follow-up filed: #67 (notify requester when receiver cancels).
2026-04-21 18:58:19 +00:00
riyadhafraa 41d5c7e757 Task #63: Service Orders client UI (place order + My Orders)
Adds the user-facing half of the service ordering workflow on top of the
backend foundation merged in Task #62.

What's new:
- Order button on each available service card (hidden when service is
  unavailable) opens a focused OrderServiceModal showing only the
  service name + image with a multi-line notes textarea (500-char cap
  with live counter). Submitting calls POST /orders, shows a toast, and
  invalidates the My Orders cache.
- New /my-orders page lists the user's own orders newest-first with a
  status pill, a horizontal 4-step timeline (pending → received →
  preparing → completed), and a red terminal pill for cancelled orders.
- Cancel action with confirm dialog is shown only while the order is
  pending or received; it calls PATCH /orders/:id/status with
  cancelled.
- "My Orders" link added to the services page header.
- Realtime: the existing notifications socket also invalidates the
  my-orders query on notification_created (when type === 'order') and
  on a new order_updated event.
- Locale keys mirrored in ar.json and en.json for services.order/*,
  myOrders.*, and orderStatus.*. RTL/LTR handled.
- Friendly load-error state with retry, plus client-side sort by
  createdAt desc.

Verification: typecheck clean, all 3 e2e tests pass, manual end-to-end
UI flow (place + cancel + locale switch) verified via testing skill,
backend smoke test confirmed POST /orders + GET /orders/my wiring.
2026-04-21 18:48:48 +00:00
riyadhafraa c20d411c7f Task #63: Service Orders client UI (place order + My Orders)
Adds the user-facing half of the service ordering workflow on top of the
backend foundation merged in Task #62.

What's new:
- Order button on each available service card (hidden when service is
  unavailable) opens a focused OrderServiceModal showing only the
  service name + image with a multi-line notes textarea (500-char cap
  with live counter). Submitting calls POST /orders, shows a toast, and
  invalidates the My Orders cache.
- New /my-orders page lists the user's own orders newest-first with a
  status pill, a horizontal 4-step timeline (pending → received →
  preparing → completed), and a red terminal pill for cancelled orders.
- Cancel action with confirm dialog is shown only while the order is
  pending or received; it calls PATCH /orders/:id/status with
  cancelled.
- "My Orders" link added to the services page header.
- Realtime: the existing notifications socket also invalidates the
  my-orders query on notification_created (when type === 'order') and
  on a new order_updated event.
- Locale keys mirrored in ar.json and en.json for services.order/*,
  myOrders.*, and orderStatus.*. RTL/LTR handled.
- Friendly load-error state with retry, plus client-side sort by
  createdAt desc.

Verification: typecheck clean, all 3 e2e tests pass, manual end-to-end
UI flow (place + cancel + locale switch) verified via testing skill,
backend smoke test confirmed POST /orders + GET /orders/my wiring.
2026-04-21 18:47:30 +00:00
riyadhafraa a8b30baaac Task #62: Service Orders backend foundation (corrected)
After prior code review rejection, refactored to match spec exactly:
- Permission renamed orders:receive → orders.receive (dot form), seeded with order_receiver role
- service_orders table: user_id, service_id, notes, status (pending/received/preparing/completed/cancelled with CHECK), assigned_to, created_at, updated_at
- /confirm-receipt is now the receiver atomic claim (UPDATE WHERE pending+unassigned), 409 already_claimed on miss
- /status accepts preparing|completed|cancelled with permission matrix:
  * preparing/completed: assigned receiver or admin
  * cancelled: owner (pending|received) OR admin (any non-cancelled status)
- requirePermission middleware no longer auto-bypasses admin; admin gets the permission via explicit seed grant
- Notifications use type='order', relatedType='order'
- Realtime emits notification_created (per receiver) + order_incoming_changed (broadcast) + order_updated (owner)
- OpenAPI Order schema rewritten (no quantity, no per-status timestamps), endpoint summaries updated, codegen run
- Tests cover: client place+list, non-receiver 403, no-role 403, parallel claim race (200/409), status matrix, owner-cancel rules, admin-cancel-completed, descriptions excluded from service summary

All 24 api-server tests pass. Ready for code review re-check.
2026-04-21 18:36:25 +00:00
riyadhafraa 2602edaca0 Task #62: Service Orders backend foundation (corrected)
After prior code review rejection, refactored to match spec exactly:
- Permission renamed orders:receive → orders.receive (dot form), seeded with order_receiver role
- service_orders table: user_id, service_id, notes, status (pending/received/preparing/completed/cancelled with CHECK), assigned_to, created_at, updated_at
- /confirm-receipt is now the receiver atomic claim (UPDATE WHERE pending+unassigned), 409 already_claimed on miss
- /status accepts preparing|completed|cancelled with permission matrix:
  * preparing/completed: assigned receiver or admin
  * cancelled: owner (pending|received) OR admin (any non-cancelled status)
- requirePermission middleware no longer auto-bypasses admin; admin gets the permission via explicit seed grant
- Notifications use type='order', relatedType='order'
- Realtime emits notification_created (per receiver) + order_incoming_changed (broadcast) + order_updated (owner)
- OpenAPI Order schema rewritten (no quantity, no per-status timestamps), endpoint summaries updated, codegen run
- Tests cover: client place+list, non-receiver 403, no-role 403, parallel claim race (200/409), status matrix, owner-cancel rules, admin-cancel-completed, descriptions excluded from service summary

All 24 api-server tests pass. Ready for code review re-check.
2026-04-21 18:34:14 +00:00
riyadhafraa cdf5bf4d33 Service Orders — backend foundation (Task #62)
- New `service_orders` table (status pending|claimed|delivered|received|cancelled)
- New `orders:receive` permission + `order_receiver` role; admins implicitly allowed
- Added `requirePermission(name)` and `userHasPermission(userId, name)` middleware helpers
- New routes:
  - POST   /api/orders                    place order (authenticated)
  - GET    /api/orders/my                 list current user's orders
  - GET    /api/orders/incoming           receivers see pending+active visible orders
  - PATCH  /api/orders/:id/status         claim (atomic), deliver, cancel
  - PATCH  /api/orders/:id/confirm-receipt requester confirms a delivered order
- Atomic claim via UPDATE ... WHERE status='pending' AND assigned_to IS NULL
  (returns 409 already_claimed on race)
- Realtime: emits `notification_created` to receivers/requester,
  `order_incoming_changed` to all receivers, `order_updated` to requester
- Service shape in order responses limited to id/nameAr/nameEn/imageUrl
  (no description fields), per spec
- OpenAPI updated with new paths and schemas; codegen run
- Seed updated idempotently (permission, role, role_permissions)
- New tests in artifacts/api-server/tests/service-orders.test.mjs
  (full lifecycle, atomic claim race, unauth rejection) — all 21 api tests pass

No deviations from the planned scope. Tasks #63 (client UI) and #64
(receiver page + admin role toggle) remain blocked-by #62 and are next.
2026-04-21 18:24:20 +00:00
riyadhafraa 59105b0bbf Move the ability to hide the top bar clock into the clock settings menu
Integrates the top bar clock's visibility toggle into the ClockStylePicker component and removes the standalone eye button. Updates locale files for Arabic and English to reflect the new string for showing the top bar clock.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 89fb8094-c538-4564-8b9b-29c0f4236486
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PwtbShH
Replit-Helium-Checkpoint-Created: true
2026-04-21 18:04:17 +00:00
riyadhafraa 8ad2925ee3 Add ability to hide the top bar clock display independently
Introduced a new hook `useTopbarClockVisibility` in `artifacts/teaboy-os/src/components/clock.tsx` to manage the visibility of the top bar clock, separate from the home clock widget. Updated `artifacts/teaboy-os/src/pages/home.tsx` to utilize this new hook, allowing the eye icon to toggle the top bar clock's visibility.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 08cff793-79ae-45ad-9958-d029baf1737d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PwtbShH
Replit-Helium-Checkpoint-Created: true
2026-04-21 18:02:45 +00:00
riyadhafraa ea9dc3f121 Add ability to hide the small clock in the top bar
Conditionally render the Clock component in HomePage based on `homeClockVisible` state.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: c54122d2-bcd6-45f5-9567-1082d8006c1b
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PwtbShH
Replit-Helium-Checkpoint-Created: true
2026-04-21 18:00:59 +00:00
riyadhafraa 09f3bf72e4 Task #61: Add quick hide-clock toggle to home top bar
Added a dedicated Eye/EyeOff icon button next to the existing
ClockStylePicker in the home top bar. Clicking it instantly hides or
shows the home wall-clock widget without opening any popover.

- Wired to the existing useHomeClockVisibility hook so it stays in
  sync with the toggle inside the clock-style popover (same storage
  key + custom event).
- Reuses existing locale keys home.clockStyle.hideWidget /
  home.clockStyle.showWidget for aria-label and title (flips with
  state).
- aria-pressed reflects hidden state for accessibility.
- No new locale keys, no changes to default position behavior, and
  the popover toggle is left intact as requested.

Files:
- artifacts/teaboy-os/src/pages/home.tsx (added Eye/EyeOff imports
  and the new icon button between ClockStylePicker and the language
  toggle).
2026-04-21 17:58:59 +00:00
riyadhafraa d5b5ebb7f0 Task #61: Add quick hide-clock toggle to home top bar
Added a dedicated Eye/EyeOff icon button next to the existing
ClockStylePicker in the home top bar. Clicking it instantly hides or
shows the home wall-clock widget without opening any popover.

- Wired to the existing useHomeClockVisibility hook so it stays in
  sync with the toggle inside the clock-style popover (same storage
  key + custom event).
- Reuses existing locale keys home.clockStyle.hideWidget /
  home.clockStyle.showWidget for aria-label and title (flips with
  state).
- aria-pressed reflects hidden state for accessibility.
- No new locale keys, no changes to default position behavior, and
  the popover toggle is left intact as requested.

Files:
- artifacts/teaboy-os/src/pages/home.tsx (added Eye/EyeOff imports
  and the new icon button between ClockStylePicker and the language
  toggle).
2026-04-21 17:57:12 +00:00
riyadhafraa 8b0c5b3984 Allow clock to be pinned to the far left in RTL mode
Update home component logic to correctly pin the clock to the far-left position in RTL layouts when it's unset or dragged to the end of the app list, ensuring it occupies the first two columns.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: a6f7b52f-d48d-41b0-9435-36a5f4524394
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PwtbShH
Replit-Helium-Checkpoint-Created: true
2026-04-21 17:53:16 +00:00
riyadhafraa 4d78628fa1 Fix clock placement and display issues in different screen sizes and orientations
Adjust clock positioning logic to correctly handle large clock sizes and RTL layouts, ensuring it displays properly on the screen.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: fa247766-9a81-4c0c-856e-735de5ba41b7
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PwtbShH
Replit-Helium-Checkpoint-Created: true
2026-04-21 17:51:35 +00:00
riyadhafraa f5d50aa4e7 Adjust the large clock's default position to the far left in RTL view
Set the large clock's starting column to ensure it spans the last two columns in RTL, visually pinning it to the leftmost position.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: d13a5b95-5806-4d14-8588-81f7e67a4eac
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PwtbShH
Replit-Helium-Checkpoint-Created: true
2026-04-21 17:50:23 +00:00
riyadhafraa 0fa79d6b3c Set clock to the far left by default in Arabic mode
Modify the home page component to allow the clock tile to be pinned to the leftmost grid column in RTL layouts when its position is unset.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 1bb970e6-955c-4abc-a859-c78241c792f1
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PwtbShH
Replit-Helium-Checkpoint-Created: true
2026-04-21 17:45:06 +00:00
riyadhafraa a4b9ad9b86 Remove close button from clock widget and simplify its props
Remove the 'X' close button from the clock widget and refactor its props, simplifying the component by removing unused `onHide` and `hideLabel` functionalities.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: b2e77920-44ee-4103-9c61-e01b6ccb0094
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PwtbShH
Replit-Helium-Checkpoint-Created: true
2026-04-21 17:41:46 +00:00
riyadhafraa 369e418fd3 Hide scrollbars across the entire website while keeping scrolling functional
Add global CSS rules to `index.css` to hide scrollbars using `scrollbar-width: none`, `-ms-overflow-style: none`, and `::-webkit-scrollbar` properties, ensuring scrolling remains functional via mouse wheel or touch.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: a69cb334-1c57-4350-9daf-a639b8f19357
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PwtbShH
Replit-Helium-Checkpoint-Created: true
2026-04-21 17:39:31 +00:00
riyadhafraa 1ef055c245 Remove home greeting and date block (Task #60)
Per user request, the home page no longer shows the time-of-day greeting
("مساء الخير، {name}") nor the day-name + Gregorian date subtitle. The
apps grid (and the admin stats row when applicable) now sits directly
under the top bar.

Changes (artifacts/teaboy-os/src/pages/home.tsx):
- Removed the entire greeting block (the wrapper div, the <h1> greeting
  and the date <p>) from the Main Content section.
- Removed the now-unused `dayName`, `gregorianDate` and `greeting`
  locals plus the `formatWeekday` and `greetingKey` imports.
- Kept `displayName` (still used in the avatar header) and the
  `home.greeting.*` translation keys (left in place; cheap to keep and
  avoids touching unrelated locale files).

Mid-task fix:
- First pass also dropped `displayName` which broke the avatar
  (`displayName is not defined`). Restored it; `displayName` is needed
  by the user-card initials and name labels.

Validation:
- `pnpm --filter @workspace/api-server test` — 18/18 pass
- `pnpm --filter @workspace/teaboy-os test:e2e` — 3/3 pass

Notes:
- Pre-existing TypeScript errors in admin.tsx / clock-style-picker.tsx /
  clock.tsx are unrelated to this task (codegen drift from earlier
  tasks).
2026-04-21 17:37:46 +00:00
riyadhafraa 729f00d156 Revert clock-hidden hint and remove "My Apps" header (rebased)
Original task #58 was to add a dismissible hint when the home clock is
hidden. The hint was implemented and verified end-to-end (EN + AR/RTL),
but the user then asked to remove it. This commit removes the hint and
also removes the "تطبيقاتي · N" / "My Apps · N" header above the apps
grid per the user's request to show the apps directly.

Changes:
- artifacts/teaboy-os/src/pages/home.tsx
  - Removed the hidden-clock hint UI block, its session-storage state
    (clockHintDismissed) and the related useEffect/dismiss helper.
  - Removed the unused Clock-icon lucide import.
  - Removed the "My Apps" heading row (h3 + count) above the apps grid.
- artifacts/teaboy-os/src/locales/en.json, ar.json
  - Removed the new keys: home.clockStyle.hiddenHint,
    hiddenHintAction, dismissHint.

Rebase notes:
- Rebased onto main (a05249f). Two conflicts:
  * artifacts/teaboy-os/public/opengraph.jpg — binary asset unrelated
    to this task; took main's version (--ours during rebase).
  * artifacts/teaboy-os/src/pages/home.tsx — main re-styled the apps
    grid header (flex/gap layout with inline " · N" count) and added
    a useGridCols() hook. My commit removes that header entirely per
    user request, so kept the user-requested removal while preserving
    main's useGridCols() addition. No semantic divergence — just the
    deletion still applies cleanly to the new header markup.

Notes:
- Clock can still be hidden via the X on the clock tile and shown
  again via the clock-style popover in the top bar (existing behavior).
- Translation key home.myApps is still defined but no longer used on
  the home page; left in place to avoid touching other consumers.
- Pre-existing TypeScript errors on AuthUser.clockStyle/clockHour12
  in chat.tsx/home.tsx are unrelated to this task.

Replit-Task-Id: 14544719-933c-49f8-84a0-6577d06fbc14
2026-04-21 17:32:28 +00:00
riyadhafraa d9a4c1ba34 Task #59: Clean home clock tile and tighten My Apps header.
Original ask (Arabic):
- Why does a small "4" appear on the left of the home page?
- Make the clock movable to all positions, default to the visual-left,
  remove its frame, and keep it parallel/aligned with the app icons.

Changes:
- artifacts/teaboy-os/src/pages/home.tsx
  - My Apps header: replaced `justify-between` with `gap-2`, prefixed
    the count with "·" so it now reads "تطبيقاتي · 5" / "MY APPS · 5"
    inline with the title. The standalone count badge that floated to
    the opposite edge in RTL is gone.
  - SortableClockTile inner box: removed the `bg-white/85
    dark:bg-white/10 backdrop-blur-md` and the `icon-tile` rounded card
    classes. The analog clock face now renders directly on the
    wallpaper. Outer width/height (78px compact, 168px large) and the
    hover/active scale animation are preserved so grid alignment with
    the app icons is unchanged.
  - Default clock position is now language-aware: when no per-user
    saved position exists, the clock defaults to the visually-leftmost
    cell of the first row — last DOM index in RTL (Arabic),
    first DOM index in LTR (English).

- artifacts/teaboy-os/src/components/clock.tsx
  - `readHomeClockPosition` now returns -1 as the "unset" sentinel
    instead of 0, so home.tsx can apply a language-aware default
    without overwriting saved positions.

Verification:
- Manual e2e: confirmed header has the inline count, clock has no
  frame, RTL default position is on the visual-left, drag-and-drop
  still persists across reload, long-press still toggles compact ↔
  large. All passed.

Out of scope: no server-side persistence; no changes to the topbar
clock, the dock, or the clock-style picker.
2026-04-21 13:40:51 +00:00
riyadhafraa 518d3267b6 Task #59: Clean home clock tile and tighten My Apps header.
Original ask (Arabic):
- Why does a small "4" appear on the left of the home page?
- Make the clock movable to all positions, default to the visual-left,
  remove its frame, and keep it parallel/aligned with the app icons.

Changes:
- artifacts/teaboy-os/src/pages/home.tsx
  - My Apps header: replaced `justify-between` with `gap-2`, prefixed
    the count with "·" so it now reads "تطبيقاتي · 5" / "MY APPS · 5"
    inline with the title. The standalone count badge that floated to
    the opposite edge in RTL is gone.
  - SortableClockTile inner box: removed the `bg-white/85
    dark:bg-white/10 backdrop-blur-md` and the `icon-tile` rounded card
    classes. The analog clock face now renders directly on the
    wallpaper. Outer width/height (78px compact, 168px large) and the
    hover/active scale animation are preserved so grid alignment with
    the app icons is unchanged.
  - Default clock position is now language-aware: when no per-user
    saved position exists, the clock defaults to the visually-leftmost
    cell of the first row — last DOM index in RTL (Arabic),
    first DOM index in LTR (English).

- artifacts/teaboy-os/src/components/clock.tsx
  - `readHomeClockPosition` now returns -1 as the "unset" sentinel
    instead of 0, so home.tsx can apply a language-aware default
    without overwriting saved positions.

Verification:
- Manual e2e: confirmed header has the inline count, clock has no
  frame, RTL default position is on the visual-left, drag-and-drop
  still persists across reload, long-press still toggles compact ↔
  large. All passed.

Out of scope: no server-side persistence; no changes to the topbar
clock, the dock, or the clock-style picker.
2026-04-21 13:39:22 +00:00
riyadhafraa 967cd628fa Add e2e test for home clock position and size persistence
Task #57: add an automated check that exercises the same flow that was
previously only manually verified — drag the home-page clock tile to a new
grid slot, long-press to flip it from compact to large, reload, and assert
both the position and size survive.

Implementation
- New Playwright spec at
  artifacts/teaboy-os/tests/home-clock-persistence.spec.mjs, mirroring the
  conventions of the existing leave-group-successor.spec.mjs (DB-seeded
  user, UI login, cleanup in afterAll).
- Drives a real pointer drag that satisfies dnd-kit's 8px activation
  threshold, then a separate long-press (>500ms, no movement) to trigger
  the size toggle in home.tsx without aborting the timer.
- Asserts both per-user localStorage keys are written
  (teaboy:home-clock-position:<id> and teaboy:home-clock-size:<id>) and
  that, after page.reload(), the clock tile still has col-span-2 / row-span-2
  classes and lives at the persisted index in the grid (not just in
  storage).

Verification
- Ran the new spec in isolation: 1 passed.
- Ran the full validation workflow (api-server tests + teaboy-os
  test:e2e): 18 + 3 passed.

No production code changes.

Replit-Task-Id: bd8d8372-358c-4676-b842-956dc0813cb8
2026-04-21 13:22:37 +00:00
riyadhafraa f56cd425c8 Add one-tap hide control to home clock tile
Original task: #56 — Let users hide or show the home clock with one tap.

Changes:
- artifacts/teaboy-os/src/pages/home.tsx: Added a small dismiss "×"
  button overlay on the SortableClockTile. The button stops pointer
  propagation so it does not trigger drag or the existing long-press
  size toggle. It calls setHomeClockVisible(false) (now destructured
  from useHomeClockVisibility) to hide the tile in one tap. The button
  uses absolute positioning with `end-1` so it works in both LTR and
  RTL. It is always visible at large size and revealed on hover/focus
  at compact size to keep the tile clean.
- artifacts/teaboy-os/src/locales/{en,ar}.json: Added
  `home.clockStyle.hideWidget` ("Hide clock" / "إخفاء الساعة") used
  as the button's aria-label and tooltip.

Re-showing the clock is handled by the existing toggle in the clock
style picker popover (already bilingual), so users can bring the
clock back later. Hiding/showing keeps the persisted grid position.

Notes / deviations:
- Did not add a context-menu since long-press is already used for the
  size toggle on the same tile and would conflict.
- Pre-existing TypeScript errors in unrelated files (admin.tsx,
  clock-style-picker.tsx codegen drift) are not addressed here.

Replit-Task-Id: 87bb057d-dc81-4916-8bb1-b24e34ff4794
2026-04-21 13:16:15 +00:00
riyadhafraa 2eddc6d407 Task #55: Movable, resizable home clock with Latin digits
- Refactor AnalogClockWidget: Latin 1-12 digits (was Roman), `size`
  (px) prop with scale-based interior, optional `showLabels`.
- Add useHomeClockSize (compact|large) and useHomeClockPosition hooks
  in clock.tsx — both persist to localStorage and broadcast a custom
  event so consumers stay in sync within the tab.
- home.tsx: introduce SortableClockTile that participates in the same
  dnd-kit grid as app icons via id `__clock`. App ids now prefixed
  `app-` so they don't collide. Long-press (500ms) toggles
  compact↔large; large = col-span-2 row-span-2. Custom pointer
  handlers chain with dnd-kit's listener so drag still works.
- handleDragEnd computes new app order excluding the clock and only
  fires updateOrder when app order actually changed.
- Remove the standalone AnalogClockWidget block above the apps grid;
  clock now lives inside the grid.
- When homeClockVisible is false, the clock is also excluded from
  sortable items (no dnd-kit warnings).
- e2e test passed: in-grid placement, Latin digits, long-press
  resize, drag-reorder, position persists across reload.
2026-04-21 13:04:45 +00:00