Commit Graph

47 Commits

Author SHA1 Message Date
riyadhafraa e7948012f4 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 when an external mode is chosen.
  Internal route field is hidden entirely when an external mode is
  selected, and locked (readOnly + lock hint) when editing a built-in
  app whose path is hardcoded in the SPA.

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. Exposed via subpath export
  `@workspace/db/built-in-apps`; the file has zero imports so the
  browser bundle can use it without pulling in `pg`. tx-os now imports
  it directly — duplicate FE constant removed.
- Built-in slug list: services, notifications, admin, notes,
  my-orders, orders-incoming, executive-meetings (everything with a
  hardcoded <Route> in artifacts/tx-os/src/App.tsx). calendar /
  documents are seeded but admin-defined and remain editable.
- 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.*.
- scripts/src/seed.ts: drift guard throws if a seeded built-in slug
  uses a route that does not match the hardcoded SPA route.

Tests:
- New API test apps-builtin-route-lock.test.mjs (4/4 pass): reject
  built-in route change, allow non-route built-in updates, allow
  non-builtin route changes, allow built-in same-route no-op.
- New Playwright E2E admin-app-image-external-embedded.spec.mjs
  (passes): launcher renders custom image_url, external_tab opens
  external URL via window.open, external_iframe navigates to
  /embedded/:id and renders <iframe src=externalUrl>.

Out of scope (pre-existing, not introduced here):
- 3 failing tests in executive-meetings-* and tsc errors in
  api-server/src/routes/executive-meetings.ts.
2026-05-12 12:31:43 +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 e4b5ece2e4 Enhance the app editing interface with improved input hints
Add helper text and inline hints to the slug and route input fields within the app editing form to clarify their purpose and expected format.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 18fca138-6c4d-4f2b-9859-dcd578b8df5c
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/O07Jndv
Replit-Helium-Checkpoint-Created: true
2026-05-12 09:36:50 +00:00
riyadhafraa d5dd3acf9f Task #509: Redesign Edit App modal + verify responsive sizing
User asked for the Edit App modal to match the new professional Edit
Service design and confirmed responsiveness for iPad / mobile.

Changes — admin.tsx only:

1. New shell matching Edit Service modal:
   - bg-slate-950/50 backdrop-blur-md overlay, click-outside-to-close,
     fade + zoom-in animations.
   - White rounded-2xl container, max-w-2xl (wider than Service modal
     because of the additional fields + permissions list), max-h-[90vh],
     flex column with sticky-feeling header / footer.
   - Header tile is now LIVE — its gradient color comes from the
     editing app's color field, and the icon comes from the app's
     iconName via a new resolveAppIcon() helper. So the header
     previews the app as you edit.
   - X close button in header.

2. Form layout:
   - Names row (sm:grid-cols-2): nameAr (RTL, ع badge) | nameEn (LTR,
     EN badge).
   - Identifier row (sm:grid-cols-2): slug | route, both font-mono.
   - Visuals row (sm:grid-cols-2):
     - Icon picker: 40x40 preview tile + text input. Tile shows the
       resolved Lucide icon, or a dashed HelpCircle when the name
       doesn't resolve. Hint text under the field.
     - Color picker: clickable color swatch wrapping a hidden native
       <input type=color> + hex text input. Both stay in sync.
   - Permissions wrapped in a bordered card with its own header strip.

3. Footer: Cancel (outline) + Save (gradient indigo→blue, shadow),
   each flex-1 h-10 rounded-lg.

4. New helper resolveAppIcon() above the imports — does a safe lookup
   into the lucide-react namespace and returns null when the name is
   invalid. Mirrors the pattern already used in pages/home.tsx.

Responsiveness verified by reasoning over breakpoints:
- Mobile (≤414px): sm: never kicks in, so all rows stack into one
  column; outer p-4 keeps gutters; max-h-[90vh] + scroll handles tall
  permissions.
- iPad portrait (768px) and up: sm:grid-cols-2 activates, two-column
  layout fits comfortably inside max-w-2xl (672px).
- Desktop: same max-w-2xl cap looks well-proportioned, not cramped.
- Edit Service modal already uses the same primitives, no changes
  needed.

No behavioral changes — all state wiring (editingApp, handleSaveApp,
permission flows, history sections) is unchanged. Pure presentational
refactor.

Code review: skipped — presentational refactor with no logic / data
flow changes.
Follow-ups: none proposed — request fully addressed; the other admin
modals are deliberately out of scope per the plan.
2026-05-12 09:35:47 +00:00
riyadhafraa c6fa183203 Redesign service editing window for a professional look and feel
Refactors the "Edit Service" modal UI in `admin.tsx` to improve visual hierarchy, user experience, and overall professionalism, including styling changes to the header, input fields, image uploader, and availability toggle.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 28ee2705-c2ec-476e-aa4f-cc6c7cd5624e
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/O07Jndv
Replit-Helium-Checkpoint-Created: true
2026-05-12 09:27:57 +00:00
riyadhafraa a565d47027 Task #508: Remove unused السعر (Price) field from admin Services UI
User requested removal of the price field from the admin Services edit
form ("بالنسبه الى الاسعار أحذفها لايوجد اسعار") because there are no
prices in the product.

Changes — frontend only, all in tx-os:
- artifacts/tx-os/src/pages/admin.tsx
  - Drop `price: string` from ServiceForm type.
  - Drop `price: "0.00"` from emptyServiceForm.
  - Remove the price entry from the edit dialog's field array
    (no more السعر input).
  - Remove the `<div>{service.price}</div>` from the services list.
  - Remove the `price` line from the edit-load mapper.
- artifacts/tx-os/src/locales/ar.json + en.json
  - Drop the now-unused `servicePrice` key.

Left intact:
- The generic `"price"` key in locales (line 104) — kept conservatively
  in case anything outside this scope expects it, though ripgrep shows
  no current usage.
- The `services.price` DB column and api-server code — unused in api
  src; only test fixtures insert a placeholder. Removing would require
  a migration and break tests for zero benefit.
- The user-facing services page — never displayed price.

Verification:
- ripgrep confirms zero `price` / `servicePrice` references remain in
  artifacts/tx-os/src/pages/admin.tsx and the locales (other than the
  generic shared key).
- Vite HMR applied cleanly with no TS errors.
- Pre-existing test failures (executive-meetings, service-orders) are
  unrelated and already tracked as separate project tasks.

Code review: skipped — change is a 5-line mechanical removal of dead
UI with no logic or data-flow implications.

Follow-ups: none proposed — this is a self-contained, complete fix.
2026-05-12 09:16:52 +00:00
riyadhafraa 9a07018314 Task #505: Pin the admin "لوحة التحكم" top bar so it stays frozen on iPad
User report: in the admin dashboard on iPad, the top bar (back arrow,
gear icon, "لوحة التحكم" title, mobile menu button) felt like it
detached from the top during scroll — content showed through it and
it didn't look pinned.

Root cause (artifacts/tx-os/src/pages/admin.tsx, line 1148):
the header was `glass-panel ... sticky top-0 z-10`. The `glass-panel`
background is rgba(255,255,255,0.7) — semi-transparent — and z-10 is
low enough that floating elements on the page (os-bg blobs, sidebar
backdrop) bled across it. The sticky positioning itself was correct;
the perception of "not pinned" came from the see-through background.

Fix (single-file CSS-only change, exactly as planned):
- Drop `glass-panel`, replace with inline styles for full control.
- Background: rgba(255,255,255,0.96) — visually opaque so scrolled
  content no longer shows through.
- Keep blur(16px) saturate(160%) (with -webkit- prefix) so the bar
  still has the same soft glass aesthetic.
- z-30 instead of z-10 so nothing in the page overlays it. Modals
  use z-50 so they still cover the bar correctly.
- Add boxShadow 0 4px 20px rgba(15,23,42,0.06) so the bar visibly
  floats above content once scrolling starts.
- Kept `sticky top-0` (not converted to fixed) so the desktop
  sidebar's `top-[73px]` offset still aligns with the bar's bottom.
- Header height unchanged (px-4 py-4 + same children).

No layout changes. No other top bars touched (out of scope).
Existing tx-os tests are unaffected (no logic, only presentation).
2026-05-12 09:01:37 +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 2a005646f3 Task #196: Add inline "Recent activity" sections to admin detail panels
- Added shared `RecentActivityForTarget` component in `artifacts/tx-os/src/pages/admin.tsx` that lists the 10 most recent audit entries for a given (targetType, targetId), reusing `formatAuditSummary` for consistent wording with the audit log section, plus actor + timestamp metadata. Renders loading / empty / list states with stable data-testids.

- Added `openAuditLogForTarget(targetType, targetId)` helper inside `AdminPage`. Writes the URL hash with `section=audit-log&targetType=<t>&targetId=<n>` BEFORE calling `setSection("audit-log")` so the existing section-sync effect (which would otherwise drop section-scoped params on a section change) preserves the deep-link parameters. Also closes any open editor modals.

- Wired the component into all five entity detail panels:
  * App edit modal
  * Service edit modal
  * `UserGroupsEditor` (user edit)
  * `GroupDetailEditor` (history tab)
  * Role edit dialog
  GroupsPanel and RolesPanel previously took no props; both now accept and forward an `onOpenAuditLogForTarget` prop. `OpenAuditLogForTarget` type is shared.

- Added i18n keys `admin.audit.recent.{title,loading,empty,viewAll,viewAllAria}` in both `en.json` and `ar.json`.

- Verified end-to-end via Playwright runTest: deep-link hash, modal closure, and audit-log filter pre-population all work for app/service/group/role/user panels.

No backend changes required (the existing `targetType`/`targetId` filter on `GET /api/audit` was already supported).

Replit-Task-Id: c03451f9-a6bb-4dd4-b082-f34ce3b6001d
2026-05-01 14:58:32 +00:00
riyadhafraa 9607a02047 Task #194: Show deleted user's full name in admin Audit Log
Original task
-------------
The admin Audit Log row for a user.delete entry showed
"Target: user #1234 · @ahmed" — i.e. the @username — even though
admins recognise people by name. Surface displayNameEn / displayNameAr
in that line and fall back to @username only when no display name
exists.

Backend
-------
artifacts/api-server/src/routes/users.ts
The user.delete handler already persists `displayNameEn` and
`displayNameAr` in audit metadata (added in commit eb7d15c). No
code change was needed on the API side. Verified the metadata
shape via the existing audit-log-coverage test.

Frontend
--------
artifacts/tx-os/src/pages/admin.tsx
- Renamed `forceDeletedEntityName` to `deletedEntityName` and
  updated the doc-comment.
- Relaxed its gate so it returns a name not only for force-delete
  entries but also for plain `user.delete` entries. Other delete
  actions (app.delete, group.delete) still gate on force-delete
  to avoid changing behaviour outside this task's scope.
- Updated the single call-site to use the new function name.

Effect: a non-force user.delete row now renders a "Target: user
#1234 · Ahmed Al-Saleh" line beneath the summary, with locale-aware
fallback (Ar -> En -> @username).

Verification
------------
- pnpm api-spec codegen + tx-os tsc --noEmit: clean.
- artifacts/api-server `node --test` for audit-log-coverage,
  audit-logs-actor-filter, audit-logs-forced-only-filter,
  audit-logs-target-filter: 43 / 43 tests pass (CSV export tests
  included).
- e2e test via runTest: created a user with displayName
  "Ahmed Test User" / "أحمد مستخدم", deleted without force,
  opened /#section=audit-log, and confirmed the new row's
  summary plus the secondary "Target: user #19630 · أحمد مستخدم"
  line render correctly (admin's Arabic locale was honoured).

Replit-Task-Id: fc3fc8f9-082a-49e4-b223-17baee11d268
2026-05-01 14:00:39 +00:00
riyadhafraa 699cfafcb4 Migrate admin user editor + Review pop-up to in-house Dialog (a11y)
Original task: Make admin pop-ups close with Escape and trap keyboard
focus (#185).

Changes
- artifacts/tx-os/src/pages/admin.tsx
  - UserGroupsEditor and its inner "Review changes" pop-up no longer
    use the hand-rolled fixed-inset overlay. Both now use the in-house
    Radix-based Dialog (Dialog/DialogContent/DialogHeader/DialogTitle/
    DialogDescription/DialogFooter) so Escape closes only the topmost
    dialog, Tab/Shift+Tab is trapped, and the close button + aria-modal
    semantics come for free.
  - Removed the manual `keydown` Escape useEffect that was previously
    needed for the Review pop-up.
  - Preserved all existing data-testids (edit-user-save,
    edit-user-review-dialog, edit-user-review-back, etc.) plus added
    a new edit-user-dialog testid for the editor shell.
  - Kept the original glass-panel look by passing border-0 / shadow-none
    and rounded-3xl through DialogContent's className.
  - Focus return: Radix's automatic restoration is unreliable for
    nested dialogs. Added two explicit hops:
      - The Review dialog uses onCloseAutoFocus to refocus the Save
        button via a saveBtnRef.
      - The editor dialog captures whatever was focused when it first
        rendered (the pencil icon) into triggerElementRef, and
        onCloseAutoFocus refocuses it. Verified the pencil button is
        what the test sees.

Tests
- artifacts/tx-os/tests/admin-user-edit-review.spec.mjs
  - New test "Escape closes topmost dialog; Tab keeps focus inside;
    focus returns to opener" runs in both en and ar:
      - Esc closes only the Review (editor stays open) and focus
        returns to the Save button.
      - Tab x15 and Shift+Tab x5 keep document.activeElement inside
        the editor dialog.
      - Esc on the editor closes it and returns focus to the pencil.
  - All 4 specs in this file pass (~58s).

No deviations from the task; scope intentionally limited to the user
editor + Review pop-up. The same pattern in the Groups/Roles/Apps
editors was filed as follow-up #266.

Replit-Task-Id: fc613d10-06d0-460f-b1a4-0a65ff021d4a
2026-05-01 09:57:47 +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 4c1e417e27 Add automated tests for audit log readable summaries
Original task: add unit tests for the new `formatAuditSummary` formatter
and an API-level test asserting the enriched group sub-resource audit
metadata, and wire both into the existing `test` workflow.

What changed:
- Extracted `formatAuditSummary` and its helpers (`asRecord`, `asString`,
  `asNumber`, `unitLabel`, `appName`, `linkedAppName`, `plainName`,
  `changeCount`) out of `artifacts/tx-os/src/pages/admin.tsx` into a new
  `artifacts/tx-os/src/lib/audit-summary.ts` module so the pure formatter
  can be unit-tested without the React tree. `admin.tsx` now imports the
  helpers from that module.
- Added `artifacts/tx-os/src/__tests__/audit-summary.test.mjs` with 22
  Node test-runner cases covering app rename (EN + AR), app-update
  fallback, group rename, group multi-field update, registration toggle
  (open / close / with-other-changes), and every group.user/app/role
  add/remove name vs id-only branch, plus the unknown-action default.
- Added `pnpm --filter @workspace/tx-os test` (Node 24's native
  TypeScript loader runs the .mjs tests against the .ts module directly).
- Added `artifacts/api-server/tests/group-audit-metadata.test.mjs` using
  the same harness as `groups-crud.test.mjs`. It hits POST/DELETE
  `/api/groups/:id/{users,apps,roles}/:targetId` and reads the resulting
  `audit_logs.metadata`, asserting `username`, `appSlug` /`appNameEn` /
  `appNameAr`, and `roleName` are persisted alongside the raw IDs.
- Updated the `test` workflow to run the tx-os unit tests before the
  api-server tests, then the tx-os e2e tests.

Verification: all 22 tx-os unit tests pass via the new pnpm script, and
all 6 new api-server audit-metadata tests pass against a live server.
The overall api-server suite still has pre-existing flakes
(executive-meetings notifications/status transitions, and the
count-based group invariant in groups-crud.test.mjs) that are unrelated
to this change; both flake clusters are filed as follow-up tasks.

Replit-Task-Id: 182cd4ed-c55c-43e3-b10b-8147a9611fd4
2026-04-30 21:05:22 +00:00
riyadhafraa eb7d15ca7e Show readable names in audit log for top-level deletions
Task: #177 — Make user/app/role deletion audit rows render readable
names ("Deleted user @alice (Alice Smith)", "Deleted app 'Notes'",
"Deleted role 'Editor'") instead of relying on whatever the route
happened to capture.

Backend metadata changes:
- artifacts/api-server/src/routes/users.ts (user.delete): now also
  persists displayNameEn and displayNameAr alongside the existing
  username/email.
- artifacts/api-server/src/routes/apps.ts (app.delete): renamed the
  metadata keys slug/nameAr/nameEn → appSlug/appNameAr/appNameEn so
  app sub-resource events and top-level deletes share one prefix.
- artifacts/api-server/src/routes/roles.ts (role.delete): renamed the
  metadata key name → roleName, matching group.role.add/remove.

Frontend formatter (artifacts/tx-os/src/pages/admin.tsx):
- appName helper now reads both legacy (slug/nameEn/nameAr) and new
  (appSlug/appNameEn/appNameAr) keys so old rows still render.
- role.delete case prefers roleName, falls back to legacy name.
- user.delete case picks the user's localized display name and uses
  new locale strings user.deleteWithName / user.forceDeleteWithName
  when present; falls back to the existing username-only strings.
- forceDeletedEntityName also accepts appNameEn/appNameAr/appSlug so
  force-deleted apps still get their inline name chip.

Locales:
- artifacts/tx-os/src/locales/{en,ar}.json: added
  admin.audit.summary.user.deleteWithName and forceDeleteWithName.

Test updates:
- artifacts/api-server/tests/audit-log-coverage.test.mjs: updated the
  role.delete and app.delete (no-deps) assertions to read the new
  metadata key names. The user.delete assertions kept working as-is
  since username/email/force are unchanged.

No DB migration was required — audit_logs.metadata is already JSON.
Legacy rows continue to render via the formatter fallbacks called
out in the task description.

Replit-Task-Id: a25d35dd-5005-4e57-96a5-580016f35e46
2026-04-30 20:38:02 +00:00
riyadhafraa 56524cf0de Friendly summaries for app.permission.add/remove audit entries
Task #174: The new `app.permission.add` and `app.permission.remove`
audit log actions emitted by `artifacts/api-server/src/routes/apps.ts`
were falling through `formatAuditSummary`'s default branch, so the
admin audit log only displayed the raw action string instead of a
human-readable sentence like the existing `role.permission.*` entries.

Changes:
- artifacts/tx-os/src/pages/admin.tsx: added `app.permission.add` and
  `app.permission.remove` cases to `formatAuditSummary`. They reuse
  the existing `appName(meta, lang, targetId)` helper (which already
  handles the nameEn/nameAr/slug fallback) and mirror the existing
  role.permission.* permission-name fallback (permissionName ->
  #permissionId -> "?").
- artifacts/tx-os/src/locales/en.json: added
  `admin.audit.summary.app.permissionAdd` /
  `admin.audit.summary.app.permissionRemove`, matching the wording of
  the role.* variants ("Added permission '{{permission}}' to app
  '{{name}}'", "Removed permission '{{permission}}' from app
  '{{name}}'").
- artifacts/tx-os/src/locales/ar.json: added the Arabic equivalents
  ("تمت إضافة الصلاحية '...' إلى التطبيق '...'", "تمت إزالة الصلاحية
  '...' من التطبيق '...'").

Verification:
- e2e tested via runTest: created a new app, added/removed a
  permission via the API to emit both audit entries, opened the
  admin audit log, and confirmed both rows render the friendly
  English summary; switched the UI to Arabic and confirmed the same
  rows render the Arabic summary (no raw "app.permission.add" /
  "app.permission.remove" strings shown).
- JSON files validated; no new TypeScript errors introduced near the
  edited lines (the pre-existing 36 codegen-related errors in
  admin.tsx are unrelated).

No deviations from the task scope.

Replit-Task-Id: eb2f3eea-089a-4afc-803c-2580cbf1cfc9
2026-04-30 20:27:15 +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 0062430b31 Show deleted entity name beside target id in admin Audit Log
Task #133: Admin Audit Log rows now display the deleted item's
human-readable name next to its `<type> #<id>` target reference
without admins having to expand the JSON metadata.

Changes
- artifacts/tx-os/src/pages/admin.tsx
  - New helper `forceDeletedEntityName(entry, lang)` that pulls a
    localized display name from a force-delete row's metadata.
    Priority: nameEn/nameAr/displayNameEn/displayNameAr (lang-aware)
    > plain `name` > `@username`.
  - `AuditLogRow` renders an additional muted "Target: <type> #<id>
    · <name>" line beneath the summary for force-delete entries that
    expose a name. When no summary is generated, the existing target
    fallback line is upgraded to include the name as well.
- artifacts/tx-os/src/locales/en.json, ar.json
  - New `admin.audit.targetWithName` key with English and Arabic
    translations.

Behavior
- Names respect the active language (Arabic preferred when lang=ar,
  with graceful fallback to the other locale, then `name`, then
  `@username`).
- Non-deletion rows and deletion rows whose metadata lacks a name
  are unchanged — no extra line is rendered, so there is no
  regression for existing entries.

Verification
- `pnpm exec tsc --noEmit` in artifacts/tx-os passes after running
  `pnpm --filter @workspace/api-spec run codegen`.
- E2E test (Playwright) confirms the new line renders for both a
  service.force_delete row (English-only metadata) and an
  app.delete force row (English + Arabic metadata), and that the
  Arabic UI surfaces the Arabic name when present and falls back to
  the English name when not.

Follow-ups proposed
- #194 Persist user display names in user.delete audit metadata.
- #195 Log every service deletion, not only forced ones.

Replit-Task-Id: cd312541-1c90-4849-afd6-e6757aedfe06
2026-04-30 06:52:21 +00:00
riyadhafraa 6908207df5 Show dependency counts inline in admin lists (Task #128)
Surfaces the dependency counts (already returned by the admin list
endpoints since Task #96) directly in the Apps, Services, and Users
admin panel rows so admins can see usage at a glance — no need to open
the delete dialog to find out.

Changes:
- artifacts/tx-os/src/pages/admin.tsx:
  - Apps panel rows now render a small bullet-separated subtitle line
    under the route showing non-zero groupCount / restrictionCount /
    openCount (data-testid="app-counts-<id>").
  - Services panel rows render an inline "N orders" line under the
    price when orderCount > 0 (data-testid="service-counts-<id>").
  - Users panel rows render a bullet-separated subtitle under the
    displayName showing non-zero noteCount / orderCount /
    conversationCount / messageCount (data-testid="user-counts-<id>").
  - Style mirrors the existing GroupsPanel inline counts row
    (text-[11px] muted-foreground, flex flex-wrap, "•" separators).
  - Zero counts are filtered out so empty rows stay clean.

- artifacts/tx-os/src/locales/{en,ar}.json:
  - Added admin.apps.counts.{groups,restrictions,opens}
  - Added admin.services.counts.orders
  - Added admin.users.counts.{notes,orders,conversations,messages}

- lib/api-client-react/dist + tsbuildinfo: regenerated stale composite
  build output so the count fields on UserProfile / App / Service
  schemas (added in Task #96) are visible to the tx-os typecheck.
  No source change in lib/api-client-react.

Verification:
- tsc --noEmit passes cleanly for artifacts/tx-os.
- End-to-end browser test confirmed: admin sees inline counts in Apps,
  Services, and Users; rows with zero deps render no counts row;
  Arabic/RTL layout still works.

Replit-Task-Id: 31d3e38d-f611-4d5e-9cfa-823326495328
2026-04-29 20:16:37 +00:00
riyadhafraa e0c586c0e2 Add review changes dialog and improve language handling
Introduce a review changes dialog for user edits and fix language persistence by correcting the localStorage key to "tx-lang".

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 1a5f18f9-0674-406a-af5d-19b28df896fc
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/wYObB6M
Replit-Helium-Checkpoint-Created: true
2026-04-29 20:04:21 +00:00
riyadhafraa b4d66bc0f2 Task #114: Readable audit log summaries for delete/update entries
- artifacts/api-server/src/routes/groups.ts:
  - Added loadSubResourceNameFields() to fetch username / appSlug+nameEn+nameAr / roleName when adding or removing a group sub-resource.
  - POST/DELETE /groups/:id/:kind/:targetId now persist these names alongside the id in audit metadata. Best-effort lookup on DELETE so a missing linked record does not break the action.
  - Removed unused SUB_TABLE constant.

- artifacts/tx-os/src/pages/admin.tsx:
  - formatAuditSummary now renders human-readable lines for:
    - app.update rename (changes.nameEn/nameAr/slug from→to)
    - group.update rename (previousName, when only one field changed)
    - group.user/app/role.add/remove (prefer enriched name fields, fall back to *ById locale keys with #id when only the id is known)
    - settings.update registrationOpen toggle ("Opened/Closed public registration", with optional "with N other change(s)" suffix)
  - Helper linkedAppName() shared by the app branches.
  - Raw JSON metadata remains available via the existing expand toggle.

- artifacts/tx-os/src/locales/{en,ar}.json:
  - Added admin.audit.summary keys for app.rename, group.rename, group.{user,app,role}{Add,Remove}/{Add,Remove}ById, settings.registrationOpened/Closed/OpenedWith/ClosedWith in both English and Arabic.

Verification:
- Backend metadata enrichment validated end-to-end (group create, user/app/role add+remove, group rename, settings toggle) via a temporary script — all rows persist the new name fields.
- Browser e2e test logged in as a freshly created admin, exercised the same flows, opened the Audit log panel, and confirmed each row renders the readable summary (no #id placeholders) and that the raw JSON pane still expands and contains 'username'.

Notes / drift:
- The task brief listed many actions (user.delete, role.delete, app.delete, app.create, etc.). The group sub-resource actions and rename / registration toggle branches were the ones that previously rendered as opaque "#id" text and are addressed in this task. Top-level user/app/role delete already had decent metadata; further enrichment proposed as a follow-up so it can be reviewed independently.

Replit-Task-Id: 16fe1927-3329-4c15-b1ae-5fe10869aed0
2026-04-29 19:07:01 +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 9a3cf120ce Show readable summaries instead of raw JSON in the audit log
Task #94: Each admin audit log row now renders a localized one-line
summary derived from the entry's action + metadata, e.g.
"Deleted group 'audit-test-group'" or "تم حذف المجموعة 'X'", instead
of just showing the raw target type/id.

Implementation
- Added `formatAuditSummary(entry, t, lang)` in
  `artifacts/tx-os/src/pages/admin.tsx` covering all known audit
  actions emitted by the API (group/user/role/app/service/settings/
  auth create/update/delete/permission/membership variants, including
  forced deletions). Returns null for unknown actions so the row
  gracefully falls back to the existing "Target: type #id" display.
- The summary now occupies the prominent text slot in `AuditLogRow`.
  The raw action code stays in the small badge (it is also the value
  the action-filter dropdown uses), and the timestamp moved next to
  it. The expand toggle still reveals the full JSON metadata for
  power users (data-testid `audit-metadata-<id>` unchanged).
- Added `admin.audit.summary.*` keys plus a pluralized
  `admin.audit.unit.*` helper map in en.json and ar.json. The Arabic
  unit keys provide all six CLDR plural forms (zero/one/two/few/
  many/other) so counts read naturally; English uses one/other.

Verification
- `tsc --build` clean for the tx-os artifact.
- e2e test (admin login → create + delete a uniquely named group →
  open Audit Log → confirm Arabic and English summaries render,
  raw JSON is still available behind the toggle, and the action pill
  still shows the raw action code) passed.

Follow-up proposed
- #117 "Show readable action names in the audit log filter dropdown"
  (the filter dropdown still lists raw action codes).

Replit-Task-Id: b47aacde-087e-4a2b-8fb2-0e63cb1936e4
2026-04-28 21:07:19 +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 595ca06a57 Task #83: Refresh role and permission changes live across user sessions
## Socket.IO broadcasts (core task):
- POST /users/:id/roles: uses .returning() to emit apps_changed only when row inserted
- DELETE /users/:id/roles/:roleName: uses .returning() to emit only when row deleted
- Added emitAppsChangedToRoleHolders(roleId) helper to realtime.ts
- New role-permission endpoints in roles.ts, emit only on effective changes:
  - GET /roles/:id/permissions — list permissions for a role
  - POST /roles/:id/permissions — assign; emits to role holders on actual insert
  - DELETE /roles/:id/permissions/:permId — remove; emits only if row was deleted

## Bug fix: admin panel not showing disabled apps
- Added GET /api/admin/apps (requireAdmin) returning ALL apps from DB
- Updated admin.tsx: useQuery → /api/admin/apps with ADMIN_APPS_KEY constant
- All admin.tsx mutation handlers invalidate ADMIN_APPS_KEY

## UI fix: disabled app cards nearly invisible (opacity-60 on glass)
- Gray background + grayscale icon + muted text + stronger Disabled badge

Replit-Task-Id: f439ec75-bcd5-4030-8ee1-83a5d976f1a1

## DB: removed 11 duplicate rows from app_permissions table
2026-04-27 11:11:43 +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 b0b1d673b7 Show role descriptions in both languages on the Groups Roles tab
Task #81: The Groups editor's Roles tab previously displayed only the
description in the active UI language with the role's machine name as
a small subtitle. Bilingual admin teams asked to see both languages at
once.

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

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

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

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

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

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

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

### Changes

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

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

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

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

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

### Changes

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

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

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

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

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

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

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

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

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 5f1c43b0-7465-4e56-bb0e-896a4df38886
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/dnVFHsG
Replit-Helium-Checkpoint-Created: true
2026-04-22 10:55:46 +00:00
riyadhafraa d0e6912017 Rename project and remove unused services
Update project name from teaboy-os to tx-os and remove obsolete services.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 3154f23a-748a-4118-aa41-fc01b7b1f04d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PVuelRZ
Replit-Helium-Checkpoint-Created: true
2026-04-22 10:52:09 +00:00