# System Patterns ## Architecture Single-container Express app serves both the React SPA and the REST API. MongoDB runs in a separate container on an internal Docker network. ``` Browser └─► Caddy (caddy_network) └─► deafmissoulaorg-app-1:801 ├─ GET /api/* → Express routes ├─ GET /uploads/* → multer upload volume ├─ GET /photos/*, /videos/*, etc. → client/build/ static assets └─ GET * → client/build/index.html (SPA catch-all) └─► deafmissoulaorg-mongodb-1:27017 (internal network only) ``` ## Data Flow All frontend data is fetched from the API at runtime — nothing is hardcoded in components. Each component does a `useEffect` → `fetch('/api/...')` → `useState` pattern. ## File Organization ``` client/src/components/ ├── Manage.tsx # /manage dashboard (auth + all admin tabs) ├── App.tsx # Routing: /manage/* outside Header/Footer ├── Home.tsx # Landing page (fetches events) ├── Calendar.tsx # Events calendar (fetches events) ├── Meet-Board.tsx # Board member profiles (fetches /api/board) ├── Gallery.tsx # Photo gallery (fetches /api/gallery) ├── VideoGallery.tsx # Video gallery (hardcoded paths in client/public/videos/) ├── Minutes.tsx # Meeting minutes table (fetches /api/minutes) ├── Sponsors.tsx # Sponsors (fetches /api/sponsors) ├── Bylaws.tsx # Static PDF links (hardcoded paths in client/public/) └── Resources.tsx # Montana/National Deaf resources directory (static) server/src/ ├── models/ # Mongoose schemas │ ├── BoardMember.ts # Includes auth fields (password, mustChangePassword, resetToken) │ ├── Event.ts │ ├── GalleryItem.ts │ ├── Minute.ts # Has 'order' field for drag-and-drop sorting │ ├── Sponsor.ts │ ├── Member.ts │ └── User.ts # Admin-only account (not BoardMember) ├── routes/ │ ├── auth.ts # Login, forgot/reset password, change password │ ├── board.ts # CRUD + reorder + welcome email on create │ ├── minutes.ts # CRUD + reorder (new entries auto-insert at top) │ ├── events.ts │ ├── gallery.ts │ ├── sponsors.ts │ ├── members.ts │ └── email.ts # Contact form ├── middleware/ │ ├── auth.ts # JWT verification, exports AuthRequest │ ├── upload.ts # Multer config for headshots/photos/sponsors/minutes │ └── errorHandler.ts # ZodError → 400, others → 500 ├── db.ts # mongoose.connect() ├── index.ts # Express app setup, static serving, route mounting └── seed.ts # One-time data population (idempotent — skips if data exists) ``` ## Dashboard (`/manage`) Patterns - **Route security:** `/manage` is not linked from public nav (obscurity) + JWT required for all write operations - **Login:** Checks BoardMember by email first, then User (admin fallback) - **Drag-and-drop:** `useDragReorder` custom hook using HTML5 drag API + `useRef` for drag index tracking; calls `PATCH /api/[resource]/reorder` with `{ ids: string[] }` on drop - **Image preview:** `URL.createObjectURL(file)` in `ImagePicker` component before upload - **Forced password change:** `mustChangePassword: true` → server issues `tempToken` (1-hour) → client shows change-password form before dashboard loads ## Deployment Pattern 1. Code changes pushed to Gitea (`git push origin main`) 2. SSH to production server (`ssh chaulmark@10.4.0.205`) 3. `git pull origin main && docker compose down && docker compose up -d --build` No Portainer. No webhooks. Manual pull + rebuild. ## Rollback Pattern `v1` tag in Git points to the last hardcoded version (no database): ```bash git checkout v1 docker compose down docker compose up -d --build ```