Просмотр исходного кода

docs: update memory.md — add simulator, captures API, plan overview

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
kingkong 2 месяцев назад
Родитель
Сommit
da583af155
1 измененных файлов с 268 добавлено и 121 удалено
  1. 268 121
      memory.md

+ 268 - 121
memory.md

@@ -1,51 +1,214 @@
 # Construction Timelapse — Session Memory
 
+> File này là **nhật ký phát triển** của dự án. Mỗi khi có thay đổi lớn, cập nhật phía dưới cùng (entry mới nhất).
+
+---
+
 ## Tổng quan dự án
-Hệ thống timelapse construction monitoring.
+
 - **Repo Gogs**: `https://git.k9tech.space/kingkong/timelapse-2`
 - **Token**: `a6224640fb6f576f6cba46f0e1646500c87226b6` (Personal Access Token)
-- **Stack**: Next.js 14 + NestJS + PostgreSQL (Drizzle) + Redis + Python (device-agent)
-- **Package manager**: Bun (KHÔNG dùng npm  workspace protocol không hoạt động với nexus registry)
+- **Stack**: Next.js 14 + NestJS + PostgreSQL (Drizzle) + Redis + Python
+- **Package manager**: Bun (KHÔNG dùng npm  workspace protocol không hoạt động với nexus registry)
 - **Registry**: Luôn thêm `--registry https://registry.npmjs.org/` khi cài package
 
 ---
 
-## Kiến trúc hiện tại
+## Kiến trúc hệ thống
 
 ```
-MacOS (code) → Gogs push → webhook → Pi 4 (deploy)
-                                           ↓
-                              ┌─ postgres (docker)
-                              ├─ redis (docker)
-                              ├─ api-server (NestJS)
-                              ├─ web-dashboard (Next.js)
-                              └─ worker (Node)
+MacOS (code) → push Gogs → webhook → Pi 4 (deploy)
+                                          ↓
+                    ┌─ postgres (docker, port 5432)
+                    ├─ redis    (docker, port 6379)
+                    ├─ api-server    (NestJS, port 3001)
+                    ├─ web-dashboard (Next.js, port 3000)
+                    ├─ worker        (Node, stub)
+                    └─ simulator     (Python, tự động)
 ```
 
+**Simulator** = container giả lập device (claim → approve → heartbeat + upload ảnh).
+
 ---
 
 ## Workspace config
-- **KHÔNG** dùng `apps/*` trong `workspaces` — `device-agent` là Python
-- Workspace config đúng:
-  ```json
-  "workspaces": ["apps/api-server", "apps/web-dashboard", "apps/worker", "packages/*"]
-  ```
-- `@shared-types` **KHÔNG hợp lệ** — dùng `@shared/types`
-- Luôn `cd` vào workspace trước khi `bun add`
+
+```json
+"workspaces": ["apps/api-server", "apps/web-dashboard", "apps/worker", "packages/*"]
+```
+- `device-agent` và `simulator` là Python — KHÔNG đưa vào workspaces
+- Dùng `@shared/types`, không phải `@shared-types`
+
+---
+
+## Các lỗi đã gặp và cách fix
+
+| # | Lỗi | Fix |
+|---|------|-----|
+| 1 | `@shared-types` không hợp lệ | Đổi thành `@shared/types` |
+| 2 | Python packages trong npm workspaces | Loại khỏi workspaces |
+| 3 | `npm install` không hoạt động | `bun install --registry https://registry.npmjs.org/` |
+| 4 | `bun add` với nexus registry | Luôn `--registry https://registry.npmjs.org/` |
+| 5 | `db` không export được | Import từ `database.module.ts` |
+| 6 | `experimentalDecorators` thiếu | Thêm vào `packages/config/tsconfig.node.json` |
+| 7 | TSC output vào `src/` | Cleanup `*.js/*.d.ts/*.js.map` + fix outDir |
+
+---
+
+## Database (Drizzle Schema)
+
+- **File**: `apps/api-server/src/db/schema.ts`
+- **16 tables**: organizations, projects, users, memberships, sessions, magic_links, devices, device_heartbeats, captures, videos, video_jobs, commands, alert_rules, alerts, audit_logs, activity_logs
+- **Enums**: org_status, project_status, device_status (có `pending`), capture_status, video_status, alert_severity, alert_type, alert_state, user_role, command_result_status
+
+---
+
+## Auth — Đã hoàn thành ✅
+
+| Feature | Chi tiết |
+|---------|---------|
+| Register/Login/Refresh/Logout | JWT + refresh token + bcrypt |
+| Magic links | Email-based passwordless |
+| Google OAuth | Stub (redirect + callback) |
+| JWT guard | `@UseGuards(JwtAuthGuard)` |
+| Dashboard auth flow | `storeTokens()`, `clearAuth()`, `apiFetch()` |
+| Protected routes | `pages/_app.tsx` redirect if no token |
+
+---
+
+## RBAC — Đã hoàn thành ✅
+
+| Role | Quyền |
+|------|--------|
+| `org_admin` | Full: org/project/member CRUD |
+| `project_manager` | Tạo/sửa project |
+| `viewer` | Read-only |
+
+---
+
+## API Endpoints (tổng hợp)
+
+### Auth (`/v1/auth/`)
+`POST register`, `POST login`, `POST refresh`, `POST logout`, `GET me`, `POST magic-link`
+
+### Orgs (`/v1/orgs/`)
+`POST`, `GET`, `GET /:id/members`, `POST /:id/members`, `PATCH /:id/members/:userId`, `DELETE /:id/members/:userId`
+
+### Projects (`/v1/projects/`)
+`POST`, `GET`, `GET /:id`, `PATCH /:id`, `DELETE /:id` — đều có JwtAuthGuard
+
+### Devices (`/v1/devices/`)
+| Endpoint | Auth | Mô tả |
+|----------|------|--------|
+| `POST claim` | none | Device xin claim code |
+| `GET claim/:code/status` | none | Poll trạng thái, nhận bootstrap API key |
+| `GET pending` | JWT (org_admin) | List pending devices |
+| `POST pending/:code/approve` | JWT (org_admin) | Approve + assign project |
+| `POST pending/:code/approve-internal` | X-Internal-Key | Simulator tự approve |
+| `POST pending/:code/reject` | JWT (org_admin) | Reject |
+| `POST :deviceId/heartbeat` | X-API-Key | Device heartbeat (bcrypt verify) |
+| `GET` | JWT | List all devices |
+| `GET stats` | JWT | Dashboard stats |
+| `GET :id` | JWT | Device detail |
+| `GET :id/heartbeats` | JWT | Heartbeat history |
+
+### Captures (`/v1/captures/`)
+| Endpoint | Auth | Mô tả |
+|----------|------|--------|
+| `POST upload` | X-API-Key | Upload ảnh (multipart), lưu local `/uploads/captures/` |
+| `POST register` | X-API-Key | Đăng ký metadata capture |
+| `GET project/:projectId` | JWT | List captures theo project |
+| `GET device/:deviceId` | JWT | List captures theo device |
+| `GET :id` | JWT | Chi tiết capture |
+
+### Realtime (Socket.io `/realtime`)
+Events: `device.heartbeat`, `device.status.changed`, `capture.uploaded`, `alert.opened`
+Dashboard subscribe theo project room: `project:X`
+
+---
+
+## Device Provisioning Flow
+
+```
+Device (Pi+DSLR)                        Server API                       Dashboard
+      │                                    │                                │
+      │── POST /v1/devices/claim ──────────>│                                │
+      |<─ { claimCode: "ABC123" } ──────────│                                │
+      │                                    │── WS realtime ─────────────────>│ (new pending)
+      │                                    │                                │
+      │                                    │<── POST /v1/devices/pending/:code/approve
+      │                                    │     (org_admin JWT)            │
+      │── GET /v1/devices/claim/:code/status│                                │
+      |<─ { status: approved, apiKey } ─────│                                │
+      │                                    │                                │
+      │── POST :id/heartbeat (X-API-Key) ──>│                                │
+      │── POST /v1/captures/upload (file) ──>│                                │
+      │                                    │── capture.uploaded WS ─────────>│
+```
+
+---
+
+## Simulator Container (`apps/simulator`)
+
+**Mục đích**: Giả lập device kết nối thật vào server (POC mà không cần phần cứng thật).
+
+**Config**: env var trong `docker-compose.yml`
+```env
+SIM_SERVER_URL=http://api-server:3001
+SIM_DEVICE_COUNT=2          # số device giả lập
+SIM_CAPTURE_INTERVAL=30     # giây giữa mỗi capture
+SIM_INTERNAL_KEY=...         # key để approve-internal endpoint
+```
+
+**Luồng tự động**:
+1. Claim device → nhận claim code
+2. Auto-approve qua `/approve-internal` (X-Internal-Key auth)
+3. Poll `/claim/:code/status` → nhận bootstrap API key
+4. Heartbeat loop (30s)
+5. Capture loop (30s): Pillow tạo JPEG nhẹ (640×480, quality 60) → upload lên `/v1/captures/upload`
+
+**Files**:
+- `apps/simulator/simulator/main.py`
+- `apps/simulator/Dockerfile`
+- `apps/simulator/requirements.txt`
+
+**Lưu ý**: Cần ít nhất 1 project trong DB để simulator tự assign. Chạy seed trước.
+
+---
+
+## Device Agent (Pi thật — `apps/device-agent`)
+
+Chạy trên Pi 4 thật + DSLR:
+- `agent/main.py` — claim, poll, heartbeat, đọc thermal/disk metrics
+- `systemd/timelapse-agent.service` — systemd unit
+- `scripts/install-agent.sh` — Pi-only installer (kiểm tra EUID)
+- Config: `/etc/timelapse/agent.env`
+- Key lưu: `/boot/timelapse/agent.key` (chmod 600)
+- Metadata: `/boot/timelapse/agent.json`
+
+---
+
+## Mock Device CLI (`apps/device-agent/mock-device-cli.py`)
+
+Dùng cho test nhanh trên MacOS (không cần Docker):
+```bash
+# Claim device
+python3 apps/device-agent/mock-device-cli.py claim \
+  --server http://localhost:3001 --count 5
+
+# Heartbeat cho seed devices
+python3 apps/device-agent/mock-device-cli.py heartbeat \
+  --server http://localhost:3001 --demo --interval 5
+```
 
 ---
 
 ## Gogs API
+
 ```bash
 # List repos
 curl -H "Authorization: token <TOKEN>" https://git.k9tech.space/api/v1/user/repos
 
-# Create repo
-curl -X POST -H "Authorization: token <TOKEN>" \
-  -H "Content-Type: application/json" \
-  -d '{"name":"repo-name","private":false}' \
-  https://git.k9tech.space/api/v1/user/repos
-
 # Create webhook
 curl -X POST -H "Authorization: token <TOKEN>" \
   -H "Content-Type: application/json" \
@@ -59,118 +222,45 @@ git push -u origin main
 
 ---
 
-## Pi 4 Deploy Setup (TODO)
-Trên Pi 4 cần:
-1. Clone repo: `git clone https://git.k9tech.space/kingkong/timelapse-2.git /opt/timelapse`
-2. Tạo `/opt/timelapse/.env` với:
-   ```
-   GOGS_TOKEN=<deploy_token>
-   GOGS_REPO_URL=https://git.k9tech.space/kingkong/timelapse-2.git
-   DATABASE_URL=postgres://user:pass@postgres:5432/timelapse
-   REDIS_URL=redis://redis:6379
-   JWT_SECRET=...
-   ```
-3. Chạy webhook server: `node scripts/webhook-server.js`
-4. Update Gogs webhook URL với IP thật của Pi 4
-5. Deploy script: `bash scripts/deploy.sh`
+## Docker Compose (Pi 4)
 
----
-
-## Docker Compose (in repo)
 ```bash
 docker compose up -d postgres redis
-docker compose up -d --build api-server web-dashboard worker
+docker compose up -d --build api-server web-dashboard worker simulator
 ```
 
----
-
-## Phase 1 — Trạng thái: ✅ HOÀN THÀNH
-
-### Backend (apps/api-server)
-- `src/modules/auth/` — AuthController/Service/Strategies stubs
-- `src/modules/devices/` — heartbeat endpoint + API key guard + repo/service/controller
-- `src/common/` — guards, decorators
-- `src/modules/orgs|projects|captures|videos|alerts/` — stubs
-- `src/realtime/` — stub
+**Volumes**:
+- `postgres_data` — PostgreSQL data
+- `redis_data` — Redis data
+- `timelapse_uploads` — Captured images (gắn vào api-server `/uploads/captures`)
 
-### Dashboard (apps/web-dashboard)
-- Dashboard home, device list, device detail pages
-- Tailwind CSS v3 + React Query
+**Env mới**:
+- `UPLOAD_DIR=/uploads/captures` (api-server)
+- `SIM_INTERNAL_KEY` (api-server + simulator)
 
 ---
 
-## Database (Drizzle Schema)
-- File: `apps/api-server/src/db/schema.ts`
-- Enum: org_status, project_status, device_status, capture_status, video_status, alert_severity, alert_type, user_role, command_result_status
-- Tables: organizations, projects, users, memberships, sessions, magic_links, devices, device_heartbeats, captures, videos, video_jobs, commands, alert_rules, alerts, audit_logs, activity_logs
+## Cài đặt trên Pi 4
 
----
+```bash
+cd /opt/timelapse
+git pull
 
-## Các lỗi đã gặp và cách fix
-1. `@shared-types` → `@shared/types`
-2. `device-agent` (Python) trong npm workspaces → loại khỏi workspaces
-3. `npm install` không hoạt động → `bun install --registry https://registry.npmjs.org/`
-4. `bun add` với nexus registry → luôn `--registry https://registry.npmjs.org/`
-5. `db` không export được → import từ `database.module.ts` riêng
-6. `experimentalDecorators` thiếu → thêm vào `packages/config/tsconfig.node.json`
-7. TSC output vào `src/` → cleanup *.js/*.d.ts/*.js.map + fix outDir
+# Rebuild
+docker compose up -d --build
 
----
+# Logs
+docker compose logs -f simulator
+docker compose logs -f api-server
 
-## Realtime WebSocket — ✅ ĐÃ LÀM
-- Socket.io gateway tại `src/realtime/gateways/realtime.gateway.ts`
-- Events: `device.heartbeat`, `device.status.changed`, `capture.uploaded`, `alert.opened`
-- Dashboard tự refetch khi nhận WS event (useSocket hook)
-- Client subscribe theo project room: `subscribe:project`
-
-## Device installer (Pi) — ✅ ĐÃ LÀM
-- `apps/device-agent/agent/main.py`
-  - claim device, poll approval, save key, heartbeat loop
-- `apps/device-agent/systemd/timelapse-agent.service`
-- `apps/device-agent/scripts/install-agent.sh`
-- `apps/device-agent/requirements.txt`
-- `apps/device-agent/README.md`
-
-## Device installer (Pi) — ✅ ĐÃ LÀM
-- `apps/device-agent/agent/main.py`
-  - claim device, poll approval, save key, heartbeat loop
-- `apps/device-agent/systemd/timelapse-agent.service`
-- `apps/device-agent/scripts/install-agent.sh`
-- `apps/device-agent/requirements.txt`
-- `apps/device-agent/README.md`
-
-## TODO — Phase tiếp theo
-1. **DB migration**: drizzle-kit push + seed data (trên Pi)
-2. **Role-based access control nâng cao** (org_admin/project_manager/viewer)
-3. **Device-agent** (Python) heartbeat sender
-4. **Realtime WebSocket** cho status update
-5. **Google OAuth thật** (redirect + callback)
-6. **Worker thực tế** (hiện vẫn restart vì stub)
-
-## Đã hoàn thành gần đây
-- Auth thực sự: register/login/refresh/logout + JWT guard + /auth/me
-- Dashboard auth flow: /login, ProtectedRoute, logout, auto attach Bearer token
-- Orgs CRUD API (`/v1/orgs`) có JWT guard
-- RBAC theo role:
-  - `org_admin`: full org/project/member management
-  - `project_manager`: create/update project
-  - `viewer`: read-only
-- Membership management endpoints:
-  - `GET /v1/orgs/:id/members`
-  - `POST /v1/orgs/:id/members` (org_admin)
-  - `PATCH /v1/orgs/:id/members/:userId` (org_admin)
-  - `DELETE /v1/orgs/:id/members/:userId` (org_admin)
-- Projects CRUD API:
-  - `POST /v1/projects`
-  - `GET /v1/projects?orgId=`
-  - `GET /v1/projects/:id`
-  - `PATCH /v1/projects/:id`
-  - `DELETE /v1/projects/:id`
-  (đều có JwtAuthGuard)
+# DB migration (cần chạy 1 lần sau khi schema thay đổi)
+docker compose exec api-server bun run migrate
+```
 
 ---
 
 ## Lệnh nhanh (MacOS dev)
+
 ```bash
 # Cài deps
 bun install --registry https://registry.npmjs.org/
@@ -179,7 +269,7 @@ bun install --registry https://registry.npmjs.org/
 cd apps/api-server && bun run dev
 cd apps/web-dashboard && bun run dev
 
-# Build (verify)
+# Build verify
 cd apps/api-server && bun run build
 cd apps/web-dashboard && bun run build
 
@@ -187,5 +277,62 @@ cd apps/web-dashboard && bun run build
 curl -X POST http://localhost:3001/v1/devices/test-device/heartbeat \
   -H "Content-Type: application/json" \
   -H "X-API-Key: dev-key-123" \
-  -d '{"deviceId":"test-device","apiKey":"dev-key-123","status":"online","storageFreeGb":50,"capturesToday":12,"firmwareVersion":"1.0.0"}'
+  -d '{"deviceId":"test-device","status":"online","storageFreeGb":50,"capturesToday":12,"firmwareVersion":"1.0.0"}'
 ```
+
+---
+
+## TODO
+
+- [ ] **DB migration + seed** trên Pi (`drizzle-kit push` + tạo org/project seed)
+- [ ] **Dashboard Pending Devices page** — approve/reject/assign device trên UI
+- [ ] **Worker thực** — BullMQ capture queue processing (thumbnail, validate)
+- [ ] **S3/MinIO** thay local storage
+- [ ] **Video generation** — ghép ảnh thành video
+- [ ] **Google OAuth thật**
+- [ ] **Alert rules engine**
+- [ ] **Dashboard captures gallery** — xem ảnh đã upload
+- [ ] **Pi 4 IP** — cập nhật Gogs webhook URL
+
+---
+
+## Nhật ký phát triển
+
+### 2026-03-26 — Simulator container + Captures upload API
+- Thêm `CapturesModule`: `POST /v1/captures/upload` (multipart, lưu local `/uploads/captures/`), `POST /v1/captures/register` (metadata only), các GET endpoints (JWT auth)
+- Upload lưu vào `/uploads/captures/{projectId}/{YYYY-MM-DD}/{id}.jpg`, tính SHA256 checksum
+- Realtime event `capture.uploaded` emit khi upload thành công
+- Tạo `apps/simulator/` — container Python tự động: claim → approve-internal → heartbeat + Pillow JPEG upload
+- Thêm `POST /v1/devices/pending/:code/approve-internal` endpoint (X-Internal-Key auth) cho simulator
+- `docker-compose.yml`: thêm `timelapse_uploads` volume, `SIM_INTERNAL_KEY` env, `simulator` service
+- api-server Dockerfile: tạo `/uploads/captures` directory
+- Commit: `f3b3091 feat: add simulator container + captures upload API`
+
+### 2026-03-?? — Realtime WebSocket + Device Provisioning
+- Socket.io gateway tại `src/realtime/gateways/realtime.gateway.ts`
+- Events: `device.heartbeat`, `device.status.changed`, `capture.uploaded`, `alert.opened`
+- Dashboard subscribe theo project room: `subscribe:project`
+- Device provisioning: claim → approve → bootstrap API key (bcrypt hashed)
+- `approveDeviceInternal()` auto-assign first project nếu không có projectId
+- Commit: device provisioning flow
+
+### 2026-03-?? — RBAC + Auth hoàn chỉnh
+- Register/Login/Refresh/Logout + JWT + bcrypt
+- Dashboard login/register/protected routes
+- Orgs CRUD + Membership management (org_admin/project_manager/viewer)
+- Projects CRUD với role enforcement
+- Commit: RBAC + dashboard auth
+
+### 2026-03-?? — Device agent (Pi thật)
+- `apps/device-agent/agent/main.py` — claim, poll, heartbeat loop, thermal/disk metrics
+- `systemd/timelapse-agent.service`
+- `scripts/install-agent.sh` (Pi-only, kiểm tra EUID)
+- `mock-device-cli.py` — claim + heartbeat simulator cho MacOS dev
+- Commit: `515619f`, `636ae53`
+
+### 2026-03-?? — Setup ban đầu
+- Scaffold NestJS api-server + Next.js dashboard + worker stub
+- Docker Compose (postgres, redis, 3 services)
+- Drizzle schema với 16 tables
+- Realtime module (stub)
+- Commit đầu tiên