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.
Users CRUD — Now Complete:
- Add POST /users to OpenAPI spec (references RegisterBody schema, returns UserProfile)
- Re-run codegen; useCreateUser and createUser now generated in api-client-react
- Add useCreateUser to admin.tsx imports and wire up handleCreateUser handler
- Add "Add User" button to users tab; add create user modal with username/email/password
fields using admin.username, admin.email, admin.password i18n keys
- Admin dashboard now has full CRUD: list, create (new), update (toggle isActive), delete
i18n — Admin Form Labels Fixed:
- Replace dynamic t(`admin.${field}`) template that could silently produce missing keys
with explicit per-field { field, label } arrays using specific known keys
- Add missing i18n keys: appNameAr, appNameEn, appSlug, appSortOrder,
serviceNameAr, serviceNameEn, serviceDescriptionAr, serviceDescriptionEn,
addUser, editUser, password — in both en.json and ar.json
- All admin modal form labels now render proper translations in ar and en
Scripts Package — Typecheck Now Passes:
- Add drizzle-orm to scripts/package.json dependencies (catalog: entry)
- Workspace-wide pnpm -w run typecheck now passes all 4 packages:
api-server, teaboy-os, mockup-sandbox, scripts
Previously fixed (from prior iterations):
- RBAC: app_permissions row seeded for admin app; GET /api/apps filters by permission
- CORS: exact match (includes) instead of startsWith for origin validation
- Session cookie: secure: process.env.NODE_ENV === "production"
- Socket.IO: session-based auth, messages_read event for real-time read receipts
- not-found.tsx: uses t("notFound.*") keys, no hardcoded English
- login/register: use t("common.appName"), no hardcoded "TeaBoy OS"
- @replit comments removed from badge.tsx and button.tsx
- Admin redirect uses useEffect (rules of hooks compliance)
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