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
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/openCounton apps,orderCounton services,noteCount/orderCount/conversationCount/messageCounton users) returned by the list endpoints (GET /api/admin/apps,GET /api/services,GET /api/users); the lazy 409 conflict response fromDELETE /api/{apps,services,users}/:id(with?force=trueto override) remains as a safety net. - Session-based Auth: RBAC with roles (admin/user)
- Executive Meetings (Phase 2): bilingual full-stack module under
/executive-meetingswith 9 sections — Schedule (centered cells, attendees widest column, RTL-locked column order # / الاجتماع / الحضور / الوقت), Manage Meetings (CRUD with attendee replace, transactional), Change Requests (submit / withdraw, supportsmeetingId=nullfor 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 viarecordExecutiveMeetingNotificationsto bothexecutive_meeting_notificationsand the globalnotificationsbell, then broadcast via Socket.IOnotification_createdper-user +executive_meeting_notifications_changedglobally; approvers also receive a best-effort email side-channel viasendExecutiveMeetingEmailthat 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 amakeRequireRolesmiddleware factory;/api/executive-meetings/mereturns{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 samedb.transaction(...)so audit entries cannot drift from state. Routes userouter.param("id")withnext("route")to handle path-to-regexp 8 (no inline:id(\\d+)support).
Key Commands
pnpm run typecheck— full typecheck across all packagespnpm run build— build all packagespnpm --filter @workspace/api-spec run codegen— regenerate API hooks from OpenAPI specpnpm --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_sessionsis created manually (not auto-created) — needed in DB before first run - Vite dev server proxies
/api→localhost:8080for cookie-based auth to work - All API calls use
credentials: "include"vialib/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.jsonanden.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, andexecutive_meeting_attendees.namewere widened fromvarchar(500)/varchar(255)totextto hold sanitized Tiptap HTML. The schema declarations live inlib/db/src/schema/executive-meetings.ts. -
pnpm --filter @workspace/db run push-forcemust run in every environment (dev, staging, production) after deploying schema changes. Dev is covered automatically byscripts/post-merge.sh. Staging and production must run the same command on each deploy so theirtitle_ar/title_en/namecolumns 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-forcein 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:- Duplicate rows in
app_permissions(the schema declares a composite primary key on(app_id, permission_id)). - Orphan rows in
executive_meeting_notificationswhosemeeting_idno longer exists (the schema declaresON 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. - Duplicate rows in