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

7.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, 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.

  • One-time pre-push cleanup (run BEFORE pnpm --filter @workspace/db run push-force in any environment that has not been pushed since these constraints were added). Two pieces of legacy data block the push because the old DB never enforced the constraints the schema now declares:

    1. Duplicate rows in app_permissions (the schema declares a composite primary key on (app_id, permission_id)).
    2. Orphan rows in executive_meeting_notifications whose meeting_id no longer exists (the schema declares ON DELETE CASCADE, which was never enforced because the FK was missing).

    Run this idempotent SQL once per environment before the push:

    BEGIN;
    
    -- Collapse duplicate app_permissions rows to one per (app_id, permission_id)
    CREATE TEMP TABLE app_permissions_dedup AS
      SELECT DISTINCT app_id, permission_id FROM app_permissions;
    DELETE FROM app_permissions;
    INSERT INTO app_permissions (app_id, permission_id)
      SELECT app_id, permission_id FROM app_permissions_dedup;
    
    -- Drop notifications whose meeting was already deleted
    DELETE FROM executive_meeting_notifications n
     WHERE n.meeting_id IS NOT NULL
       AND NOT EXISTS (SELECT 1 FROM executive_meetings m WHERE m.id = n.meeting_id);
    
    COMMIT;
    

    Then run pnpm --filter @workspace/db run push-force. The push has been verified end-to-end against the dev database and is idempotent on subsequent runs.