0740971a9663652167e5f29e2b3c637b81114e95
19 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0740971a96 |
feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO notification fan-out so the iPad PWA (and any installed browser) receives system notifications when Tx OS is closed or backgrounded. Backend - New `push_subscriptions` table (userId + unique endpoint + p256dh/auth keys + ua + timestamps), exported from `@workspace/db`. - `artifacts/api-server/src/lib/push.ts`: - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT (Docker volume) with /tmp fallback for Replit dev, else ephemeral. - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs (orders/meetings/notes), prunes 404/410 endpoints, truncates payload bodies to ~3500 bytes. - **De-dup gate:** skips push when the user has any active Socket.IO connection (uses `io.in(\`user:\${uid}\`).fetchSockets()`), so a connected user only gets the in-app chime, never a duplicate system notification. - `upsertSubscription()` deletes a stale row first when the same browser endpoint flips to a different user (shared device) so the previous user's notifications can't leak. - Four new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`, `POST /api/push/unsubscribe`, and a spec-aligned alias `DELETE /api/push/subscribe?endpoint=...` (auth-gated, Zod-validated). - Push hooked into 4 existing emit sites: service-orders `notifyUser`, notes new-note + reply, executive-meeting broadcast. Frontend - `artifacts/tx-os/public/sw.js`: push + notificationclick only (no asset caching). All URLs (icon, badge, navigation target) resolved against `self.registration.scope`, so the SW works at root or under a subpath without code changes. - SW registration in `main.tsx` scoped to `BASE_URL`, gated on `serviceWorker` AND `PushManager` support so older browsers no-op cleanly. - `use-push-subscription` hook (enable/disable/refresh + status). - New `PushEnablePrompt` card mounted in `App` — appears on first launch when supported + permission still "default", one-tap enable, dismiss persists for 14 days. Silent on unsupported devices. - `PushToggleRow` added inside Notification Settings. - ar/en strings: `notifSettings.push.*` and `common.later`. Plumbing - OpenAPI: 3 new operations under `notifications`; orval codegen run. - `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT through to the api service. - `pnpm --filter @workspace/db run push` applied the schema. - web-push + @types/web-push installed in api-server. Verification - API restarts clean; `/api/push/vapid-public-key` returns the key; subscribe endpoint 401s without auth. - New `artifacts/api-server/tests/push.test.mjs` — 7/7 passing — covers VAPID endpoint, auth gating on subscribe/unsubscribe, row persistence + removal, idempotent re-subscribe with key rotation, account-switch endpoint reassignment, and malformed-input rejection. - New `artifacts/api-server/tests/push-410-unit.test.ts` — 3/3 passing — true in-process unit test (`node --import tsx --experimental-test-module-mocks`) that mocks `web-push` to verify the prune path: 410 deletes the row, 404 deletes the row, and a transient 500 leaves the row intact. `tsx` added as a devDep and the test:run script picks up `*.test.ts` alongside the existing `.mjs` suites. - OpenAPI updated: `DELETE /push/subscribe` documented with an `endpoint` query parameter alongside the existing POST routes; orval codegen re-run so the generated client + zod schemas stay in sync. - Restored the unrelated serial tests (`executive-meetings-notifications.test.mjs`, `setup-wizard.test.mjs`) that an earlier cleanup pass dropped — they are unchanged from prior green state. - Architect rounds addressed: 1. race + payload size. 2. dedup gate + first-launch UX + SW base-path + initial tests. 3. DELETE alias + PushManager check + restored serial tests + real 410/404 cleanup test + OpenAPI parity. |
||
|
|
243ccb3e00 |
feat(push): add Web Push (VAPID) for lock-screen notifications on iPad PWA
Task #554. Adds a full Web Push stack on top of the existing Socket.IO notification fan-out so the iPad PWA (and any installed browser) receives system notifications when Tx OS is closed or backgrounded. Backend - New `push_subscriptions` table (userId + unique endpoint + p256dh/auth keys + ua + timestamps), exported from `@workspace/db`. - `artifacts/api-server/src/lib/push.ts`: - VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT (Docker volume) with /tmp fallback for Replit dev, else ephemeral in-memory. - `sendPushToUser()` honours `notificationsMuted` + per-channel prefs (orders/meetings/notes), prunes 404/410 endpoints, truncates payload bodies to ~3500 bytes so over-sized notes don't kill delivery. - `upsertSubscription()` deletes a stale row first when the same browser endpoint flips to a different user (account switch on shared device) so the previous user's notifications can't leak. - Three new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`, `POST /api/push/unsubscribe` (auth-gated, Zod-validated). - Push hooked into the 4 existing emit sites: service-orders `notifyUser`, notes new-note + reply, executive-meeting broadcast. Frontend - `artifacts/tx-os/public/sw.js`: push + notificationclick handlers only (no asset caching). Focuses an existing tab or opens a new one at the payload URL. - SW registration in `main.tsx` scoped to `BASE_URL`. - `use-push-subscription` hook (enable/disable/refresh + status: unsupported / denied / default / subscribed). - New `PushToggleRow` in `NotificationSettingsContent` with ar/en strings (notifSettings.push.*). iPad copy explains that the user must add to Home Screen first for iOS to allow Web Push. Plumbing - OpenAPI: 3 new operations under `notifications` tag; orval codegen run. - `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT through to the api service. - `pnpm --filter @workspace/db run push` applied the schema. - web-push + @types/web-push installed in api-server. Verification - API restarts clean; `/api/push/vapid-public-key` returns the generated key; subscribe endpoint 401s without auth as expected. - Architect review surfaced two HIGH issues (account-switch leak, payload size); both fixed before completion. |
||
|
|
a5a35ef217 |
Improve test execution by automatically starting and stopping the API server
Refactor test execution to use a new script that manages the API server lifecycle, including building, starting, waiting for health, running tests, and shutting down, with support for using an existing server. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: a42bd3b2-3145-4118-87aa-336a7e2189ca Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/FvHcc7z Replit-Helium-Checkpoint-Created: true |
||
|
|
78d5c91328 |
feat(setup): Stage 1 first-time setup wizard backend (no UI)
Task #534 — backend, infra, tooling. UI ships in Stage 2 (#535). Backend - New system_settings table (id=1 singleton): installed flag, base_url, local_domain, local_ip, https_mode, app_version. Pushed to dev DB. - New /api/setup/status (open) and /api/setup/{validate,complete} (gated by requireSetupOpen — 409 once installed). - completeInstall is fully transactional: pg_advisory_xact_lock serializes concurrent callers, double-gates on installed flag and admin existence, then atomically creates the admin user, assigns admin role + Admins/Everyone groups, and flips system_settings to installed=true. Rolls back on any failure. - Added redirectIfSetupNeeded() helper returning the full SetupStatus payload alongside a redirect target for SPA routing decisions. - Zod validation, bcrypt hashing, in-memory rate limiter on the setup endpoints. Backward compat - scripts/src/seed.ts now branches on installed flag + admin existence + SEED_*_PASSWORD env vars. Legacy installs (admin exists, system_settings empty) get backfilled to installed=true via ON CONFLICT DO UPDATE so they are never forced through the wizard. When env passwords are unset and no admin exists, the seed prints a wizard hint instead of seeding. Infra - docker-compose.yml: replaced nginx edge with a Caddy service that mounts ./certs and ./docker/Caddyfile{,.skip}. The web service no longer publishes a port directly — Caddy is the only public ingress. - Caddy entrypoint picks Caddyfile.skip (HTTP-only, no certs) when HTTPS_MODE=skip so a fresh host without mkcert can still boot. - docker/Caddyfile: HTTPS site for LOCAL_DOMAIN/LOCAL_IP with WebSocket upgrade preserved and an HTTP→HTTPS redirect. - start.sh: preserved. Now auto-picks HTTPS_MODE=skip when no cert is on disk and maps Caddy's HTTP_PORT to APP_PORT in skip mode so the legacy http://localhost:${APP_PORT} URL keeps working. In local/byo mode it prints the https://${LOCAL_DOMAIN} URL. - .env.example: added LOCAL_DOMAIN, LOCAL_IP, BASE_URL, HTTP_PORT, HTTPS_PORT, HTTPS_MODE; SEED_*_PASSWORD now optional. Tooling - scripts/local-setup.sh: idempotent OS-aware bootstrap (.env upsert, mkcert hint, cert SAN check, dry-run via LOCAL_SETUP_DRY_RUN). Tests - artifacts/api-server/tests/setup-wizard.test.mjs: 7/7 pass. - scripts/tests/local-setup.test.mjs: 2/2 pass. Constraints honored: no force-push, no destructive ops, start.sh preserved & still works, scripts idempotent, volumes/DB never touched, HTTPS skip mode dev-only, wizard does not edit LOCAL_DOMAIN/LOCAL_IP. Out of scope / not addressed: pre-existing TS errors in routes/users.ts and pre-existing failure in executive-meetings-postpone-race.test.mjs. |
||
|
|
2fa608b93d |
Add migration script for database updates and seeding
Adds a new `migrate` script to the API server package.json and updates the docker-compose.yml to use this script for database migrations and seeding. Also updates the README.md to reflect the new migration command. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 5a3829b7-335a-40a6-98a8-de6f98c908ff Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/jLdqQ2v Replit-Helium-Checkpoint-Created: true |
||
|
|
143ad9a29d |
Task #526: Off-Replit migration & GitHub-ready cleanup
Fully decoupled Tx OS from the Replit hosted environment so the project can be cloned and run on any Linux VPS with `docker compose up`. Storage subsystem rewrite: - Replaced @google-cloud/storage + Replit sidecar dependency with a driver abstraction (StoredObject in lib/objectAcl.ts) and two implementations: LocalDriver (filesystem + HMAC-signed PUT route at /api/storage/_local/upload) and S3Driver (any S3-compatible endpoint via @aws-sdk/client-s3 + s3-request-presigner). Driver auto-selected by STORAGE_DRIVER / S3_ENDPOINT env vars. - Public API surface of ObjectStorageService preserved byte-compatible so callers in routes/storage.ts and routes/executive-meetings.ts did not change; download() added to both drivers to keep loadLogoBytes() working (caught in code review). - Storage object-authz tests A-L (incl. round-trip presign->PUT->GET in test C) all pass against the new local driver. Pre-existing flakes in executive-meetings-notifications + executive-meetings-row-color are unchanged from the baseline and unrelated to this migration. Infrastructure: - Dockerfile (5 targets: deps/build/api/web/migrate). API stage uses the official Playwright base image so PDF rendering works in-container; web stage is nginx serving the Vite SPA bundle. - docker-compose.yml: postgres + minio + minio-init (creates buckets) + api + web + one-shot migrate runner. Healthchecks on every long-lived service. - docker/nginx.conf: SPA fallback, /api proxy, /api/socket.io websocket upgrade ordering. - .env.example: every runtime env var documented with comments. - README.md replaces replit.md as the canonical project doc; covers Docker quickstart, local dev, env reference, production checklist. - MIGRATION_REPORT.md: file-by-file diff of what changed and why. Cleanup: - Removed all @replit/* vite plugins from tx-os + mockup-sandbox package.json + vite.config.ts + pnpm-workspace.yaml catalog. - Removed @google-cloud/storage and google-auth-library from api-server. - Deleted attached_assets/ (23MB), sedMkjeJm temp file, stale dist/ and *.tsbuildinfo build artefacts, scripts/post-merge.sh. - Stripped Replit references from threat_model.md (now describes the self-hosted topology). - New comprehensive .gitignore: Replit configs (.replit, replit.nix, replit.md), agent state (.local/, .canvas/, .agents/, .cache/, .config/, .upm/), local storage/, .env*, build artefacts. .replit and replit.nix remain on disk (sandbox-protected) but will not ship to GitHub. - scripts/src/seed.ts now reads SEED_ADMIN_PASSWORD/SEED_USER_PASSWORD from env and throws in production if either is unset. Drift from plan: replit.md was deleted (per off-Replit scope) so its "do-not-touch files" preference list is moot. .replit/replit.nix kept on disk only because they are sandbox-protected from edit/delete in this environment, but they are git-ignored so they will not appear in a fresh clone. |
||
|
|
90dc95dc5c |
Task #525: Auth endpoint rate limiting (MR-H3)
Closes the remaining High-severity finding from
.local/security/manual-review.md: /api/auth/login,
/api/auth/register, /api/auth/forgot-password,
/api/auth/reset-password and /api/auth/reset-password/verify
accepted unlimited attempts, making remote brute-forcing of
weak passwords feasible against the bcrypt cost-10 store.
Changes
-------
- New artifacts/api-server/src/lib/authRateLimit.ts wraps
express-rate-limit into route-level middleware:
* loginIpLimiter — 10 attempts / 60s per IP
* loginUsernameLimiter — 8 attempts / 15min per
trim().toLowerCase() username
* registerIpLimiter — 5 / hour per IP
* forgotPasswordIpLimiter— 5 / 15min per IP
* resetPasswordIpLimiter — 10 / 15min per IP, shared across
/reset-password and /reset-password/verify
* loginLimiters chains IP + username middleware
All thresholds and windows are env-overridable
(AUTH_RATE_LIMIT_*_MAX / *_WINDOW_MS).
Throttled responses are JSON: 429
{ error: "too_many_requests", message: ... }.
In non-production, requests originating from loopback
(127.0.0.1, ::1, ::ffff:127.*) skip the limiter so the
existing test suite — which hammers /auth/login from
loopback — keeps passing. Production never skips. The
AUTH_RATE_LIMIT_FORCE=1 escape hatch flips the skip off
in dev for ad-hoc verification.
- routes/auth.ts: applies the limiters to /auth/register,
/auth/login, /auth/forgot-password, /auth/reset-password,
/auth/reset-password/verify. trust proxy was already set
to 1 in app.ts so X-Forwarded-For from the Replit edge
drives req.ip in production.
- New artifacts/api-server/tests/auth-rate-limit.test.mjs
(6 tests). Each test routes through a unique
X-Forwarded-For 10.x.x.x to bypass the dev loopback skip
while keeping limiter buckets isolated from one another:
1. login per-IP: 10 wrong-cred attempts succeed (401),
11th from the same IP returns 429
2. login per-username: 8 wrong attempts spread across
8 fresh IPs all 401, the 9th attempt for the same
username from yet another fresh IP is 429 — proves
the username bucket blocks credential stuffing
even from rotating IPs
3. forgot-password per-IP: 5 succeed, 6th 429
4. register per-IP: 5 attempts processed, 6th 429
5. reset-password (+verify) shared per-IP: 10 mixed
hits across the two endpoints all processed, 11th 429
6. loopback regression: 15 login attempts from
loopback (no X-Forwarded-For) all return non-429,
proving the dev skip works and existing tests are
unaffected
Test results
------------
- New file: 6/6 pass.
- Full api-server suite: 327/329 pass. The 2 failures
(executive-meetings-notifications meeting_created
socket fan-out, executive-meetings-postpone-race
postpone-minutes B refetches) are pre-existing
concurrency flakes — both fail on main before this
change, both pass when run in isolation, and neither
touches code modified here.
Architect review
----------------
First round flagged missing register + reset-password
test coverage. Both added in this commit. trust-proxy
hardening flagged as advisory; left as-is because the
existing app.ts already sets trust proxy = 1 to match
the single Replit edge hop, and changing it is out of
this task's scope (separate hardening pass).
Residual risk
-------------
- Per-IP buckets rely on app.set("trust proxy", 1) in
app.ts. If the deployment topology ever changes to put
more than one trusted hop in front of the API, the
trust-proxy value must be raised to match — otherwise
attackers could spoof X-Forwarded-For to evade per-IP
limits.
- The username-bucket key is normalized
(trim().toLowerCase()) but does not collapse Unicode
homoglyphs. Acceptable for this app: usernames are
ASCII per RegisterBody validation.
Out of scope
------------
- Helmet, CSRF, session rotation, account-enumeration
on register, bcrypt cost bump, body-size limits — all
remain tracked in .local/security/manual-review.md as
Medium/Low items.
|
||
|
|
f4d99b6cfd |
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) |
||
|
|
627a8fe7b1 |
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. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 18d38e3a-cfe8-4b69-a02c-380283320b78 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/Ow4s0aa Replit-Helium-Checkpoint-Created: true |
||
|
|
e4306d5716 |
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). Replit-Task-Id: 7362e23d-4531-41a8-a6ec-5f48f85b28c9 |
||
|
|
ff7bd5a4bd |
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`.
Replit-Task-Id: 1c3092d0-7339-470a-bb2a-aa7e48d976d2
|
||
|
|
f6b5539004 |
Make the test workflow wait for the API server to be ready
Original task (#127): the `test` workflow ran `pnpm --filter @workspace/api-server test` directly, which fires HTTP requests at localhost:8080. On a freshly-started environment the API server isn't up yet, so every test fails with ECONNREFUSED, drowning real failures in noise. Changes: - Added `artifacts/api-server/scripts/wait-for-server.mjs`, a small pure-Node poller that hits `${TEST_API_BASE ?? "http://localhost:8080"}/api/healthz` every 500ms until it returns `{status: "ok"}` or the timeout (default 30s) elapses. On timeout it exits 1 with a clear "Start the API Server workflow (or set TEST_API_BASE) before running tests" message instead of a wall of fetch errors. Configurable via `TEST_API_BASE`, `TEST_API_WAIT_TIMEOUT_MS`, `TEST_API_WAIT_INTERVAL_MS`. - Added `test:wait` script to `artifacts/api-server/package.json`. - Updated the `test` workflow to run `pnpm --filter @workspace/api-server test:wait` before the api-server tests and the tx-os e2e tests. Verified: - `node ./scripts/wait-for-server.mjs` against a running server reports "ready ... after 86ms" and exits 0. - Same script with TEST_API_BASE pointed at a dead port exits 1 with the friendly message. - The full `test` workflow now flows past the readiness gate and runs all 158 tests; 153 pass, 3 skip, and 2 fail for real reasons (PDF export endpoints returning 500). Filed follow-up #179 for the PDF bugs. No deviations from the task. `.replit` was edited via the workflow configuration tool (direct edits are blocked). Replit-Task-Id: 99673314-dd15-4442-8c7d-f375431719c0 |
||
|
|
11aaaf2abe |
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 (
|
||
|
|
5114b207da |
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. |
||
|
|
0b7b571a1e |
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.
|
||
|
|
72cd414208 |
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.
Replit-Task-Id: b7422abb-cc1b-4727-b70b-cde090f1a748
|
||
|
|
8df5e76d29 |
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. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 804c1330-3360-45df-814d-221ee0d46866 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/JyUisd3 Replit-Helium-Checkpoint-Created: true |
||
|
|
001e9023b2 |
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 |
||
|
|
8337c4b212 | Initial commit |