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

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

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