This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
NestJS backend for the PalmOilAI system. Provides server-side YOLOv8 ONNX inference, SQLite history persistence (with disk image archiving), WebSocket gateways for real-time vision streaming and n8n chat proxying, and a process surveillance monitor for n8n and Ollama.
Detection classes: Ripe, Unripe, Underripe, Overripe, Abnormal, Empty_Bunch (MPOB standard).
npm install
npm run start:dev # Watch mode dev server (port 3000)
npm run start # Single-run dev server
npm run start:prod # Production (from dist/)
npm run build # Compile → dist/
npm run lint # ESLint with auto-fix
npm run test # Jest unit tests
npm run test:watch # Jest watch mode
npm run test:cov # Coverage report
npm run test:e2e # End-to-end tests
jest --testPathPattern=palm-oil # Run a single test file
Set PORT env var to override port 3000. Set N8N_WEBHOOK_URL in .env for chat proxy and agent readiness probing.
PalmOilModule (src/palm-oil/)
ScannerProvider — ONNX inference engine loaded on OnModuleInit:
preprocess(buffer) — sharp resize to 640×640, strip alpha, convert HWC→CHW, normalize to [0.0, 1.0], output [1, 3, 640, 640] tensorinference(tensor) — runs onnxruntime-node sessionpostprocess(tensor, origW, origH, threshold=0.25) — output is [1, N, 6] (x1, y1, x2, y2, confidence, class_index):
raw_tensor_sample for technical evidenceclass_index → MPOB_CLASSESis_health_alert for Abnormal | Empty_BunchPalmOilService — Orchestrates inference + persistence. analyzeImage(imageBuffer, filename, batchId?) pipeline:
ScannerProviderinference_ms, processing_ms)raw_tensor_samplearchive/ directory on diskHistoryEntity) — fully awaited before returning (guarantees DB write before socket emit)AnalysisResponse with Base64 image, archive_id, full technical_evidence blockAdditional methods: getHistory() (last 50 records), getRecordByArchiveId(), deleteRecord() (removes DB row + disk image), clearAllHistory() (removes all records + disk images).
VisionGateway — WebSocket gateway on /vision namespace:
vision:analyze handler: receives VisionStreamPayload (frame: string, sourceLabel?, batchId?), strips data-URI prefix, decodes Base64 → Buffer, calls PalmOilService.analyzeImage(), emits vision:result or vision:errorchat:send handler: receives ChatPayload, POSTs to N8N_WEBHOOK_URL server-to-server (bypasses browser CORS), unwraps array response to first element, emits chat:result or chat:errorHistoryEntity — TypeORM entity fields:
id (PK auto), archive_id (unique), batch_id (nullable, indexed), filenametotal_count, industrial_summary (simple-json), detections (simple-json array)inference_ms, processing_ms, image_path (disk reference)created_at (auto timestamp)mpob-standards.ts (src/palm-oil/constants/) — Source of truth:
MPOB_CLASSES: index 0–5 → class name stringsGRADE_COLORS: class name → hex colorHEALTH_ALERT_CLASSES: ['Abnormal', 'Empty_Bunch']SurveillanceModule (src/surveillance/)
SurveillanceService — Boots on OnModuleInit, runs two background loops:
ollama_llama_server or ollama), then calls pidusage for CPU/memory metricsgetLatestMetrics(), callback registration for tick eventsSurveillanceGateway — WebSocket gateway on /monitor namespace:
SurveillanceService callbacks: every 500ms tick broadcasts monitor:data to all clientsmonitor:subscribe handler: acknowledges and re-sends current snapshotSQLite file palm_history.db in the project root. Managed by TypeORM with synchronize: true (auto-creates/migrates tables — dev only). No external DB setup required.
best.onnx — YOLOv8 ONNX model, must be placed in the project root (not src/). Loaded by ScannerProvider at startup.archive/ directory — created automatically by PalmOilService for disk image storage..env)N8N_WEBHOOK_URL=<n8n webhook URL>
PORT=3000 # optional
| Namespace | Client → Server | Payload | Server → Client |
|---|---|---|---|
/vision |
vision:analyze |
{ frame: string, sourceLabel?: string, batchId?: string } |
vision:result, vision:error |
/vision |
chat:send |
{ message: string } |
chat:result, chat:error |
/monitor |
monitor:subscribe |
— | monitor:data (MonitorPayload[]) |