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

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