60a18e1c7c
The "Required permissions" section was previously edit-only because `POST /api/apps/:id/permissions` needs an app id, leaving a brief window where a freshly created app was visible to everyone before the admin could re-open the dialog and gate it. The Add app dialog now lets the admin pick required permissions up front and the new app + its `app_permissions` rows are written in a single transaction. Changes: - `lib/api-spec/openapi.yaml`: extended `CreateAppBody` with an optional `permissionIds: integer[]` field. Ran `pnpm --filter @workspace/api-spec run codegen` so `lib/api-zod` and `lib/api-client-react` reflect it. - `artifacts/api-server/src/routes/apps.ts`: `POST /apps` now de-dupes and pre-validates `permissionIds`, returns 404 if any id is unknown (without creating the app), and inside one transaction inserts the app, the `app_permissions` rows (with `.onConflictDoNothing()` against the composite primary key), and a single `permission_audit` row (`previousIds: []`, `newIds: requestedIds`). After the transaction it also writes one `app.permission.add` audit_logs entry per inserted permission so the admin log mirrors the post-create flow. - `artifacts/tx-os/src/pages/admin.tsx`: added `permissionIds: number[]` to `AppForm`, a new `NewAppPermissionsPicker` component (rendered only in create mode — edit mode keeps the existing `AppPermissionsEditor` with its impact preview) that lets admins add/remove permissions locally before submit, and wired `handleSaveApp` to forward the selected ids when creating. Existing edit path strips the field so the update payload remains unchanged. - `replit.md`: documented the new picker and POST /api/apps behavior. No impact preview is shown in the create-mode picker because a brand new app starts with zero users seeing it, so adding permissions cannot hide it from anyone. Code-review follow-up: tightened input validation so non-integer or non-positive `permissionIds` now return 400 with a clear error instead of being silently dropped by the previous filter. The legacy single-add endpoint already used this exact 400 message, so behavior stays consistent across both create and update paths. Verification: - `pnpm --filter @workspace/api-spec run codegen` passes. - `pnpm --filter @workspace/api-server` typechecks with no new errors (executive-meetings.ts errors are pre-existing and unrelated). - Ran the existing app-permission test suites (`app-permission-audit.test.mjs`, `app-permissions-crud.test.mjs`, `app-permissions-impact.test.mjs`) directly — all 16 tests pass. - Ran an e2e Playwright test (login as admin → Add app → pick a permission → save → verify the row shows 1 restriction → reopen and confirm the assigned permission). All steps passed. Follow-up proposed: automated tests for the new create-with-permissions endpoint behavior (#231). Replit-Task-Id: c229777c-4036-4a6a-b4cb-05ccc18f6c9b
87 lines
9.0 KiB
Markdown
87 lines
9.0 KiB
Markdown
# 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. The Add App dialog includes a `NewAppPermissionsPicker` that lets admins pre-set `permissionIds[]` so `POST /api/apps` creates the app and inserts its `app_permissions` rows in the same transaction (with `onConflictDoNothing`), avoiding the brief unrestricted window between create and follow-up gating.
|
|
- **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 `/api` → `localhost: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'`.
|