a5a35ef217
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
303 lines
13 KiB
Markdown
303 lines
13 KiB
Markdown
# Tx OS
|
|
|
|
A bilingual (Arabic / English) internal "office OS" web platform. Single-tenant,
|
|
self-hosted, designed to run on any Linux server (cloud or on-prem) behind a
|
|
TLS-terminating reverse proxy.
|
|
|
|
The repo is a pnpm monorepo containing:
|
|
|
|
| Package | Purpose |
|
|
| ------------------------------------ | -------------------------------------------------- |
|
|
| `artifacts/api-server` | Express 5 + Socket.IO + Drizzle ORM API |
|
|
| `artifacts/tx-os` | React 19 + Vite SPA (the user-facing app) |
|
|
| `artifacts/mockup-sandbox` | Internal component preview server (dev-only) |
|
|
| `lib/db` | Drizzle schema + migrations |
|
|
| `lib/api-zod` + `lib/api-client-react` | OpenAPI-generated Zod schemas + React Query hooks |
|
|
| `scripts` | DB seed + maintenance scripts |
|
|
|
|
---
|
|
|
|
## Features
|
|
|
|
- **Glassmorphism OS UI** with animated gradient backgrounds and an app grid / dock.
|
|
- **Bilingual** (RTL Arabic + LTR English) with per-user persisted locale.
|
|
- **Session auth** (`express-session` + Postgres-backed `connect-pg-simple`,
|
|
bcrypt password hashing) with full RBAC (admin / user roles + role-permission
|
|
matrix + group-derived permissions).
|
|
- **Real-time** chat / notifications / executive-meeting alerts via Socket.IO.
|
|
- **Executive Meetings module** — scheduling, change requests, approvals,
|
|
optimistic locking on postpones, audit log, and a Playwright-rendered HTML
|
|
PDF export.
|
|
- **S3-compatible object storage** with signed-URL uploads (production: MinIO;
|
|
local dev: built-in filesystem driver — no extra services required).
|
|
|
|
---
|
|
|
|
## Quick start
|
|
|
|
Two ready-made install scripts cover the common cases — pick the one that
|
|
matches your machine. Both are idempotent (safe to re-run) and never delete
|
|
existing data.
|
|
|
|
### Option A — One-shot Docker install (recommended)
|
|
|
|
For any Linux server with Docker + Docker Compose. Boots the full stack
|
|
(Postgres, MinIO, API, SPA, Caddy edge) with a single command:
|
|
|
|
```bash
|
|
git clone <this-repo>
|
|
cd tx-os
|
|
./start.sh
|
|
```
|
|
|
|
`start.sh` will:
|
|
|
|
1. Copy `.env.docker.example` → `.env` on first run (and pause so you can
|
|
edit secrets — at minimum `SESSION_SECRET`, `POSTGRES_PASSWORD`).
|
|
2. Build the images, bring up the database + object storage, run migrations,
|
|
then start the API and web containers behind Caddy.
|
|
3. Print the URLs you can reach the system on.
|
|
|
|
After install, open the printed URL and the **first-time setup wizard** will
|
|
let you create the admin account in the browser. (You can also pre-seed an
|
|
admin by setting `SEED_ADMIN_PASSWORD` in `.env` before running.)
|
|
|
|
### Option B — Local development with HTTPS (`mkcert`)
|
|
|
|
For developers running the stack on their own laptop with trusted local
|
|
certificates (so the SPA can talk to a real `https://tx.local`):
|
|
|
|
```bash
|
|
git clone <this-repo>
|
|
cd tx-os
|
|
./scripts/local-setup.sh
|
|
```
|
|
|
|
`local-setup.sh` will:
|
|
|
|
1. Detect your OS / package manager and tell you how to install
|
|
[`mkcert`](https://github.com/FiloSottile/mkcert) if it's missing.
|
|
2. Prompt for a local domain (default `tx.local`) and detect your LAN IP
|
|
(so phones / tablets on the same Wi-Fi can reach the system too).
|
|
3. Generate a trusted local TLS certificate covering `localhost`, the
|
|
chosen domain, and the LAN IP.
|
|
4. Bootstrap `.env` from `.env.example` (preserving any keys you've already
|
|
set), then bring the Docker stack up.
|
|
|
|
### Manual Docker install (advanced)
|
|
|
|
If you'd rather drive `docker compose` yourself:
|
|
|
|
```bash
|
|
git clone <this-repo>
|
|
cd tx-os
|
|
cp .env.example .env
|
|
# Edit .env — at minimum change SESSION_SECRET, POSTGRES_PASSWORD,
|
|
# S3_SECRET_ACCESS_KEY. SEED_ADMIN_PASSWORD / SEED_USER_PASSWORD are
|
|
# OPTIONAL — leave them blank to use the in-browser setup wizard.
|
|
$EDITOR .env
|
|
|
|
docker compose build
|
|
docker compose up -d db minio minio-init
|
|
docker compose run --rm migrate # one-shot: pnpm run migrate (db push + seed)
|
|
docker compose up -d api web
|
|
```
|
|
|
|
After this, the running stack exposes:
|
|
|
|
| Service | Default host port | URL |
|
|
| ---------------- | ----------------- | ------------------------------ |
|
|
| SPA (`web`) | `3000` | `http://<host>:3000/` |
|
|
| API (`api`) | `8080` | `http://<host>:8080/api/healthz` |
|
|
| MinIO console | not exposed | (proxy to `:9001` if you need it) |
|
|
| `mockup-sandbox` | `8081` | dev profile only — `docker compose --profile dev up -d mockup-sandbox` then `http://<host>:8081/__mockup` |
|
|
|
|
Front the SPA (port `WEB_PORT`, default `3000`) with Caddy / Nginx / Traefik
|
|
for TLS and HTTP/2; bind the API port (`API_PORT`, default `8080`) to
|
|
`127.0.0.1` in production so the edge proxy is the sole external entry.
|
|
|
|
### Reverse proxy / Tailscale / external URL
|
|
|
|
If you front Tx OS with a TLS-terminating proxy that you don't control
|
|
(Tailscale `serve`, ngrok, Cloudflare Tunnel, …), two things matter:
|
|
|
|
1. **Add the public URL to `ALLOWED_ORIGINS` in `.env`.** If you skip
|
|
this, the browser blocks every API call from the SPA with a CORS
|
|
error and the app appears frozen on the login screen.
|
|
|
|
```bash
|
|
ALLOWED_ORIGINS=http://localhost:3000,https://it-demo.tail70b2bc.ts.net
|
|
PUBLIC_BASE_URL=https://it-demo.tail70b2bc.ts.net
|
|
```
|
|
|
|
2. **Set `TRUST_PROXY_HTTPS=true` in `.env`.** Tunnels like Tailscale
|
|
serve forward HTTPS traffic to the upstream as plain HTTP without
|
|
sending an `X-Forwarded-Proto: https` header. Without this flag,
|
|
`req.secure` is false, the session cookie is silently dropped, and
|
|
the user is bounced back to the login screen on every refresh.
|
|
|
|
> **Security note**: only enable `TRUST_PROXY_HTTPS=true` when the
|
|
> API container/port is **not** reachable directly over plain HTTP
|
|
> from outside (i.e. only the TLS edge proxy can hit it). The
|
|
> default Docker compose only exposes the SPA port (`3000`) to the
|
|
> host and keeps the API on the internal `tx-net` network, which
|
|
> satisfies this requirement. If you change the compose file to
|
|
> expose the API port publicly, do not enable this flag.
|
|
|
|
After editing `.env`, restart the stack:
|
|
|
|
```bash
|
|
docker compose up -d
|
|
```
|
|
|
|
You should be able to log in via the public HTTPS URL with no other
|
|
changes.
|
|
|
|
### Default seeded accounts
|
|
|
|
| Username | Password | Role |
|
|
| -------- | ---------------------------------- | ----- |
|
|
| `admin` | value of `SEED_ADMIN_PASSWORD` | admin |
|
|
| `ahmed` | value of `SEED_USER_PASSWORD` | user |
|
|
|
|
Both are seeded by `docker compose run --rm migrate` and **only** if they don't
|
|
already exist (the seed is idempotent on conflict).
|
|
|
|
### Common compose commands
|
|
|
|
```bash
|
|
docker compose logs -f api # tail API logs
|
|
docker compose exec db psql -U tx tx_os # psql shell
|
|
docker compose run --rm migrate # re-run migrations (safe to repeat)
|
|
docker compose down # stop everything (data persists)
|
|
docker compose down -v # stop AND wipe volumes (DANGER)
|
|
```
|
|
|
|
---
|
|
|
|
## Local development (without Docker)
|
|
|
|
You can run the stack natively if you have Node 24+, pnpm 10, and Postgres 16.
|
|
|
|
```bash
|
|
pnpm install
|
|
createdb tx_os
|
|
export DATABASE_URL=postgres://localhost/tx_os
|
|
export PORT=8080 BASE_PATH=/ ALLOWED_ORIGINS=http://localhost:25785
|
|
export SESSION_SECRET=$(openssl rand -hex 32)
|
|
export PRIVATE_OBJECT_DIR=/local/private
|
|
export PUBLIC_OBJECT_SEARCH_PATHS=/local/public
|
|
# Note: with no S3_ENDPOINT set, the API falls back to the local-FS storage
|
|
# driver and persists uploads under ./storage/. Safe for development only.
|
|
|
|
pnpm --filter db run push # apply schema
|
|
pnpm --filter scripts run seed # seed admin/ahmed accounts
|
|
pnpm --filter @workspace/api-server dev
|
|
# In another terminal:
|
|
PORT=25785 BASE_PATH=/ pnpm --filter @workspace/tx-os dev
|
|
```
|
|
|
|
---
|
|
|
|
## Configuration reference
|
|
|
|
Every option is read from environment variables. See `.env.example` for the
|
|
complete list with comments. Highlights:
|
|
|
|
| Variable | Purpose |
|
|
| --------------------------------- | ---------------------------------------------------------------------- |
|
|
| `DATABASE_URL` | Postgres connection string. **Required.** |
|
|
| `SESSION_SECRET` | HMAC key for session cookies + local-driver upload tokens. **Required in production.** |
|
|
| `ALLOWED_ORIGINS` | Comma-separated CORS allow-list. MUST list every URL the SPA is reached at (LAN IP, Tailscale name, custom domain, ...). |
|
|
| `TRUST_PROXY_HTTPS` | `true` when fronted by a TLS-terminating proxy that doesn't forward `X-Forwarded-Proto` (Tailscale serve, ngrok free, some Cloudflare Tunnels). Required for the session cookie to persist on those setups. |
|
|
| `SEED_DEMO_MEETINGS` | `true` to populate Executive Meetings with a day of demo data on first boot. Default off so real installs start empty. |
|
|
| `STORAGE_DRIVER` | `s3` or `local`. Defaults to `s3` if `S3_ENDPOINT` set, else `local`. |
|
|
| `S3_ENDPOINT` etc. | MinIO / S3 connection. Required when `STORAGE_DRIVER=s3`. |
|
|
| `PRIVATE_OBJECT_DIR` | Path inside the bucket for private uploads, e.g. `/tx-private/private`.|
|
|
| `PUBLIC_OBJECT_SEARCH_PATHS` | Comma-separated bucket paths searched by `GET /storage/public-objects/*`. |
|
|
| `LOCAL_STORAGE_ROOT` | Filesystem root used by the local driver. Defaults to `./storage`. |
|
|
| `SEED_ADMIN_PASSWORD` / `SEED_USER_PASSWORD` | Required by the seed script in production. |
|
|
| `SMTP_*` | Optional outbound mail config for Executive Meetings notifications. |
|
|
| `LOG_LEVEL` | pino log level. Defaults to `info`. |
|
|
|
|
### Storage drivers
|
|
|
|
The API server has two object-storage backends, selected automatically:
|
|
|
|
- **`s3`** (production): targets any S3-compatible endpoint (MinIO, AWS S3, R2,
|
|
Backblaze B2, ...) via `@aws-sdk/client-s3` + presigned PUT URLs.
|
|
- **`local`** (dev fallback, default when `S3_ENDPOINT` is unset): persists
|
|
files under `LOCAL_STORAGE_ROOT` and issues HMAC-signed upload URLs that
|
|
the API server validates and accepts on `PUT /api/storage/_local/upload`.
|
|
Single-host only; not suitable for production.
|
|
|
|
The route layer is unaware of which driver is active — both expose the same
|
|
`StoredObject` interface in `lib/objectStorage.ts`.
|
|
|
|
---
|
|
|
|
## Tests
|
|
|
|
```bash
|
|
pnpm test # full suite, auto-boots API
|
|
pnpm --filter @workspace/api-server test # api-server tests only
|
|
pnpm --filter @workspace/tx-os test:e2e # Playwright UI tests
|
|
```
|
|
|
|
Both `pnpm test` and `pnpm --filter @workspace/api-server test` go through
|
|
`artifacts/api-server/scripts/with-server.mjs`, which builds the API server,
|
|
spawns it on `PORT` (default `8080`), waits for `/api/healthz`, runs the
|
|
wrapped command with `TEST_API_BASE` exported, and tears the server down on
|
|
exit (including `Ctrl+C` and failures). If startup fails the helper prints
|
|
the tail of the server's stderr/stdout so the root cause is visible.
|
|
|
|
To run the tests against an already-running API server (e.g. the dev
|
|
workflow), set `TEST_API_BASE` before invoking the script — the helper will
|
|
skip spawning and use that URL instead:
|
|
|
|
```bash
|
|
TEST_API_BASE=http://localhost:8080 pnpm --filter @workspace/api-server test
|
|
```
|
|
|
|
Tests use the same database specified in `DATABASE_URL` and clean up after
|
|
themselves with `LIKE`-prefixed fixture rows. The `test:wait` script remains
|
|
available for anyone who only wants the bare health-check wait behaviour.
|
|
|
|
---
|
|
|
|
## Production checklist
|
|
|
|
Before fronting Tx OS with a public domain:
|
|
|
|
1. **Set every secret in `.env`.** No defaults in production.
|
|
2. **Run behind TLS.** Caddy / Nginx / Traefik should terminate HTTPS and
|
|
forward to `web` on `${WEB_PORT}`. The API server trusts `X-Forwarded-For`
|
|
from the first proxy hop (`app.set("trust proxy", 1)`).
|
|
3. **Restrict the API port.** The `api` service should never be exposed
|
|
directly to the internet — only `web` is intended to be reachable.
|
|
4. **Back up the volumes.** `db_data` and `minio_data` are the only stateful
|
|
surfaces. Snapshot them on a schedule.
|
|
5. **Rotate seeded passwords.** The first thing an admin should do is change
|
|
the seeded `admin` and `ahmed` passwords from the user-management screen.
|
|
6. **Review `threat_model.md`.** Document-level threat model lives in the repo
|
|
root and lists the trust boundaries this deployment relies on.
|
|
|
|
---
|
|
|
|
## Project conventions
|
|
|
|
- **Package manager**: pnpm 10. The repo refuses to install with npm/yarn.
|
|
- **Node**: 24 LTS. Older Nodes will not run the API bundle.
|
|
- **TypeScript**: 5.9, strict mode, project-references build (`pnpm typecheck`).
|
|
- **API contracts**: hand-written Zod schemas in `lib/api-zod` are the source
|
|
of truth; the React-Query client in `lib/api-client-react` is generated
|
|
from the same OpenAPI spec via Orval.
|
|
- **Migrations**: Drizzle Kit. `pnpm --filter db run push-force` for dev,
|
|
`pnpm --filter db run push` for production.
|
|
|
|
---
|
|
|
|
## License
|
|
|
|
MIT.
|