Commit Graph

21 Commits

Author SHA1 Message Date
Riyadh 1381e38044 Task #517: App image upload, external links, built-in route lock
Admin Add/Edit App now supports:
- Custom image upload (or fall back to Lucide icon) via the existing
  ServiceImageUploader; rendered on the home launcher when set.
- Open mode picker: internal (default), external_tab (window.open),
  external_iframe (renders inside /embedded/:id). External URL input
  shown conditionally and required when an external mode is chosen.
  Internal route field is hidden entirely when an external mode is
  selected, and locked (readOnly + lock hint) when editing a built-in
  app whose path is hardcoded in the SPA.

Backend:
- apps schema gains image_url, external_url, open_mode (default
  'internal'); drizzle-kit push applied.
- New lib/db/src/built-in-apps.ts exports BUILTIN_APP_SLUGS +
  isBuiltinAppSlug. Exposed via subpath export
  `@workspace/db/built-in-apps`; the file has zero imports so the
  browser bundle can use it without pulling in `pg`. tx-os now imports
  it directly — duplicate FE constant removed.
- Built-in slug list: services, notifications, admin, notes,
  my-orders, orders-incoming, executive-meetings (everything with a
  hardcoded <Route> in artifacts/tx-os/src/App.tsx). calendar /
  documents are seeded but admin-defined and remain editable.
- PATCH /apps/:id rejects route changes whose previous slug is
  built-in with 400 + code='builtin_route_locked'. Same-route no-op
  is allowed; non-route updates on built-ins still work.

Other:
- New SPA route /embedded/:id and embedded-app page (iframe host with
  back + open-in-new-tab + error/not-embeddable states).
- OpenAPI App / CreateAppBody / UpdateAppBody extended; codegen ran.
- en/ar locales: admin.appImage, appExternalUrl, appOpenMode.*,
  builtinPathLocked, embeddedFrame.*.
- scripts/src/seed.ts: drift guard throws if a seeded built-in slug
  uses a route that does not match the hardcoded SPA route.

Tests:
- New API test apps-builtin-route-lock.test.mjs (4/4 pass): reject
  built-in route change, allow non-route built-in updates, allow
  non-builtin route changes, allow built-in same-route no-op.
- New Playwright E2E admin-app-image-external-embedded.spec.mjs
  (passes): launcher renders custom image_url, external_tab opens
  external URL via window.open, external_iframe navigates to
  /embedded/:id and renders <iframe src=externalUrl>.

Out of scope (pre-existing, not introduced here):
- 3 failing tests in executive-meetings-* and tsc errors in
  api-server/src/routes/executive-meetings.ts.
2026-05-12 12:31:43 +00:00
Riyadh 7c5a9748a2 Rebuild PDF generation: HTML <table> + Playwright replaces PDFKit manual drawing
Task #372: Replaced the manual PDFKit coordinate-based PDF renderer with an
HTML table-based approach using Playwright's headless Chromium.

Key changes:
- Created `pdf-html-renderer.ts` with `buildScheduleHtml()` (generates full
  HTML with embedded base64 fonts, proper `<table>`, RTL/LTR support, row
  colors, merged cells, attendees formatting) and `htmlToPdfBuffer()` (renders
  HTML to PDF via Playwright Chromium).
- Updated `pdf-renderer.ts`: `renderSchedulePdf()` now delegates to the new
  HTML renderer via dynamic import. Legacy PDFKit code preserved as
  `renderSchedulePdf_LEGACY()` for fallback reference.
- Added `playwright-core` dependency and configured esbuild externals.
- Chromium binary discovery: searches `.cache/ms-playwright/` directories with
  env var override (`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH`). Browser instance
  is cached as singleton with launch mutex to prevent concurrent double-launch.
- Process shutdown hook closes browser on exit.

Requirements met:
- Each meeting in exactly one `<tr>` with 4 `<td>` cells
- No text outside table, attendees ~3 per line inline
- Time always in its column, `page-break-inside: avoid`, `table-layout: fixed`
- Matches reference design (blue header, centered text, DIN Next LT Arabic)
2026-05-04 10:24:08 +00:00
Riyadh 4018373bb0 Fix Arabic text rendering in generated PDFs
Add Arabic text shaping functionality to convert base characters to their contextual presentation forms before bidi reordering and PDF rendering. Includes a new regression test to verify correct shaping by asserting the presence of presentation form codepoints in the PDF's embedded ToUnicode CMap.
2026-05-03 15:26:38 +00:00
Riyadh b54d4e35d9 Send executive-meeting notification emails for real (SMTP delivery)
Original task #163: replace the no-op outbox-log behaviour in
`sendExecutiveMeetingEmail` with real SMTP delivery so approvers
actually get pinged in their inbox when an executive-meeting request
is awaiting review.

Implementation
- Added `nodemailer` (and `@types/nodemailer`) as a dependency of
  `@workspace/api-server`. nodemailer was already in the build's
  external list, so the bundle stays slim and resolves it at runtime.
- Rewrote `sendExecutiveMeetingEmail` in
  `artifacts/api-server/src/lib/executive-meeting-notify.ts`:
  - Lazily builds a cached nodemailer transporter from
    `SMTP_HOST` / `SMTP_PORT` (default 587) / `SMTP_USER` /
    `SMTP_PASS`, with optional `SMTP_SECURE` and `SMTP_FROM`.
    Cache is keyed on a config signature, so changing env vars in
    tests / hot reloads naturally rebuilds the transporter.
  - When `SMTP_HOST` is unset the previous outbox-style log is kept
    verbatim as a fallback. Once `SMTP_HOST` is set the fallback
    branch is no longer reached.
  - Picks subject/body in the recipient's preferred language
    (Arabic vs English) with a sensible fallback.
  - Sends one mail per addressed recipient in parallel; per-recipient
    failures are caught and logged at warn level. The outer
    try/catch keeps the function from ever throwing into the
    transaction caller. SMTP `rejected` arrays are also logged at
    warn so bounces are visible.

Code-review comment follow-ups (applied in this commit)
- Transport cache signature now hashes `SMTP_PASS` (sha256, first
  16 hex chars) instead of just "***", so rotating to a new password
  actually rebuilds the cached transporter without leaking plaintext.
- Fallback log message now distinguishes "no SMTP_HOST configured"
  from "SMTP misconfigured" so operators can tell why a delivery
  was skipped.

Verification
- Typecheck passes for the modified file (other unrelated pre-existing
  typecheck failures remain).
- `pnpm --filter @workspace/api-server run build` succeeds.
- API server boots cleanly with the new dependency.

Deviations / scope
- No automated tests added; the existing test harness is integration-
  style and the project already tracks "Add automated tests for the
  notification fan-out logic" as a separate task.
- Persisting per-recipient delivery state into the
  `executive_meeting_notifications` audit table and a startup
  `transporter.verify()` were intentionally deferred and proposed as
  follow-ups (#232 and #233).
2026-04-30 18:29:43 +00:00
Riyadh 089aa886ff Task #151: Add automated tests for the live role-permission updates
Adds `artifacts/api-server/tests/role-permissions-realtime.test.mjs`,
covering the `role_permissions_changed` socket event emitted from
`PUT /api/roles/:id/permissions`.

The test:
- Stands up an admin caller plus three holders/non-holders:
  - a direct holder via `user_roles`,
  - an indirect holder reached via `group_roles` -> `user_groups`,
  - an outsider with no claim on the role.
- Logs each in via `/api/auth/login`, opens an authenticated
  `socket.io-client` socket per user against `/api/socket.io` (the
  same path/transports the real client uses) and waits for `connect`
  before issuing the PUT, so the user-room join is guaranteed.
- Asserts both holders receive exactly one
  `role_permissions_changed` event with payload `{ roleId }`.
- Asserts the outsider receives zero such events.

Also adds `socket.io-client` as a devDependency on
`@workspace/api-server` (no production code touched).

Notes / non-deviations:
- Pre-existing typecheck errors in `routes/groups.ts` and
  `routes/executive-meetings.ts` are unrelated and were not
  introduced by this change.
- The audit-style file `role-permission-audit.test.mjs` was used as
  the setup/teardown template per the task description.
- After code review feedback, replaced the fixed 250 ms sleep with
  a promise-based `waitForEvent(timeoutMs)` helper so the holder
  assertions resolve as soon as the broadcast lands (test now
  finishes in ~700 ms instead of ~1850 ms). A short 100 ms grace
  is still used before the outsider negative-case assertion to
  give a regression emit time to arrive.

Follow-ups proposed:
- #215 cover the same fan-out for POST/DELETE permission endpoints.
- #216 cover the sibling `apps_changed` fan-out via
  `emitAppsChangedToRoleHolders`.
2026-04-30 12:48:17 +00:00
Riyadh c18f84952c Task #148: Make pnpm --filter @workspace/db run push work without manual SQL
## Original task
`pnpm --filter @workspace/db run push` was failing with a duplicate-key
error on `app_permissions` (and a missing FK on
`executive_meeting_notifications`) because legacy data in the dev DB
violates constraints the schema now declares. Devs had to drop into psql
to fix it, which made bootstrapping painful and left
`role_permission_audit` reliant on hand-applied SQL.

## Changes
- Added `lib/db/scripts/pre-push-cleanup.ts`, an idempotent cleanup that:
  - Collapses duplicate `app_permissions` rows to one per
    `(app_id, permission_id)` so the composite PK can be added.
  - Deletes orphan `executive_meeting_notifications` rows so the new
    `ON DELETE CASCADE` FK can be added.
  - Skips both checks when the tables don't exist yet (fresh DB
    no-op).
- Wired the script into `lib/db/package.json` so both `push` and
  `push-force` run cleanup first (`pnpm run pre-push-cleanup && drizzle-kit push ...`).
- Added `tsx` to `lib/db` devDependencies (catalog version) so the
  package can run the cleanup without leaning on another workspace.
- Updated `replit.md` Deployment / Migration Runbook to reflect that
  cleanup is now automatic — no manual SQL required in any environment.

## Verification
- `pnpm --filter @workspace/db run push` now collapses 2 duplicate
  groups + removes 1978 orphan notifications, then succeeds with
  `[✓] Changes applied`.
- Re-running push is idempotent: second run reports no duplicates and
  no orphans, then succeeds.
- `pnpm --filter @workspace/db run push-force` (used by
  `scripts/post-merge.sh`) was also verified end-to-end.
- Confirmed `app_permissions` now has the composite PK,
  `executive_meeting_notifications` has the cascade FK, and
  `role_permission_audit` matches the schema.

## Notes
- A pre-existing unrelated typecheck error in
  `artifacts/api-server/src/routes/executive-meetings.ts` (font
  settings `scope` overload) was confirmed to exist on `main` before
  any changes here and is out of scope for this task.
- Proposed follow-up #213 to move from `push`/`push-force` to
  versioned `drizzle-kit migrate` so legacy-data backfills are checked
  in instead of living in a generic pre-push script.
2026-04-30 12:07:19 +00:00
Riyadh 92324eca60 Replace executive-meetings print-to-PDF with server-side PDF generator
The "Download PDF" button on the executive-meetings page now hits a real
backend endpoint that returns a true PDF (no more browser print dialog),
respects each user's font preferences (family, size, weight, alignment)
with proper Arabic RTL shaping, and archives every download.

The renderer maps each saved fontFamily ("system", "Cairo", "Tajawal",
"Noto Naskh Arabic", "Amiri") to a concrete pair of bundled font files
so the chosen family genuinely changes the embedded glyphs — Cairo and
Tajawal pick Noto Sans Arabic, the Naskh-style families and the system
default pick Noto Naskh Arabic, and Latin glyphs render in DejaVu Sans
across the board. Headers, body cells, and footer all flow through the
same script-aware font selection.

Backend (artifacts/api-server)
- New GET /api/executive-meetings/pdf?date=&lang= route in
  src/routes/executive-meetings.ts that fetches the day's meetings +
  attendees, renders a PDF, uploads it to object storage, writes an
  executive_meeting_pdf_archives row (date, generated_by, byte_size,
  storage_url), and streams the file back inline.
- New src/lib/pdf-renderer.ts using pdfkit + bidi-js with bundled
  Noto Naskh Arabic and DejaVu Sans fonts in assets/fonts/.
- Added byte_size column on executive_meeting_pdf_archives (also in
  lib/db schema) and rebuilt lib/db.
- Added ambient types for bidi-js; installed @swc/helpers to satisfy
  fontkit at runtime.
- build.mjs now copies pdfkit's data/ folder (Helvetica.afm, etc.)
  into dist/data so the bundled server can construct PDFDocument.

Frontend (artifacts/tx-os)
- PdfSection in src/pages/executive-meetings.tsx now renders a single
  "Download PDF" button that fetches the endpoint, builds a Blob, and
  downloads it. Removed the print/archive-creation buttons.
- Archive list shows a Download button for new /objects/... rows and a
  read-only "Legacy snapshot" badge for older print: rows.
- Added byteSize on PdfArchive + size formatting; updated en/ar locales.

Tests
- New test "PDF GET /executive-meetings/pdf returns a real PDF and
  archives it" in tests/executive-meetings.test.mjs covers: bad-date
  400, unauthenticated 401, real %PDF body + content-type/disposition,
  archive row with byteSize/generatedBy/filePath, empty-day handling,
  and font-family mapping (Cairo embeds NotoSansArabic; Noto Naskh
  Arabic embeds NotoNaskhArabic).
- All 23 executive-meetings tests pass.

Rebase
- Rebased onto main-repl/main (13d82b8 "Update the website's shared
  image"). The only conflict was the binary asset
  artifacts/tx-os/public/opengraph.jpg — accepted the incoming/main
  version since it's unrelated to this PDF work.

Drift
- Kept the legacy /executive-meetings/print SPA route and the existing
  POST /pdf-archives endpoint to preserve old archive snapshots and
  the existing snapshot test. Proposed follow-up #169 to clean these
  up once stakeholders confirm.
2026-04-29 18:01:19 +00:00
Riyadh 1094d1dedc Executive Meetings: inline editing, drag reorder, and current-meeting highlight
Implements Task #122 — four features on the Executive Meetings schedule:
1. Word-like inline editing of meeting title and attendee names with bold,
   italic, underline, color, font-family, font-size, and text-align controls
   (Tiptap-backed EditableCell + FormattingToolbar).
2. Drag-to-reorder column headers (dnd-kit), persisted in the existing
   em-schedule-cols-v1 storage key. Old up/down buttons removed.
3. Drag-to-reorder rows within a day. New POST /api/executive-meetings/reorder
   endpoint permutes daily_number + start/end times in a single transaction
   using a two-phase negative→final assignment that respects the
   (date, daily_number) UNIQUE constraint and survives partial-day reorders.
   Audit-logged and role-guarded.
4. Highlight the meeting whose start/end window is "now", refreshed every
   60 s. Toggle and color picker live in the customize popover and persist
   to localStorage (em-current-meeting-highlight-v1).

Backend changes
- Widened titleAr / titleEn / attendees.name to text.
- New artifacts/api-server/src/lib/sanitize.ts with an allowlist for
  inline tags + safe inline styles. Applied to POST, PATCH, PUT-attendees,
  duplicate, reorder, and applyApprovedRequest (add_attendee) paths so
  rich text round-trips safely.
- Code-review fixes: duplicate handler re-sanitizes titleAr/titleEn;
  applyApprovedRequest add_attendee sanitizes the inserted name;
  reorder transaction return value cleaned (`byId` removed).
- 5 new tests cover sanitization stripping, reorder happy path, cross-day
  rejection, permission denial, and applyApprovedRequest sanitization.
  Pre-existing app_permissions failures are unrelated (tracked by #126/#127).

Frontend changes
- artifacts/tx-os/src/components/editable-cell.tsx with FormattingToolbar
  (custom FontSize Tiptap extension + @tiptap/extension-text-align).
- ScheduleSection wires saveTitle, saveAttendeeName, reorder mutation
  (optimistic with revert on error), highlight tick, and HighlightPrefs.
- New SortableHeader component for column drag.
- AttendeesCell passes the original attendee index to PUT writes so edits
  reach the right row even after grouping into virtual/internal/external.
- Print page renders sanitized rich text via dangerouslySetInnerHTML on
  names and titles; attendee.title is rendered as a plain text node (XSS).
- Translation keys added in ar.json and en.json.
2026-04-29 08:17:24 +00:00
Riyadh 335f351c9b Executive Meetings: inline editing, drag reorder, and current-meeting highlight
Implements Task #122 — four features on the Executive Meetings schedule:
1. Word-like inline editing of meeting title and attendee names with bold,
   italic, underline, color, font-family, font-size, and text-align controls
   (Tiptap-backed EditableCell + FormattingToolbar).
2. Drag-to-reorder column headers (dnd-kit), persisted in the existing
   em-schedule-cols-v1 storage key. Old up/down buttons removed.
3. Drag-to-reorder rows within a day. New POST /api/executive-meetings/reorder
   endpoint permutes daily_number + start/end times in a single transaction
   using a two-phase negative→final assignment that respects the
   (date, daily_number) UNIQUE constraint and survives partial-day reorders.
   Audit-logged and role-guarded.
4. Highlight the meeting whose start/end window is "now", refreshed every
   60 s. Toggle and color picker live in the customize popover and persist
   to localStorage (em-current-meeting-highlight-v1).

Backend changes
- Widened titleAr / titleEn / attendees.name to text.
- New artifacts/api-server/src/lib/sanitize.ts with an allowlist for
  inline tags + safe inline styles. Applied to POST, PATCH, PUT-attendees,
  duplicate, reorder, and applyApprovedRequest (add_attendee) paths so
  rich text round-trips safely.
- Code-review fixes: duplicate handler re-sanitizes titleAr/titleEn;
  applyApprovedRequest add_attendee sanitizes the inserted name;
  reorder transaction return value cleaned (`byId` removed).
- 5 new tests cover sanitization stripping, reorder happy path, cross-day
  rejection, permission denial, and applyApprovedRequest sanitization.
  Pre-existing app_permissions failures are unrelated (tracked by #126/#127).

Frontend changes
- artifacts/tx-os/src/components/editable-cell.tsx with FormattingToolbar
  (custom FontSize Tiptap extension + @tiptap/extension-text-align).
- ScheduleSection wires saveTitle, saveAttendeeName, reorder mutation
  (optimistic with revert on error), highlight tick, and HighlightPrefs.
- New SortableHeader component for column drag.
- AttendeesCell passes the original attendee index to PUT writes so edits
  reach the right row even after grouping into virtual/internal/external.
- Print page renders sanitized rich text via dangerouslySetInnerHTML on
  names and titles; attendee.title is rendered as a plain text node (XSS).
- Translation keys added in ar.json and en.json.
2026-04-29 08:03:25 +00:00
Riyadh 592350339d Executive Meetings: inline editing, drag reorder, and current-meeting highlight
Implements Task #122 — four features on the Executive Meetings schedule:
1. Word-like inline editing of meeting title and attendee names with bold,
   italic, underline, color, font-family, font-size, and text-align controls
   (Tiptap-backed EditableCell + FormattingToolbar).
2. Drag-to-reorder column headers (dnd-kit), persisted in the existing
   em-schedule-cols-v1 storage key. Old up/down buttons removed.
3. Drag-to-reorder rows within a day. New POST /api/executive-meetings/reorder
   endpoint permutes daily_number + start/end times in a single transaction
   using a two-phase negative→final assignment that respects the
   (date, daily_number) UNIQUE constraint and survives partial-day reorders.
   Audit-logged and role-guarded.
4. Highlight the meeting whose start/end window is "now", refreshed every
   60 s. Toggle and color picker live in the customize popover and persist
   to localStorage (em-current-meeting-highlight-v1).

Backend changes
- Widened titleAr / titleEn / attendees.name to text.
- New artifacts/api-server/src/lib/sanitize.ts with an allowlist for
  inline tags + safe inline styles. Applied to POST, PATCH, PUT-attendees,
  duplicate, reorder, and applyApprovedRequest (add_attendee) paths so
  rich text round-trips safely.
- Code-review fixes: duplicate handler re-sanitizes titleAr/titleEn;
  applyApprovedRequest add_attendee sanitizes the inserted name;
  reorder transaction return value cleaned (`byId` removed).
- 5 new tests cover sanitization stripping, reorder happy path, cross-day
  rejection, permission denial, and applyApprovedRequest sanitization.
  Pre-existing app_permissions failures are unrelated (tracked by #126/#127).

Frontend changes
- artifacts/tx-os/src/components/editable-cell.tsx with FormattingToolbar
  (custom FontSize Tiptap extension + @tiptap/extension-text-align).
- ScheduleSection wires saveTitle, saveAttendeeName, reorder mutation
  (optimistic with revert on error), highlight tick, and HighlightPrefs.
- New SortableHeader component for column drag.
- AttendeesCell passes the original attendee index to PUT writes so edits
  reach the right row even after grouping into virtual/internal/external.
- Print page renders sanitized rich text via dangerouslySetInnerHTML on
  names and titles; attendee.title is rendered as a plain text node (XSS).
- Translation keys added in ar.json and en.json.
2026-04-29 07:53:51 +00:00
Riyadh e93ca043f8 Executive Meetings: inline editing, drag reorder, and current-meeting highlight
Implements Task #122 — four features on the Executive Meetings schedule:
1. Word-like inline editing of meeting title and attendee names with bold,
   italic, underline, color, and font controls (Tiptap-backed EditableCell).
2. Drag-to-reorder column headers (dnd-kit), persisted in the existing
   em-schedule-cols-v1 storage key. Old up/down buttons removed.
3. Drag-to-reorder rows within a day. New POST /api/executive-meetings/reorder
   endpoint swaps daily_number in a single transaction using a two-phase
   negative→final assignment that respects the (date, daily_number) UNIQUE
   constraint. Audit-logged and role-guarded.
4. Highlight the meeting whose start/end window is "now", refreshed every
   60 s. Toggle and color picker live in the customize popover and persist
   to localStorage.

Backend changes
- Widened titleAr / titleEn / attendees.name to text (applied via direct
  ALTER TABLE because db:push --force is currently blocked by Task #126).
- New artifacts/api-server/src/lib/sanitize.ts with an allowlist for
  inline tags + safe inline styles. Applied to POST, PATCH, PUT-attendees,
  duplicate, and reorder paths so rich text round-trips safely.
- Code-review fix: duplicate handler now re-sanitizes titleAr/titleEn.
- Code-review fix: print page no longer interpolates the unsanitized
  attendee.title into dangerouslySetInnerHTML.
- 4 new tests cover sanitization stripping, reorder happy path, cross-day
  rejection, and permission denial. Pre-existing app_permissions failures
  are unrelated and tracked by Task #126.

Frontend changes
- artifacts/tx-os/src/components/editable-cell.tsx with FormattingToolbar.
- ScheduleSection wires saveTitle, saveAttendeeName, reorder mutation
  (optimistic with revert on error), highlight tick, and HighlightPrefs.
- New SortableHeader component for column drag.
- AttendeesCell now passes the original attendee index to PUT writes so
  edits reach the right row even after grouping into virtual/internal/
  external sections.
- Print page renders sanitized HTML via dangerouslySetInnerHTML on names
  and titles (titles still rendered as plain text on the print page).
- Translation keys added in ar.json and en.json.

Drift
- Sanitization for attendee.title was identified by the architect review
  as a defense-in-depth gap and proposed as Task #130 rather than fixing
  inline; the print-page XSS path that depended on it was fixed directly.
2026-04-29 07:39:24 +00:00
Riyadh 2f10ae6dc4 Task #108 — Executive Meetings Phase 2 (RBAC + audit + Zod + i18n)
Schedule UI / shell:
- Centered cells, locked RTL column order # | الاجتماع | الحضور | الوقت,
  attendees column widest, tighter padding, navy/white/gray + red highlight only.
- Header label "م" → "#" (ar.json).

Schema (lib/db/src/schema/executive-meetings.ts):
- executive_meeting_requests.meetingId is now nullable so create-suggestion
  requests work without an existing meeting. db push applied.

Backend (artifacts/api-server/src/routes/executive-meetings.ts) — full rewrite:
- All routes (including /me) gated by requireExecutiveAccess. Fine-grained
  middleware: requireMutate / requireApprove / requireRequest /
  requireAdminAudit on the appropriate routes.
- Zod schemas validate every mutating body via parseBody().
- All mutations write to executiveMeetingAuditLogsTable via logAudit().
- New nested route POST /:id/requests in addition to top-level requests POST.
- Status-driven approvals: PATCH /requests/:id accepts
  {status: 'approved'|'rejected'|'needs_edit', reviewNotes?} or
  {action:'withdraw'} for the requester.
- PUT /font-settings (canonical) plus PATCH alias — both wired directly
  to the same shared upsertFontSettingsHandler (no method-rewrite hack).
- GET /tasks supports date and assigneeId filters.
- Removed all `as never` casts via $inferInsert/Partial typing.

Auth middleware (artifacts/api-server/src/middlewares/auth.ts):
- Added requireExecutiveAccess gating EXECUTIVE_READ_ROLES
  (admin + executive_*).

Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Notifications visibility now uses canRead (was canViewAudit).
- Approvals: review() sends {status, reviewNotes} and a new
  "Send back for edits" (needs_edit) button is rendered alongside
  Approve/Reject.
- PdfSection: primary "Print + Archive" button (testid em-print) does
  archive then print in one click; em-print-only and em-archive remain.
- AuditSection: 6th column renders the new in-file AuditDiffSummary
  component (compact "field: old → new" diff with truncation).
- Print page (executive-meetings-print.tsx): kept its inline bilingual T
  table — it loads in a standalone print window before the i18n provider
  mounts, so an inline dictionary is the right pattern. No hardcoded UI
  text remains in the main page.

i18n (en.json + ar.json):
- New keys executiveMeetings.approvals.{needsEdit, needsEditDone},
  .audit.col.changes, .audit.{created, removed, moreChanges},
  .audit.entity.pdf_archive,
  .audit.action.{approved, rejected, needs_edit, withdraw, apply,
  duplicate, replace_attendees, done},
  .pdf.{print = "Print + archive"/"طباعة وأرشفة", printOnly,
  archivedAndPrinted}.

Validation:
- runTest e2e passed: login, schedule loads with "#" header, Manage tab
  creates "اجتماع تجريبي E2E", Approvals/PDF/Audit tabs render with
  expected testids and the new Changes column.
- curl smoke: login 200, /me 200, dates 200, create meeting 201,
  nested POST /:id/requests 201, PATCH /requests/:id status approval 200,
  audit-logs 200, DELETE meeting 204, PUT and PATCH /font-settings both
  200 with persisted data.
- Architect re-reviewed twice: VERDICT: APPROVED on the second pass
  after fixing /me gating and the PATCH font-settings dispatch.

Out of scope (handled in follow-ups #110/#111/#112): real notifications
delivery, server-side PDF rendering, mobile polish.

Drift: none material. Followed plan T1–T8.
2026-04-28 07:19:07 +00:00
Riyadh aa462f9f52 Rename project and remove unused services
Update project name from teaboy-os to tx-os and remove obsolete services.
2026-04-22 10:52:09 +00:00
Riyadh 3264122a45 Add a Playwright UI test for the leave-group successor picker
Original task: Add a UI test for the leave-group successor chooser dialog
in artifacts/teaboy-os/src/pages/chat.tsx, mirroring the backend coverage
in artifacts/api-server/tests/conversations-leave.test.mjs.

What I added:
- artifacts/teaboy-os/tests/leave-group-successor.spec.mjs
  A real Playwright e2e test that drives the chat UI in a browser:
  * Cancel path: opens the leave dialog as the sole admin, verifies the
    successor chooser is visible with the auto option pre-selected and
    one option per remaining member, clicks Cancel, and asserts via DB
    that the conversation membership and admin flags are unchanged.
  * Chosen-successor path: re-opens the dialog, picks the *later-joined*
    member (so the choice differs from the auto pick), confirms the
    leave, asserts the POST /conversations/:id/leave call returns 200,
    and verifies via DB that the leaver was removed and the explicitly
    chosen member became the new admin.
  Both tests seed their own users + group via SQL and clean up after
  themselves so they can run repeatedly against the live dev DB.

- artifacts/teaboy-os/playwright.config.mjs
  Minimal Playwright config: testDir=tests, *.spec.mjs match, headless,
  baseURL defaults to http://localhost:80 (the workspace proxy), and
  is overridable via TEST_WEB_BASE.

- artifacts/teaboy-os/package.json
  Added @playwright/test and pg as devDependencies and a `test:e2e`
  script: `playwright test --config playwright.config.mjs`.

- .gitignore
  Ignored Playwright's test-results/ and playwright-report/ output dirs.

How to run:
  pnpm --filter @workspace/teaboy-os exec playwright install chromium
  DATABASE_URL=... pnpm --filter @workspace/teaboy-os run test:e2e

Verification:
  Both tests pass locally (2 passed in ~9s) against the running dev
  workspace. Required system libraries for headless Chromium were
  installed via the workspace's system-deps mechanism (glib, nss, nspr,
  atk, cups, dbus, libdrm, libxkbcommon, libgbm, alsa-lib, pango, cairo,
  and the relevant xorg libs), so `playwright test` works out of the
  box on this environment.

No application source files were modified.
2026-04-21 12:17:20 +00:00
Riyadh 2481064bf9 Add automated coverage for app-open tracking edge cases (Task #22)
Original task: verify POST /api/apps/:id/open behaves correctly under slow
networks (the keepalive POST must survive the user navigating away),
unauthenticated callers (401 with no row inserted), and unknown app ids
(404 with no row inserted).

Changes
- New committed test file artifacts/api-server/tests/apps-open.test.mjs
  using Node's built-in node:test runner (no new test framework added):
  * happy path: authenticated POST returns 204 and inserts an app_opens row
  * unauthenticated POST returns 401 and inserts no row
  * authenticated POST to a non-existent app id (max(id)+100000) returns 404
    and inserts no row
  * slow-network simulation: opens a raw http.request to /api/apps/:id/open,
    aborts the socket ~50 ms after sending so the client never reads the
    response (mimicking a navigation-aborted keepalive POST), then asserts
    the server still inserted the row. This proves the route's
    `await db.insert(...)` runs to completion independently of whether the
    client is still around to read the 204.
- The tests create a dedicated test user with a precomputed bcrypt hash for
  "TestPass123!", assign the standard "user" role, log in via
  POST /api/auth/login to obtain a connect.sid cookie, run the four cases,
  and clean up (app_opens / user_roles / users) in an `after` hook.
- Added `pg` as a devDependency on @workspace/api-server (used by the
  tests for direct DB assertions) and a `test` script:
  `node --test 'tests/**/*.test.mjs'`.
- Also ran in-browser end-to-end coverage via the testing skill that
  exercised the keepalive + wouter navigation flow against a live home
  page with a 3 s route delay; that run also passed.

Schema drift fixed during the run
- The dev DB was missing the `app_opens` table and the `users.clock_style`
  column referenced by the running schema. `pnpm --filter @workspace/db
  push` blocked on an interactive rename/create prompt that could not be
  answered non-interactively, so I brought the dev DB in line with the
  Drizzle schema using idempotent SQL (CREATE TABLE IF NOT EXISTS for
  app_opens with its two indexes; ALTER TABLE users ADD COLUMN IF NOT
  EXISTS clock_style varchar(30)). No schema files were modified.

No production code changes were required — the existing route already
returns 401/404/204 correctly and the tests now lock that behavior in.
2026-04-21 06:38:55 +00:00
Riyadh 8354156099 Add ability to reorder apps on the home screen
Implements drag-and-drop functionality for reordering apps using @dnd-kit, adds a new `userAppOrdersTable` to the database schema to store user-specific app order preferences, and introduces a new API endpoint `/api/me/app-order` for updating these preferences.
2026-04-20 12:09:14 +00:00
Riyadh 16091523fe Update dependencies to resolve lockfile inconsistencies
Correct mismatched dependency versions in pnpm-lock.yaml for react and react-dom to align with manifest specifications.
2026-04-20 11:44:43 +00:00
Riyadh 0efc4032be Add ability to upload and manage service images
Integrates Uppy.js for file uploads, adds new API endpoints for requesting upload URLs, and updates UI components to support image uploads.
2026-04-20 10:55:11 +00:00
Riyadh 731a125ae1 fix: resolve final code review rejections — users CRUD, i18n, typecheck
Users CRUD — Now Complete:
- Add POST /users to OpenAPI spec (references RegisterBody schema, returns UserProfile)
- Re-run codegen; useCreateUser and createUser now generated in api-client-react
- Add useCreateUser to admin.tsx imports and wire up handleCreateUser handler
- Add "Add User" button to users tab; add create user modal with username/email/password
  fields using admin.username, admin.email, admin.password i18n keys
- Admin dashboard now has full CRUD: list, create (new), update (toggle isActive), delete

i18n — Admin Form Labels Fixed:
- Replace dynamic t(`admin.${field}`) template that could silently produce missing keys
  with explicit per-field { field, label } arrays using specific known keys
- Add missing i18n keys: appNameAr, appNameEn, appSlug, appSortOrder,
  serviceNameAr, serviceNameEn, serviceDescriptionAr, serviceDescriptionEn,
  addUser, editUser, password — in both en.json and ar.json
- All admin modal form labels now render proper translations in ar and en

Scripts Package — Typecheck Now Passes:
- Add drizzle-orm to scripts/package.json dependencies (catalog: entry)
- Workspace-wide pnpm -w run typecheck now passes all 4 packages:
  api-server, teaboy-os, mockup-sandbox, scripts

Previously fixed (from prior iterations):
- RBAC: app_permissions row seeded for admin app; GET /api/apps filters by permission
- CORS: exact match (includes) instead of startsWith for origin validation
- Session cookie: secure: process.env.NODE_ENV === "production"
- Socket.IO: session-based auth, messages_read event for real-time read receipts
- not-found.tsx: uses t("notFound.*") keys, no hardcoded English
- login/register: use t("common.appName"), no hardcoded "TeaBoy OS"
- @replit comments removed from badge.tsx and button.tsx
- Admin redirect uses useEffect (rules of hooks compliance)
2026-04-20 10:02:09 +00:00
Riyadh 80de117bbe feat: build TeaBoy OS — bilingual Arabic/English internal OS platform
Completed full-stack TeaBoy OS build:

Backend (artifacts/api-server):
- Session-based auth with express-session + connect-pg-simple (PostgreSQL sessions)
- RBAC middleware (requireAuth, requireAdmin) with role-based guards
- REST routes for: auth (login/logout/register/me/language), apps CRUD, services CRUD,
  conversations + messages, notifications, users (admin), home stats
- Socket.IO mounted on same HTTP server at /api/socket.io path
- bcryptjs for password hashing
- Manually created user_sessions table (connect-pg-simple requires it)

Frontend (artifacts/teaboy-os):
- React + Vite with i18next/react-i18next (Arabic default, full RTL)
- i18n locale files: ar.json + en.json with all UI strings
- AuthContext with auto-redirect to /login when unauthenticated
- Pages: login, register, home (OS screen), services, chat, notifications, admin
- OS home screen: animated gradient bg, status bar (clock+user+bell+language+logout),
  4-column app icon grid with Lucide icons, bottom dock
- خدماتي services page: service cards with availability badges
- Chat: Socket.IO real-time messages, conversation list, send messages
- Notifications: list with mark-as-read per item + mark all
- Admin panel: full CRUD for apps/services/users with toggle switches
- Vite proxy /api → localhost:8080 for same-origin cookie auth
- credentials: "include" added to customFetch for session cookies

Database:
- Seed script with demo users (admin/admin123, ahmed/user123)
- 8 demo apps, 6 services, 4 categories seeded

All e2e tests pass: login, home screen, services, language toggle, admin, logout
2026-04-20 09:20:50 +00:00
Riyadh 963cc0a114 Initial commit 2026-04-18 02:00:09 +00:00