Commit Graph

49 Commits

Author SHA1 Message Date
riyadhafraa fe736e3ff7 Task #146: Filter and paginate role permission history
Summary
- Backend: GET /api/roles/:id/audit now accepts limit (default 10, max 200),
  offset, actorUserId (0 = no filter), and from/to (YYYY-MM-DD UTC). The
  response is now a paginated envelope `{entries, totalCount, limit, offset,
  nextOffset}` instead of a bare array.
- OpenAPI: lib/api-spec/openapi.yaml updated with the new params and a new
  RolePermissionAuditList schema; client hooks regenerated via
  `pnpm --filter @workspace/api-spec run codegen`.
- Frontend: RolePermissionHistory in artifacts/tx-os/src/pages/admin.tsx is
  now self-contained — owns its own filter state, fetches the user list for
  the actor dropdown, applies actor changes immediately, and validates date
  inputs (rejecting invalid / inverted ranges).
- Pagination: switched to TRUE OFFSET PAGINATION. The first page comes from
  React Query (so live cache invalidations after a save still refresh it),
  and subsequent "Load more" clicks fetch `offset = nextOffset` imperatively
  via getRolePermissionAudit() and append the rows to local state. There is
  no client-side ceiling on how far back an admin can page; we simply stop
  showing the button when nextOffset is null. A filtersKey effect resets the
  appended pages whenever any filter (actor / from / to) changes so we never
  serve overlapping or out-of-order rows.
- i18n: added missing keys in artifacts/tx-os/src/locales/{en,ar}.json
  (historyEmptyFiltered, historyShowing, historyLoadMore, historyFilters.*,
  historyErrors.*).
- Tests: artifacts/api-server/tests/role-permission-audit.test.mjs updated
  to read the new envelope and now also covers offset-based pagination,
  actorUserId filtering (including the "0 = no filter" semantic), and
  date-range filtering (in-range / past-range / inverted / garbage). All 9
  audit tests pass; tx-os typecheck clean.
- E2E: ran the testing skill end-to-end against /admin → role edit dialog →
  history panel: created a fresh role through the UI, made 25 permission
  writes, verified 10 → 20 → 25 pagination with no duplicates and correct
  hide-on-end behaviour, filter-by-actor reset to first page, empty date
  range showed empty state, inverted dates surfaced the validation error.

Drift / notes
- Pre-existing executive-meetings tests in the `test` workflow are still
  failing — unchanged by this task.
- The Arabic language toggle isn't exposed in the page header in this build,
  so the e2e Arabic step was skipped; locale strings are in place and the
  test ids do not change with locale.
- Code review (initial pass) flagged a 200-row UI ceiling in the previous
  "growing limit" approach. Replaced with true offset pagination (described
  above) so admins can scroll back through arbitrarily long histories.

Code review follow-ups (round 2)
- Tightened parseRoleAuditUtcDate to reject impossible calendar days
  (2024-02-31, 2025-13-01, etc.) instead of silently rolling forward.
- Aligned OpenAPI: actorUserId schema is now `minimum: 0` so the spec
  matches the runtime "0 = no filter" contract; client regenerated.
- Added a targeted backend test that asserts impossible dates → 400.

Code review follow-ups (round 3)
- UX: when applied filters become invalid, the History list now hides the
  stale entries and shows the "fix filters first" hint instead, and the
  Load more button is hidden until filters are valid again. Avoids
  presenting yesterday's results as the current view.

Code review follow-ups (round 4)
- Belt-and-braces: also reset the appended history pages when the first
  page's totalCount + first-row id signature changes, so an external cache
  invalidation (e.g. another save while the dialog is still open) cannot
  leave appended pages out of sync with the refreshed first page.

Replit-Task-Id: cc3c9e83-921f-4f72-b443-79f95f6467b1
2026-04-30 10:45:07 +00:00
riyadhafraa 058cb8b817 Make forced-delete dependency chips clickable to pivot the audit log
Task #134: Admins investigating a forced deletion can now click any
dependency chip on a force_delete row to jump to a pre-filtered audit log
view of the related history.

Backend (artifacts/api-server, lib/api-spec):
- Added targetType + targetId query params to GET /api/admin/audit-logs
  and its CSV export. targetId is validated as a positive integer (400
  on bad input). Codegen regenerated for the React API client.

Frontend (artifacts/tx-os/src/pages/admin.tsx):
- Dependency chips on forced-delete rows are now real <button> elements
  with aria-labels and keyboard focus. Non-deletion rows are unchanged.
- Chip → pivot mapping: groupCount→targetType=group,
  memberCount→targetType=user, appCount→targetType=app,
  roleCount→targetType=role; cascade chips (orderCount, messageCount,
  noteCount, etc.) pivot to the parent (targetType+targetId).
- Active filter is reflected in the URL hash (deep-linkable + reload
  safe) and shown as a dismissible pill in the panel; the pill includes
  a Clear button. Switching audit sub-section drops the filter.
- Section sync logic preserves hash params and uses a one-shot
  skipNextSectionSync ref so initial deep-linked hashes aren't clobbered.
- New i18n keys in en.json and ar.json for filter labels and chip
  aria-labels.

Tests:
- New backend tests in artifacts/api-server/tests/audit-logs-target-filter.test.mjs
  cover targetType narrowing, targetType+targetId narrowing, invalid
  targetId rejection, combination with forcedOnly, and CSV export
  honoring the new filters (7 tests, all passing).
- Verified end-to-end via the browser testing skill: chip click,
  filter pill, clear, deep-link reload all behave correctly.

Pre-existing unrelated failures (not touched): two PDF archive tests in
executive-meetings.test.mjs and the matching typecheck errors in
executive-meetings.ts.

Replit-Task-Id: 46f972ef-2874-4fc3-95c5-53d0ff0732e9
2026-04-30 07:25:16 +00:00
riyadhafraa 8b2fe3164d Task #109: Admin UI to manage app required permissions
- Added 3 admin-only API endpoints in artifacts/api-server/src/routes/apps.ts:
  - GET    /api/apps/:id/permissions   — list permissions gating an app
  - POST   /api/apps/:id/permissions   — add a permission (idempotent via
    onConflictDoNothing() on the (app_id, permission_id) composite PK)
  - DELETE /api/apps/:id/permissions/:permissionId — remove (idempotent, 204)
- Documented the new endpoints in lib/api-spec/openapi.yaml with two new schemas
  (AddAppPermissionBody, AppPermissionLink) and re-ran codegen.
- Added a "Required permissions" section (AppPermissionsEditor) to the existing
  Edit App dialog in artifacts/tx-os/src/pages/admin.tsx, using the generated
  hooks. The section is shown only when editing an existing app (it needs an
  app id). Wired up admin.appPermissions.* i18n keys in en.json + ar.json.
- Added artifacts/api-server/tests/app-permissions-crud.test.mjs with 5 tests
  (empty list, idempotent add, 404 on unknown app/perm, idempotent delete,
  403 for non-admins). All 5 pass; related tests
  (app-permissions-unique, apps-group-visibility, list-dependency-counts) still pass.
- Verified the new admin UI end-to-end with the testing skill: admin login,
  open Edit App dialog, add/remove a required permission, and confirm the
  section is hidden in the Add App dialog.

Notes / scope:
- Pre-existing duplicate rows in app_permissions had to be deduped and
  `pnpm --filter @workspace/db run push` was run once so the composite PK
  could be added (separate task "Re-run the Drizzle schema push" already
  exists for this project-wide chore).
- No audit logging here — separate existing task already covers it.
- The "test" workflow shows a pre-existing ECONNREFUSED race; an existing
  task already tracks making the test workflow wait for the API server.

Replit-Task-Id: 912bb163-6b6f-43d3-9934-41fc97519337
2026-04-29 15:31:42 +00:00
riyadhafraa a14b589006 Audit role permission changes (Task #100)
Record an audit trail every time a role's permissions change and surface
recent history inside the role edit dialog.

Schema (lib/db):
- New `role_permission_audit` table: id, roleId (FK roles), actorUserId
  (FK users, nullable on delete), previousPermissionIds int[],
  newPermissionIds int[], createdAt. Created via raw SQL because
  `drizzle push` currently fails on a pre-existing app_permissions
  duplicate (followup #148 tracks fixing this).

API (artifacts/api-server):
- PUT /api/roles/:id/permissions now wraps the permission delete/insert,
  the legacy audit_logs row, and the new role_permission_audit row in
  a *single* transaction so the permission set and its audit trail
  always commit (or roll back) together. Previous IDs are also read
  inside the transaction to avoid races.
- A role_permission_audit row is written on EVERY PUT call (per task
  spec), including no-op saves; the GET handler computes
  addedPermissionIds/removedPermissionIds so the History UI can
  distinguish meaningful changes from no-op saves.
- New GET /api/roles/:id/audit (admin-only, ?limit=1..50, default 10)
  returns recent entries newest-first with computed
  added/removedPermissionIds and joined actor info.
- New tests in tests/role-permission-audit.test.mjs cover write,
  every-call write (incl. no-op), GET ordering+diff+actor, no-op diff
  reporting, 404, and limit.

Drive-by fixes:
- Fixed pre-existing syntax corruption in tests/apps-open.test.mjs
  (stray `ccaPassa,` line from a prior bad merge that prevented the
  whole api-server test workflow from running).
- Made tests/roles-crud.test.mjs "rejects an invalid name" robust to
  parallel test execution by scoping the leak check to the bad name
  instead of relying on a global row count.

API spec / codegen (lib/api-spec, lib/api-client-react):
- Added `getRolePermissionAudit` operation and
  `RolePermissionAuditEntry` schema; ran orval codegen.

Frontend (artifacts/tx-os):
- New RolePermissionHistory component renders inside the role edit
  dialog (between permissions and Save/Cancel) using
  useGetRolePermissionAudit. Shows timestamp, actor, added/removed
  permission names (resolved via permissionsById), and total count.
- Save invalidates the audit query so the new entry appears immediately
  on the next open.
- Bilingual i18n strings (en + ar with full Arabic plural variants:
  zero/one/two/few/many/other) under admin.roles.history*.

Verification:
- tx-os typecheck passes.
- All 5 new audit tests + 13 existing roles tests pass.
- E2E (testing skill) verified the full flow: empty state on a fresh
  role, save adds a permission, reopened dialog shows the new entry
  with actor name in Arabic.

Docs:
- replit.md updated to list `role_permission_audit` in Database Tables.

Follow-ups proposed: #146 paginate/filter history, #147 audit other
admin permission changes, #148 fix drizzle push duplicate.

Replit-Task-Id: 9b8886a2-6e76-4072-b345-a772421fa161
2026-04-29 13:16:02 +00:00
riyadhafraa 3aa6e81ed7 Show users/groups impact before removing role permissions (Task #99)
Adds a per-permission impact preview to the role editor so admins can see
who would lose each permission before saving — and a confirmation step
when the change would actually revoke access from at least one user.

API
- New endpoint POST /api/roles/:id/permissions/impact-preview
- Body: { permissionIds: number[] } (the proposed kept set)
- Response: { removed: [{ permissionId, permissionName, userCount,
  groupCount, groups: [{id,name}] }], totalAffectedUsers }
- A user is counted as "affected" only when no other role they hold
  (direct or via groups) still grants the removed permission.
- Implemented in artifacts/api-server/src/routes/roles.ts using two
  drizzle selectDistinct queries (direct + via groups) merged in JS.
  Switched away from a sql`${arr}::int[]` approach that failed because
  drizzle expands JS arrays as a parameter list, not as a single array
  parameter. Now uses inArray().

OpenAPI / client
- Added schemas RolePermissionsImpactBody, RolePermissionImpactGroup,
  RolePermissionImpactItem, RolePermissionsImpact in
  lib/api-spec/openapi.yaml.
- Regenerated lib/api-client-react/src/generated/* (new
  usePreviewRolePermissionsImpact hook).

UI
- artifacts/tx-os/src/pages/admin.tsx RolesPanel:
  - Debounced (350ms) on-demand fetch only when at least one permission
    has been unchecked relative to the saved state.
  - Inline amber summary panel listing each removed permission with
    user/group counts, plus a total-affected-users line.
  - Confirmation modal before save when totalAffectedUsers > 0.
- Added EN/AR translations (impactTitle, impactPerPermission,
  impactViaGroups, impactTotal, confirmRemoval*) using i18next
  _one/_other plural keys to match existing pluralized strings.

Tests
- New artifacts/api-server/tests/role-permissions-impact.test.mjs
  (6 tests, all green): empty removals, group-derived impact,
  "covered by another role" exclusion, 404 on unknown role, 400 on
  invalid body, 403 for non-admin.
- Existing roles-crud tests still pass.
- E2E flow verified end to end via the testing tool: amber summary
  appears, confirmation dialog gates the destructive save, and the
  resulting permission set persists.

Code-review follow-ups (applied in this same task)
- Tightened the OpenAPI 404 description for the new endpoint to clarify
  that unknown permission IDs in the body are tolerated (only a missing
  role yields 404). Regenerated the API client.
- Added a request-sequencing guard in the role editor so a stale
  in-flight impact-preview response cannot overwrite a newer selection.
- When the impact preview fails while removals exist, the inline panel
  now shows an explicit error message and the Save button is disabled
  until the user resolves it (e.g. by changing the selection so the
  preview can re-run successfully). EN/AR translations added under
  admin.roles.impactError.

Notes
- The pre-existing test workflow failure (ECONNREFUSED on 127.0.0.1:8080
  before the API server is ready) is unrelated and already tracked.
- replit.md not updated — change is endpoint-level and does not alter
  documented architecture.
- A modification to artifacts/tx-os/public/opengraph.jpg may show up in
  the diff; it is a build artifact regenerated by the vite dev server
  on restart and is not part of this change.

Replit-Task-Id: 30b7a2f7-7b60-429a-89d4-79280a81803f
2026-04-29 12:45:19 +00:00
riyadhafraa 59c7338d63 Task #98: Warn admins before renaming a role used in code references
- Added GET /api/roles/:id/usage endpoint returning {userCount, groupCount},
  guarded by requireAdmin and validating the role id.
- Updated PATCH /api/roles/:id audit metadata to record renamed=true,
  renamedFrom, and renamedTo when the role name changes (in addition to
  existing name fields), satisfying the traceability requirement.
- Added RoleUsage schema + /roles/{id}/usage operation to openapi.yaml,
  regenerated lib/api-zod via orval. Codegen script in lib/api-spec was
  updated intentionally to preserve `export * from './manual'` in
  api-zod's index, which orval was overwriting.
- RolesPanel edit dialog (admin.tsx) now tracks the original role name
  on open and shows an amber warning banner with from/to text plus a
  usage line (users + groups counts) whenever the trimmed name input
  differs from the original. Warning is suppressed for system or
  protected roles since their name input is disabled.
- Added en/ar locale strings: renameWarningTitle, renameWarningBody,
  renameUsage.
- Reverted accidental opengraph.jpg change picked up earlier in the
  session — it is unrelated to this task.

Verification: e2e test created a non-system role, assigned the admin
user to it, opened the edit dialog, observed the warning + usage line,
saved the rename, and confirmed both the role name persisted and the
audit log captured renamed/renamedFrom/renamedTo.

Note: The pnpm `test` workflow shows pre-existing failures in
executive-meetings.ts that are unrelated to this task (verified via
git stash before changes).

Replit-Task-Id: c1ee048d-50c6-4439-a430-ee0b7ec09959
2026-04-29 10:57:44 +00:00
riyadhafraa 896f690ac0 Git commit prior to merge 2026-04-29 10:32:54 +00:00
riyadhafraa c0cf2115dd Task #96: Show dependency counts in admin app/service/user lists
Goal: make the admin delete dialog show its dependency warning on
the FIRST click (matching the existing Groups UX), instead of
needing a 409 round-trip from the DELETE endpoint to populate the
warning.

Changes:
- lib/api-spec/openapi.yaml: added optional count fields to App
  (groupCount, restrictionCount, openCount), Service (orderCount),
  and UserProfile (noteCount, orderCount, conversationCount,
  messageCount).
- artifacts/api-server/src/routes/apps.ts: GET /admin/apps now
  batch-loads groupCount/restrictionCount/openCount via grouped
  COUNT queries on group_apps, app_permissions, and app_opens.
- artifacts/api-server/src/routes/services.ts: GET /services now
  batch-loads orderCount from service_orders.
- artifacts/api-server/src/routes/users.ts: GET /users now batch-
  loads noteCount/orderCount/conversationCount/messageCount from
  notes, service_orders, conversations.created_by, and
  messages.sender_id.
- artifacts/tx-os/src/pages/admin.tsx: Apps, Services, and Users
  delete buttons pre-populate their conflict-state from the row's
  count fields so the DeletionWarningDialog shows the warning on
  the first click. The lazy 409 fallback still works as a safety
  net for any caller without counts.
- replit.md: documented the new admin-panel delete UX.

Tests:
- Added artifacts/api-server/tests/list-dependency-counts.test.mjs
  with 6 tests verifying each list endpoint exposes the new count
  fields with the expected non-zero values when dependents exist
  AND zero values when they do not (apps, services, users).
- Existing artifacts/api-server/tests/delete-force-warnings.test.mjs
  (9 tests) still passes — DELETE behavior unchanged.
- Verified end-to-end with a Playwright browser test: clicking the
  service delete icon ONCE opens the confirmation modal with the
  dependency warning ("orders", count >= 1) immediately.

Notes / drift:
- Count fields were added to the shared App/Service/UserProfile
  schemas (not list-only sub-schemas) so the same shape is returned
  from list and detail endpoints. Code review flagged this as
  acceptable but broader than strictly necessary; left as is to
  keep the API consistent.

Out of scope (already pre-existing on main, tracked as follow-ups):
- artifacts/api-server/tests/apps-open.test.mjs has a syntax error.
- artifacts/api-server/tests/app-permissions-unique.test.mjs has
  two failing assertions about the composite primary key.
- artifacts/api-server/src/routes/executive-meetings.ts has 3 pre-
  existing typecheck errors around the font-settings scope column.
- The `test` workflow exits with ECONNREFUSED when the api-server
  isn't already up — needs a readiness check.

Replit-Task-Id: bd14fe73-9961-431b-ab5a-ab70f116e8c7
2026-04-29 08:48:29 +00:00
riyadhafraa be4ecb30bb Task #95: Let admins export the audit log as CSV
What was done
- Added a new admin-only endpoint GET /api/admin/audit-logs/export that streams
  the filtered audit log as a CSV download. Honors the same `action`, `from`,
  and `to` query parameters as the list endpoint, validated through a shared
  parseFilters/buildWhere helper extracted from the existing handler.
- Columns: action, actor_username, target_type, target_id, created_at,
  metadata (compact JSON). Rows ordered newest-first and capped at 50,000 to
  bound response size. UTF-8 BOM prepended so spreadsheet apps (incl. Excel)
  detect encoding correctly for Arabic content stored in metadata.
- Response sets Content-Type: text/csv; charset=utf-8,
  Content-Disposition: attachment; filename="audit-log-YYYY-MM-DD.csv",
  Cache-Control: no-store, and is written via res.write streaming.
- Updated lib/api-spec/openapi.yaml with operationId exportAuditLogsCsv and
  ran codegen to regenerate the React client + Zod schemas.
- Frontend (artifacts/tx-os/src/pages/admin.tsx → AuditLogPanel): added an
  "Export CSV" button next to the existing filter actions. Clicking it
  fetches the export URL with current filters using same-origin cookies,
  triggers a browser download, and surfaces a localized error if the request
  fails. The button is disabled while filters are invalid or while a
  download is in flight, and a spinner replaces the icon during export.
- Added bilingual translation keys admin.audit.export.button /
  admin.audit.export.failed in en.json and ar.json.

Security hardening (post code-review)
- csvEscape now prefixes a single quote when a cell value begins with one of
  =, +, -, @, tab, or CR, mitigating CSV/spreadsheet formula injection
  (Excel, LibreOffice, Google Sheets evaluate such cells as formulas).
  Verified end-to-end by inserting a synthetic audit row whose action started
  with "=" and confirming the export wrote it as "'=...".

Verification
- Typecheck of tx-os passes; api-server has only pre-existing executive-meetings
  errors unrelated to this change.
- Manual curl smoke test: 200 with correct headers, BOM present, action and
  date filters honored, invalid date returns 400.
- E2e UI test (Playwright via testing skill): logged in as admin, opened
  the Audit Log section, downloaded the unfiltered CSV (correct filename and
  header row), then re-exported with action=app.delete and confirmed only
  matching rows came back.
- Targeted security check confirmed CSV injection mitigation (see above).

Notes / drift
- None for the spec. The generated `exportAuditLogsCsv()` helper in
  lib/api-client-react is typed Promise<Blob> but, because customFetch
  auto-infers text/csv as text, would currently return text if anyone called
  it directly. The Audit Log UI uses a raw fetch with credentials: "include"
  to download the blob, so this does not affect the feature. Updating the
  generated client's responseType handling is project-wide config and is
  intentionally out of scope here.

Follow-ups proposed
- #123 Add automated tests for the audit log CSV export (test_gaps)
- #124 Let admins pick which columns to include when exporting the audit log
  (next_steps)

Replit-Task-Id: 88a50100-caa7-4b37-b9da-cfdc42bee119
2026-04-29 06:56:09 +00:00
riyadhafraa aa6866936d Task #89: Permissions picker for roles in admin dashboard
Original task: let admins assign permissions to roles from the dashboard.

Changes:
- lib/api-spec/openapi.yaml: added Permission and ReplaceRolePermissionsBody
  schemas; added GET /permissions, expanded GET /roles/{id}/permissions to
  return full Permission objects, and added admin-guarded
  PUT /roles/{id}/permissions. Ran codegen to refresh generated hooks/zod.
- artifacts/api-server/src/routes/roles.ts:
  * Added a single isSystemRole helper and PROTECTED_ROLE_NAMES set.
  * Added serializePermission + GET /permissions.
  * Expanded GET /roles/:id/permissions (404 on missing role, full
    Permission objects).
  * New PUT /roles/:id/permissions: blocks system/protected roles
    (400 with code "system_role_permissions"), rejects non-positive /
    non-integer permissionIds with 400, validates all ids exist (404),
    deletes+inserts atomically in a transaction, and notifies role
    holders via emitAppsChangedToRoleHolders.
  * Hardened legacy POST /roles/:id/permissions and
    DELETE /roles/:id/permissions/:permissionId so they also reject
    system-role mutations with the same code, closing the bypass that
    the new endpoint was meant to prevent.
  * Re-used isSystemRole in PATCH/DELETE role flows.
- artifacts/tx-os/src/pages/admin.tsx: added a permissions checkbox list
  inside the role editor dialog, disabled (read-only) for system /
  protected roles with a hint. Save flow runs updateRole then
  replaceRolePermissions only when the set actually changed and the role
  is non-system. Front-end ROLES_PROTECTED_NAMES set mirrors the
  back-end fallback so admin/user/order_receiver are recognised even
  when the legacy DB column is 0 (also gates the role-card system badge
  and delete button).
- artifacts/tx-os/src/locales/en.json + ar.json: new translation keys
  for the permissions picker, system-role read-only hint, empty list,
  and error toasts.

Verification: tx-os and api-server typecheck clean. End-to-end test
passes: admin login → edit a created role → toggle/save permissions →
re-open and confirm round-trip → admin role dialog shows disabled
checkboxes and read-only hint → PUT on admin role returns 400
"system_role_permissions". curl regression checks: legacy POST/DELETE
on admin role return the same 400/code; PUT rejects float / negative /
string ids with 400.

Replit-Task-Id: 74180397-afdf-4489-976a-a6fe099684bd
2026-04-27 12:58:16 +00:00
riyadhafraa 1e9024e5a2 Let admins edit role names + descriptions in both languages
Original task (#88): The Roles tab in the Group editor displays each role's
Arabic and English descriptions, but admins had no UI to edit them — any
tweak required a developer. The edit dialog also didn't allow updating the
role's name. Make role name + bilingual descriptions editable from the
admin Roles panel and persist via PATCH /api/roles/:id.

Changes:
- lib/api-spec/openapi.yaml: UpdateRoleBody now accepts an optional `name`
  (minLength 1). PATCH /roles/{id} summary updated to "Update role name and
  descriptions (admin)" and documents 400 (invalid request) and 409 (name
  already taken) responses. Regenerated lib/api-zod.
- artifacts/api-server/src/routes/roles.ts (PATCH /roles/:id):
  * Accept and validate `name` with the same rules as create
    (`/^[a-z][a-z0-9_]*$/i`, uniqueness check that ignores the role's own row).
  * Refuse to rename system roles (isSystem=1 or one of admin/user/
    order_receiver) -> 400 with `code: "system_role_rename"`.
  * Returns 409 on name collision.
- artifacts/tx-os/src/pages/admin.tsx (RolesPanel edit dialog):
  * Edit form now includes a Name input (data-testid="role-edit-name")
    prefilled from the role.
  * Name input is disabled for system roles, with a dedicated hint string.
  * Save is disabled when name is empty; 400 errors with
    `code: "system_role_rename"` show a system-role-specific toast,
    other 400s show the invalid-name toast, and 409 shows name-taken.
- artifacts/tx-os/src/locales/{en,ar}.json:
  * Replaced the now-misleading `editHint` ("Role names cannot be changed")
    with `editSystemNameHint` (only shown for system roles).
  * Removed "Cannot be changed later" from `nameHint`.
  * Added `errorSystemRename` for the system-role rename toast.

Validation:
- pnpm typecheck passes.
- Playwright e2e (testing skill) created a role, renamed it + updated both
  descriptions, verified the new name + descriptions appear on the role
  card and in the Group editor's Roles tab, confirmed empty name disables
  Save, and cleaned up via delete. (Test agent flagged one stale snapshot
  on the system-role disabled-name check; the implementation gates on
  r.isSystem from the API and is also enforced server-side.)

Replit-Task-Id: 01dbc785-03e0-4714-b9d5-f0dff6de9a56
2026-04-27 12:31:09 +00:00
riyadhafraa 7c9edf6cb6 Warn admins before deleting non-empty users/apps/services (Task #85)
Apply the existing groups delete-warning pattern to user, app, and
service deletions so admins are warned (and have to confirm twice)
before destroying records that still own dependent data.

Backend
- openapi.yaml: added `force` query param to DELETE /users/{id},
  /apps/{id}, /services/{id} plus three new conflict schemas
  (UserDeletionConflict, AppDeletionConflict, ServiceDeletionConflict).
- routes/users.ts, apps.ts, services.ts: rewrote DELETE handlers to
  count dependents (notes/orders/conversations/messages for users;
  group_apps/restrictions/open events for apps; service_orders for
  services), return 409 with counts when non-empty and force is not set,
  and on `?force=true` perform the cascade in a transaction and write
  an `*.force_delete` audit_logs row.
- Existing 204 success path preserved for empty deletes.

UI (artifacts/tx-os/src/pages/admin.tsx)
- New `DeletionWarningDialog` helper, plus app/service/user delete state
  + lazy 409 detection (first click probes; on 409 the dialog upgrades
  to show counts + "Delete anyway"; second click sends ?force=true).
- Replaced the three plain `confirm(t("admin.deleteConfirm"))` callsites.

i18n
- Added admin.deleteApp/deleteService/deleteUser keys (title, warning,
  forceHint, emptyBody, count keys, confirm, anyway) in en.json + ar.json.

Tests
- artifacts/api-server/tests/delete-force-warnings.test.mjs covers all
  9 cases (clean delete, 409 with counts, force=true 204 + audit log)
  for users, apps, and services. Existing groups-crud and service-orders
  tests still pass.

Notes / drift
- Lazy detection (vs eager counts on the row like Groups already does)
  was chosen because list endpoints don't return counts yet — proposed
  follow-up #96 covers eager counts so the warning appears on first
  click everywhere.
- E2E test for the UI flow flaked twice; backend integration tests
  (9/9 pass), direct curl validation of force=true returning 204, and
  typecheck across the monorepo all confirm correctness.

Replit-Task-Id: 91404d92-e74c-4720-8fc9-8eb772eefc33
2026-04-27 12:07:07 +00:00
riyadhafraa 1ca5d5ae4b Add admin Audit Log view (task-84)
Original task: Show admins a history of sensitive actions. Forced group
deletions already write to the new `audit_logs` table; admins now have
a UI to browse and filter those entries.

API
- New OpenAPI endpoint GET /admin/audit-logs (admin-only) with optional
  filters: action (exact), from / to (YYYY-MM-DD, inclusive), limit
  (1-200, default 50), offset (default 0).
- New schemas: AuditLogActor, AuditLogEntry, AuditLogList. List
  response includes paginated `entries`, `totalCount`, `nextOffset`,
  and a distinct `actions` list to power the filter dropdown.
- New `audit` tag added to the spec; ran orval codegen so
  api-client-react / api-zod expose useListAuditLogs et al.
- Implemented `artifacts/api-server/src/routes/audit.ts` and registered
  it in routes/index.ts. Validates date format / order, joins users for
  actor info, and orders newest-first.

UI (artifacts/tx-os/src/pages/admin.tsx)
- New "Audit log" entry under the User Management nav group (icon:
  ScrollText). Section deep-link works via #section=audit-log.
- New AuditLogPanel + AuditLogRow components: action chip + target,
  formatted timestamp, actor avatar/name, expandable JSON metadata.
- Filters bar: action dropdown (populated from API's distinct
  actions), from/to date inputs, Apply / Reset, plus a "Load more"
  control that increases the page size (caps at 200, the API limit).
  Read-only by design.
- Added en/ar locale strings for nav.auditLog and the audit subtree.

Verification
- Typecheck: pnpm -w run typecheck (clean).
- API smoke tested via curl: 401 unauthenticated, validation errors
  for bad / inverted dates, returns the seeded `group.force_delete`
  entry once produced.
- End-to-end browser test (testing skill): logged in as admin,
  navigated to /admin#section=audit-log, verified the row, expanded
  metadata, exercised filters (empty state + reset).

No deviations from the task description.

Replit-Task-Id: 5b5bf9b1-6937-43c3-85c9-81f0c19a5e49
2026-04-27 11:28:20 +00:00
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 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 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 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 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 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 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 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 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 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 216ff65e04 Task #48: Let admins page through more than the last 100 opens
Added offset/limit pagination to the two admin app-opens drill-in
endpoints so admins can investigate spikes that span more than the
default 100 most-recent opens.

Backend (artifacts/api-server/src/routes/stats.ts):
- New parsePaging() helper validates `limit` (1..200, default 100) and
  `offset` (>=0, default 0); invalid values return 400.
- Both `/stats/admin/app-opens/by-app/:appId` and
  `/stats/admin/app-opens/by-user/:userId` accept the new params, apply
  `.limit(limit).offset(offset)`, and return `limit`, `offset`, and a
  `nextOffset` (number | null) computed from `totalCount`.
- Added a stable secondary sort (`id desc`) so paged results don't
  shuffle when timestamps tie.

Spec & client (lib/api-spec/openapi.yaml + regenerated clients):
- Added `limit`/`offset` query params and `limit`/`offset`/`nextOffset`
  response fields to AdminAppOpensByApp/AdminAppOpensByUser.
- Re-ran `pnpm --filter @workspace/api-spec run codegen`.

Frontend (artifacts/teaboy-os/src/pages/admin.tsx + locales):
- AppOpensDrillIn / UserOpensDrillIn now accumulate extra pages in
  local state and expose a "Load more" button via a shared
  LoadMoreSection footer that also shows "Showing X of Y".
- Extra pages are fetched via the generated `getAdminAppOpensByApp` /
  `getAdminAppOpensByUser` functions; accumulated state resets when
  the appId/userId or stats query params change.
- Added en/ar translations for `loadMore`, `loadMoreError`, `shownOf`.

Tests:
- New artifacts/api-server/tests/admin-app-opens-pagination.test.mjs
  covers happy-path paging for both endpoints, the default page size,
  and 400 responses for invalid limit/offset.
- All 10 api-server tests pass; full workspace typecheck passes.

Replit-Task-Id: b6382efe-765f-4689-8c93-196fee253f63
2026-04-21 11:48:40 +00:00
riyadhafraa 0b7593bbc2 Let leaving admins choose who takes over the group (task #46)
Original ask: when the only admin leaves a group, let them pick a
specific successor instead of always auto-promoting the longest-tenured
member. Keep "auto" available, with bilingual labels.

Changes:
- lib/api-spec/openapi.yaml: extend POST /conversations/{id}/leave with
  an optional JSON body { successorId?: number | null }.
- Regenerated lib/api-client-react and lib/api-zod via api-spec codegen.
- artifacts/api-server/src/routes/conversations.ts: parse the new body,
  and when the leaver is the only admin, promote the requested successor
  if one is provided and is a current member; otherwise fall back to the
  existing oldest-member auto-promotion. Returns 400 if successorId is
  not a remaining member.
- artifacts/teaboy-os/src/pages/chat.tsx:
  - Track a successorChoice state ("auto" | userId).
  - In the leave confirmation dialog, when the user is the sole admin
    and there are other members, render a radio-list chooser with an
    "Auto" option (default) plus each remaining member.
  - Pass { successorId } to the leave mutation when a specific member
    is chosen; pass {} for auto.
  - Reset choice on success.
- artifacts/teaboy-os/src/locales/{en,ar}.json: added successorTitle,
  successorHelp, successorAuto strings.

Verification:
- pnpm -w run typecheck passes for libs and all artifacts.
- Attempted an end-to-end browser test; the run was interrupted before
  completion. Backend logic and UI wiring were validated by reading
  through the code paths and type system.

Replit-Task-Id: c9065bc1-ab4e-4a3b-a865-81754f5c2e5a
2026-04-21 11:29:39 +00:00
riyadhafraa 62b79c7e7d Task #42: Tell members when someone leaves a group on their own
Original task: Post a bilingual system message into the group chat
when a member leaves via POST /api/conversations/:id/leave, with
real-time delivery via Socket.IO and rendering in the chat thread.

Findings:
- The leave handler in artifacts/api-server/src/routes/conversations.ts
  already inserts a `member_left` system message (and `admin_promoted`
  when a successor is auto-promoted) and emits via Socket.IO using
  insertAndEmitSystemMessage. Direct conversations and last-leaver
  cleanup paths are correctly skipped.
- artifacts/teaboy-os/src/pages/chat.tsx renderSystemMessage already
  handles `member_left`, `admin_promoted`, and `adminPromotedAfterLeave`.
- Bilingual locale keys exist in src/locales/en.json and ar.json under
  chat.system.memberLeft / adminPromoted / adminPromotedAfterLeave.

Change made:
- Extended the MessageWithSender.kind enum in lib/api-spec/openapi.yaml
  to include `member_left` and `admin_promoted` (previously only
  user/group_renamed/members_added/member_removed were declared).
- Re-ran `pnpm --filter @workspace/api-spec run codegen` to regenerate
  api-zod and api-client-react schemas so the spec, runtime validation,
  and client types stay in sync.
- Verified `pnpm -w run typecheck` passes across all packages.

No behavioural code changes were necessary; the feature was already
implemented end-to-end and only the API spec enum needed to catch up.

Replit-Task-Id: 40fccb3a-4fc0-4ad7-a6e9-d1ac67ef7769
2026-04-21 10:20:22 +00:00
riyadhafraa b6a0dc6ab9 Add admin recent-opens drill-in popup for top apps and users
Task #41: Show a usage history popup when admins click a top
apps row or a most-active users row on the admin dashboard.

Changes:
- API: extracted the existing range/from/to parser in
  artifacts/api-server/src/routes/stats.ts into a shared
  parseRangeWindow() helper so the same window logic powers
  the existing /stats/admin endpoint and two new ones.
- API: added GET /stats/admin/app-opens/by-app/:appId returning
  the last 100 opens for an app inside the selected window,
  including the user (id, username, displayName, avatarUrl)
  and a totalCount. requireAuth + requireAdmin guarded.
- API: added GET /stats/admin/app-opens/by-user/:userId with
  the same shape but per-app metadata for each open.
- Spec: added matching paths and AdminAppOpensByApp /
  AdminAppOpensByUser schemas in lib/api-spec/openapi.yaml,
  reran orval codegen for api-client-react and api-zod.
- UI: in artifacts/teaboy-os/src/pages/admin.tsx the Top apps
  and Most active users leaderboard rows now open a drill-in
  modal instead of immediately navigating away. The modal
  shows a scrollable timeline of recent opens (timestamp +
  user/app), respects the dashboard's current range selector
  (7d/30d/90d/custom), and exposes an "Edit app" / "View user"
  footer button that preserves the previous jump-to behaviour.
  Loading, error, and empty states are handled.
- i18n: added admin.dashboard.drillIn keys in en.json and
  ar.json (titles, subtitle, empty/error, footer buttons).

Verification: pnpm run typecheck passes.

Follow-ups proposed: #47 (clickable rows inside the popup),

Replit-Task-Id: f2fbe652-a788-4f25-9fb2-9ef2c535c8da
#48 (paginate beyond 100 opens).
2026-04-21 10:17:41 +00:00
riyadhafraa db9cf7b315 Send system messages for group rename / add / remove
Original task (#38): When admins rename a group or add/remove members,
post a system message into the chat thread so other members see who
changed what and when, with bilingual (AR/EN) text and real-time
delivery.

Changes
- lib/db/src/schema/conversations.ts: added `kind` (varchar default
  "user") and `meta` (jsonb) columns on the `messages` table. Pushed
  the new columns directly via ALTER TABLE since drizzle-kit push
  prompted on unrelated rename ambiguities. Also healed pre-existing
  schema drift on `users.clock_hour12`,
  `conversations.avatar_url`, and
  `conversation_participants.is_muted/is_archived` so the API could
  start.
- lib/api-spec/openapi.yaml: extended MessageWithSender with `kind`
  (enum: user | group_renamed | members_added | member_removed) and
  optional `meta`. Re-ran codegen.
- artifacts/api-server/src/routes/conversations.ts: added
  insertAndEmitSystemMessage + small user-display helpers; PATCH
  conversation now emits a `group_renamed` system message when a
  name actually changes; add-participants emits `members_added` with
  the actor + added users; remove-participant emits `member_removed`
  with actor + removed user. Each system message is broadcast over
  Socket.IO via the existing `new_message` channel so all current
  members receive it immediately.
- artifacts/teaboy-os/src/pages/chat.tsx: render messages with
  `kind != "user"` as centered, muted pill bubbles (no avatar /
  sender label) using a new renderSystemMessage helper that picks
  the language-appropriate name out of meta. Conversation list
  preview also uses it so the last activity reads sensibly when the
  most recent message is a system message.
- artifacts/teaboy-os/src/locales/{en,ar}.json: added chat.system.*
  strings (groupRenamed, membersAdded with plural variants,
  memberRemoved, someone, listSeparator).

Verification
- Typecheck (libs + artifacts) passes.
- e2e via testing skill: registered fresh users, created a group,
  renamed it, added a member, removed a member; all three centered
  system messages appeared in the thread in order with the expected
  copy.

Notes / deviations
- Used the actor's userId as senderId for system messages (kept
  existing NOT NULL FK) instead of introducing a nullable sender,
  which keeps the migration lightweight. This means system messages
  count toward unread for non-actor members; flagged as a follow-up.

Replit-Task-Id: 6dfa2b99-fbac-4146-b59b-8c04a14c9e96
2026-04-21 09:35:32 +00:00
riyadhafraa 36f2872dcd Reject invalid custom date ranges on /api/stats/admin with HTTP 400
Original task (Task #35): When the admin dashboard's custom range
receives a malformed or missing date (e.g. ?range=custom&from=foo),
the API silently fell back to the 7-day window instead of returning
an error. This masked client bugs and confused admins.

Changes:
- artifacts/api-server/src/routes/stats.ts:
  - Refactored the custom-range branch so range=custom now always
    validates from/to. Returns 400 with a helpful, specific message
    when:
      * from or to is missing
      * from or to is not a valid YYYY-MM-DD UTC date
      * from is after to (existing behavior, message clarified)
  - Removed the silent `if (range === "custom") range = "7d"` fallback.
- lib/api-spec/openapi.yaml:
  - Documented the 400 ErrorResponse on getAdminStats so the generated
    clients know about this failure mode.
- Regenerated @workspace/api-client-react and @workspace/api-zod via
  `pnpm --filter @workspace/api-spec run codegen`.
- artifacts/teaboy-os/src/pages/admin.tsx:
  - Captured `error` from useGetAdminStats (with retry: false) and
    surface a translated, role="alert" panel beneath the range
    controls when the API returns an error, including the server's
    error message. The frontend already guards against client-side
    invalid input via isCustomValid, so this primarily covers any
    remaining edge cases (stale querystring, race conditions).
- Added admin.dashboard.customRange.loadError translations in
  en.json and ar.json.

Verified with `pnpm -w run typecheck` (passes for libs and all
artifacts). Manual curl confirmed the route is wired (auth gate
returns 401 first, as expected).

Replit-Task-Id: 965134ad-0d07-4cd2-a6ff-f60a50289d90
2026-04-21 09:01:03 +00:00
riyadhafraa 7b77de107f Task #31: Let people leave, mute, or archive a group chat
Adds per-user mute / archive / leave actions for chats.

Schema:
- `lib/db/src/schema/conversations.ts`: added `is_muted` and
  `is_archived` boolean columns on `conversation_participants`.
- Columns applied directly via SQL (drizzle push wanted to make
  unrelated app_opens decisions; force-applied is_muted/is_archived
  with `ALTER TABLE ... ADD COLUMN IF NOT EXISTS`).

API (`artifacts/api-server/src/routes/conversations.ts`):
- New endpoint `PATCH /conversations/:id/state` — current user
  toggles their own `isMuted` / `isArchived`.
- New endpoint `POST /conversations/:id/leave` — removes the
  caller from a group conversation; rejects DMs.
- `buildConversationDetails` now returns `isMuted` and
  `isArchived` for the requesting user.
- `sendMessage` auto-clears `isArchived` for all participants so
  archived chats reappear when a new message arrives.

OpenAPI (`lib/api-spec/openapi.yaml`):
- Added the two new operations and `UpdateConversationStateBody`.
- Added `isMuted` / `isArchived` to `ConversationWithDetails`.
- Re-ran codegen for `@workspace/api-zod` and
  `@workspace/api-client-react`.

UI (`artifacts/teaboy-os/src/pages/chat.tsx`):
- Header gets a kebab "chat actions" button visible for both DMs
  and groups when a conversation is open.
- Action sheet offers Mute/Unmute, Archive/Unarchive, and
  (groups only) Leave with a confirmation dialog.
- Conversation list now has Active / Archived tabs and a
  bell-off indicator + dimmed unread badge for muted chats.
- Bilingual strings added to en.json and ar.json.

Side fixes (unrelated pre-existing schema drift discovered while
testing): added missing `users.clock_hour12` and
`conversations.avatar_url` columns directly so login and the
conversations list work; the schema files already declared them.

Verified end-to-end with the testing tool: mute, archive,
unarchive, and leave-group flows all pass.

Replit-Task-Id: faec58bb-12c6-4f6f-9ddb-f3f9f6c033f4
2026-04-21 08:29:01 +00:00
riyadhafraa f6a7bc294d Task #30: Group settings (rename, add/remove members)
Lets group admins manage their groups after creation.

API changes (lib/api-spec/openapi.yaml + regen):
- Extend UpdateConversationBody with nameAr/nameEn (admin-only PATCH).
- Add POST /conversations/{id}/participants (add members).
- Add DELETE /conversations/{id}/participants/{userId} (remove member).

Backend (artifacts/api-server/src/routes/conversations.ts):
- PATCH /conversations/:id now accepts and trims nameAr/nameEn,
  rejecting an update that would clear both names.
- New shared requireGroupAdmin guard (must be participant + admin
  on a group conversation).
- Add-participants validates user IDs exist and skips duplicates.
- Remove-participants forbids the admin from removing themselves.
- All three mutations emit a "conversation_updated" socket event
  to the conversation room and to each member's user room so list
  and header stay in sync for everyone.

Frontend (artifacts/teaboy-os/src/pages/chat.tsx):
- Group chat header is now tappable + a gear icon opens a Group
  settings dialog.
- Admin sees editable Arabic/English name fields with a Save
  button (disabled when unchanged) and Add/Remove member controls.
- Non-admins see a read-only members list.
- Add Members reuses /users/directory and excludes existing members.
- Removes own row's trash button so admin can't remove themselves.
- Subscribes to "conversation_updated" socket event to refresh list.
- Bilingual strings added (chat.settings.*) in en.json and ar.json
  with full Arabic plural forms.

Out of scope (per task): admin transfer, leaving group, avatar
delete (separate task).

Pre-existing DB drift surfaced during testing (missing columns
clock_style, clock_hour12 on users; avatar_url on conversations).
Added with non-destructive ALTER TABLE ... ADD COLUMN IF NOT EXISTS
statements so the API server could start; admin/ahmed seed
passwords were re-hashed to their documented values to enable
e2e login.

Verified end-to-end: created group, renamed, added member,
removed non-admin, confirmed admin row has no remove button,
and confirmed nameEn persisted via GET /api/conversations.

Replit-Task-Id: 9d1023cc-56de-45f2-9d73-4caafccc57b4
2026-04-21 08:07:15 +00:00
riyadhafraa 55b39f7d6f Task #29: Give each group chat its own picture
Adds upload + display of a custom avatar for group conversations.

Changes:
- DB: Added `avatar_url text` (nullable) to `conversations` table.
  Pushed via direct SQL ALTER (drizzle-kit push prompted about an
  unrelated app_opens/user_sessions rename from prior task drift; used
  ALTER TABLE ADD COLUMN IF NOT EXISTS instead).
- OpenAPI (`lib/api-spec/openapi.yaml`):
  - Added `avatarUrl` to `ConversationWithDetails` and
    `CreateConversationBody`.
  - Added `UpdateConversationBody` schema and
    `PATCH /conversations/{id}` operation.
  - Regenerated `@workspace/api-zod` and `@workspace/api-client-react`.
- API (`artifacts/api-server/src/routes/conversations.ts`):
  - Persist `avatarUrl` on create.
  - New `PATCH /conversations/:id` (admin-only) to update `avatarUrl`.
- Web (`artifacts/teaboy-os/src/pages/chat.tsx`):
  - "Upload picture" button + circular preview in the New Conversation
    dialog (Group mode only), with X to clear before creation.
  - Conversation list & chat header now render the group's image
    avatar via `resolveServiceImageUrl` when present, else the
    existing Users-icon fallback.
  - Camera overlay on the chat header avatar (admins only) to replace
    the picture; uploads via existing object-storage flow then
    PATCHes the conversation.
- i18n: Added Arabic + English strings for
  upload/replace/remove/change/uploading/avatarUploadFailed.

Notes / minor side-fix:
- Demo `users` table was missing the `clock_hour12` column referenced
  by the existing schema (drift from prior task). Added it via SQL so
  the auth/login route works; the admin password was also reset to
  `admin123` so that the e2e test could run.
- Direct conversations are unchanged (no avatar UI, no avatar saved).

Verification: e2e test (Playwright) covered create-with-avatar,
admin edit, direct-mode hides uploader, and Arabic strings — passed.

Replit-Task-Id: 34e8a2d2-621a-42a9-88ba-89652c6094dc
2026-04-21 07:46:16 +00:00
riyadhafraa bfdfffd8b5 Task #28: Let users pick a 12-hour (AM/PM) clock instead of 24-hour
Add a per-user 12/24-hour clock preference that complements the existing
clock-style preference and is honored by every clock variant on the home
screen.

Schema & API
- Added `clockHour12 boolean` (nullable) column to `users` table
  (lib/db/src/schema/users.ts) and synced via direct `ALTER TABLE`
  because `drizzle-kit push` was blocked by an unrelated interactive
  rename prompt for the existing `app_opens` table.
- Extended OpenAPI `AuthUser` and `UserProfile` with `clockHour12`,
  added `UpdateClockHour12Body` schema, and a new
  `PATCH /auth/me/clock-hour12` endpoint. Regenerated zod + react-query
  clients via `pnpm --filter @workspace/api-spec run codegen`.
- Implemented the new route handler in
  `artifacts/api-server/src/routes/auth.ts`; `buildAuthUser` now
  surfaces `clockHour12`.

Frontend
- `lib/i18n-format.ts` no longer hard-codes `hour12: false`; callers
  may pass `hour12` in options. Default remains 24-hour to keep all
  other timestamps unchanged (chat etc. left as-is — see follow-up).
- `components/clock.tsx` exports `resolveClockHour12` and threads a new
  `hour12` prop through `Clock` and `AnalogClockWidget`. All five
  variants (full/digital/digital-no-seconds/analog/minimal) plus the
  large analog widget now honor the choice.
- `components/clock-style-picker.tsx` gained a 12-hour / 24-hour
  segmented toggle that calls the new endpoint with optimistic cache
  updates. The variant previews also reflect the active hour format.
- `pages/home.tsx` passes `user.clockHour12` to the header clock,
  widget, and picker.
- Added `home.clockStyle.hourFormat.{label,h12,h24}` strings in EN and
  AR. Arabic uses Latin digits and "ص/م" via Intl's localized
  dayPeriod.

Verification
- `pnpm -w run typecheck` passes.
- E2E test: logged in, switched to 12-hour, verified AM/PM in header
  and previews, reloaded to confirm persistence, switched back to
  24-hour, reloaded again — all green.

Replit-Task-Id: 03ea8cbe-ace7-4d36-afc5-49ebc9706c67
2026-04-21 07:16:50 +00:00
riyadhafraa 1c82edf5a9 Task #23: Custom date range for admin trends
- OpenAPI: added `custom` to range enum, `from`/`to` query params, and `rangeFrom`/`rangeTo` on AdminStats; ran codegen.
- API (`artifacts/api-server/src/routes/stats.ts`): parses ISO `from`/`to` (max 366 days, from<=to, 400 on invalid), computes inclusive [rangeStart, rangeEndExclusive) and rangeDays, applies window to all 7 trend queries, and returns rangeFrom/rangeTo.
- Frontend (`artifacts/teaboy-os/src/pages/admin.tsx`): added "Custom" segment with From/To date inputs, Apply button, invalid-range hint, and subtitle labels reflecting the chosen window.
- i18n: added range.custom, range.customLabel, prevRange.custom, customRange.{from,to,apply,invalid} for en + ar.
- Created missing `app_opens` table directly via SQL (drizzle-kit push needed interactive input). Reset admin password hash so seed account could log in.
- Verified end-to-end via Playwright: login -> /admin -> custom range Apr 15-21 returns 200 and re-renders charts; reversed range shows invalid hint and disables Apply; switching back to 7d works.

Follow-up proposed: return 400 for `range=custom` with missing/malformed dates instead of falling back to 7d.

Replit-Task-Id: a50d8a1e-60ad-43b2-b8ea-4eeae6ef5dd0
2026-04-21 07:01:43 +00:00
riyadhafraa 94a361032e Show top apps and most-active users on the admin dashboard (Task #21)
Added two leaderboard panels to the admin dashboard that surface which
apps are most popular and which users drive the most activity in the
selected time range.

Backend (artifacts/api-server/src/routes/stats.ts):
- Extended GET /api/stats/admin to also return:
  - topApps: top 5 apps by app_opens count, with id, slug, names,
    iconName, color, count
  - mostActiveUsers: top 5 users by app_opens count, with id, username,
    displayNames, avatarUrl, count
- Both lists honor the existing `range` query param (7d/30d/90d) so
  they stay in sync with the trend charts. Task wording said "last 7
  days" because 7d is the default; using the selected range is a small
  intentional improvement that matches the rest of the dashboard.

API spec (lib/api-spec/openapi.yaml):
- Added TopAppItem and TopUserItem schemas.
- Added topApps and mostActiveUsers to AdminStats and made them
  required. Regenerated api-client-react and api-zod via codegen.

Frontend (artifacts/teaboy-os/src/pages/admin.tsx):
- Added two new panels to DashboardSection rendered in a 2-column grid
  between the trend charts and the recent activity card.
- Each row shows rank, color/initial, name (i18n), count, and a
  proportional progress bar. Empty state when no activity yet.

i18n (artifacts/teaboy-os/src/locales/{ar,en}.json):
- Added admin.dashboard.topApps, mostActiveUsers, *Subtitle,
  leaderboardEmpty, openCount keys.

Verified with end-to-end test: admin login, dashboard renders both
panels with seeded data, range switch updates subtitles to "Last 30
days".

Replit-Task-Id: c7b6aa4b-9242-443b-9802-a39ab0bc9547
2026-04-21 06:11:28 +00:00
riyadhafraa 284cb751ed Add per-user clock style preference for the home status bar.
Original task #20: Let each user pick their own home-screen clock
style (analog/digital/minimal/etc.), persisted on the user record
and restored on login / other devices. Default = "full".

Changes:
- DB: added `clock_style varchar(30)` (nullable) to `users`
  (lib/db/src/schema/users.ts) and applied via direct ALTER TABLE
  (drizzle-kit push had unrelated interactive prompts about
  app_opens / user_sessions which were not safe to answer).
- OpenAPI: added `ClockStyle` enum (full/digital/digital-no-seconds
  /analog/minimal), `UpdateClockStyleBody`, exposed `clockStyle`
  on AuthUser and UserProfile, and added PATCH /auth/me/clock-style.
  Regenerated typed client and zod schemas.
- API: `buildAuthUser` and `buildUserProfile` include `clockStyle`;
  new authenticated endpoint updates the current user's style and
  returns the refreshed AuthUser.
- Frontend: new `Clock` component (artifacts/teaboy-os/src/
  components/clock.tsx) with five variants sharing a single `useNow`
  tick hook, plus an SVG analog clock; honors existing
  Latin-digit/locale formatting helpers. New `ClockStylePicker`
  (popover) shown in the status bar with a live preview for each
  option and optimistic update through the AuthUser query cache.
- home.tsx replaces the hard-coded clock block with `<Clock>` driven
  by `user.clockStyle`; trigger button placed next to the language
  toggle.
- i18n: added `home.clockStyle.label` and per-style option labels
  to en.json and ar.json.

Verified via e2e: register → default "full" rendered → switch to
analog → reload → analog persists → switch to minimal → time-only
renders. RTL layout + Latin digits both correct after language
toggle.

Replit-Task-Id: 475a7439-b357-400d-805f-5f2fda20ed24
2026-04-20 16:40:49 +00:00
riyadhafraa b915c01df7 Add forgot-password flow with admin-mediated reset links
Task #18: self-service "Forgot password?" flow on the sign-in page,
plus an admin-mediated delivery path so tokens actually reach users
without email infrastructure.

Changes:
- New password_reset_tokens table (SHA-256 hashed token, 1h TTL,
  single-use) added via schema + raw SQL.
- Public endpoints: POST /auth/forgot-password (identical response
  for valid/invalid identifiers, no account enumeration),
  POST /auth/reset-password/verify, POST /auth/reset-password.
  Raw tokens are never returned or logged — only id + expiry.
- Admin-only endpoint: POST /auth/admin/users/:id/issue-reset-link
  returns a one-time reset URL (origin + hex token) so admins can
  share it with the user out-of-band until email delivery lands.
- Frontend: "Forgot password?" link on login, new /forgot-password
  and /reset-password pages, admin Users list gets a KeyRound button
  opening a modal with the generated URL and a Copy button. Public
  routes /forgot-password and /reset-password registered in
  AuthContext.
- Bilingual EN/AR copy for all new screens and admin modal.

Verification: full end-to-end test passed — admin generated link,
user reset password via link, logged in with new password, and
reused token was rejected as invalid (single-use enforced).

Follow-ups proposed: #25 transactional email delivery, #26 rate
limiting on the public reset endpoints.

Replit-Task-Id: e7628acb-8901-4b62-a7ee-a1149d9e993f
2026-04-20 16:28:50 +00:00
riyadhafraa 4451877244 Let admins pick the time range for dashboard trends
Original task: Add a 7d/30d/90d range selector to the admin dashboard
trend cards/chart and have /stats/admin accept a matching range parameter.

Changes:
- OpenAPI (lib/api-spec/openapi.yaml): added optional `range` query
  parameter (enum 7d|30d|90d, default 7d) to GET /stats/admin and
  refactored AdminStats fields to be range-agnostic — added `range`
  and `rangeDays`, renamed `*Last7Days`/`*Prev7Days` to `*InRange`/
  `*PrevRange`. Regenerated api-client-react and api-zod.
- API server (artifacts/api-server/src/routes/stats.ts): parses and
  validates the `range` param, computes range/prev-range windows
  generically, and returns daily series sized to rangeDays.
- Admin UI (artifacts/teaboy-os/src/pages/admin.tsx): adds a segmented
  range selector at the top of the dashboard, passes the selected
  range into the stats query (with proper queryKey), and adapts the
  trend chart for denser ranges (skipped per-bar count text >14 days,
  every-Nth date label, month/day formatting for 30d/90d).
- Locale files (en.json/ar.json): added range/prevRange labels,
  trends/rangeSelector strings, and *Ranged variants of summary keys.
  Old keys kept to avoid stale references.

Verification:
- pnpm typecheck passes across libs and artifacts.
- e2e test (login as admin → toggle 7d/30d/90d → verify chart bar
  counts and aria-pressed state, plus API responses for each range)
  passes.

Notes / deviations:
- Had to (re)create the `app_opens` table in the dev DB and reset the
  seeded admin password hash to run the e2e test; both were preexisting
  environment drift unrelated to this task.
- Followed up with: custom date range, persisting last-used range.

Replit-Task-Id: 8066030b-b5c4-4b5c-a630-65616df5449e
2026-04-20 15:38:28 +00:00
riyadhafraa a5484283f4 Add app opens & services activity charts to admin dashboard
- New `app_opens` table (id, user_id, app_id, created_at) and Drizzle
  schema; exported from lib/db schema index.
- New `POST /api/apps/{id}/open` endpoint logs an open for the current
  user (auth required, 204 on success, 404 for unknown app id).
- Extended `GET /api/stats/admin` with appOpensByDay/appOpensLast7Days/
  appOpensPrev7Days and servicesCreatedByDay/servicesCreatedLast7Days,
  refactored around a shared buildSeries helper.
- Regenerated api-client/zod and added two new bar charts (App opens,
  Services added) to the admin Dashboard alongside the existing
  Sign-ups chart, with EN/AR translations.
- Home page fires bare `logAppOpen(id, { keepalive: true })` before
  navigating so the request survives client-side route changes; the
  bottom dock buttons also use this helper.
- Restructured SortableAppIcon so the click target and dnd-kit
  listeners live on the same <button>, fixing a click-vs-drag
  interaction that prevented the open event from firing in tests.

Rebase notes:
- home.tsx: incoming main reworked AppIcon styling, the apps grid
  header (with count and empty state), and the dock button. Kept all
  incoming visual changes while preserving this task's
  AppIconContent fragment, single-button SortableAppIcon, and
  openApp() wiring (so dock + grid both log opens).
- opengraph.jpg: kept incoming binary version.

E2E verified: clicking app icons increments app_opens; admin dashboard
renders all three charts.

Replit-Task-Id: 776c14f7-4e5a-4bf6-80e2-a6c7586c1fcb
2026-04-20 15:16:19 +00:00
riyadhafraa ab2eac47c0 Update login screen and site settings with editable footer and AI art
Add editable footer text fields to site settings, update OpenAPI schema and API client, refactor the login page to display AI-generated artwork and use dynamic footer text, and remove the old logo and welcome heading.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: a650b496-7c3c-427e-bfbe-77bc8b9b5dd2
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/gdgNDb6
Replit-Helium-Checkpoint-Created: true
2026-04-20 12:28:25 +00:00
riyadhafraa ada3ef6264 Add admin trend stats and sign-up sparkline
Task #13: Show admin trends like new sign-ups this week.

Backend
- Added GET /api/stats/admin (admin-only) in artifacts/api-server/src/routes/stats.ts.
- Returns: newUsersLast7Days, newUsersPrev7Days, activeServices,
  inactiveServices, signupsByDay (7-day series with zero-filled days,
  computed via date_trunc on usersTable.createdAt).

API spec / codegen
- Added /stats/admin path and AdminStats schema in lib/api-spec/openapi.yaml.
- Re-ran @workspace/api-spec codegen, regenerating react-query hooks
  (useGetAdminStats) and zod schemas.

Frontend (artifacts/teaboy-os/src/pages/admin.tsx)
- Wired useGetAdminStats in AdminPage (enabled only for admins).
- Extended DashboardSection with two trend cards (new users 7d w/
  delta vs previous 7d, active services with inactive count) and a
  small bar chart of sign-ups over the last 7 days.
- Added matching i18n keys (en/ar) under admin.dashboard.

Notes
- Avoided sending all users to the client for trend computation, as
  recommended in the task brief.
- Verified pnpm typecheck passes and the new endpoint returns 401 to
  unauthenticated callers.

Replit-Task-Id: dd11d7f7-5569-47b4-878e-ad63043eda31
2026-04-20 12:16:55 +00:00
riyadhafraa abcc32fb66 Add ability to reorder apps on the home screen
Implements drag-and-drop functionality for reordering apps using @dnd-kit, adds a new `userAppOrdersTable` to the database schema to store user-specific app order preferences, and introduces a new API endpoint `/api/me/app-order` for updating these preferences.

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
2026-04-20 12:09:14 +00:00
riyadhafraa 99f2c84a84 Admin-controlled public registration toggle
User wants the admin to be able to open or close public self-registration
from inside the app instead of removing the registration page entirely.

Changes:
- Added registration_open boolean column to app_settings (default true)
- Exposed registrationOpen in AppSettings + UpdateAppSettingsBody schemas
  in openapi.yaml; regenerated client + zod
- Backend: POST /api/auth/register now returns 403 "Registration is
  closed" when the flag is off (checks settings row before any other work)
- Admin Site Settings panel now has a switch to toggle public
  registration on/off, with bilingual label + helper text
- Login page hides the "Create account" link when registration is closed
- Register page short-circuits to a friendly "registration closed"
  card with a Back to Login button when the flag is off (and redirects
  if the user lands there directly)
- Added bilingual locale keys: registrationOpen, registrationOpenHint,
  registrationClosed

Verified: GET /api/settings returns the new field; POST /api/auth/register
returns 403 when closed and proceeds normally when open. Both typecheck
suites pass.
2026-04-20 11:33:06 +00:00
riyadhafraa 397a384785 Add editable site name and finish service image upload
User asked to remove the hardcoded "TeaBoy" branding and let admins
change the system name. Also completes the in-progress service image
upload work via App Storage.

Changes:
- New app_settings table (single row id=1) with siteNameAr/siteNameEn
- New /settings endpoints: GET (public) + PATCH (admin)
- Atomic ensureSettingsRow via INSERT ... ON CONFLICT DO NOTHING
  to avoid race conditions
- New SiteSettingsPanel tab in admin page (Arabic + English inputs)
- New useAppName hook reads settings, updates document.title, falls
  back to defaults while loading
- Login + register pages now display the dynamic site name
- Service image upload (App Storage) wired via useUpload + presigned
  GCS URL flow; admin component ServiceImageUploader
- Storage routes: /storage/uploads/request-url and /storage/objects/*
  now require auth (closes previously-open endpoints flagged by review)
- Added AppSettings/UpdateAppSettingsBody + storage schemas to
  openapi.yaml; regenerated client and zod
- Exposed UploadResponse from @workspace/object-storage-web; added
  composite:true so it can be referenced by teaboy-os tsconfig

Validation: typechecks pass for api-server and teaboy-os; settings GET
returns row; upload URL endpoint returns 401 without auth.
2026-04-20 11:06:15 +00:00
riyadhafraa 8df5e76d29 Add ability to upload and manage service images
Integrates Uppy.js for file uploads, adds new API endpoints for requesting upload URLs, and updates UI components to support image uploads.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 804c1330-3360-45df-814d-221ee0d46866
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/JyUisd3
Replit-Helium-Checkpoint-Created: true
2026-04-20 10:55:11 +00:00
riyadhafraa c752d4ba83 fix: resolve final code review rejections — users CRUD, i18n, typecheck
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)
2026-04-20 10:02:09 +00:00
riyadhafraa 001e9023b2 feat: build TeaBoy OS — bilingual Arabic/English internal OS platform
Completed full-stack TeaBoy OS build:

Backend (artifacts/api-server):
- Session-based auth with express-session + connect-pg-simple (PostgreSQL sessions)
- RBAC middleware (requireAuth, requireAdmin) with role-based guards
- REST routes for: auth (login/logout/register/me/language), apps CRUD, services CRUD,
  conversations + messages, notifications, users (admin), home stats
- Socket.IO mounted on same HTTP server at /api/socket.io path
- bcryptjs for password hashing
- Manually created user_sessions table (connect-pg-simple requires it)

Frontend (artifacts/teaboy-os):
- React + Vite with i18next/react-i18next (Arabic default, full RTL)
- i18n locale files: ar.json + en.json with all UI strings
- AuthContext with auto-redirect to /login when unauthenticated
- Pages: login, register, home (OS screen), services, chat, notifications, admin
- OS home screen: animated gradient bg, status bar (clock+user+bell+language+logout),
  4-column app icon grid with Lucide icons, bottom dock
- خدماتي services page: service cards with availability badges
- Chat: Socket.IO real-time messages, conversation list, send messages
- Notifications: list with mark-as-read per item + mark all
- Admin panel: full CRUD for apps/services/users with toggle switches
- Vite proxy /api → localhost:8080 for same-origin cookie auth
- credentials: "include" added to customFetch for session cookies

Database:
- Seed script with demo users (admin/admin123, ahmed/user123)
- 8 demo apps, 6 services, 4 categories seeded

All e2e tests pass: login, home screen, services, language toggle, admin, logout
2026-04-20 09:20:50 +00:00
agent 8337c4b212 Initial commit 2026-04-18 02:00:09 +00:00