- Add state management for heats (myHeats, userHeats Map, showHeatsBanner, hideMyHeats, showHeatsModal) - Load user's heats and all users' heats on component mount - Display HeatsBanner when user has no heats declared - Add "Edit Heats" button in header for users with declared heats - Add modal for editing heats via HeatsBanner component - Display heat badges under usernames in sidebar (format: J&J NOV 1 L) - Show max 3 badges per user, with "+N" indicator for more - Add filter checkbox to hide users from same heats - Implement filter logic (hide if ANY heat matches: division + competition_type + heat_number) - Disable UserPlus (match) button for users without declared heats - Add Socket.IO heats_updated listener for real-time updates - Update todo list to mark EventChatPage integration as completed
531 lines
18 KiB
Markdown
531 lines
18 KiB
Markdown
# TODO - spotlight.cam
|
|
|
|
**Active tasks and roadmap - optimized for quick reference**
|
|
|
|
---
|
|
|
|
## 🎯 CURRENT STATUS
|
|
|
|
**Phase:** 1.6 (Competition Heats System) - ⏳ IN PROGRESS
|
|
**Previous Phase:** 1.5 (Email & WSDC & Profiles & Security & QR Check-in) - ✅ COMPLETED
|
|
**Next Phase:** 2 (Core Features - Matches API + Ratings + WebRTC) - ⏳ PENDING
|
|
**Progress:** ~68% complete
|
|
|
|
### ✅ Completed
|
|
- Phase 0: Frontend mockup with all views
|
|
- Phase 1: Backend Foundation
|
|
- Node.js + Express API
|
|
- PostgreSQL database with Prisma ORM
|
|
- JWT authentication (register, login)
|
|
- Socket.IO real-time chat (event & match rooms)
|
|
- Comprehensive test coverage (81%+)
|
|
- Phase 1.5: Email & WSDC & Profiles & Security
|
|
- Email verification (AWS SES with link + PIN)
|
|
- Password reset workflow
|
|
- WSDC API integration (auto-fill registration)
|
|
- User profiles (social media links, location)
|
|
- Public profiles (/{username})
|
|
- Event participation tracking (auto-save joined events)
|
|
- Event security (unique slugs, prevent ID enumeration)
|
|
- **QR code event check-in system** (physical presence required at venue)
|
|
|
|
### ⏳ Next Priority
|
|
**Core Features** - Matches API + Ratings + WebRTC Signaling
|
|
|
|
**See:** `docs/COMPLETED.md` for full list of completed tasks
|
|
|
|
---
|
|
|
|
## 📌 Phase 1: Backend Foundation - ✅ COMPLETED
|
|
|
|
**Completed:** 2025-11-12
|
|
**Time Spent:** ~14 hours
|
|
|
|
### ✅ Step 1: Backend Setup (COMPLETED)
|
|
- [x] Add `backend` service to docker-compose.yml
|
|
- [x] Initialize Node.js + Express project
|
|
- [x] Create folder structure (routes, controllers, middleware, etc.)
|
|
- [x] Add healthcheck endpoint: `GET /api/health`
|
|
- [x] Update nginx config to proxy `/api/*` to backend
|
|
- [x] Unit tests (7 tests passing)
|
|
|
|
### ✅ Step 2: PostgreSQL Setup (COMPLETED)
|
|
- [x] Add `db` service (PostgreSQL 15) to docker-compose.yml
|
|
- [x] Configure volumes for data persistence
|
|
- [x] Prisma ORM implementation
|
|
- [x] Create database schema (6 tables with relations)
|
|
- [x] Add indexes for performance
|
|
- [x] Create seed data (3 events, 2 users, chat rooms)
|
|
- [x] Fix OpenSSL compatibility issue for Prisma
|
|
|
|
### ✅ Step 3: Authentication API (COMPLETED)
|
|
- [x] Install dependencies: bcrypt, jsonwebtoken, express-validator
|
|
- [x] Implement password hashing with bcrypt (10 salt rounds)
|
|
- [x] Implement JWT token generation and verification
|
|
- [x] Create endpoints: register, login, /users/me
|
|
- [x] Create auth middleware for protected routes
|
|
- [x] Update frontend AuthContext to use real API
|
|
- [x] Unit tests (30 tests, 78%+ coverage)
|
|
|
|
### ✅ Step 4: WebSocket Chat (COMPLETED)
|
|
- [x] Install Socket.IO 4.8.1 on backend
|
|
- [x] Setup Socket.IO server with Express integration
|
|
- [x] JWT authentication for socket connections
|
|
- [x] Event rooms (join/leave/messages/active users)
|
|
- [x] Match rooms (private 1:1 chat)
|
|
- [x] Install socket.io-client on frontend
|
|
- [x] Update EventChatPage with real-time messaging
|
|
- [x] Update MatchChatPage with real-time chat
|
|
- [x] Unit tests (12 tests, 89% coverage for Socket.IO module)
|
|
|
|
---
|
|
|
|
## 📌 Phase 1.6: Competition Heats System - ⏳ IN PROGRESS
|
|
|
|
**Estimated Time:** 6-8 hours
|
|
**Priority:** HIGH (blocking for proper matchmaking)
|
|
**Status:** Design phase completed, ready for implementation
|
|
|
|
### Business Logic Summary
|
|
- Users must declare their competition heats before matchmaking
|
|
- One user can compete in multiple divisions/heats (e.g., J&J Novice + Strictly Advanced)
|
|
- **Constraint:** Cannot compete in same role in same division+competition type (e.g., cannot have "J&J Novice Leader" twice)
|
|
- Role is optional (can be NULL = undeclared)
|
|
- Heat numbers: 1-9
|
|
- Format example: "J&J NOV 1 L" (Jack & Jill, Novice, Heat 1, Leader)
|
|
|
|
### Step 1: Database Schema (1-2h) ⏳
|
|
- [ ] Create migration for 3 new tables:
|
|
- `divisions` - Pre-defined competition divisions
|
|
- Columns: id, name (varchar), abbreviation (varchar 3), display_order (int)
|
|
- Seed data: Newcomer (NEW), Novice (NOV), Intermediate (INT), Advanced (ADV), All-Star (ALL), Champion (CHA)
|
|
- `competition_types` - Pre-defined competition types
|
|
- Columns: id, name (varchar), abbreviation (varchar 3)
|
|
- Seed data: Jack & Jill (J&J), Strictly (STR)
|
|
- `event_user_heats` - User's declared heats for event
|
|
- Columns: id, user_id, event_id, division_id, competition_type_id, heat_number (1-9), role (enum: Leader/Follower/NULL), created_at, updated_at
|
|
- **UNIQUE constraint:** (user_id, event_id, division_id, competition_type_id, role)
|
|
- Foreign keys: user_id → users.id, event_id → events.id, division_id → divisions.id, competition_type_id → competition_types.id
|
|
- Indexes: (user_id, event_id), (event_id)
|
|
- [ ] Update Prisma schema
|
|
- [ ] Run migration
|
|
- [ ] Verify seed data
|
|
|
|
### Step 2: Backend API (2-3h) ⏳
|
|
- [ ] Create routes and controllers:
|
|
- `GET /api/divisions` - List all divisions (public)
|
|
- `GET /api/competition-types` - List all competition types (public)
|
|
- `POST /api/events/:slug/heats` - Add/update user's heats (authenticated)
|
|
- Input: array of { divisionId, competitionTypeId, heatNumber, role? }
|
|
- Validation: unique constraint, heat number 1-9
|
|
- Replace all existing heats (upsert pattern)
|
|
- `GET /api/events/:slug/heats/me` - Get current user's heats (authenticated)
|
|
- `GET /api/events/:slug/heats/all` - Get all users' heats for sidebar (authenticated)
|
|
- Returns: userId, username, avatar, heats[]
|
|
- `DELETE /api/events/:slug/heats/:id` - Delete specific heat (authenticated)
|
|
- [ ] Validation middleware:
|
|
- Heat number 1-9
|
|
- Role enum (Leader/Follower/NULL)
|
|
- Unique constraint enforcement
|
|
- User must be event participant
|
|
- [ ] Unit tests (CRUD operations, validation, constraints)
|
|
|
|
### Step 3: Socket.IO Events (0.5h) ⏳
|
|
- [ ] Add event: `heats_updated` - Broadcast when user updates heats
|
|
- Payload: { userId, username, heats[] }
|
|
- Send to all users in event room
|
|
- [ ] Update active_users event to include heats data
|
|
|
|
### Step 4: Frontend Components (2-3h) ⏳
|
|
- [x] Create HeatsBanner component (sticky between header and chat) - ✅ DONE
|
|
- [x] Show only if user has no heats declared
|
|
- [x] Form with dynamic heat entries (add/remove)
|
|
- [x] Fields per entry: Competition Type (select), Division (select), Heat Number (1-9), Role (optional: Leader/Follower)
|
|
- [x] "Save Heats" button → POST /api/events/:slug/heats
|
|
- [x] On save success: hide banner, show success message
|
|
- [ ] Add "Edit Heats" button in EventChatPage header (next to "Leave Event") - ⏳ TODO
|
|
- Opens modal with same form as banner
|
|
- Pre-fill with existing heats
|
|
- "Update Heats" button
|
|
- [ ] Update EventChatPage sidebar (Active Users) - ⏳ TODO
|
|
- Display heat badges under username
|
|
- Format: "J&J NOV 1 L", "STR ADV 3" (no role if NULL)
|
|
- Max 3 visible badges, "+" indicator if more
|
|
- Add checkbox: "Hide users from my heats"
|
|
- Logic: Hide users with ANY matching (division + competition_type + heat_number)
|
|
- Disable UserPlus icon if user has no heats declared
|
|
- [x] Create frontend API methods in services/api.js - ✅ DONE
|
|
- [x] divisionsAPI.getAll()
|
|
- [x] competitionTypesAPI.getAll()
|
|
- [x] heatsAPI.saveHeats(slug, heats[])
|
|
- [x] heatsAPI.getMyHeats(slug)
|
|
- [x] heatsAPI.getAllHeats(slug)
|
|
- [x] heatsAPI.deleteHeat(slug, heatId)
|
|
- [ ] Socket.IO integration - ⏳ TODO
|
|
- Listen to `heats_updated` event
|
|
- Update active users list in real-time
|
|
|
|
### Step 4.1: EventChatPage Integration - ⏳ IN PROGRESS (Remaining work)
|
|
|
|
**What needs to be done:**
|
|
|
|
1. **Add state management for heats:**
|
|
```javascript
|
|
const [myHeats, setMyHeats] = useState([]);
|
|
const [userHeats, setUserHeats] = useState(new Map()); // userId → heats[]
|
|
const [showHeatsBanner, setShowHeatsBanner] = useState(false);
|
|
const [hideMyHeats, setHideMyHeats] = useState(false);
|
|
const [showHeatsModal, setShowHeatsModal] = useState(false);
|
|
```
|
|
|
|
2. **Load heats on component mount:**
|
|
```javascript
|
|
useEffect(() => {
|
|
const loadHeats = async () => {
|
|
const [myHeatsData, allHeatsData] = await Promise.all([
|
|
heatsAPI.getMyHeats(slug),
|
|
heatsAPI.getAllHeats(slug),
|
|
]);
|
|
setMyHeats(myHeatsData);
|
|
setShowHeatsBanner(myHeatsData.length === 0);
|
|
|
|
// Map userHeats
|
|
const heatsMap = new Map();
|
|
allHeatsData.forEach(userHeat => {
|
|
heatsMap.set(userHeat.userId, userHeat.heats);
|
|
});
|
|
setUserHeats(heatsMap);
|
|
};
|
|
loadHeats();
|
|
}, [slug]);
|
|
```
|
|
|
|
3. **Add HeatsBanner before chat:**
|
|
```jsx
|
|
{showHeatsBanner && (
|
|
<HeatsBanner
|
|
slug={slug}
|
|
onSave={() => {
|
|
setShowHeatsBanner(false);
|
|
// Reload heats
|
|
}}
|
|
/>
|
|
)}
|
|
```
|
|
|
|
4. **Add "Edit Heats" button in header (next to "Leave Event"):**
|
|
```jsx
|
|
<button
|
|
onClick={() => setShowHeatsModal(true)}
|
|
className="flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
|
|
>
|
|
<Edit size={16} />
|
|
Edit Heats
|
|
</button>
|
|
```
|
|
|
|
5. **Create modal for editing heats (reuse HeatsBanner logic)**
|
|
|
|
6. **Add heat badges to sidebar under username:**
|
|
```jsx
|
|
{activeUsers.map(activeUser => {
|
|
const userHeatsForThisUser = userHeats.get(activeUser.userId) || [];
|
|
const hasHeats = userHeatsForThisUser.length > 0;
|
|
|
|
return (
|
|
<div key={activeUser.userId}>
|
|
<div className="flex items-center">
|
|
<img src={activeUser.avatar} />
|
|
<span>{activeUser.username}</span>
|
|
</div>
|
|
|
|
{/* Heat badges */}
|
|
<div className="flex flex-wrap gap-1 mt-1">
|
|
{userHeatsForThisUser.slice(0, 3).map(heat => (
|
|
<span key={heat.id} className="text-xs px-2 py-0.5 bg-blue-100 text-blue-700 rounded">
|
|
{heat.competitionType.abbreviation} {heat.division.abbreviation} {heat.heatNumber}
|
|
{heat.role && ` ${heat.role[0]}`}
|
|
</span>
|
|
))}
|
|
{userHeatsForThisUser.length > 3 && (
|
|
<span className="text-xs px-2 py-0.5 bg-gray-100 text-gray-600 rounded">
|
|
+{userHeatsForThisUser.length - 3}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* UserPlus button - disabled if no heats */}
|
|
<button
|
|
onClick={() => handleMatchWith(activeUser.userId)}
|
|
disabled={!hasHeats}
|
|
className="p-1 text-primary-600 hover:bg-primary-50 rounded disabled:opacity-30 disabled:cursor-not-allowed"
|
|
>
|
|
<UserPlus className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
);
|
|
})}
|
|
```
|
|
|
|
7. **Add filter checkbox above active users:**
|
|
```jsx
|
|
<label className="flex items-center gap-2 text-sm text-gray-700 mb-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={hideMyHeats}
|
|
onChange={(e) => setHideMyHeats(e.target.checked)}
|
|
className="rounded border-gray-300"
|
|
/>
|
|
Hide users from my heats
|
|
</label>
|
|
```
|
|
|
|
8. **Filter logic:**
|
|
```javascript
|
|
const filteredUsers = hideMyHeats
|
|
? activeUsers.filter(activeUser => {
|
|
const theirHeats = userHeats.get(activeUser.userId) || [];
|
|
return !theirHeats.some(theirHeat =>
|
|
myHeats.some(myHeat =>
|
|
myHeat.divisionId === theirHeat.divisionId &&
|
|
myHeat.competitionTypeId === theirHeat.competitionTypeId &&
|
|
myHeat.heatNumber === theirHeat.heatNumber
|
|
)
|
|
);
|
|
})
|
|
: activeUsers;
|
|
```
|
|
|
|
9. **Socket.IO heats_updated listener:**
|
|
```javascript
|
|
useEffect(() => {
|
|
const socket = getSocket();
|
|
if (!socket) return;
|
|
|
|
socket.on('heats_updated', ({ userId, username, heats }) => {
|
|
setUserHeats(prev => {
|
|
const newMap = new Map(prev);
|
|
newMap.set(userId, heats);
|
|
return newMap;
|
|
});
|
|
|
|
// If it's current user, update myHeats
|
|
if (userId === user.id) {
|
|
setMyHeats(heats);
|
|
setShowHeatsBanner(heats.length === 0);
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
socket.off('heats_updated');
|
|
};
|
|
}, [user.id]);
|
|
```
|
|
|
|
### Step 5: Styling & UX (0.5-1h) ⏳
|
|
- [ ] Heat badges design (color-coded by division?)
|
|
- [ ] Banner responsive design (mobile + desktop)
|
|
- [ ] Modal for editing heats
|
|
- [ ] Loading states for heat operations
|
|
- [ ] Error handling & validation messages
|
|
- [ ] Empty states ("No heats declared yet")
|
|
|
|
### Step 6: Testing & Edge Cases (0.5-1h) ⏳
|
|
- [ ] Test unique constraint violation (frontend + backend)
|
|
- [ ] Test filter "Hide users from my heats"
|
|
- [ ] Test real-time updates when someone changes heats
|
|
- [ ] Test UserPlus button disabled for users without heats
|
|
- [ ] Test banner dismissal and re-opening via "Edit Heats"
|
|
- [ ] Test multiple heats display in sidebar
|
|
- [ ] Test role optional (NULL) handling
|
|
|
|
### Technical Notes
|
|
- **Abbreviations:**
|
|
- Divisions: NEW, NOV, INT, ADV, ALL, CHA
|
|
- Competition Types: J&J, STR
|
|
- Roles: L (Leader), F (Follower), empty (NULL)
|
|
- **Display format:** "{CompType} {Div} {Heat} {Role?}" → "J&J NOV 1 L"
|
|
- **Future enhancement:** When Matches API is implemented, editing heats with active match requires partner confirmation
|
|
|
|
---
|
|
|
|
## 📌 NEXT STEPS - Phase 2: Core Features
|
|
|
|
**Estimated Time:** 12-15 hours
|
|
**Priority:** HIGH
|
|
|
|
### Step 1: Matches API (3-4h) ⏳
|
|
- [ ] Create Match controller and routes
|
|
- [ ] `POST /api/matches` - Create match request
|
|
- [ ] `POST /api/matches/:id/accept` - Accept match request
|
|
- [ ] `GET /api/matches` - List user's matches (active, pending, completed)
|
|
- [ ] `GET /api/matches/:id` - Get match details
|
|
- [ ] Frontend integration:
|
|
- Match request button in EventChatPage
|
|
- Match notification handling
|
|
- Match acceptance flow
|
|
- [ ] Unit tests (match creation, acceptance, validation)
|
|
|
|
### Step 2: Ratings API (2-3h) ⏳
|
|
- [ ] Create Rating controller and routes
|
|
- [ ] `POST /api/ratings` - Submit rating after collaboration
|
|
- [ ] `GET /api/users/:id/ratings` - Get user's ratings & average
|
|
- [ ] `GET /api/matches/:id/rating` - Check if match already rated
|
|
- [ ] Frontend integration:
|
|
- Update RateMatchPage to use real API
|
|
- Display user ratings on profile/active users
|
|
- [ ] Validation (1-5 stars, comment length)
|
|
- [ ] Unit tests (rating submission, retrieval, validation)
|
|
|
|
### Step 3: WebRTC Signaling (3-4h) ⏳
|
|
- [ ] Add Socket.IO signaling events:
|
|
- `webrtc_offer` - Send SDP offer
|
|
- `webrtc_answer` - Send SDP answer
|
|
- `webrtc_ice_candidate` - Exchange ICE candidates
|
|
- [ ] Frontend WebRTC setup:
|
|
- RTCPeerConnection initialization
|
|
- STUN server configuration
|
|
- Signaling flow implementation
|
|
- [ ] Connection state monitoring
|
|
- [ ] Unit tests (signaling message exchange)
|
|
|
|
### Step 4: WebRTC File Transfer (4-5h) ⏳
|
|
- [ ] RTCDataChannel setup (ordered, reliable)
|
|
- [ ] File metadata exchange (name, size, type)
|
|
- [ ] File chunking implementation (16KB chunks)
|
|
- [ ] Progress monitoring (sender & receiver)
|
|
- [ ] Error handling & reconnection logic
|
|
- [ ] Complete P2P video transfer flow:
|
|
- Select video file
|
|
- Establish P2P connection
|
|
- Transfer file via DataChannel
|
|
- Save file on receiver side
|
|
- [ ] Test with various file sizes
|
|
- [ ] Fallback: Link sharing (already implemented in UI)
|
|
|
|
---
|
|
|
|
## 🎯 Future Phases (Reference)
|
|
|
|
### Phase 3: MVP Finalization (2-3 weeks)
|
|
- [ ] Security hardening:
|
|
- Rate limiting (express-rate-limit)
|
|
- Input validation & sanitization
|
|
- CORS configuration
|
|
- SQL injection prevention
|
|
- XSS protection
|
|
- [ ] Testing:
|
|
- Integration tests (API endpoints)
|
|
- E2E tests (Playwright/Cypress)
|
|
- WebRTC connection tests
|
|
- [ ] PWA features:
|
|
- Web app manifest
|
|
- Service worker (offline support)
|
|
- App icons & splash screens
|
|
- Install prompt
|
|
- [ ] Deployment:
|
|
- Production Docker images
|
|
- Environment configuration
|
|
- Database backups
|
|
- Monitoring & logging
|
|
- CI/CD pipeline
|
|
|
|
### Phase 5: Optional Extensions
|
|
- [ ] User badges & trust system
|
|
- [ ] Block users
|
|
- [ ] Public profiles
|
|
- [ ] Push notifications
|
|
- [ ] Video compression
|
|
- [ ] Multi-file transfer
|
|
|
|
---
|
|
|
|
## 📝 Active Development Tasks
|
|
|
|
### Documentation
|
|
- [x] ✅ README.md
|
|
- [x] ✅ QUICKSTART.md
|
|
- [x] ✅ CONTEXT.md
|
|
- [x] ✅ TODO.md
|
|
- [x] ✅ SESSION_CONTEXT.md
|
|
- [x] ✅ ARCHITECTURE.md
|
|
- [x] ✅ COMPLETED.md
|
|
- [x] ✅ RESOURCES.md
|
|
- [ ] ⏳ API documentation (Swagger/OpenAPI) - after backend
|
|
- [ ] ⏳ Architecture diagrams - after backend
|
|
- [ ] ⏳ WebRTC flow diagram - after WebRTC implementation
|
|
|
|
### Infrastructure
|
|
- [x] ✅ Docker Compose (nginx, frontend)
|
|
- [ ] ⏳ Docker Compose (backend, db)
|
|
- [ ] ⏳ Production Dockerfile optimization (multi-stage builds)
|
|
- [ ] ⏳ CI/CD pipeline (GitHub Actions)
|
|
- [ ] ⏳ HTTPS setup (Let's Encrypt)
|
|
|
|
### Testing
|
|
- [ ] ⏳ Backend tests (Jest + Supertest)
|
|
- [ ] ⏳ Frontend tests (Vitest + React Testing Library)
|
|
- [ ] ⏳ E2E tests (Playwright / Cypress)
|
|
- [ ] ⏳ WebRTC manual testing (different devices)
|
|
|
|
---
|
|
|
|
## 🚀 Quick Commands
|
|
|
|
**Start development:**
|
|
```bash
|
|
docker compose up --build
|
|
```
|
|
|
|
**Rebuild after changes:**
|
|
```bash
|
|
docker compose down && docker compose up --build
|
|
```
|
|
|
|
**Access:**
|
|
- Frontend: http://localhost:8080
|
|
- Backend (future): http://localhost:8080/api
|
|
|
|
**Git workflow:**
|
|
```bash
|
|
git status
|
|
git add .
|
|
git commit -m "feat: description"
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 Progress Tracking
|
|
|
|
| Phase | Status | Progress | Estimated Time |
|
|
|-------|--------|----------|----------------|
|
|
| Phase 0: Frontend Mockup | ✅ Done | 100% | ~8h (completed) |
|
|
| Phase 1: Backend Foundation | ✅ Done | 100% | ~14h (completed) |
|
|
| Phase 1.5: Email & WSDC & Profiles | ✅ Done | 100% | ~12h (completed) |
|
|
| Phase 2: Core Features | ⏳ Next | 0% | ~15-20h |
|
|
| Phase 3: MVP Finalization | ⏳ Pending | 0% | ~15-20h |
|
|
| Phase 4: Extensions | ⏳ Pending | 0% | TBD |
|
|
|
|
**Overall Progress:** ~65% (Phase 0, 1, 1.5 completed)
|
|
|
|
---
|
|
|
|
## 📝 Notes
|
|
|
|
- Frontend mockup is presentation-ready
|
|
- All views work with mock data - easy to connect real API
|
|
- WebRTC P2P mockup in MatchChatPage - needs real implementation
|
|
- Focus on Phase 1 next (backend foundation)
|
|
- Update task status: ⏳ → 🔄 → ✅
|
|
|
|
---
|
|
|
|
**For detailed task history:** See `docs/COMPLETED.md`
|
|
**For learning resources:** See `docs/RESOURCES.md`
|
|
**For quick session context:** See `docs/SESSION_CONTEXT.md`
|
|
**For technical details:** See `docs/ARCHITECTURE.md`
|
|
|
|
---
|
|
|
|
**Last Updated:** 2025-11-14
|