Files
TX/replit.md
T
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

12 KiB
Raw Blame History

Tx OS

Overview

Tx OS — a bilingual (Arabic/English, RTL/LTR) full-stack internal web platform styled as an OS-like interface with glassmorphism aesthetics. Built as a pnpm monorepo.

Architecture

/
├── artifacts/
│   ├── api-server/       Express 5 backend (port 8080)
│   └── tx-os/            React + Vite frontend (path: /, port dynamic)
├── lib/
│   ├── db/               Drizzle ORM + PostgreSQL schema
│   ├── api-spec/         OpenAPI spec + Orval codegen
│   ├── api-client-react/ Generated React Query hooks (from Orval)
│   └── api-zod/          Generated Zod schemas (from Orval)
└── scripts/              Seed script (pnpm run seed)

Stack

  • Monorepo: pnpm workspaces
  • Node.js: 24, TypeScript 5.9
  • Backend: Express 5, express-session + connect-pg-simple (PostgreSQL sessions), bcryptjs, Socket.IO
  • Database: PostgreSQL + Drizzle ORM, Zod validation
  • API codegen: Orval (OpenAPI → React Query hooks + Zod schemas)
  • Frontend: React + Vite, Tailwind CSS v4, wouter (routing), i18next (i18n), react-i18next
  • Real-time: Socket.IO (path: /api/socket.io)
  • Build: esbuild (API), Vite (frontend)

Features

  • Arabic default with full RTL layout; English toggle persisted in localStorage + user profile
  • Glassmorphism OS UI: animated gradient background, frosted glass panels
  • OS Home Screen: live clock status bar (per-user clock style: full / digital / digital-no-seconds / analog / minimal, picker in status bar), app grid, bottom dock
  • خدماتي (My Services): service card grid with availability status
  • Internal Chat: real-time messages via Socket.IO, conversation list
  • Notifications: unread tracking, mark-all-read
  • Admin Panel: CRUD for apps, services, users (admin role required). Delete dialogs show a dependency warning on the FIRST click using count fields (groupCount/restrictionCount/openCount on apps, orderCount on services, noteCount/orderCount/conversationCount/messageCount on users) returned by the list endpoints (GET /api/admin/apps, GET /api/services, GET /api/users); the lazy 409 conflict response from DELETE /api/{apps,services,users}/:id (with ?force=true to override) remains as a safety net. The Add App dialog includes a NewAppPermissionsPicker that lets admins pre-set permissionIds[] so POST /api/apps creates the app and inserts its app_permissions rows in the same transaction (with onConflictDoNothing), avoiding the brief unrestricted window between create and follow-up gating.
  • Session-based Auth: RBAC with roles (admin/user)
  • Executive Meetings (Phase 2): bilingual full-stack module under /executive-meetings with 9 sections — Schedule (centered cells, attendees widest column, RTL-locked column order # / الاجتماع / الحضور / الوقت), Manage Meetings (CRUD with attendee replace, transactional), Change Requests (submit / withdraw, supports meetingId=null for create-suggestions), Approvals (approve/reject with review notes), Tasks (CRUD with assignee status updates — assignees and mutators can change status, only mutators can delete), Notifications (per-user feed; meeting/request/task events fan out via recordExecutiveMeetingNotifications to both executive_meeting_notifications and the global notifications bell, then broadcast via Socket.IO notification_created per-user + executive_meeting_notifications_changed globally; approvers also receive a best-effort email side-channel via sendExecutiveMeetingEmail that logs an outbox entry until SMTP is wired up), Audit Log (full action chain, admin role required), PDF (window.print export), Font Settings (per-user + global scope, family/size/weight/alignment with live preview). RBAC enforced via 5 role sets (READ/MUTATE/APPROVE/REQUEST/ADMIN_AUDIT) and a makeRequireRoles middleware factory; /api/executive-meetings/me returns {userId, roles, canRead, canMutate, canApprove, canSubmitRequest, canViewAudit}. 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. Routes use router.param("id") with next("route") to handle path-to-regexp 8 (no inline :id(\\d+) support).

Key Commands

  • pnpm run typecheck — full typecheck across all packages
  • pnpm run build — build all packages
  • pnpm --filter @workspace/api-spec run codegen — regenerate API hooks from OpenAPI spec
  • pnpm --filter @workspace/db run push — push DB schema changes (dev)
  • pnpm --filter @workspace/scripts run seed — seed demo data

Demo Accounts

  • Admin: admin / admin123 (admin + user roles)
  • User: ahmed / user123 (user role)

Database Tables

users, roles, permissions, user_roles, role_permissions, role_permission_audit, permission_audit, apps, app_permissions, service_categories, services, conversations, conversation_participants, messages, message_reads, notifications, user_sessions, audit_logs, executive_meetings, executive_meeting_attendees, executive_meeting_requests, executive_meeting_tasks, executive_meeting_notifications, executive_meeting_audit_logs, executive_meeting_pdf_archives, executive_meeting_font_settings

Important Notes

  • Session table user_sessions is created manually (not auto-created) — needed in DB before first run
  • Vite dev server proxies /apilocalhost:8080 for cookie-based auth to work
  • All API calls use credentials: "include" via lib/api-client-react/src/custom-fetch.ts
  • Socket.IO server path: /api/socket.io
  • Frontend connects to Socket.IO via same-origin proxy (no separate URL needed)
  • i18n locale files: artifacts/tx-os/src/locales/ar.json and en.json
  • Default receivers group is named Tx (renamed from legacy "TeaBoy"); a one-time migration in the seed script renames any pre-existing legacy group on next run.

Deployment / Migration Runbook

  • Rich-text columns are PostgreSQL text (no length cap). executive_meetings.title_ar, executive_meetings.title_en, and executive_meeting_attendees.name were widened from varchar(500) / varchar(255) to text to hold sanitized Tiptap HTML. The schema declarations live in lib/db/src/schema/executive-meetings.ts.

  • pnpm --filter @workspace/db run push-force must run in every environment (dev, staging, production) after deploying schema changes. Dev is covered automatically by scripts/post-merge.sh. Staging and production must run the same command on each deploy so their title_ar / title_en / name columns match the code; otherwise long rich-text saves will be rejected by the old varchar limits.

  • Pre-push cleanup is automatic. Both pnpm --filter @workspace/db run push and push-force now run lib/db/scripts/pre-push-cleanup.ts first. That script is idempotent and:

    1. Collapses duplicate rows in app_permissions to one per (app_id, permission_id) so the composite primary key declared by the schema can be created on legacy DBs.
    2. Deletes orphan executive_meeting_notifications rows whose meeting_id no longer exists, so the new ON DELETE CASCADE foreign key the schema declares can be added on legacy DBs.

    Both checks are skipped automatically on a fresh DB (the table-existence guard makes them no-ops). No manual SQL is needed in any environment — pnpm --filter @workspace/db run push runs cleanly against both fresh and existing dev DBs, and scripts/post-merge.sh continues to use push-force so the same cleanup runs after every task merge. All schema tables (notably role_permission_audit and permission_audit) are created via the normal push path.

Task #207 — Custom subheadings inside attendee cells (April 2026)

executive_meeting_attendees.kind (varchar(16) NOT NULL DEFAULT 'person') lets meetings interleave free-text section headers ("subheadings") with person rows. Subheadings are excluded from the running attendee number and from the per-meeting attendee count surface, but reorder/delete identically to person rows. The schema lives in lib/db/src/schema/executive-meetings.ts. All four insert paths (POST, PATCH attendees replace, PUT attendees, duplicate) round-trip kind. The PDF renderer (artifacts/api-server/src/lib/pdf-renderer.ts) prints subheadings as — label — and skips them when incrementing personIdx.

Deployment / migration step (run once per environment before the next release): the new kind column has NOT NULL DEFAULT 'person', so existing rows are auto-backfilled by Postgres on add-column. Apply via either pnpm --filter @workspace/db run push-force (recommended; idempotent) or, if push is blocked by other legacy data in that environment, run this one-line SQL: ALTER TABLE executive_meeting_attendees ADD COLUMN IF NOT EXISTS kind varchar(16) NOT NULL DEFAULT 'person';. Verify backfill with SELECT kind, COUNT(*) FROM executive_meeting_attendees GROUP BY kind; — every existing row should report kind = 'person'.

Task #273 — 5-minute pre-meeting alert (May 2026)

Floating, draggable upcoming-meeting alert that appears on every Tx OS page when an Executive Meeting is within five minutes of starting. Mounted globally inside <AuthProvider> in artifacts/tx-os/src/App.tsx as <UpcomingMeetingAlert />; gated by the executive_meetings:read capability returned by /api/me.

  • New table executive_meeting_alert_state (meetingId, userId, dismissed, acknowledged, updatedAt) with unique (meeting_id, user_id) — declared in lib/db/src/schema/executive-meetings.ts. Apply with pnpm --filter @workspace/db run push-force in every environment.
  • New routes in artifacts/api-server/src/routes/executive-meetings.ts:
    • GET /executive-meetings/alert-state?date=YYYY-MM-DD
    • POST /executive-meetings/:id/alert-state (action: shown | acknowledged | dismissed)
    • POST /executive-meetings/:id/postpone-minutes
    • POST /executive-meetings/:id/reschedule
    • POST /executive-meetings/:id/cancel
  • All three mutation routes lock the meeting row 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 (no duplicate audit if already cancelled).
  • i18n keys: executiveMeetings.alert.* (en + ar), including the cancel-confirm prompt.
  • Component: artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx — draggable with localStorage position persistence, polls every 30 s, renders RTL when locale is ar, shows the startend window, postpone-by-minutes chips [5,10,15,30,45,60], full reschedule sub-form, and a Cancel-meeting destructive flow that requires an explicit second-step confirmation before firing.
  • E2E coverage: artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs (6 specs: appear+Done, postpone-10 shifts times, cancel-with-confirm, dismiss audit, postpone-chip+conflict-warning, AR/RTL).

Task #283 — Optimistic locking for postpone (May 2026)

Concurrent postpones from two users no longer silently stack. The mutation routes in artifacts/api-server/src/routes/executive-meetings.ts now accept an optional expectedUpdatedAt: string (ISO) in the request body. Inside the existing SELECT … FOR UPDATE transaction, the handler compares expectedUpdatedAt against the locked row's updated_at; on mismatch it returns HTTP 409 with code: "stale_meeting" and a conflict payload ({ currentStartTime, currentEndTime, currentStatus, lastModifiedAt, lastActor: { id, username, displayNameAr, displayNameEn } }). Every successful update also writes updatedBy = userId.

Client (artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx) sends expectedUpdatedAt: meeting.updatedAt from the postpone dialog. A new ApiError class surfaces the response body so the dialog can render a rose-tinted "X just changed this meeting — Add N more minutes anyway?" block (postpone-stale-block) instead of a destructive toast. The user can either retry without the token (force-apply) or back out. New i18n keys: executiveMeetings.alert.staleMeetingTitle / staleMeetingByUser / staleMeetingCurrentTime / staleMeetingApplyAnyway in both en.json and ar.json.

Coverage: artifacts/api-server/tests/executive-meetings-postpone-race.test.mjs exercises the win/lose/refetch+retry/force-apply paths and confirms the loser's stale call does not stack a second shift.