8.9 KiB
AGENTS.md — Africa Alert PWA
Compact orientation for AI coding agents. Update this file when commands, structure, or load-bearing conventions change.
What this repo is
Offline-first PWA school-management system. React 18 + Vite + TypeScript client, Node 20 + Express + SQLite server, bidirectional sync to Supabase, Paynow (Zimbabwe) for fee payments. Deployed as a single Docker container.
Quickstart (local dev, two terminals)
# Terminal 1 — API on :3001
cd server
npm install
npm run db:init # one-off; creates server/data/school.db
npm run dev
# Terminal 2 — client on :3000 (proxies /api → :3001)
cd client
npm install
npm run dev
Login with admin@school.com / admin123. Other demo accounts (seeded by server/src/database/seeds/demo.js): teacher@school.com / teacher123, student@school.com / student123, parent@school.com / parent123, principal@school.com / principal123, bursar@school.com / bursar123, hr@school.com / hr123, librarian@school.com / librarian123.
For production: docker-compose up --build (single port 3001). The Dockerfile and docker-compose.yml are now a single deploy story — node:20-slim runtime with the React client served from Express, healthcheck against /api/health.
Repository layout (don't deviate)
client/src/
App.tsx router, ProtectedRoute, role-based getRoutes()
main.tsx
components/ shared UI (Nav, PaynowPayment)
pages/<role>/ role-specific pages
pages/dashboard/ role landing pages
store/ Zustand slices — use client/src/store/api.ts as the axios instance
server/src/
index.js Express bootstrap, registers controllers under /api/<x>
controllers/ one file per resource, inline auth middleware
services/ SyncEngine.js, AuditService.js
database/migrations/knex/ Knex migration files (source of truth; `init.js` just runs them)
data/ SQLite file (gitignored, WAL mode → .db-shm + .db-wal alongside)
uploads/ multer uploads (gitignored)
.harness/ team memory (AGENTS.md, docs/conventions.md, reins/, changelogs/)
There is no root package.json. Run npm install and scripts inside client/ and server/.
Hard rules
- All code changes go in a worktree at
.worktrees/<name>/, branchfeature/<kebab>orfix/<kebab>, branched fromdev. Never edit the main checkout. - Branch model is Git Flow. Work and merge on
dev. Promote tomainonly whendevis green and deployable. No direct commits tomain. - Single axios instance lives at
client/src/store/api.ts. Don't create a second one in a store or component. - All tables must have
uid TEXT UNIQUE,last_synced_at,sync_statusCHECK in ('synced','pending','conflict') DEFAULT 'pending',is_deleted INTEGER DEFAULT 0. Add toinit.jsand append the table name totablesToSyncinSyncEngine.js(in FK dependency order — parents first). - Route gating is done in
App.tsxvia<ProtectedRoute allowedRoles={…}>— do not gate inside page components. - Demo accounts are seeded in
server/src/database/init.js. Don't add new ones in a migration; updateinit.js. .envis gitignored. Never commit secrets, never paste.envcontents into chat. The repo's actual remote is Gitea (git.techarvest.co.zw), not GitHub.
Port allocation
Both stacks (tenant + SuperAdmin) can run simultaneously without collision. See PORTS.md for the canonical plan.
| Port | Service | Stack | Env var |
|---|---|---|---|
| 3000 | Tenant client (Vite) | tenant | VITE_PORT |
| 3001 | Tenant API (Express) | tenant | PORT |
| 3002 | SuperAdmin API (Express) | superadmin | SUPERADMIN_PORT |
| 3003 | SuperAdmin client (Vite) | superadmin | VITE_PORT (superadmin) |
Vite dev servers run with strictPort: true so a port already in use
fails fast instead of silently picking a different port.
Commands cheat sheet
| What | Command | Where |
|---|---|---|
| Install deps | npm install |
client/ and server/ separately |
| Dev server (client) | npm run dev |
client/ — Vite, port 3000, proxies /api → :3001 |
| Dev server (server) | npm run dev |
server/ — nodemon, port 3001 |
| Init / migrate DB | npm run db:init |
server/ — runs src/database/init.js |
| Production build | npm run build |
client/ — outputs client/dist/, generates the PWA service worker |
| Preview build | npm run preview |
client/ |
| Full stack (prod-like) | docker-compose up --build |
repo root |
| Stop stack | docker-compose down |
repo root |
| Lint / typecheck | not configured | n/a — see Open follow-ups |
| E2E tests (Playwright) | npm run test:e2e |
client/ — Playwright config + e2e/*.spec.ts; needs both client (3000) and server (3001) running; first run also: npx playwright install chromium |
npm run build (not npm run dev) is required to exercise the PWA service worker — vite-plugin-pwa only ships the SW in production builds.
Canonical reference for new features
The exams module is the vertical slice to mimic: server/src/controllers/exams.controller.js, client/src/store/exams.ts, client/src/pages/exams/, route + nav entry in App.tsx. See IMPLEMENTATION_SUMMARY.md for the design notes.
Things that will trip you up
- Hardcoded JWT secret
'africa-alert-secret-key-2024'is the fallback inserver/src/index.jsand duplicated across controllers. SetJWT_SECRETenv var in any non-dev environment. - CORS is wide open (
app.use(cors())inserver/src/index.js). Don't add more routes without a CORS review. outputs/andlogo/at the repo root are unused build artifacts — don't import from them. Real brand assets live inclient/public/.- Sync engine uses raw
http/https, notfetchoraxios. 10s timeout, 100 rows/table push, 50/table delete, server-wins conflict, 30s default interval.SUPABASE_KEYempty → sync no-ops, app keeps working. - Paynow integration in
server/src/controllers/payments.controller.js. Webhook is idempotent. Payment +student_fees.statusupdate wrapped indb.transaction(). - Auth is JWT only (7-day expiry). No session cookies, no refresh tokens. Logout is client-side only (clears Zustand store).
- Roles in active UI:
school_admin,systems_admin,principal,hr,bursar,teacher,student,parent,librarian,clubs_head. Schema also definesaccountantandnursebut those have no UI. index.jsexposesmodule.exports = appAND starts the server withapp.listen. Importing it in tests will bind a port — use a separate bootstrap or guard the listen call.
Where to read more
.harness/AGENTS.md— project-level memory (demo accounts, branch model, "exams" as canonical reference).
Team (2026-07-22)
- fchin (
fchinembiri24) — orchestrator / reviewer / merge owner. Owns Phase 1 (auxiliary roles + cohorts + class/teacher assignments). - Arthur (
PhaseOfficial/panashearthurmhonde@gmail.com) — 163 commits, heaviest contributor. Owns Phase 2 (analytics dashboard). - Craig (
CraigDataNerd/craig.chadiwa@students.uz.ac.zw) — 7 commits, lighter (newer or narrower scope). Owns Phase 3 (offboarding).
Phase 2 + Phase 3 rollout: decide after Phase 1 lands (deferred 2026-07-22).
Commit volume is signal, not strength. Roles/skills for Craig and Arthur are not on record yet; record them here once known.
.harness/docs/conventions.md— SQL contract, controller/store templates, auth+RBAC, sync contract, Paynow flow, env vars..harness/docs/testing.md— test stack and manual recipes (when set up)..harness/reins/— agent role definitions (backend-expert, frontend-expert, sync-expert, tester, etc.).IMPLEMENTATION_SUMMARY.md— modules and their tables; rationale for sync engine.DATABASE_SCHEMA.md— full schema reference.SCREENS.md— UI map and role/permission matrix (canonical for screen-level access).GEMINI.md— high-level overview (older, points here for details).
Open follow-ups (do not silently take these on — ask first)
- No test framework is installed or configured anywhere. The
testerrein owns adding Vitest/Jest/Playwright. - No CI — no GitHub Actions, no Gitea Actions workflow. Add when the user asks.
- Schema lives in Knex migrations under
server/src/database/migrations/knex/(since Track D, P1-9).npm run db:initcallsrunMigrations()→knex.migrate.latest(). New tables: drop a new file in that folder with the next timestamp. No ALTER-in-init.js — the migration is the source of truth. - No
lintortypecheckscript in eitherpackage.json.tsconfig.jsonhas"strict": false— the client typechecks leniently. - JWT secret, CORS, request-size limit (
express.json({ limit: '50mb' })) are all loose. Don't relax further; tighten when touching the area.