# Africa Alert PWA — Project Conventions

This document captures the load-bearing conventions for the Africa Alert PWA. Each rein's `agent.md` links here instead of inlining these rules. Keep this file short and operational.

## Tech stack (lock these in)

- **Frontend:** React 18 + Vite + TypeScript, `react-router-dom` v6, `axios`, `zustand`, `recharts`, `lucide-react`, `vite-plugin-pwa`.
- **Backend:** Node.js + Express, `better-sqlite3` (NOT `sqlite3`), `jsonwebtoken` + `bcryptjs` (NOT native `bcrypt`), `multer`, `@supabase/supabase-js` (used by SyncEngine).
- **DB:** SQLite, WAL mode, `foreign_keys = ON`.
- **Cloud:** Supabase REST API (`https://api.next_gen.techarvest.co.zw`).
- **Payments:** Paynow (Zimbabwe).
- **Deploy:** Docker Compose (multi-stage `Dockerfile`).

## Directory layout (don't deviate)

```
africa-alert-pwa/
├── client/
│   └── src/
│       ├── App.tsx                # router + ProtectedRoute + role-based getRoutes()
│       ├── main.tsx
│       ├── components/            # Nav, PaynowPayment, shared UI
│       ├── pages/                 # feature pages
│       │   ├── admin/             # admin-only pages
│       │   ├── teacher/           # teacher-only pages
│       │   ├── student/           # student-only pages
│       │   ├── parent/            # parent-only pages
│       │   ├── dashboard/         # role landing pages
│       │   └── exams/             # exam module (canonical reference)
│       └── store/                 # Zustand slices (auth, api, exams, …)
├── server/
│   └── src/
│       ├── index.js               # Express bootstrap, registers 12 controllers
│       ├── controllers/           # one file per resource
│       ├── services/
│       │   └── SyncEngine.js      # bidirectional SQLite ↔ Supabase
│       └── database/
│           └── init.js            # schema, idempotent
├── data/                          # SQLite file (gitignored)
├── uploads/                       # multer uploads (gitignored)
├── docker-compose.yml
├── Dockerfile                     # multi-stage
└── .harness/                      # team definition (committed)
    ├── agent.md                   # orchestrator
    ├── reins/                     # specialists
    ├── docs/                      # project conventions
    └── changelogs/                # daily change log
```

## SQL contract (every table must have these)

```sql
id INTEGER PRIMARY KEY AUTOINCREMENT,
uid TEXT UNIQUE,                              -- stable cross-system id
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_synced_at DATETIME,
sync_status TEXT DEFAULT 'pending' CHECK(sync_status IN ('synced', 'pending', 'conflict')),
is_deleted INTEGER DEFAULT 0
```

Adding a table is a two-step commit:
1. Add the `CREATE TABLE IF NOT EXISTS` block in `server/src/database/init.js` (backend-expert).
2. Append the table name to `tablesToSync` in `server/src/services/SyncEngine.js` (sync-expert) — in **dependency order** (parents before children).

## Controller template (canonical)

```js
const express = require('express');
const path = require('path');
const Database = require('better-sqlite3');
const db = new Database(process.env.DB_PATH || path.join(__dirname, '../../../data/school.db'));
db.pragma('foreign_keys = ON');

const router = express.Router();

// inline auth middleware (or use the one from server/src/index.js)
const auth = (req, res, next) => { /* … */ };

// CRUD handlers — all writes set sync_status = 'pending'
router.get('/', auth, (req, res) => { /* … */ });
router.post('/', auth, (req, res) => { /* … */ });
// …

module.exports = router;
```

Register in `server/src/index.js`:

```js
app.use('/api/<x>', require('./controllers/<x>.controller'));
```

Alphabetical order, please.

## Frontend store template (canonical)

```ts
// client/src/store/<x>.ts
import { create } from 'zustand';
import { api } from './api';

interface XStore {
  items: any[];
  currentItem: any | null;
  fetchItems: (filters?: any) => Promise<void>;
  createItem: (data: any) => Promise<any>;
  updateItem: (id: number, data: any) => Promise<void>;
  deleteItem: (id: number) => Promise<void>;
}

export const useXStore = create<XStore>((set, get) => ({
  items: [],
  currentItem: null,

  fetchItems: async (filters) => {
    const params = new URLSearchParams(filters || {});
    const { data } = await api.get(`/<x>?${params}`);
    set({ items: data });
  },
  createItem: async (data) => { /* … */ },
  updateItem: async (id, data) => { /* … */ },
  deleteItem: async (id) => { /* … */ },
}));
```

## Frontend page template (canonical) — verified 2026-06-12

A new page needs **five** coordinated edits, not just one file. Skipping any of
them is a defect. The frontend team has shipped this checklist twice (exams
module, then the 2026-06-12 attendance + front-office extension) — it works.

1. **Store** at `client/src/store/<x>.ts` (shape above).
2. **Page** at `client/src/pages/<role>/<Page>.tsx` (or cross-role under
   `client/src/pages/`). Controlled inputs with `useState`; no new form lib.
3. **Route** in `client/src/App.tsx` — add the `{ path, element, roles: [...] }`
   entry inside the correct role's `switch` arm of `getRoutes()`. `ProtectedRoute`
   is the role gate; do **not** check `user.role` inside the page.
4. **Nav entry** in `client/src/components/Nav.tsx` — add to the matching
   `NAV_CONFIG.<role>` array(s). Icons from `lucide-react`; no new icon lib.
5. **Changelog** entry in `.harness/changelogs/YYYY-MM-DD-<topic>.md` listing
   every new file + the roles that see each route. Commit it on the branch.

If a page is used by more than one role, extract the shared view into
`client/src/components/` and have each role's page be a thin wrapper that
loads the role-specific data source (e.g. teacher loads all students via
`/api/users?role=student`; parent loads linked children via
`/api/users/children`).

## Parallel multi-context store pattern (unified slice, context-routed)

When the same operation (take attendance, list records, …) has to hit
different backend endpoints depending on a runtime context, **don't** create
one slice per context. Create a single slice that takes a `context` enum
and routes internally:

```ts
type AttendanceContext = 'class' | 'hostel' | 'transport' | 'club';

const baseFor = (ctx: AttendanceContext): string =>
  ctx === 'class' ? '/attendance' : `/${ctx}-attendance`;

// fetchRoster / submitBulkAttendance / fetchHistory all switch on ctx
// and call api.get/post(`${baseFor(ctx)}/...`).
```

Reference: `client/src/store/attendance.ts` (2026-06-12, attendance + front-office
extension). Pair with the multi-tab page pattern (single page, top tabs that
swap the picker + roster source + submit endpoint).

## Dev port drift (corrected 2026-06-12)

`vite.config.ts` in this repo runs the dev server on port **3000**, not the
5173 mentioned earlier in this file. The API still lives on 3001. Update
any other doc or tool that still says 5173.

## Auth + RBAC

- JWT signed with `process.env.JWT_SECRET` (default for dev only, loud warning).
- 7-day expiry.
- Roles: `admin`, `teacher`, `student`, `parent`, `accountant`, `librarian`, `nurse`.
- Backend: every protected route goes through `auth` middleware; admin-only writes guard with `req.user.role !== 'admin'`.
- Frontend: route gating in `App.tsx` via `<ProtectedRoute allowedRoles={[…]}>` — do not gate inside pages.

## Dev commands

```bash
# Backend
cd server
npm install
npm run db:init          # runs server/src/database/init.js (idempotent)
npm run dev              # nodemon, port 3001

# Frontend
cd client
npm install
npm run dev              # Vite, port 5173 (proxies /api → localhost:3001)
npm run build            # production build → client/dist/
npm run preview          # serve built bundle

# Full stack (production-like)
docker-compose up --build
```

## Sync engine contract

- Singleton: `getSyncEngine()`.
- Default interval: 30s (`SYNC_INTERVAL` env var, in ms).
- Tables listed in `tablesToSync` (server/src/services/SyncEngine.js), in FK dependency order.
- Push: `sync_status = 'pending' AND is_deleted = 0`, limit 100/table.
- Delete: `is_deleted = 1 AND sync_status = 'pending'`, limit 50/table.
- Pull: `updated_at >= last_sync`.
- Conflict policy: server-wins, always.
- Offline mode: empty `SUPABASE_KEY` → cycle no-ops, app keeps working.
- HTTP helper: raw `http`/`https` (no `fetch`/`axios`), 10s timeout.

## Payments (Paynow)

- Endpoints: `POST /api/payments/initiate`, `GET /api/payments/status/:id`, `POST /api/payments/webhook`, `GET /api/payments/student/:id`, `DELETE /api/payments/:id`.
- Webhook handler is idempotent (Paynow retries).
- Payment + `student_fees.status` update wrapped in `db.transaction(() => { … })()`.
- Env vars: `PAYNOW_INTEGRATION_ID`, `PAYNOW_INTEGRATION_KEY`, `PAYNOW_RETURN_URL`, `PAYNOW_BLOCKING_URL`.

## Env vars (`.env.example` is the source of truth)

```env
PORT=3001
JWT_SECRET=change-me-in-production
DB_PATH=./data/school.db
SUPABASE_URL=https://api.next_gen.techarvest.co.zw
SUPABASE_KEY=
SYNC_INTERVAL=30000
PAYNOW_INTEGRATION_ID=
PAYNOW_INTEGRATION_KEY=
PAYNOW_RETURN_URL=http://localhost:3000/fees
PAYNOW_BLOCKING_URL=http://localhost:3000/api/payments/webhook
```

`.env` is gitignored. Never commit it.

## Roles to remember (the people, not the agents)

- **admin** — full access, user/department management, reports, settings.
- **teacher** — own classes, attendance, assignments, grades for assigned students.
- **student** — own courses, assignments, grades, attendance read.
- **parent** — own child's academic progress, attendance, messages, fees, calendar.
- **accountant / librarian / nurse** — role-specific scopes; consult schema for column-level access.
