# CLAUDE.md — frontend Angular 20 SPA. Multi-tenant dashboard with a lazy-loaded `src.palm.vision` sub-app for MPOB-standard palm oil fruit bunch (FFB) ripeness detection. Three inference engines: browser ONNX WASM, NestJS remote (WebSocket), and n8n edge passthrough. ## Commands ```bash npm start # Dev server at 0.0.0.0:4200 npm run build # Production build → dist/fisapp-ui npm run build:dev # Dev build → dist/dev npm run build:prod # Prod build → dist/rc npm run build:leave:prod # Leave-module variant npm run build:quot:prod # Quotation variant npm run build:maf:quot:prod # MAF quotation variant npm run test # Karma + Jasmine npm run clean # Clear Angular + npm caches ``` ## Architecture ### Application Shell (`src/app/`) - **`app.config.ts`** — `ApplicationConfig`: bootstraps NGXS root store (`withNgxsStoragePlugin`, `withNgxsReduxDevtoolsPlugin`), Angular Router (hash location), HTTP client, service worker. - **`app.routes.ts`** — Root routes: `dashboard`, `auth`, `leave`, `tender`, `src.palm.vision` (all lazy-loaded except dashboard). - **`app.component.ts`** — Root component: session timeout, PWA install prompt, theme management. ### PalmVision Sub-App (`src/src.palm.vision/`) Lazy-loaded at `/src.palm.vision`. Provides its own NGXS `VisionState` via `provideStates([VisionState])` in the feature module. **Sub-routes:** | Path | Component | Purpose | |---|---|---| | `analyzer` (default) | `AnalyzerComponent` | Inference UI — file/camera input, bounding box canvas | | `vault` | `HistoryComponent` | Batch history vault with canvas thumbnails | | `chat` | `ChatbotComponent` | Industrial Intelligence Portal (n8n chat proxy) | #### Services (`src/src.palm.vision/services/`) **`InferenceService`** — Local WASM inference: - `analyze(file, batchId?)` → `Observable` — preprocesses image → posts tensor to Web Worker → emits result - `preprocessImage(dataUrl)` — resizes to 640×640, converts RGBA→CHW, normalizes `[0.0, 1.0]` - `results$: Subject` — all completed frames - `queueDepth$: BehaviorSubject` — pending worker frames **`RemoteInferenceService`** — WebSocket bridge to NestJS backend: - `analyze(file, sourceLabel?, batchId?)` → `Observable` — sends `PalmVision:analyze` via FIS envelope - `getHistory()` → `Observable` — `History:getAll` - `deleteRecord(archiveId)` → `Observable<{ deleted: boolean }>` — `History:delete` - `clearHistory()` → `Observable<{ deleted: number }>` — `History:clearAll` - `getImage(archiveId)` → `Observable<{ archiveId, image_data }>` — `PalmHistory:GetImage` - `saveExternalResult(payload)` → `Observable` — `PalmHistory:SaveExternalResult` Connection config loaded from `src/src.palm.vision/config/config.json`: ```json { "connection": { "uacp": "http://localhost:3000", "uacp_ws": "ws://localhost:3000/socket.io", "uacpEmulation": "on" } } ``` #### State (`src/src.palm.vision/store/`) **`VisionStateModel`:** ```typescript { items: any[]; loading: boolean; expandedBatchIds: string[]; currentInference: InferenceFrame | null } ``` **Selectors:** `VisionState.items`, `VisionState.loading`, `VisionState.expandedBatchIds`, `VisionState.currentInference` **Actions (`vision.actions.ts`):** | Action | Payload | Effect | |---|---|---| | `SubmitBatchAnalysis` | `{ files: File[]; mode: 'local'\|'remote'\|'n8n' }` | Run batch inference | | `ToggleBatchGroup` | `{ batchId: string }` | Expand/collapse history accordion | | `LoadGroupImages` | `{ batchId: string }` | Lazy-load archived images for a batch | | `LoadHistory` | — | Fetch last 50 records from SQLite | | `DeleteHistoryRecord` | `{ id: string }` | Delete record + disk image | | `ClearAllHistory` | — | Wipe all records | #### Components **`AnalyzerComponent`** — Three-mode inference UI: - Engine selector: `local` (WASM), `remote` (NestJS), `n8n` (edge) - Input: drag-and-drop file zone or live webcam (`getUserMedia`, environment-facing) - Canvas overlay via `renderPredictionsWithBoxes(frame)` — draws bounding boxes + confidence labels - MPOB color palette: `Ripe #4caf50`, `Unripe #ff9800`, `Underripe #ffeb3b`, `Overripe #9c27b0`, `Abnormal #f44336`, `Empty_Bunch #607d8b` **`HistoryComponent`** — Batch vault: - Groups `items` by `batch_id` into `BatchGroup[]` via `combineLatest([items$, expandedBatchIds$])` - `@ViewChildren('thumbCanvas')` — canvas thumbnails rendered via `renderThumbnailWithBoxes()` using `norm_box` coordinates - `paintAllVisibleCanvases()` with `setTimeout(0)` deferral; `data-archive-id` attribute maps canvas elements to data items - Mode badge variants: `local` (WASM), `remote` (Server), `n8n` **`ChatbotComponent`** — Thin wrapper around `angularlib/chat/chat.component` with title "Industrial Intelligence Portal". #### Web Worker (`src/src.palm.vision/workers/inference.worker.ts`) Receives `{ frameId, batchId, imageDataUrl, tensor, processingStart }`, runs ONNX inference, returns `InferenceFrame`. Keeps inference off the main thread for WASM local mode. ### Key Interfaces ```typescript interface DetectionResult { bunch_id: number; class: string; // MPOB class name confidence: number; // [0, 1] is_health_alert: boolean; // true if Abnormal | Empty_Bunch box: [x1, y1, x2, y2]; // pixel coords norm_box?: [nx1, ny1, nx2, ny2]; // normalized [0,1] coords } interface InferenceFrame { frameId: string; batchId?: string; imageDataUrl: string; detections: DetectionResult[]; inference_ms: number; processing_ms: number; total_count: number; industrial_summary: Record; source: 'wasm-local' | 'remote' | 'n8n'; } ``` ### Shared In-Source Dependencies (`src/dependencies/`) | Alias | Path | Contents | |---|---|---| | `angularlib/*` | `src/dependencies/angularlib/` | UI components, auth, forms, chat | | `dp-ui/*` | `src/dependencies/dp-ui/` | Custom Material extensions, `NgxSocketService` | | `fis/*` | `src/dependencies/fis/` | Domain modules (leave, tender, approval) | | `fis-commons/*` | `src/dependencies/fis-commons/` | CDN-hosted shared utilities | ### Models & WASM Assets - `src/assets/models/onnx/best.onnx` — YOLOv8 ONNX model for browser WASM inference - `src/assets/models/tflite/best_float16.tflite` / `best_float32.tflite` — TFLite variants - `src/assets/wasm/` — ONNX Runtime JS WASM bundles - `src/assets/tflite-wasm/` — TensorFlow Lite WASM runtime ### Multi-Build Strategy `angular.json` defines four production configurations that swap assets and menu via `fileReplacements`: | Config | Output | Swaps | |---|---|---| | `production` | `dist/rc` | — | | `leave-prod` | `dist/leave` | `menu.ts → menu.leave.ts`, leave assets | | `quotation-prod` | `dist/quotation` | `menu.ts → menu.quotation.ts`, quotation assets | | `maf-quot-prod` | `dist/maf/quotation` | MAF quotation assets | Development build uses a mock socket service (`dp.service.t.ts`) for offline development. ### PWA Service worker registered immediately (`registerImmediately`) via `ngsw-config.json`. PWA install prompt handled in `AppComponent`. Three language packs: `en_US`, `ms_MY`, `zh_Hans`. ### CORS / Connection Frontend connects to NestJS backend at `http://localhost:3000` (configurable via `config.json`). Backend CORS whitelist is hardcoded in `server-desktop/src/main.ts` — add new device IPs there.