Commit Graph

23 Commits

Author SHA1 Message Date
riyadhafraa f4d99b6cfd Rebuild PDF generation: HTML <table> + Playwright replaces PDFKit manual drawing
Task #372: Replaced the manual PDFKit coordinate-based PDF renderer with an
HTML table-based approach using Playwright's headless Chromium.

Key changes:
- Created `pdf-html-renderer.ts` with `buildScheduleHtml()` (generates full
  HTML with embedded base64 fonts, proper `<table>`, RTL/LTR support, row
  colors, merged cells, attendees formatting) and `htmlToPdfBuffer()` (renders
  HTML to PDF via Playwright Chromium).
- Updated `pdf-renderer.ts`: `renderSchedulePdf()` now delegates to the new
  HTML renderer via dynamic import. Legacy PDFKit code preserved as
  `renderSchedulePdf_LEGACY()` for fallback reference.
- Added `playwright-core` dependency and configured esbuild externals.
- Chromium binary discovery: searches `.cache/ms-playwright/` directories with
  env var override (`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH`). Browser instance
  is cached as singleton with launch mutex to prevent concurrent double-launch.
- Process shutdown hook closes browser on exit.

Requirements met:
- Each meeting in exactly one `<tr>` with 4 `<td>` cells
- No text outside table, attendees ~3 per line inline
- Time always in its column, `page-break-inside: avoid`, `table-layout: fixed`
- Matches reference design (blue header, centered text, DIN Next LT Arabic)
2026-05-04 10:24:08 +00:00
riyadhafraa ef37facf7e Update documentation to clarify custom font settings and behavior
Modify `replit.md` to accurately describe the custom font settings, their locations, and how they are synchronized across various parts of the application.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: a2b81182-db0a-48c4-81a7-dd2bdc42507a
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/cQYdf5V
Replit-Helium-Checkpoint-Created: true
2026-05-01 19:57:42 +00:00
riyadhafraa 471c480824 Task #303: DIN Next LT Arabic site default + working font picker
User report: "the dropdown for changing the site font does not work."
Root cause: FontSettingsSection listed Cairo / Tajawal / Noto Naskh
Arabic / Amiri, but only Tajawal had been registered with @font-face.
Picking the others was a no-op. Site default body font was also not
DIN Next LT Arabic as designed.

Changes:
- artifacts/tx-os/src/index.css: --app-font-sans now starts with
  "DIN Next LT Arabic"; dropped IBM Plex Mono.
- artifacts/tx-os/index.html: removed Google Fonts <link> + preconnect.
- artifacts/tx-os/src/pages/executive-meetings.tsx: FontSettingsSection
  dropdown replaced with the 5 actually-bundled families + system.
- artifacts/api-server/src/routes/executive-meetings.ts: FONT_FAMILIES
  Zod allowlist updated to match.
- artifacts/api-server/src/lib/sanitize.ts: FONT_NAME_PART regex now
  allows the new families (kept IBM Plex Sans Arabic for backward
  compat). Fixes a latent bug from #301 where the rich-text sanitizer
  silently stripped attendee font picks on save.
- artifacts/api-server/src/lib/pdf-renderer.ts: FAMILY_MAP and
  ARABIC_FONT_NAMES updated. Sans-style families map to the bundled
  NotoSansArabic; Naskh-style families to NotoNaskhArabic.
- artifacts/api-server/tests/executive-meetings.test.mjs: PDF
  sans-vs-naskh assertion updated from Cairo/Noto Naskh Arabic to
  DIN Next LT Arabic/Majalla, preserving its semantics. Other
  Cairo/Noto Naskh Arabic occurrences swapped to the new names.
- replit.md: documented site default font + lockstep allowlist rule.

Deviations from plan: kept DEFAULT_FONT.fontFamily = "system" on the
frontend (and the backend Zod default) instead of changing it to
"DIN Next LT Arabic". "system" semantically means "no override → use
the CSS default", which is now DIN Next LT Arabic — same end result
without forcing a migration on existing rows. The sanitizer change
was not in the original plan but was required to make the picker
actually persist.

Verification:
- Frontend tsc clean.
- Backend tsc shows the same pre-existing unrelated errors as before
  (isHighlighted boolean/number; Drizzle scope typing).
- E2E browser test verified all 7 assertions: site default font,
  no Google Fonts requests, exactly 6 dropdown options, font picker
  applies + reverts correctly, attendee font picks persist after save.
- Code review verdict: PASS.
2026-05-01 19:55:39 +00:00
riyadhafraa adf70e4f28 Task #301: Custom editor fonts + Tab quick-add for attendees
Curated 22 font files from the user's uploaded zip into
artifacts/tx-os/public/fonts/ and exposed five families in the
in-place editor's font dropdown:
  - DIN Next LT Arabic (5 weights)
  - Tajawal (7 weights)
  - Helvetica Neue LT Arabic (3 weights)
  - Helvetica Neue (5 weights)
  - Majalla (2 weights)
Declared via @font-face in artifacts/tx-os/src/custom-fonts.css with
font-display: swap so weights are only fetched on demand. Each
<option> in the toolbar dropdown now previews itself in its own
family.

Tab quick-add: pressing Tab inside an attendee name's EditableCell
commits the current value and immediately opens a new pending
attendee row right after it, so users can type names continuously
without mousing. Implemented via:
  - new optional `onTabNext` prop on EditableCell, captured into a
    ref so the handler (created once via useEditor with [] deps)
    always fires the latest callback;
  - new `chainStartAdd` shared prop on AttendeeFlow that bypasses
    the parent's `hasAnyPending` UI gate (the state-level guard in
    `setPendingAttendee(prev => prev ? prev : new)` still prevents
    truly overlapping pendings);
  - wiring on both existing-attendee and pending-attendee cells.

Drift from plan:
- The plan also mentioned Shift+Tab to focus the previous person.
  Scoped out — focus-from-outside isn't supported by EditableCell
  today, and the user only asked for forward Tab. Shift+Tab now
  falls through to default browser focus behaviour.

E2E test (testing skill) passed for both features. Architect review
returned a Pass with one minor note that IBM Plex Sans Arabic has
no @font-face entry in custom-fonts.css — that family is already
loaded via the existing Google Fonts <link> in index.html (it is
the app's default --app-font-sans), so the dropdown option works
as expected.

Pre-existing api-server test failures are unrelated and predate
this task.
2026-05-01 19:10:25 +00:00
riyadhafraa ab5ec2e2e2 Add shared row highlighting to executive meeting scheduler
Implement shared row highlighting for executive meetings by adding a `rowColor` field to the database schema and API, and migrating existing per-device colors to the new shared field.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 273accfc-a301-41b9-bd20-c121cb4e79c7
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 15:24:59 +00:00
riyadhafraa 1b1697c450 Add conflict detection and resolution for meeting postponements
Implement optimistic locking for meeting postponements to prevent concurrent edits. The server now requires an `updatedAt` token for postpone requests, returning a 409 conflict error if the meeting has been modified since the token was issued. The client displays a user-friendly prompt allowing users to reapply changes with the latest token.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 1abdc3d2-c834-4a37-b104-397852e4732a
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/8gwn7Xm
Replit-Helium-Checkpoint-Created: true
2026-05-01 14:58:12 +00:00
riyadhafraa a72c00fe19 Task #273: 5-minute pre-meeting alert for Executive Meetings
- New `executive_meeting_alert_state` table (per-user, per-meeting) tracking
  `dismissed`/`acknowledged`. Migration via `pnpm --filter @workspace/db
  run push-force`.
- New API routes in artifacts/api-server/src/routes/executive-meetings.ts:
  GET /alert-state, POST /:id/alert-state, POST /:id/postpone-minutes,
  POST /:id/reschedule, POST /:id/cancel. All three mutation routes
  acquire a row lock with SELECT ... FOR UPDATE inside the transaction,
  compute oldValue from the locked snapshot, run conflict detection in
  the same tx, and write the audit row before commit. Cancel is
  idempotent. Postpone-minutes rejects ranges that would cross midnight
  (use reschedule for cross-day moves).
- Alert-state route uses race-safe onConflictDoNothing upsert + a
  conditional UPDATE ... RETURNING so transition audits never duplicate.
- New i18n keys `executiveMeetings.alert.*` in en.json + ar.json,
  including the cancel-confirm prompt.
- New component artifacts/tx-os/src/components/executive-meetings/
  upcoming-meeting-alert.tsx — globally mounted in App.tsx inside
  AuthProvider. Draggable with localStorage position persistence,
  RTL-aware, polls every 30s, shows start–end window, postpone-by-
  minutes chips [5,10,15,30,45,60], full reschedule sub-form, and a
  Cancel-meeting flow that requires an explicit confirm step before
  the destructive call fires (gated on confirmCancel state).
- Playwright spec executive-meetings-upcoming-alert.spec.mjs with
  6 scenarios: appear+Done, postpone-10 shifts times, cancel-with-
  confirm, dismiss audit, postpone-chip+conflict-warning toast,
  AR/RTL render. All 6 pass.
- replit.md updated with the new table, routes, and migration step.

Pre-existing tsc errors at lines 489, 605, and 2039 of
executive-meetings.ts are not touched by this task.
2026-05-01 11:56:14 +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 a8467f810b Task #148: Make pnpm --filter @workspace/db run push work without manual SQL
## Original task
`pnpm --filter @workspace/db run push` was failing with a duplicate-key
error on `app_permissions` (and a missing FK on
`executive_meeting_notifications`) because legacy data in the dev DB
violates constraints the schema now declares. Devs had to drop into psql
to fix it, which made bootstrapping painful and left
`role_permission_audit` reliant on hand-applied SQL.

## Changes
- Added `lib/db/scripts/pre-push-cleanup.ts`, an idempotent cleanup that:
  - Collapses duplicate `app_permissions` rows to one per
    `(app_id, permission_id)` so the composite PK can be added.
  - Deletes orphan `executive_meeting_notifications` rows so the new
    `ON DELETE CASCADE` FK can be added.
  - Skips both checks when the tables don't exist yet (fresh DB
    no-op).
- Wired the script into `lib/db/package.json` so both `push` and
  `push-force` run cleanup first (`pnpm run pre-push-cleanup && drizzle-kit push ...`).
- Added `tsx` to `lib/db` devDependencies (catalog version) so the
  package can run the cleanup without leaning on another workspace.
- Updated `replit.md` Deployment / Migration Runbook to reflect that
  cleanup is now automatic — no manual SQL required in any environment.

## Verification
- `pnpm --filter @workspace/db run push` now collapses 2 duplicate
  groups + removes 1978 orphan notifications, then succeeds with
  `[✓] Changes applied`.
- Re-running push is idempotent: second run reports no duplicates and
  no orphans, then succeeds.
- `pnpm --filter @workspace/db run push-force` (used by
  `scripts/post-merge.sh`) was also verified end-to-end.
- Confirmed `app_permissions` now has the composite PK,
  `executive_meeting_notifications` has the cascade FK, and
  `role_permission_audit` matches the schema.

## Notes
- A pre-existing unrelated typecheck error in
  `artifacts/api-server/src/routes/executive-meetings.ts` (font
  settings `scope` overload) was confirmed to exist on `main` before
  any changes here and is out of scope for this task.
- Proposed follow-up #213 to move from `push`/`push-force` to
  versioned `drizzle-kit migrate` so legacy-data backfills are checked
  in instead of living in a generic pre-push script.

Replit-Task-Id: 2efc4e22-0c65-48a1-b573-319474698b96
2026-04-30 12:07:19 +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 8880d247e8 Add migration step for new subheading column to deployment checklist
Update replit.md to include a SQL migration step for the new `kind` column in the `executive_meeting_attendees` table.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: ddeab100-7cf7-4eb9-b1ea-df215dd0305c
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/piUNOmy
Replit-Helium-Checkpoint-Created: true
2026-04-30 11:58:07 +00:00
riyadhafraa 89df2385a8 Task #207: custom subheadings inside executive-meeting attendee cells
Adds a new `kind` column (`varchar(16) NOT NULL DEFAULT 'person'`) on
`executive_meeting_attendees` so users can interleave free-text section
labels with person rows in a meeting's attendee list. Subheadings are
excluded from the running attendee number and from the per-meeting
attendee count surface, but reorder and delete identically to person
rows.

DB
- New `kind` column in `lib/db/src/schema/executive-meetings.ts`,
  defaulting to `"person"`. Applied via direct SQL because
  `drizzle-kit push` trips on a pre-existing duplicate-row issue in
  `app_permissions` (already documented in replit.md).

API (artifacts/api-server/src/routes/executive-meetings.ts)
- `attendeeSchema` accepts `kind: z.enum(["person","subheading"])` with
  default `"person"`.
- All 4 insert paths round-trip `kind`: POST create, PATCH meeting
  update (attendees replacement), PUT `/attendees`, and duplicate.
- `pdf-renderer` mapper forwards `kind`. `PdfMeetingAttendee.kind`
  typed as `string | null` to match the DB column shape.

Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx)
- `AttendeeFlow` renders subheadings on a separate full-width row
  (`basis-full`, semibold, centered) and increments the running
  person index only for `kind === "person"`. Pending ghost row branches
  on `pendingKind`.
- Inline "+ subheading" chip via `onStartAdd(type, "subheading")`.
- Manage dialog: addSubheading button, subheading rows hide the title
  field, show a kind badge, and reorder/delete identically. Manage
  list summary count filters to person rows only (architect fix).
- Print page renders subheadings as `.em-print-subheading` and skips
  them in the running counter.
- New locale keys under `executiveMeetings.schedule` and
  `executiveMeetings.manage.attendees` in both `ar.json` and `en.json`.

PDF
- Subheadings print as `— label —` and never advance `personIdx`.

Tests
- New spec `executive-meetings-attendee-subheadings.spec.mjs` seeds
  mixed person+subheading rows and asserts (a) the subheading row
  renders, (b) numbering stays `1-`, `2-`, `3-` with a subheading
  wedged between persons, (c) zero-subheading meetings keep legacy
  numbering. Runs in both AR and EN. All 4 cases pass.

Code review
- Architect found one regression (Manage list summary count included
  subheadings) — fixed.
2026-04-30 11:45:59 +00:00
riyadhafraa f4ea0a8d10 Re-run Drizzle schema push so DB matches code (Task #131)
Original task: Re-run db:push --force so titleAr / titleEn / attendees.name
(widened in code from varchar to text) are applied in every environment,
once the upstream app_permissions duplicate blocker is cleared, and
record a deployment runbook note.

What was done in this environment:
1. Identified two legacy data integrity issues in the dev DB that
   prevented `pnpm --filter @workspace/db run push-force` from
   succeeding:
   - 71 duplicate rows in `app_permissions` (collapsing to 2 unique
     pairs) blocking the composite primary key on
     (app_id, permission_id).
   - 852 orphan rows in `executive_meeting_notifications` whose
     `meeting_id` no longer exists, blocking the new
     `ON DELETE CASCADE` foreign key.
2. Cleaned both up with idempotent SQL inside a single transaction.
3. Ran `pnpm --filter @workspace/db run push-force` -> "Changes
   applied". Re-ran it to confirm it is now idempotent (clean second
   pass).
4. Verified the resulting schema:
   - executive_meetings.title_ar / title_en  -> text
   - executive_meeting_attendees.name        -> text
   - app_permissions has PK (app_id, permission_id)
   - executive_meeting_notifications has FK meeting_id ->
     executive_meetings(id) ON DELETE CASCADE
5. Added a "Deployment / Migration Runbook" section to replit.md
   documenting the column type change, the requirement to run
   db:push --force in every environment, and the exact one-time
   pre-push cleanup SQL operators must run in staging/production
   before their first push.

Notes / deviations:
- The dev push runs automatically via the existing
  `scripts/post-merge.sh` on every merge. Staging and production
  pushes happen on deploy and are now fully documented.
- No follow-ups proposed: remaining work overlaps with the existing
  "Stop drizzle push from failing on the existing app_permissions
  duplicate" and "Unblock schema migrations so db:push works without
  a manual SQL workaround" tasks.

Replit-Task-Id: 87397d28-a7cc-4a47-833d-b77d5ef1a039
2026-04-30 06:27:59 +00:00
riyadhafraa 5f61ab7bf1 Send executive-meeting notifications via email + in-app alerts
Wired the Executive Meetings module to actually deliver notifications
when meeting/request/task events happen, instead of just storing
scheduled-notification rows.

Backend (artifacts/api-server):
- New helper `lib/executive-meeting-notify.ts`:
  - `recordExecutiveMeetingNotifications` inserts rows into both
    `executive_meeting_notifications` (the page's Notifications tab)
    and the global `notifications` table (the bell), inside the
    caller's transaction. Self-notifications are excluded; recipients
    are deduped.
  - `broadcastExecutiveMeetingNotifications` emits Socket.IO
    `notification_created` to each recipient's `user:${id}` room and
    one `executive_meeting_notifications_changed` global event. Called
    after the surrounding transaction commits.
  - `getUserIdsForRoleNames` resolves role holders via direct
    `user_roles` and indirect `group_roles` -> `user_groups`.
  - `sendExecutiveMeetingEmail` is a best-effort side-channel that
    logs an outbox entry when SMTP_HOST is unset (no nodemailer
    dependency added yet — see follow-up task).
  - `getUserDisplay` resolves bilingual display names with username
    fallback for use in notification titles/bodies.
- `routes/executive-meetings.ts` wired notifications into:
  - POST /executive-meetings (notify approvers — meeting_created)
  - POST /executive-meetings/requests and POST
    /executive-meetings/:id/requests (notify approvers + email outbox
    — request_submitted)
  - PATCH /executive-meetings/requests/:id (notify requester —
    request_approved/rejected/needs_edit; if approved with assignee,
    notify assignee — task_assigned)
  - POST /executive-meetings/tasks (notify assignee — task_assigned)
  - PATCH /executive-meetings/tasks/:id (reassign -> task_assigned;
    completion -> task_completed to original requester + previous
    assignee)

Frontend (artifacts/tx-os):
- `hooks/use-notifications-socket.ts` listens for
  `executive_meeting_notifications_changed` and invalidates the
  notifications/requests/tasks query keys so the page re-fetches in
  real time.
- `locales/en.json` + `locales/ar.json`: replaced placeholder intro
  with the real description and added type labels for the seven new
  notification types.

Verification:
- Restarted the API server (clean build).
- HTTP integration test: logged in as admin, created a meeting,
  submitted a request, approved the request — confirmed
  `executive_meeting_notifications` and `notifications` rows were
  inserted with correct counts (7 admins, actor excluded), the
  request_submitted email outbox log fired with bilingual subject/
  body and 6 deliverable email recipients, and self-notifications
  were correctly suppressed when actor == requester == reviewer.

No deviations from the original plan. Email delivery, per-user
notification preferences, and automated tests for the fan-out logic
are tracked as follow-ups.

Replit-Task-Id: accea784-663c-4b63-a492-8e20d648eb4c
2026-04-29 17:22:20 +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 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 b4d47e1012 Executive Meetings Phase 2: full module + atomic audit
Original task (#108): polish the daily-schedule UI and ship the
remaining 8 sections of the Executive Meetings module (Manage,
Requests, Approvals, Tasks, Notifications, Audit, PDF, Font Settings)
with bilingual i18n and fine-grained RBAC.

What changed:
- Schedule UI: centered cells, '#' header (not 'م'), attendees widest,
  RTL column order locked, navy/white/gray + red highlighting only.
- Schema: made executive_meeting_requests.meetingId nullable so
  'create-meeting' suggestions don't need a target row.
- Backend rewrite of artifacts/api-server/src/routes/executive-meetings.ts:
  GET /me (now returns userId), full CRUD for meetings (transactional
  attendee replace), requests CRUD + approve/reject/withdraw, tasks
  CRUD with assignee status updates, audit-logs GET (admin), notifications
  GET, font-settings GET/PATCH (per-user + global). RBAC via 5 role
  sets and a makeRequireRoles middleware factory.
- Audit-in-transaction: every mutation (meeting/request/task/font CRUD)
  wraps the DB write AND the audit-log insert in the same db.transaction
  so audit entries cannot drift from state if either insert fails. This
  was the main finding from the architect review and is now fixed.
- Path-to-regexp 8 fix: replaced inline ':id(\\d+)' with router.param('id')
  + next('route') guard, plus NaN guards in handlers.
- Frontend rewrite of artifacts/tx-os/src/pages/executive-meetings.tsx
  (~2200 lines): 8 new section components, RBAC-aware UI via /me,
  per-task assignee check now shown for status buttons.
- i18n: full executiveMeetings.* key set added in ar.json + en.json.

Verified: full e2e (admin login -> all 9 tabs -> create meeting ->
submit request -> approve -> audit shows full chain -> font save)
plus a smoke regression after the audit-in-tx refactor (atomic
audit row created in lockstep with mutation).

Out of scope (proposed as follow-ups #110-#112): real notification
delivery, real PDF generation (currently window.print), and
automated integration tests for the new endpoints. The pre-existing
'apps-open.test.mjs' syntax error in the test workflow is unrelated
to this task.
2026-04-28 06:18:08 +00:00
riyadhafraa 66bc96f97f Add executive meeting management system with role-based access
Implement a new API endpoint and UI for managing executive meetings, including role-based access control and database schema changes for meeting data and attendees.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 079afca5-8c32-40ea-b7d1-8bf26358ff75
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/BRoHDEI
Replit-Helium-Checkpoint-Created: true
2026-04-27 12:05:40 +00:00
riyadhafraa d0e6912017 Rename project and remove unused services
Update project name from teaboy-os to tx-os and remove obsolete services.

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

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 07b19cb0-11b5-4be9-8932-ae4820eb73b8
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/HxTkDPZ
Replit-Helium-Checkpoint-Created: true
2026-04-22 10:26:53 +00:00
riyadhafraa 284cb751ed Add per-user clock style preference for the home status bar.
Original task #20: Let each user pick their own home-screen clock
style (analog/digital/minimal/etc.), persisted on the user record
and restored on login / other devices. Default = "full".

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

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

Replit-Task-Id: 475a7439-b357-400d-805f-5f2fda20ed24
2026-04-20 16:40:49 +00:00
riyadhafraa 001e9023b2 feat: build TeaBoy OS — bilingual Arabic/English internal OS platform
Completed full-stack TeaBoy OS build:

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

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

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

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