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

8.7 KiB

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.
  • 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'.