Commit Graph

77 Commits

Author SHA1 Message Date
riyadhafraa 3f6a0fb02f Task #613: replace per-app hide toggles with single Dock visibility switch
The per-app hidden feature shipped in #609 was rejected — user wanted one
toggle that hides/shows the entire bottom AppDock bar, leaving Home apps
untouched.

Removed:
- lib/db/src/schema/user-hidden-apps.ts (and index.ts export); table
  dropped via `pnpm --filter @workspace/db run push`
- GET /me/apps and PUT /me/apps/:appId/hidden routes
- getHiddenAppIdsForUser + getVisibleNonHiddenAppsForUser helpers
- MyApp + UpdateMyAppHiddenBody OpenAPI schemas
- MyAppsBody settings UI + related i18n keys
- Regenerated api-client-react + api-zod from the trimmed spec
- Reverted GET /apps back to getVisibleAppsForUser

Added:
- artifacts/tx-os/src/hooks/use-dock-visible.ts — localStorage-backed
  preference with a custom window event for in-tab sync and the native
  `storage` event for cross-tab sync. Default = true.
- DockBody in settings-panel: single ToggleRow under a new "App dock"
  section ("settingsPanel.section.dock" / "settingsPanel.dock.show")
- AppDock now returns null when the preference is off and clears
  `--app-dock-height` so page padding doesn't stay reserved.

Code review: PASS (architect). No remaining references to the removed
infra. Typecheck shows only pre-existing errors in executive-meetings.ts
and push.ts unrelated to this change.

Follow-up: publish to Gitea + redeploy on Mac (proposed as a follow-up
task).
2026-05-19 11:35:13 +00:00
riyadhafraa c31156be9f Task #609: Per-user app enable/disable in Settings
- New `user_hidden_apps` table (userId+appId composite PK, cascade) in
  lib/db/src/schema/user-hidden-apps.ts; registered in schema index;
  pushed to dev DB via drizzle-kit.
- Backend (artifacts/api-server/src/routes/apps.ts):
  - GET /me/apps — returns every globally-active app visible to the
    user with a `hidden` flag.
  - PUT /me/apps/:appId/hidden — toggles a row in user_hidden_apps,
    gated by getVisibleAppsForUser + isActive so a user can't toggle
    apps they can't reach.
  - GET /apps (Home/Dock) now uses getVisibleNonHiddenAppsForUser so
    hidden apps disappear immediately.
- lib/appsVisibility.ts: added getHiddenAppIdsForUser and
  getVisibleNonHiddenAppsForUser helpers.
- OpenAPI: added /me/apps + /me/apps/{appId}/hidden, MyApp and
  UpdateMyAppHiddenBody schemas; regenerated api-zod + api-client-react.
- Frontend (settings-panel.tsx): added "My apps" accordion section as
  first GroupItem with MyAppsBody — Switch per app, optimistic update,
  invalidates getListMyAppsQueryKey + getListAppsQueryKey so Home/Dock
  refresh without reload.
- Translations: added settingsPanel.section.myApps + settingsPanel.myApps
  in ar.json + en.json.
- Code review fix: /me/apps and PUT gating filter isActive even for
  admins, so inactive apps don't appear in the Settings list.
- Proposed follow-up #611 (Playwright test that hidden apps disappear
  from Home and Dock).
2026-05-19 11:06:21 +00:00
riyadhafraa e00e015b8b Improve how user roles are managed and displayed on the admin page
Introduce separation of direct and inherited roles in user profiles and API responses. Modify the admin UI to disable the "order receiver" toggle when a role is inherited, providing a clearer user experience. Update API endpoints and schemas to reflect these changes, alongside locale updates for translated strings.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 33aee19f-8f0f-4399-98e0-39fe09a87e1b
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/m92e9kU
Replit-Helium-Checkpoint-Created: true
2026-05-19 06:41:10 +00:00
riyadhafraa 1659dc4b68 Task #589: Live build stamp + server start time in System Updates panel
Problem: After `git pull && docker compose up -d --build api` on the
Mac, the admin → System Updates panel kept showing the same version
(0.1.0-dev) and the same build (20260517.b5efd9eb) because both come
from version.json, a hand-edited file that nothing rewrites on every
build. No visible signal that a new image was actually running.

Two independent signals were added — neither needs editing version.json
by hand:

1) Auto-stamp version.json at Docker build time
   - docker/api-server.Dockerfile: new ARGs GIT_SHA and BUILD_DATE.
     A `node -e` step rewrites version.json's `build` field to
     `${YYYYMMDDTHHmm}.${sha}` (e.g. 20260518T1042.a34eb5f5). When
     the args are missing/"unknown", version.json is left untouched
     so plain `docker build` and Replit `pnpm dev` still work.
   - docker-compose.yml: api.build.args wires through GIT_SHA and
     BUILD_DATE with `${GIT_SHA:-unknown}` fallbacks.
   - scripts/redeploy.sh: exports GIT_SHA (git rev-parse --short HEAD)
     and BUILD_DATE (date -u +%Y-%m-%dT%H:%M:%SZ) before `docker
     compose build`, so the helper script just works.

2) startedAt timestamp from the API
   - artifacts/api-server/src/routes/system.ts: SERVER_STARTED_AT
     captured at module load (frozen for the process lifetime) and
     added to every branch of GET /system/version (not-configured,
     error, invalid-version, up-to-date, update-available, catch).
   - lib/api-spec/openapi.yaml: added `startedAt: date-time` to
     SystemVersionResponse (required). Regenerated api-client-react
     and api-zod via `pnpm --filter @workspace/api-spec run codegen`.

3) Admin UI surfacing both signals
   - artifacts/tx-os/src/pages/admin.tsx: new formatBuildStamp parses
     `YYYYMMDD[THHmm].sha` and renders a localized date next to the
     raw token. A new "Server started" line below the build number
     formats the boot timestamp via the existing formatChecked helper.
   - artifacts/tx-os/src/locales/{en,ar}.json: added
     admin.systemUpdates.serverStartedAt ("Server started" /
     "بدأ تشغيل الخادم").

4) Docs
   - replit.md: documented how to verify a real redeploy via the
     System Updates panel and the manual one-liner for `docker
     compose build` without the helper script.

Verified: codegen succeeds, API workflow restarted clean, Vite served
fine. Pre-existing typecheck errors in push.ts and font-settings are
unrelated and untouched.

Mac next steps:
  cd ~/Downloads/TX && git pull && ./scripts/redeploy.sh
Then open admin → System Updates: build line should show a new
`YYYYMMDDTHHmm.sha` stamp and "Server started" should reflect the
new boot time.
2026-05-18 10:39:17 +00:00
riyadhafraa b5efd9eb12 Add system updates page to admin section
Add a new API endpoint and UI section for checking system updates, including internationalized locale strings and necessary dependency updates.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 1e6de894-7c78-4557-b01a-a2c1c01f4155
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/1WKAcHk
Replit-Helium-Checkpoint-Created: true
2026-05-17 14:59:36 +00:00
riyadhafraa 0740971a96 feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO
notification fan-out so the iPad PWA (and any installed browser) receives
system notifications when Tx OS is closed or backgrounded.

Backend
- New `push_subscriptions` table (userId + unique endpoint + p256dh/auth
  keys + ua + timestamps), exported from `@workspace/db`.
- `artifacts/api-server/src/lib/push.ts`:
  - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT
    (Docker volume) with /tmp fallback for Replit dev, else ephemeral.
  - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs
    (orders/meetings/notes), prunes 404/410 endpoints, truncates payload
    bodies to ~3500 bytes.
  - **De-dup gate:** skips push when the user has any active Socket.IO
    connection (uses `io.in(\`user:\${uid}\`).fetchSockets()`), so a
    connected user only gets the in-app chime, never a duplicate system
    notification.
  - `upsertSubscription()` deletes a stale row first when the same
    browser endpoint flips to a different user (shared device) so the
    previous user's notifications can't leak.
- Four new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`,
  `POST /api/push/unsubscribe`, and a spec-aligned alias
  `DELETE /api/push/subscribe?endpoint=...` (auth-gated, Zod-validated).
- Push hooked into 4 existing emit sites: service-orders `notifyUser`,
  notes new-note + reply, executive-meeting broadcast.

Frontend
- `artifacts/tx-os/public/sw.js`: push + notificationclick only (no
  asset caching). All URLs (icon, badge, navigation target) resolved
  against `self.registration.scope`, so the SW works at root or under
  a subpath without code changes.
- SW registration in `main.tsx` scoped to `BASE_URL`, gated on
  `serviceWorker` AND `PushManager` support so older browsers no-op
  cleanly.
- `use-push-subscription` hook (enable/disable/refresh + status).
- New `PushEnablePrompt` card mounted in `App` — appears on first
  launch when supported + permission still "default", one-tap enable,
  dismiss persists for 14 days. Silent on unsupported devices.
- `PushToggleRow` added inside Notification Settings.
- ar/en strings: `notifSettings.push.*` and `common.later`.

Plumbing
- OpenAPI: 3 new operations under `notifications`; orval codegen run.
- `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY /
  VAPID_SUBJECT through to the api service.
- `pnpm --filter @workspace/db run push` applied the schema.
- web-push + @types/web-push installed in api-server.

Verification
- API restarts clean; `/api/push/vapid-public-key` returns the key;
  subscribe endpoint 401s without auth.
- New `artifacts/api-server/tests/push.test.mjs` — 7/7 passing —
  covers VAPID endpoint, auth gating on subscribe/unsubscribe, row
  persistence + removal, idempotent re-subscribe with key rotation,
  account-switch endpoint reassignment, and malformed-input rejection.
- New `artifacts/api-server/tests/push-410-unit.test.ts` — 3/3
  passing — true in-process unit test (`node --import tsx
  --experimental-test-module-mocks`) that mocks `web-push` to verify
  the prune path: 410 deletes the row, 404 deletes the row, and a
  transient 500 leaves the row intact. `tsx` added as a devDep and the
  test:run script picks up `*.test.ts` alongside the existing `.mjs`
  suites.
- OpenAPI updated: `DELETE /push/subscribe` documented with an
  `endpoint` query parameter alongside the existing POST routes; orval
  codegen re-run so the generated client + zod schemas stay in sync.
- Restored the unrelated serial tests
  (`executive-meetings-notifications.test.mjs`, `setup-wizard.test.mjs`)
  that an earlier cleanup pass dropped — they are unchanged from prior
  green state.
- Architect rounds addressed:
  1. race + payload size.
  2. dedup gate + first-launch UX + SW base-path + initial tests.
  3. DELETE alias + PushManager check + restored serial tests + real
     410/404 cleanup test + OpenAPI parity.
2026-05-16 15:39:41 +00:00
riyadhafraa 243ccb3e00 feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO
notification fan-out so the iPad PWA (and any installed browser) receives
system notifications when Tx OS is closed or backgrounded.

Backend
- New `push_subscriptions` table (userId + unique endpoint + p256dh/auth
  keys + ua + timestamps), exported from `@workspace/db`.
- `artifacts/api-server/src/lib/push.ts`:
  - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT (Docker
    volume) with /tmp fallback for Replit dev, else ephemeral in-memory.
  - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs
    (orders/meetings/notes), prunes 404/410 endpoints, truncates payload
    bodies to ~3500 bytes so over-sized notes don't kill delivery.
  - `upsertSubscription()` deletes a stale row first when the same
    browser endpoint flips to a different user (account switch on shared
    device) so the previous user's notifications can't leak.
- Three new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`,
  `POST /api/push/unsubscribe` (auth-gated, Zod-validated).
- Push hooked into the 4 existing emit sites: service-orders `notifyUser`,
  notes new-note + reply, executive-meeting broadcast.

Frontend
- `artifacts/tx-os/public/sw.js`: push + notificationclick handlers only
  (no asset caching). Focuses an existing tab or opens a new one at the
  payload URL.
- SW registration in `main.tsx` scoped to `BASE_URL`.
- `use-push-subscription` hook (enable/disable/refresh + status:
  unsupported / denied / default / subscribed).
- New `PushToggleRow` in `NotificationSettingsContent` with ar/en strings
  (notifSettings.push.*). iPad copy explains that the user must add to
  Home Screen first for iOS to allow Web Push.

Plumbing
- OpenAPI: 3 new operations under `notifications` tag; orval codegen run.
- `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY /
  VAPID_SUBJECT through to the api service.
- `pnpm --filter @workspace/db run push` applied the schema.
- web-push + @types/web-push installed in api-server.

Verification
- API restarts clean; `/api/push/vapid-public-key` returns the generated
  key; subscribe endpoint 401s without auth as expected.
- Architect review surfaced two HIGH issues (account-switch leak,
  payload size); both fixed before completion.
2026-05-16 15:26:18 +00:00
riyadhafraa a794f92e61 Task #517: App image upload, external links, built-in route lock
Admin Add/Edit App now supports:
- Custom image upload (or fall back to Lucide icon) via the existing
  ServiceImageUploader; rendered on the home launcher when set.
- Open mode picker: internal (default), external_tab (window.open),
  external_iframe (renders inside /embedded/:id). External URL input
  shown conditionally and required by the form when an external mode
  is chosen.
- Route field is locked (readOnly + lock hint) when editing a built-in
  app, since those slugs are hardcoded in the SPA router.

Backend:
- apps schema gains image_url, external_url, open_mode (default
  'internal'); drizzle-kit push applied.
- New lib/db/src/built-in-apps.ts exports BUILTIN_APP_SLUGS +
  isBuiltinAppSlug, re-exported from lib/db.
- PATCH /apps/:id rejects route changes whose previous slug is
  built-in with 400 + code='builtin_route_locked'. Same-route no-op
  is allowed; non-route updates on built-ins still work.

Other:
- New SPA route /embedded/:id and embedded-app page (iframe host with
  back + open-in-new-tab + error/not-embeddable states).
- OpenAPI App / CreateAppBody / UpdateAppBody extended; codegen ran.
- en/ar locales: admin.appImage, appExternalUrl, appOpenMode.*,
  builtinPathLocked, embeddedFrame.*.
- New tests in apps-builtin-route-lock.test.mjs (4/4 pass) covering
  reject built-in route change, allow non-route built-in updates,
  allow non-builtin route changes, allow built-in same-route no-op.

Notes / drift:
- BUILTIN_APP_SLUGS is duplicated inline in admin.tsx
  (BUILTIN_APP_SLUGS_FE) because the browser bundle cannot import
  @workspace/db (pulls pg). Comment points at the canonical source;
  drift risk filed as a follow-up.
- Pre-existing failures unrelated to this task: 3 tests in
  executive-meetings-* and tsc errors in
  api-server/src/routes/executive-meetings.ts. Out of scope.
2026-05-12 12:23:38 +00:00
riyadhafraa 84398de390 Task #511: Fully remove Chat feature
Destructive removal per user confirmation ("حذف نهائي ما يرجع").

Removed:
- API: routes/conversations.ts, schema/conversations.ts, all chat
  socket handlers in src/index.ts, /admin/users/:id/dependents/
  conversations+messages endpoints, conversation/message dependency
  counts in users/stats routes.
- Web: pages/chat.tsx, /chat route, dock chat filter, MessageSquare
  icon and messages StatCard on home, all chat-related UI in
  notifications + admin (dependency badges, delete-dialog rows,
  UserDependentConversations/Messages sections, count map keys).
- Locales: nav.chat, home.stats.messages, full chat.* block,
  admin.deleteUser conv/msgCount, admin.users.counts.conv/msg,
  admin.audit.unit.conversation_*/message_*, admin.dependents.user*.
- OpenAPI spec: tags, all /conversations/* paths, conv/msg dependent
  paths, related schemas (ConversationWithDetails, MessageWithSender,
  UserDependentConversation/MessageItem+Page, etc.), UserProfile and
  UserDeletionConflict conv/msg fields, HomeStats.unreadMessages.
  Regenerated client via orval.
- Database: dropped message_reads, messages,
  conversation_participants, conversations (CASCADE); deleted
  notifications with related_type='conversation' or type='chat';
  deleted apps row with slug='chat'; ran drizzle push-force.
- Seed: removed chat:access permission + user-role assignment +
  seeded chat app entry from scripts/src/seed.ts.
- Tests: deleted conversations-leave.test.mjs; cleaned chat refs from
  list-dependency-counts, delete-force-warnings, audit-log-coverage,
  and admin-inline-dependency-counts (e2e) — replaced chat dependents
  with note dependents where needed for force-delete coverage.

Notes preserved: notes.tsx noConversationsYet/conversationWith refer
to NOTE THREADS (not chat) and were intentionally NOT touched.

executive-meetings.ts not modified per replit.md restriction.
Pre-existing flaky test failures in executive-meetings/group/etc
suites remain unrelated to this task.
2026-05-12 10:51:31 +00:00
riyadhafraa 6e49010940 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 08:05:12 +00:00
riyadhafraa 1889407565 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 07:55:32 +00:00
riyadhafraa 6826b475b2 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 07:50:39 +00:00
riyadhafraa aaadd9b520 feat(notes): make incoming-note popup attention-grabbing
Task #409: incoming notes now mirror UpcomingMeetingAlert's attention model
so recipients can't miss them.

Changes:
- DB: add `notification_sound_note` (default "knock"), `notify_notes_enabled`,
  `vibration_enabled_note` to users schema; pushed via drizzle.
- API spec: add 3 new fields to AuthUser + UpdateNotificationPreferencesBody;
  regenerated codegen.
- Auth route: include new fields in buildAuthUser + PATCH allowlist.
- Socket hook: play notification sound + vibrate on `note_received`,
  gated by mute/notifyNotesEnabled/socket warmup, with playedNoteIdsRef
  dedupe (mirrors upcoming-meeting-alert playedRef).
- IncomingNotePopupContext: enqueue() now returns boolean acceptance so
  the socket hook can suppress sound on own-note/duplicate.
- Popup visual: z-[110], ring-4 amber, shadow-2xl, animate-in zoom,
  avatar animate-ping pulse.
- Settings UI: refactored to SLOT_KEYS map, added 3rd "Notes" slot tab
  (grid-cols-3) with enabled/vibration toggles + sound picker.
- Locales en/ar: added notifSettings.slot.note, notesEnabled, vibrationNote.

Pre-existing typecheck errors in executive-meetings.ts (font settings
scope) are unrelated to this task.
2026-05-05 16:21:14 +00:00
riyadhafraa eade4643e5 Add ability to view personal notes and update note schemas
Adds a new endpoint for listing user-specific notes and refines OpenAPI schemas for better data structure definition.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 3565b59b-6890-4cf5-90a7-0c8041e15041
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/G3ZvHke
Replit-Helium-Checkpoint-Created: true
2026-05-05 15:28:41 +00:00
riyadhafraa 5b13fe16f5 Task #402: Convert Notes into in-app messaging
Original task: turn personal Notes into in-app messaging — sender composes
a note (title/content/color), picks recipient(s), Send. Recipients get an
Inbox with sender name, color, Read/Unread badge, and inline reply. Sender
sees Sent Notes with per-recipient status. Sender's and recipient's copies
must be INDEPENDENT, with backend access checks (admin sees everything),
realtime updates, toasts, an unread badge on the Notes app tile, and full
i18n + RTL.

Four review rounds were addressed in this commit:

Round 1 (independence):
- Snapshot columns (title/content/color) on note_recipients; FKs dropped
  on note_recipients.note_id and note_replies.note_id so recipient
  threads survive the sender deleting their note.
- /notes/received and /notes/:id/thread render the recipient snapshot.

Round 2:
- POST /notes/:id/reply: owner is now allowed to reply too.
- Added GET /notes/:id as alias of /notes/:id/thread.
- Added OpenAPI ops for the Notes routes; ran orval codegen.
- use-notifications-socket.ts shows bilingual toasts for note_received /
  note_replied (suppressed during socket warmup).
- home.tsx renders an unread badge on the Notes app tile.

Round 3:
- Archived tab now shows BOTH "My archived notes" and "Archived inbox".
- Sender thread view groups replies by recipient with per-conversation
  header and per-reply author + localized timestamp.
- Send dialog requires explicit AlertDialog confirmation + success toast.
- Realtime toast strings moved off hardcoded EN/AR to i18n keys.
- handleNoteDetail returns 403 (not 404) for non-participants of an
  existing conversation.
- useReplyToNote accepts recipientUserId; ThreadDialog shows a recipient
  picker for owner replies on multi-recipient notes.

Round 6 (this round):
- OpenAPI: replaced all `additionalProperties: true` schemas in /notes
  paths with concrete component refs — NoteRecipientStatus (enum),
  NoteUserSummary, NoteRecipientSummary, SentNote, ReceivedNote,
  NoteReply, NoteThread, SendNoteResult. Re-ran orval; api-zod and
  api-client-react regenerated cleanly.
- Trimmed narrative comments in artifacts/api-server/src/routes/notes.ts
  (handleNoteDetail, /sent, /received, /send, /read, /reply).
- Schema FK + enum policy: kept noteId as plain integer (no FK) on
  note_recipients/note_replies — this is required so recipient
  snapshots survive sender deletion. Status remains a varchar but is
  constrained at the API boundary by the new NoteRecipientStatus enum
  schema and TS string-literal union on both server and client.
- Migrations: this monorepo uses `drizzle-kit push` exclusively
  (lib/db has no migrations directory; drizzle.config.ts is push-only;
  package.json defines only push/push-force scripts). No migration
  files committed by design.

Round 5: admin authorization fix
- handleNoteDetail: admin bypass is now evaluated BEFORE the
  "non-participant 403" branch. When the sender has deleted the live
  note row but recipient snapshots remain, an admin GET /notes/:id now
  returns 200 with thread payload assembled from a fallback recipient
  snapshot (instead of 403).
- New regression test: "admin can read a note whose sender deleted
  their copy (recipient snapshot remains)".

Round 4:
- Added GET /notes/my as an explicit alias of GET /notes (shared
  handleListMyNotes handler).
- Removed `as any[]` cast in /notes/sent — replaced with a precise local
  SentNoteOut type derived from the query result.
- Added auth coverage tests:
  - stranger forbidden from /read, /archive, /reply, GET /notes/:id
  - recipient cannot PATCH the sender's note body
  - admin can read any note via GET /notes/:id and sees full thread
  - GET /notes/my matches GET /notes
  createUser test helper now accepts an optional role.

Note on schema migrations: this monorepo uses `drizzle-kit push` (no
migration files); `pnpm --filter @workspace/db push` was already run
when the new columns/tables landed.

Tests: 11 backend tests in notes-share.test.mjs + 1 e2e (notes-inbox)
all pass. tx-os typecheck is clean. Architect re-review: PASS.

Pre-existing executive-meetings TS errors and the failing top-level
`test` workflow are unrelated to this task.
2026-05-05 15:26:23 +00:00
riyadhafraa 65c5a172d6 Task #402: Convert Notes into in-app messaging
Original task: turn personal Notes into in-app messaging — sender composes a
note (title/content/color), picks recipient(s), Send. Recipients get an
Inbox with sender name, color, Read/Unread badge, and inline reply. Sender
sees Sent Notes with per-recipient status. Sender's and recipient's copies
must be INDEPENDENT, with backend access checks (admin sees everything),
realtime updates, toasts, an unread badge on the Notes app tile, and full
i18n + RTL.

Two prior code-review rounds were addressed in this commit:

Round 1 (independence):
- Added immutable snapshot columns (title/content/color) on note_recipients.
- Dropped the FK from note_recipients.note_id and note_replies.note_id so
  recipient threads survive the sender deleting their note.
- /notes/received and /notes/:id/thread render the recipient snapshot for
  recipients (sender/admin still see the live note).

Round 2 (validation REJECT fixes):
- POST /notes/:id/reply: owner is now allowed to reply too. Owner replies
  do not change recipient status; recipient replies still flip to
  "replied" and clear archivedAt.
- Added GET /notes/:id as an alias of /notes/:id/thread (shared handler).
- Added OpenAPI ops for /notes/sent, /notes/received, /notes/{id},
  /notes/{id}/send, /notes/{id}/read, /notes/{id}/archive,
  /notes/{id}/reply, and ran orval codegen.
- use-notifications-socket.ts now shows bilingual toasts for note_received
  and note_replied (suppressed during the socket warmup window).
- home.tsx renders an unread badge on the Notes app tile, fed by
  useReceivedNotes(false) filtered to status === "unread"; refreshes
  automatically on socket invalidation.

Tests: 7 backend tests in notes-share.test.mjs (independence,
archived-reply, owner-reply preserves recipient status, GET /notes/:id
alias, etc.) + the notes-inbox e2e all pass. tx-os typecheck is clean.

Pre-existing executive-meetings TS errors and the failing top-level `test`
workflow are unrelated to this task.
2026-05-05 15:01:18 +00:00
riyadhafraa d62e67af96 Task #397: Per-card and bulk select+delete on Incoming Orders
Backend (artifacts/api-server):
- Added hasReceivePermission() helper and refactored DELETE /orders/:id
  into shared authorizeAndDeleteOrder() so single and bulk paths use the
  same authorization rules.
- Added POST /orders/bulk-delete returning { deletedIds, failedIds } with
  per-id authorization, dedup, and best-effort partial success.
- Tightened receiver authorization (per code review): receivers may only
  delete pending/received/preparing orders; terminal (completed/cancelled)
  orders remain the owner's cleanup responsibility. OpenAPI description
  and the implementation now agree.

API spec (lib/api-spec/openapi.yaml):
- New /orders/bulk-delete operation with BulkDeleteServiceOrdersBody and
  BulkDeleteServiceOrdersResponse schemas; updated DELETE /orders/{id}
  description. Regenerated react-query hooks via codegen — useBulkDelete
  ServiceOrders is now available.

UI (artifacts/tx-os/src/pages/orders-incoming.tsx):
- Selection mode toggle in the page header (RTL-safe).
- Per-card Checkbox plus a Select-all control.
- Sticky bottom action bar with destructive Delete and selected count.
- AlertDialog confirmation using pluralized i18n keys; toast surfaces
  partial-failure results when some ids couldn't be deleted.

i18n: New incomingOrders keys in ar.json and en.json (select, selectAll,
clearSelection, selectedCount, delete, deleteConfirmTitle/Body, deleted,
deleteFailed, deletePartial) with pluralization.

Tests: Added two new tests in service-orders.test.mjs covering receiver
delete on incoming queue, receiver forbidden on terminal orders, and
bulk-delete with mixed authorized / unknown / dedup / unauthenticated
cases. Both pass. Pre-existing executive-meetings PDF/font test failures
are unrelated.
2026-05-05 13:35:21 +00:00
riyadhafraa f8947c0bc7 Task #389: Notification sounds + vibration with per-user customization
DB
- Added user pref columns; vibration is per-channel
  (vibrationEnabledOrder + vibrationEnabledMeeting). Defaults: ding/chime
  sounds, vibration on, mute off. Volume uses the device system level —
  no persisted volume field.

API
- PATCH /auth/me/notification-preferences (requireAuth, whitelisted partial
  update, integer guard for volume). buildAuthUser exposes all new fields.
- OpenAPI spec updated; orval client + zod regenerated.

Frontend
- Static sound library: 8 short WAV assets under public/sounds/ + manifest
  (notification-sound-manifest.ts) mapping id -> labelKey -> URL.
- HTMLAudio-based player (notification-sounds.ts) with throttle, unlock,
  cache, and AUTOPLAY_BLOCKED_EVENT dispatch when play is denied pre-gesture.
- Settings popover: global mute + slot tabs (orders/meetings) with
  per-channel sound + per-channel vibration + per-channel toggle, sound list
  with one-tap previews. Concurrency-safe optimistic updates (monotonic seq
  counter). RTL-correct toggle knob transforms.
- QuickMuteButton: one-tap topbar mute control (Volume2/VolumeX) with
  confirmation toast.
- useAudioUnlock: unlocks audio on first interaction.
- useAutoplayHint: toasts a localized "click anywhere to enable" hint when
  the player reports a blocked play attempt.
- Socket hook plays the right per-channel sound + vibration on
  notification_created (orders, executive_meeting) when the tab is hidden.
- UpcomingMeetingAlert: plays meeting reminder once per new eligible
  meeting (deduped by meetingId, survives 30s polling refetches). Skipped
  when the tab is currently visible — sound only fires when backgrounded.
- AR/EN translations added.

Pre-existing TS errors in executive-meetings.ts (font_settings) and
pre-existing test workflow failures are unrelated.
2026-05-05 08:24:27 +00:00
riyadhafraa 4af3917026 Task #389: Notification sounds + vibration with per-user customization
DB
- Added 7 user pref columns; vibration is now per-channel
  (vibrationEnabledOrder + vibrationEnabledMeeting). Defaults: ding/chime
  sounds, vibration on, mute off, volume 70.

API
- PATCH /auth/me/notification-preferences (requireAuth, whitelisted partial
  update, integer guard for volume). buildAuthUser exposes all new fields.
- OpenAPI spec updated; orval client + zod regenerated.

Frontend
- Static sound library: 8 short WAV assets under public/sounds/ + manifest
  (notification-sound-manifest.ts) mapping id -> labelKey -> URL.
- HTMLAudio-based player (notification-sounds.ts) with throttle, unlock,
  cache, and AUTOPLAY_BLOCKED_EVENT dispatch when play is denied pre-gesture.
- Settings popover: global mute, volume, slot tabs (orders/meetings) with
  per-channel sound + per-channel vibration + per-channel toggle, sound list
  with one-tap previews. Concurrency-safe optimistic updates (monotonic seq
  counter). RTL-correct toggle knob transforms.
- QuickMuteButton: one-tap topbar mute control (Volume2/VolumeX) with
  confirmation toast.
- useAudioUnlock: unlocks audio on first interaction.
- useAutoplayHint: toasts a localized "click anywhere to enable" hint when
  the player reports a blocked play attempt.
- Socket hook plays the right per-channel sound + vibration on
  notification_created (orders, executive_meeting) when the tab is hidden.
- UpcomingMeetingAlert: plays meeting reminder once per new eligible
  meeting (deduped by meetingId, survives 30s polling refetches).
- AR/EN translations added.

Pre-existing TS errors in executive-meetings.ts (font_settings) and
pre-existing test workflow failures are unrelated.
2026-05-05 08:20:54 +00:00
riyadhafraa 242f63ab26 Task #389: Notification sounds + vibration with per-user customization
- DB: 7 new user prefs (sound per slot, per-type toggles, vibration, mute, volume).
- API: PATCH /auth/me/notification-preferences with whitelisted partial update,
  integer guard for volume, and hydrated AuthUser response. AuthUser now exposes
  the 7 new fields.
- Frontend: Web Audio synth library (8 sounds), settings popover with global
  mute/vibration/volume/per-type toggles, slot tabs, sound preview buttons,
  shift+click on bell for quick mute. Concurrency-safe optimistic updates with
  monotonic seq counter. RTL-correct toggle knob transforms.
- Socket hook plays sound on notification_created (orders/meetings only) when
  tab is hidden, respecting global mute and per-type toggles.
- Audio unlock hook mounted globally to satisfy autoplay restrictions.
- AR/EN translations added.

Pre-existing TS errors in executive-meetings.ts (font_settings) and pre-existing
test failures in test workflow are unrelated to this task.
2026-05-05 08:13:38 +00:00
riyadhafraa 49bf44f0a2 Add functionality to download role permission audit history as a CSV file
Introduce a new admin-only API endpoint for exporting role permission audit data to CSV, including resolved permission names and UTF-8 BOM for Excel compatibility. Frontend button and backend logic implemented to support this feature.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 0cb48020-8f0c-42bb-8fe8-d638905f7fce
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/g7BgHDL
Replit-Helium-Checkpoint-Created: true
2026-05-01 16:11:43 +00:00
riyadhafraa 6876a83bbb Audit log: filter by actor (#197)
Admins can now filter the audit log by who acted, not just by what was
acted on.

User-facing changes
- Replaced the flat actor <select> with an autocomplete combobox
  (Popover + cmdk Command) that searches by username AND display name,
  in English or Arabic.
- Added an `actorId` URL hash param that survives full reload — picks
  up alongside the existing target filter on initial mount and is
  cleared when the admin navigates to a different section.
- Each audit row's actor avatar + name is now clickable, mirroring the
  existing target chip pivot, so admins can jump from "this row" to
  "everything by this person" in one click. Rows with a null actor
  (system / deleted user) render the same avatar/name without the
  click affordance, since the API can only filter by a concrete id.
- Active actor pill renders in sky (target pill remains emerald) and
  has a clear button.
- CSV export already accepted `actorUserId` on the backend; the
  frontend export call now forwards the filter and the OpenAPI
  description has been updated to document it.

Files
- artifacts/tx-os/src/pages/admin.tsx
  - New AuditActorPicker component, hash helpers
    (parseAuditHashActor / syncAuditHashActor), pivotToActor /
    clearActorFilter handlers, sky-colored active pill, clickable
    actor in AuditLogRow.
- artifacts/api-server/tests/audit-logs-actor-filter.test.mjs (new)
  - 6 tests covering: filter narrows results, excludes other actors
    on the same target_type, combines with target filter, invalid
    /zero/negative actorUserId → 400, CSV export honors filter.
- artifacts/tx-os/src/locales/{en,ar}.json
  - actorSearch / actorEmpty / actorWithName / actorWithId /
    clearActorFilter / actorPivotAria.
- lib/api-spec/openapi.yaml
  - Audit export endpoint description now mentions
    targetType / targetId / actorUserId.

Verification
- pnpm --filter @workspace/tx-os run typecheck → clean.
- All 6 new actor-filter tests + all 7 existing target-filter tests
  pass against a live API server.
- E2E run via runTest() succeeded: opened the picker, selected an
  actor, verified the pill + reload-safe hash, cleared, pivoted via
  a row click, and confirmed CSV export honored the filter.

Notes / drift
- URL key is `actorId` (per task spec), but the React state and API
  param remain `actorUserId` to match the existing backend.
- The broader `test` workflow has pre-existing failures in
  executive-meetings.test.mjs and service-orders.test.mjs unrelated
  to this change (admin 401 / HTML-404 fallback). Captured as
  follow-up #291.

Replit-Task-Id: 0f02b232-eda3-46db-8235-98ecce2ebdb7
2026-05-01 15:26:36 +00:00
riyadhafraa c0e55752a3 Task #183: clickable dependency counts on admin Apps/Services/Users
Turned the inline dependency-count badges on each admin row into
focusable, keyboard-accessible buttons that open a drill-in modal
listing the actual rows behind the count.

Backend (artifacts/api-server/src/routes):
  - apps.ts: GET /admin/apps/:id/dependents/{groups,restrictions,opens}
  - services.ts: GET /admin/services/:id/dependents/orders
  - users.ts: GET /admin/users/:id/dependents/{notes,orders,conversations,messages}
  All paginated (limit default 50, max 200; offset) returning
  {items, totalCount, limit, offset, nextOffset}. Restrictions joins
  the role names that include each required permission.

OpenAPI:
  - Added 8 path operations + 16 schemas (Item + Page) and re-ran
    `pnpm --filter @workspace/api-spec run codegen`.

Frontend (artifacts/tx-os/src/pages/admin.tsx):
  - New DependencyDrillIn component reuses DrillInShell (Escape +
    backdrop close, RTL/LTR safe) and the existing LoadMoreSection
    pagination pattern from AppOpensDrillIn.
  - Each count part in Apps, Services, and Users panels is now a
    <button> with a unique data-testid (e.g. app-counts-groups-213,
    user-counts-messages-5) and an aria-label that reads
    "View {count}".
  - AdminPage owns dependencyTarget state for Apps/Services counts;
    UsersPanel owns its own (it already encapsulates user list).

Translations:
  - Added admin.dependents.* (titles, subtitles, empty/error/load
    labels, conversation/order/message helper strings, order status
    enum) to en.json and ar.json.

Verification: - tx-os typecheck clean; api-server has only the pre-existing
    executive-meetings.ts errors (untouched).
  - e2e tested via runTest: login as admin, opened Apps panel,
    drilled into groups + opens (Load more grew 50→100), drilled into
    Tea orders, drilled into user 5 conversations and messages,
    switched to Arabic/RTL and re-opened the Groups drill-in to
    confirm Arabic rendering with no raw i18n keys.
Replit-Task-Id: fe96a05b-325f-4e1f-901b-3a2235fb24b5
2026-05-01 07:32:11 +00:00
riyadhafraa 90c319fb25 Show inline dependency counts on the Roles admin list (Task #182)
The Apps, Services, Users, and Groups admin panels already surface their
dependency counts inline so admins know what's affected before clicking.
The Roles panel previously hid this — admins had to open the delete dialog
to see how many users/groups would be affected. This change adds the same
inline display to the Roles panel for consistency.

Changes
- lib/api-spec/openapi.yaml: Added optional `userCount` and `groupCount`
  fields to the `Role` schema (matching the App pattern: optional, populated
  only by the admin list endpoint, with descriptive comments).
- artifacts/api-server/src/routes/roles.ts: GET /roles now batches two
  grouped count queries (user_roles, group_roles) and merges the counts
  into each list item — same shape as GET /apps. Empty-list short-circuits
  before running the aggregations.
- lib/api-zod/src/generated/api.ts: Regenerated via the api-spec codegen
  script (orval). ListRolesResponseItem now includes the optional counts.
- artifacts/tx-os/src/pages/admin.tsx (RolesPanel): Each role card renders
  an inline counts row using the existing `admin.roles.usersCount` /
  `admin.roles.groupsCount` translation keys (no new copy needed).
  Mirrors the Apps panel pattern: 11px muted-foreground text with bullet
  separators, only renders when at least one count is > 0, and exposes
  a `data-testid="role-counts-<id>"` for tests.

Notes / deviations
- The task description said "the role list endpoint already returns
  userCount/groupCount" but it didn't — the counts only existed on
  /roles/:id/usage. Added them to the list endpoint following the same
  pattern Apps and Groups already use.
- The pre-existing admin.roles.usersCount/groupsCount keys have no
  `_one`/`_other` plural variants; I kept it that way to stay consistent
  with the Apps panel keys (which also have no plural variants).

Verification
- `pnpm -w run typecheck` passes for tx-os and roles.ts (pre-existing
  unrelated typecheck errors in executive-meetings.ts remain — not touched
  by this change).
- e2e test (testing skill, status: success): logged in as the seeded
  admin, opened the Roles panel, verified inline counts render on the
  admin and user roles, bullet separator is present, and roles with zero
  dependencies don't render an empty counts area.

Replit-Task-Id: 8c99d912-8b3a-4e80-aca7-ec167e6e75e6
2026-05-01 06:58:52 +00:00
riyadhafraa b3ad701ba6 Task #243: Admin audit log — focused readability + actor-filter subset
Landed a tight subset of the 13-item umbrella, mirroring the proven
narrow-then-defer pattern from #242:

- #195 — Plain DELETE /api/services/:id now writes a `service.delete`
  audit row carrying nameEn + nameAr (force-with-deps still uses the
  dedicated `service.force_delete`). Added matching `service.delete`
  formatter case + EN/AR i18n keys, and surfaced nameAr on the existing
  `service.force_delete` summary.
- #197 — `actorUserId` filter for `/admin/audit-logs` and CSV export.
  openapi.yaml updated, codegen regenerated, server filter wired through
  parseFilters/buildWhere with 400-on-invalid handling, AuditLogPanel UI
  got an actor dropdown wired into params + export URL + reset, and a
  new audit-logs-actor-filter API test (4 cases) covers list narrowing,
  exclusion, invalid input, and CSV export.
- #178 — Formatter unit tests for user.delete (id-only, EN/AR display
  name resolution, force flag, force + name) and the new service.delete
  (id-only, EN/AR), 11 new cases (33/33 pass).

Skipped #194 — already implemented; users.ts DELETE persists displayName
fields and audit-summary already renders user.deleteWithName/forceDeleteWithName.

Deferred via follow-ups (no duplicate of existing #182/#183/#184):
- F1: #196 recent-activity endpoint + 5 admin panels
- F2: #205+#206+#208 permission history CSV/name resolution/timeline
- F3: #209+#210 cascade/bulk audit rows + e2e UI spec for History tabs

Validation: tx-os typecheck clean; pre-existing executive-meetings.ts
errors not regressed; all targeted server tests pass (delete-force-warnings 10,
audit-logs target-filter 7, forced-only 6, audit-log-coverage 27, new
actor-filter 4, broader audit/services sweep 40); e2e test verified actor
dropdown rendering, filter behavior, readable Arabic service.delete summary,
and CSV export honoring the filter.
2026-05-01 06:54:26 +00:00
riyadhafraa 60a18e1c7c Task #162: Let admins pre-set required permissions while creating an app
The "Required permissions" section was previously edit-only because
`POST /api/apps/:id/permissions` needs an app id, leaving a brief
window where a freshly created app was visible to everyone before
the admin could re-open the dialog and gate it. The Add app dialog
now lets the admin pick required permissions up front and the new
app + its `app_permissions` rows are written in a single transaction.

Changes:
- `lib/api-spec/openapi.yaml`: extended `CreateAppBody` with an optional
  `permissionIds: integer[]` field. Ran `pnpm --filter @workspace/api-spec
  run codegen` so `lib/api-zod` and `lib/api-client-react` reflect it.
- `artifacts/api-server/src/routes/apps.ts`: `POST /apps` now de-dupes
  and pre-validates `permissionIds`, returns 404 if any id is unknown
  (without creating the app), and inside one transaction inserts the
  app, the `app_permissions` rows (with `.onConflictDoNothing()` against
  the composite primary key), and a single `permission_audit` row
  (`previousIds: []`, `newIds: requestedIds`). After the transaction it
  also writes one `app.permission.add` audit_logs entry per inserted
  permission so the admin log mirrors the post-create flow.
- `artifacts/tx-os/src/pages/admin.tsx`: added `permissionIds: number[]`
  to `AppForm`, a new `NewAppPermissionsPicker` component (rendered only
  in create mode — edit mode keeps the existing `AppPermissionsEditor`
  with its impact preview) that lets admins add/remove permissions
  locally before submit, and wired `handleSaveApp` to forward the
  selected ids when creating. Existing edit path strips the field so
  the update payload remains unchanged.
- `replit.md`: documented the new picker and POST /api/apps behavior.

No impact preview is shown in the create-mode picker because a brand
new app starts with zero users seeing it, so adding permissions cannot
hide it from anyone.

Code-review follow-up: tightened input validation so non-integer or
non-positive `permissionIds` now return 400 with a clear error instead
of being silently dropped by the previous filter. The legacy single-add
endpoint already used this exact 400 message, so behavior stays
consistent across both create and update paths.

Verification:
- `pnpm --filter @workspace/api-spec run codegen` passes.
- `pnpm --filter @workspace/api-server` typechecks with no new errors
  (executive-meetings.ts errors are pre-existing and unrelated).
- Ran the existing app-permission test suites
  (`app-permission-audit.test.mjs`, `app-permissions-crud.test.mjs`,
  `app-permissions-impact.test.mjs`) directly — all 16 tests pass.
- Ran an e2e Playwright test (login as admin → Add app → pick a
  permission → save → verify the row shows 1 restriction → reopen and
  confirm the assigned permission). All steps passed.

Follow-up proposed: automated tests for the new create-with-permissions
endpoint behavior (#231).

Replit-Task-Id: c229777c-4036-4a6a-b4cb-05ccc18f6c9b
2026-04-30 18:21:21 +00:00
riyadhafraa b507b33cad Add app-permissions impact preview before tightening an app's gate
Mirrors the existing role-permissions impact preview UX for app
permissions. Admins now see how many currently-visible users would lose
access before they add a permission requirement to an app, plus the
groups (via group_apps) that offset the loss because their members keep
access regardless.

Backend
- New endpoint POST /api/apps/:id/permissions/impact-preview in
  artifacts/api-server/src/routes/apps.ts. Implements the same OR
  semantics as getVisibleAppsForUser: a user "sees" an app if they hold
  ANY required permission (direct or via a group role) OR they belong
  to a group granted the app via group_apps. Admins are excluded from
  counts since they always see every app. Short-circuits with
  noChange:true when the candidate set equals the current set.
- OpenAPI schema (lib/api-spec/openapi.yaml): adds the path,
  AppPermissionsImpactBody, AppPermissionImpactGroup,
  AppPermissionsImpact. Regenerated lib/api-client-react bindings.

Frontend
- AppPermissionsEditor (artifacts/tx-os/src/pages/admin.tsx): debounced
  (350ms) cancel-safe preview when a pending permission is selected,
  warning banner with affected/visible counts and offsetting groups,
  and a confirmation dialog when affectedUserCount > 0. Add button is
  disabled while the preview is loading or errored to keep the warning
  trustworthy.
- i18n keys added to en.json and ar.json under
  admin.appPermissions.{impactTitle, impactLoading, impactError,
  impactNone, impactSummary, impactViaGroups, confirmTitle, confirmBody,
  confirmAction}.

Tests
- artifacts/api-server/tests/app-permissions-impact.test.mjs: 7 tests
  covering noChange short-circuit, unrestricted-app tightening,
  candidate that keeps an existing permission, group_apps offset,
  unknown app (404), invalid payload (400), and admin-only enforcement.
  All 18 app-permissions tests pass.
- E2E flow verified via runTest: admin login → /admin → Apps → edit
  app → select permission → preview banner appears → Add → confirm
  dialog → cancel without writing.

Out-of-scope (filed as follow-ups #228 and #229): listing the specific
affected user IDs in the preview, and warning when REMOVING a
permission broadens access.

No deviations from the task spec.

Replit-Task-Id: 8b2ff9ea-f95e-4bdb-b268-95b5d17154ca
2026-04-30 18:03:08 +00:00
riyadhafraa 2bf2f3cd3a Task #147: Add structured permission-change audit (users, groups, apps)
Mirrors the existing role-permission audit pattern with a unified
`permission_audit` table capturing actor, target, prev/new id sets, and
timestamp written in the same transaction as the change.

Schema & API
- New `permission_audit` table (target_kind, target_id, change_kind,
  actor_user_id, previous_ids[], new_ids[], created_at) with index on
  (target_kind, target_id, created_at).
- Transactional audit writes in routes/users.ts (POST/DELETE roles,
  PATCH groupIds), routes/groups.ts (PATCH + add/remove members for
  users/roles/apps), routes/apps.ts (POST/DELETE permissions).
- Cross-entity mirroring: when group membership changes via a group
  endpoint, a user.groups row is also written for each affected user
  (and vice versa via PATCH /users), so each entity's history is
  exhaustive regardless of which editor was used.
- Admin-only GET /users/:id/audit, /groups/:id/audit, /apps/:id/audit
  with limit/offset/actorUserId/from/to filters and the same response
  shape as role audit.
- OpenAPI types + codegen regenerated.

UI
- Reusable PermissionAuditHistory component in admin.tsx wired into
  UserGroupsEditor, GroupDetailEditor (new "history" tab), and the
  editing-app dialog. App history correctly resolves permission ids
  (NOT roles) via useListPermissions.
- Bilingual i18n keys added under admin.{users,groups,apps}.history*
  in en.json + ar.json.

Tests
- New backend tests: user-permission-audit, group-permission-audit,
  app-permission-audit (14 cases — transactional capture, GET filters
  & pagination, admin-only, 404 on unknown id, plus 2 new mirror
  tests covering cross-entity audit visibility). All pass; 35
  adjacent role/groups/users/audit-coverage tests still pass.

Notes
- replit.md updated to list `permission_audit` table.
- Restored opengraph.jpg (an unrelated stray binary diff).
- Code-review comments addressed: cross-entity asymmetry fixed via
  mirroring; opengraph.jpg restored.
- Follow-ups proposed: timeline UI improvements, cascade audit on
  delete/bulk paths, e2e UI test for History sections.

Replit-Task-Id: 4ba8c8de-8dfd-42c7-a3bc-964a1ed3a1a3
2026-04-30 11:58:18 +00:00
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