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