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