diff --git a/README.md b/README.md index 3f2e7b8..90857dd 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,6 @@ POST). **DO manual flow:** open `http://:5000/operator` on a phone → Ambil Foto DO → review/edit OCR fields → Mulai Batch → count → Selesai (soft-warn if zone busy). Discard only when both sack and box nets are 0. ERD: [`docs/do-erd.md`](docs/do-erd.md). -Plan: [`docs/do-batch-implementation-plan.md`](docs/do-batch-implementation-plan.md). ## Repository layout @@ -133,6 +132,6 @@ Script-by-script reference: [`docs/scripts.md`](docs/scripts.md). - [`docs/configuration.md`](docs/configuration.md) — all config files/variables - [`docs/deployment.md`](docs/deployment.md) — Jetson services, TensorRT, deploy flow - [`docs/do-erd.md`](docs/do-erd.md) — delivery-order / batch ERD (Mermaid) -- [`docs/do-batch-implementation-plan.md`](docs/do-batch-implementation-plan.md) — DO manual batch plan +- [`docs/operator-tiles-history-plan.md`](docs/operator-tiles-history-plan.md) — operator tiles + history cleanup plan - [`docs/scripts.md`](docs/scripts.md) — entry points & utility scripts - [`CHANGELOG.md`](CHANGELOG.md) — release history diff --git a/counter_dashboard.py b/counter_dashboard.py index ea0b275..fba4bec 100644 --- a/counter_dashboard.py +++ b/counter_dashboard.py @@ -1703,10 +1703,9 @@ def export_day_xlsx(date): ws = wb.active ws.title = f"Day {date}" _style_header(ws, [ - ("A", "Batch Number"), ("B", "Count"), ("C", "Box"), ("D", "Net Sack"), - ("E", "Net Box"), ("F", "Plate"), ("G", "DO"), ("H", "Expected Sack"), - ("I", "Expected Box"), ("J", "Start Time"), ("K", "End Time"), - ("L", "Duration (min)"), + ("A", "Batch Number"), ("B", "Sacks"), ("C", "Boxes"), + ("D", "Plate"), ("E", "DO"), ("F", "DO Sacks"), ("G", "DO Boxes"), + ("H", "Start Time"), ("I", "End Time"), ("J", "Duration (min)"), ]) for r_idx, row in enumerate(rows, 2): @@ -1717,15 +1716,13 @@ def export_day_xlsx(date): ws.cell(row=r_idx, column=1, value=row["batch_number"]) ws.cell(row=r_idx, column=2, value=row["count"]) ws.cell(row=r_idx, column=3, value=row["box_loading"]) - ws.cell(row=r_idx, column=4, value=row["net_sack"] if "net_sack" in row.keys() else row["count"]) - ws.cell(row=r_idx, column=5, value=row["net_box"] if "net_box" in row.keys() else row["box_loading"]) - ws.cell(row=r_idx, column=6, value=row["plate"] if "plate" in row.keys() else "") - ws.cell(row=r_idx, column=7, value=", ".join(do_numbers)) - ws.cell(row=r_idx, column=8, value=row["expected_sack"] if "expected_sack" in row.keys() else 0) - ws.cell(row=r_idx, column=9, value=row["expected_box"] if "expected_box" in row.keys() else 0) - ws.cell(row=r_idx, column=10, value=row["start_time"]) - ws.cell(row=r_idx, column=11, value=row["end_time"]) - ws.cell(row=r_idx, column=12, value=row["duration_minutes"] or 0) + ws.cell(row=r_idx, column=4, value=row["plate"] if "plate" in row.keys() else "") + ws.cell(row=r_idx, column=5, value=", ".join(do_numbers)) + ws.cell(row=r_idx, column=6, value=row["expected_sack"] if "expected_sack" in row.keys() else 0) + ws.cell(row=r_idx, column=7, value=row["expected_box"] if "expected_box" in row.keys() else 0) + ws.cell(row=r_idx, column=8, value=row["start_time"]) + ws.cell(row=r_idx, column=9, value=row["end_time"]) + ws.cell(row=r_idx, column=10, value=row["duration_minutes"] or 0) _auto_width(ws) filename = f"{SITE_NAME}_day_detail_{date}.xlsx" diff --git a/docs/architecture.md b/docs/architecture.md index 0ab83d8..c8cfe0c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -147,7 +147,7 @@ dashboard integration is file-based.) DO pipeline (dashboard): smartphone upload → OCR draft → edit/stage → start gates → active batch → stop-preview soft-warn → stop → `batches` + DO columns. -ERD: [`docs/do-erd.md`](do-erd.md). Plan: [`docs/do-batch-implementation-plan.md`](do-batch-implementation-plan.md). +ERD: [`docs/do-erd.md`](do-erd.md). UI plan: [`docs/operator-tiles-history-plan.md`](operator-tiles-history-plan.md). ## Retired (`archive/`, not imported) diff --git a/docs/do-batch-implementation-plan.md b/docs/do-batch-implementation-plan.md deleted file mode 100644 index 5f22403..0000000 --- a/docs/do-batch-implementation-plan.md +++ /dev/null @@ -1,684 +0,0 @@ -# Implementation Plan — DO-Gated Manual Batch Counting - -> Status: **implemented** (Phase 1–5 code + docs + tests; manual device checks + Jetson deploy pending). -> Scope: **karung feedmill counter only**. -> pfm-ocr (`git.proit.id/andrew/pfm-ocr`) is **inspiration only** — no API/DB coupling. - -## 1. Goal - -**Default runtime mode: auto batching** (existing truck/sack FSM — unchanged). -**Office** (port 5721) switches mode to DO-gated manual or legacy manual; operator -(port 5000) only **views** current mode and acts within it. - -DO-gated manual workflow (when selected) — **photos taken with a smartphone** -via the operator dashboard page (browser `capture="environment"` on the phone): - -``` -smartphone photo DO(s) → review OCR → upload/stage → start batch → count -→ truck leaves → operator ends batch (soft warn + force) -``` - -- One truck may carry **multiple DOs**. -- DOs with the **same vehicle plate** belong to **one batch**. -- Same plate may return **more than once per day** — each visit is a new batch - delimited by operator start/stop, not by plate uniqueness. -- Truck detect / ROI / overlay stay **live as today** in all modes. -- Auto mode: Jetson owns truck presence / batch open-close (existing behaviour). - -## 2. Locked decisions - -| # | Topic | Decision | -|---|---|---| -| 1 | Batch discard | Discard only if **both** net karung **and** net kardus are 0 (`net_sack == 0 and net_box == 0`). Keep the row if either side is non-zero. | -| 2 | Expected vs counted | **Net** = `loading − unloading` for sacks and for boxes | -| 3 | OCR | **Light CPU OCR first** (Tesseract); **PaddleOCR-level backup** same `extract_do_fields()` dispatch. **Runtime engine** (`tesseract`\|`paddle`\|`none`) in `do_settings.json` — **toggleable from office (5721) and operator (5000)**; both UIs read/write the same store and stay in sync via `GET /api/do/settings` poll. YAML `do.ocr.engine` = seed only | -| 4 | Photo retention | **7 days** (`do.retention_days`), purge photos + draft/staged DO rows | -| 5 | `require_plate` toggle | **Office port only** (`is_office_request()`, default 5721) | -| 6 | Capture UI | Operator dashboard (`templates/operator.html`, port 5000). **Capture device: smartphone** browser camera (`accept="image/*" capture="environment" multi`); no native app, no desktop webcam required | -| 7 | Grouping | Auto-bucket staged DOs by plate; start **blocks** mixed non-empty plates | -| 8 | Plate gate | Runtime toggle: OFF = plate optional; ON = all attached DOs need plate | -| 9 | DO required to start | `do.require_do: true` (locked OK) — start needs ≥1 staged DO in **do_manual** | -| 10 | Storage | `jetson_counter.db` + local photo files under output dir | -| 11 | **Batch modes** | **3 modes**: **`auto` (default)** · `do_manual` (DO-gated flow) · `manual` (legacy start/stop, no DO gate — dev/fallback). **Mode switch POST: office port only**; GET mode: both ports | -| 11b | **Model mode (A–D)** | **Office only** (`is_office_request()`). **Remove** model-mode selector from `operator.html`; only `monitoring.html` (office) can POST `model_mode` | -| 12 | End-batch guard | Soft warn if activity in counting zone → **force** still allowed (do_manual stop; auto unchanged) | - -### 2.1 Batch mode matrix - -| Mode | Who opens/closes batch | DO gate | Notes | -|---|---|---|---| -| **`auto` (default)** | Existing FSM in `predict.py` / `src/batch.py` | n/a | **No behaviour change** vs today. Operator start/stop buttons hidden or inert. DO panel hidden. | -| **`do_manual`** | Operator start/stop only | Yes (`do.require_do`, plate rules) | New flow in this plan. Truck FSM ignored for open/close; ROI/truck detect still live. | -| **`manual`** | Operator start/stop only | No (legacy) | Existing manual buttons + `current_batch.json`; kept for dev experiments. | - -- Persist mode in **`batch_mode.json`** key `mode` (extend allowed values from - `auto|manual` → `auto|do_manual|manual`). -- **Default when file missing / invalid: `auto`.** -- `GET /api/batch/mode` — both ports (operator UI polls for badge / DO panel). -- `POST /api/batch/mode {"mode": ...}` — **office only** (`is_office_request()`); - operator POST → **403** (blocks `switchToManual()` on port 5000 as-is). -- Allowed values `auto|do_manual|manual`; unknown → 400. -- Mode switch while a batch is active: **409** until batch stopped — - avoids mixing auto-finalize with DO columns mid-batch. -- **`model_mode` (A–D)** on same endpoint: **office only** as well (locked #11b). - Operator must not POST either `mode` or `model_mode`. Office monitoring page - owns both switches; operator UI reads active model mode for badge only. -- `config.yaml` seed: `batch.default_mode: auto` (file-less first boot); runtime - still `batch_mode.json`. - -## 3. Non-goals - -- No pfm-ocr network auth, Postgres, or Flutter dependency. -- No change to YOLO modes A–D, counting line geometry, or batch FSM logic in `src/batch.py`. -- No auto open/close of batches from truck detector while mode is `do_manual` / `manual`. -- No multi-tenant / store accounts. -- No change to default production behaviour beyond **default mode string** - (`auto` remains the out-of-box runtime mode). - ---- - -## 4. Architecture - -Mode selects path; only **`do_manual`** uses the DO pipeline below: - -``` -batch_mode.json mode - ├─ auto (default) ──► predict.py existing FSM (no DO APIs required) - ├─ manual ──────────► operator start/stop, no DO gate (legacy) - └─ do_manual ───────► DO pipeline: - -operator.html (5000) - │ multi-photo capture (panel visible only when mode == do_manual) - ▼ -POST /api/do/upload ──► save JPEG under {output.dir}/do_photos/YYYY-MM-DD/ - │ OCR extract (tesseract|paddle) → draft row - ▼ -review/edit fields ──► PUT /api/do/ (no_do, plate, expected_sack, expected_box) - │ - ▼ -POST /api/batch/start {do_ids} ── validates gates ──► current_batch.json - │ + delivery_orders.status=attached - ▼ -predict.py manual branch (mode != auto) ── updates count/box_count in state file - │ - ▼ -GET /api/batch/stop-preview ── zone busy? ──► modal warn -POST /api/batch/stop ── finalize ──► batches + plate/do_numbers/expected_* - └── net_sack==0 AND net_box==0 → discard (no row) -``` - -### Expected qty (net) - -- Per DO line items (after OCR or manual entry), classify unit as sack-like - vs box-like (matched case-insensitive on the qty/unit token): - - | Class | Terms (add more if DOs use other spellings) | - |---|---| - | sack-like | `KRG`, `KARUNG`, `SACK`, `BG` | - | box-like | `BOX`, `CTN`, `KARTON`, **`DUS`**, **`KARDUS`** | - -- Store `expected_sack`, `expected_box` as **integer outer units**. -- Live variance on operator panel: - - sacks: `(loading_count − unloading_count)` vs `expected_sack` - - boxes: `(box_loading − box_unloading)` vs `expected_box` -- Same net formula written into history/export. - -### Discard rule (stop) - -- Compute `net_sack` and `net_box` at stop (see §7.3). -- **Discard** (no `batches` insert) only when `net_sack == 0` **and** `net_box == 0`. -- If either net is non-zero → **insert** the batch row (DO columns included), even - when the other class is zero (e.g. boxes only, no sacks). - ---- - -## 5. Data model - -### 5.1 New table `delivery_orders` - -```sql -CREATE TABLE IF NOT EXISTS delivery_orders ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - counting_date TEXT NOT NULL, - photo_path TEXT NOT NULL, -- relative to output.dir - no_do TEXT NOT NULL DEFAULT '', - plate TEXT NOT NULL DEFAULT '', - expected_sack INTEGER NOT NULL DEFAULT 0, - expected_box INTEGER NOT NULL DEFAULT 0, - ocr_raw TEXT, -- JSON: engine, raw text, confidence - status TEXT NOT NULL DEFAULT 'draft', -- draft|staged|attached|discarded - batch_id INTEGER, -- set when attached (informational) - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); -CREATE INDEX IF NOT EXISTS idx_do_date_status - ON delivery_orders(counting_date, status); -``` - -`counting_date` = same helper as batches (`get_counting_date()`), so photos -taken after cutoff belong to the operational day. - -### 5.2 `batches` additive columns - -```sql --- same PRAGMA table_info pattern as box_loading/model_mode migration -plate TEXT NOT NULL DEFAULT '', -do_numbers TEXT NOT NULL DEFAULT '[]', -- JSON array of No. DO -expected_sack INTEGER NOT NULL DEFAULT 0, -expected_box INTEGER NOT NULL DEFAULT 0, -net_sack INTEGER NOT NULL DEFAULT 0, -- loading - unloading at stop -net_box INTEGER NOT NULL DEFAULT 0 -``` - -Discard stop only when both nets are 0 → no row. Otherwise expected/net -columns are written with the batch. - -### 5.3 `current_batch.json` extra keys - -```json -{ - "manual_control": true, - "plate": "B 1234 XYZ", - "do_ids": [12, 13], - "do_numbers": ["DO-001", "DO-002"], - "expected_sack": 120, - "expected_box": 10 -} -``` - -`predict.py` already round-trips the whole dict; **no structural change** unless -finalize moves into predict (it does not — stop stays in dashboard). - -### 5.4 Photo tree - -``` -{output.dir}/do_photos/ - 2026-09-24/ - do_.jpg -``` - -Source: **smartphone** camera upload (see §9.1); server downscales before write. -Gitignored via existing `*.jpg` rule. Retention deletes files + rows ≤ now−7d. - -ERD (Mermaid) ships in **Phase 4** → `docs/do-erd.md`. - ---- - -## 6. Configuration - -### 6.1 `config.yaml` - -```yaml -# do = Delivery Order (surat jalan). Gated manual-batch scan flow. -# Used when batch mode == do_manual (mode itself lives in batch_mode.json). -batch: - default_mode: "auto" # auto | do_manual | manual (seed when batch_mode.json missing) - -do: - enabled: true - require_do: true # start needs ≥1 staged DO (do_manual mode) - require_plate: false # default; office UI can flip at runtime - retention_days: 7 - photo_dir: "do_photos" # relative to output.dir - max_photos_per_batch: 8 - zone_warn_seconds: 3 # stop soft-warn: recent count activity - ocr: - engine: "tesseract" # tesseract | paddle | none — YAML seed only - # Runtime override: do_settings.json ocr_engine (UI toggle, both ports). - # paddle = PaddleOCR-level backup (same extract_do_fields dispatch); - # use when tesseract field accuracy fails acceptance. -``` - -Note: existing `batch:` key already holds `timeout_seconds` / `daily_cutoff_time` — -**add** `default_mode` under it; do not duplicate the `batch:` key. - -### 6.2 Runtime settings (not config.yaml) - -| Store | Contents | Writer | -|---|---|---| -| `{output.dir}/batch_mode.json` | `mode`: `auto` \| `do_manual` \| `manual` (**default `auto`**) | **`POST /api/batch/mode` office only** | -| `{output.dir}/do_settings.json` | `require_plate`, `require_do` (office-only write); **`ocr_engine`** (`tesseract`\|`paddle`\|`none`, **both ports write**) | See §7 settings auth | - -- Existing `_read_batch_mode_file()` defaults change: missing file → - `mode = CFG.batch.default_mode` or `"auto"` (not `"manual"`). -- Invalid/legacy values: map unknown → `auto` + warn; keep ignoring legacy - `model_mode` key as today. -- Mode switch API: `mode not in ["auto", "do_manual", "manual"]` → 400; - `not is_office_request()` on POST when `mode` in body → **403**. -- POST with **`mode` and/or `model_mode`**: **office only** (403 on operator port - when either write field is present). GET unchanged on both ports. - -Auth on settings writes: - -- `POST /api/do/settings` **`ocr_engine`**: **both ports allowed** (operator and - office); same file → both UIs stay in sync on next `GET`. -- `POST /api/do/settings` **`require_plate` / `require_do`**: **403** if not - `is_office_request()`. -- `POST /api/batch/mode` with `mode` (and/or `model_mode`): **403** if not - `is_office_request()`. -- Unknown `ocr_engine` value → 400; missing deps for `paddle` still explicit - error on next upload (§8), not on settings write. - -### 6.3 `src/config_loader.py` - -Add `DoConfig` + `DoOcrConfig` dataclasses; parse `do:` block; defaults if key absent -(missing-file behaviour unchanged). - ---- - -## 7. Backend API (`counter_dashboard.py`) - -| Method | Path | Auth / port | Behaviour | -|---|---|---|---| -| `GET` | `/api/batch/mode` | both | Effective mode (`auto` \| `do_manual` \| `manual`) + `mode_editable` | -| `POST` | `/api/batch/mode` | **office only** when body has `mode` and/or `model_mode` | Persist; **403** if operator sends either; **409 if batch active** (for `mode`); 400 if unknown | -| `POST` | `/api/do/upload` | operator 5000 | **Only when mode == `do_manual`** (else 409). `multipart/form-data` field `photos` (1..N); validate type/size; save under dated dir; run OCR engine; insert `draft` rows; return `{items:[{id, photo_url, no_do, plate, expected_sack, expected_box, ocr_text}]}` | -| `GET` | `/api/do/photo/` | operator | Serve photo bytes (path from row; no traversal) | -| `GET` | `/api/do/staged` | operator | Rows `status IN (draft,staged)` for `get_counting_date()` | -| `PUT` | `/api/do/` | operator | Edit fields; set `status=staged` when saved | -| `DELETE` | `/api/do/` | operator | Remove row + photo (or mark discarded) | -| `GET` | `/api/do/settings` | both | Effective flags + **`ocr_engine`** (poll both UIs) | -| `POST` | `/api/do/settings` | **split**: `ocr_engine` → **both ports**; `require_plate`/`require_do` → **office only** (403) | Persist; 400 unknown engine value | -| `POST` | `/api/batch/start` | operator | **Mode-dependent:** `do_manual` → body `{do_ids}` + DO gates; `manual` → legacy no-DO start (existing); `auto` → **409** (FSM owns lifecycle) | -| `GET` | `/api/batch/stop-preview` | operator | `{active, zone_busy, sacks_in_zone, boxes_in_zone}` — used for **do_manual** (and optional **manual**) stop UI | -| `POST` | `/api/batch/stop` | operator | **`do_manual`/`manual` only** (auto: 409). Finalize + DO columns when present; discard only if **both** `net_sack==0` and `net_box==0` | -| `GET` | `/api/current-batch` | both | Extend with plate, do_numbers, expected_*, net so far, `mode` | - -### 7.1 Start gates (`mode == do_manual` + `do.enabled`) - -Reject `400` with machine-readable `reason` + Indonesian `message`: - -1. Batch already active (existing). -2. Mode is not `do_manual` (e.g. `auto` / `manual`) → 409 `wrong_mode`. -3. `require_do` and zero DOs selected/staged. -4. Any selected DO missing `no_do`. -5. If `require_plate`: any selected DO missing `plate`. -6. If ≥2 DOs with **non-empty** plates and plates **disagree** → block (list plates). -7. Empty plates when `require_do` and not `require_plate`: allowed; group shows “tanpa plat”. - -On success: - -- Build `batch_state` as today + `"batch_mode": "do_manual"` + DO fields - (union plate if all equal, else `""`). -- Mark selected DOs `status=attached`; keep `batch_id = NULL` until stop; store - `do_ids` only in state file. -- Atomic write `current_batch.json` (existing tmp+replace). - -**Legacy `manual` mode:** existing start/stop without DO gates (when -`mode == manual`). Start/stop-preview soft-warn optional same as do_manual. - -**`auto` mode:** start/stop manual APIs return 409; FSM only. - -### 7.2 Stop soft-warn - -`stop-preview` sources (lazy OR): - -- `live_status.json` / recent `current_batch` counts changing (fallback: sacks_in_zone unknown → no warn). -- Optional: dashboard reads last annotated frame only for display — **not required**. -- If `sack_count > 0` and `last_detection_time` within `do.zone_warn_seconds` (default 3s) → `zone_busy: true`. - -UI: modal “Masih ada aktivitas di zona (N karung) — akhir paksa?” → second confirm → same `POST /api/batch/stop` with `force=true` (force only skips client-side block; server always allows stop). - -### 7.3 Finalize SQL (stop) + discard - -```sql --- only when NOT (net_sack == 0 AND net_box == 0): -UPDATE batches SET - plate=?, do_numbers=?, expected_sack=?, expected_box=?, - net_sack=?, net_box=?, - box_loading=?, box_unloading=?, model_mode=? -WHERE counting_date=? AND batch_number=? AND camera_name=? AND object_label=?; -``` - -- `net_sack = count − unloading`, `net_box = box_count − box_unloading` - (values from `current_batch.json` at stop). -- **Discard path (dashboard stop, modes `do_manual`/`manual`):** if - `net_sack == 0` and `net_box == 0` → no `batches` write. Either non-zero → row. -- **`predict.py` `finalize_batch` (auto path) + dashboard stop:** - project counts **sacks and boxes** — discard only when **both** nets are 0 - (`net_sack == 0 and net_box == 0`). Today `finalize_batch` discards on - `final_count == 0` alone (sack only) → **must change** so box-only batches - (auto and do_manual) are kept. Rows always can store sack count **and** - box_loading/box_unloading columns. -- Need sack `unloading` on state file: **add** it in operator-driven branch - alongside `count` (`box_unloading` already written); for auto finalize, pass - net values the same way when updating columns. - ---- - -## 8. OCR design - -Single entry: `extract_do_fields(image_path, engine) -> dict` in `src/do_ocr.py`. -Engine = effective `ocr_engine` from `do_settings.json` (fallback YAML - `do.ocr.engine` / `tesseract`) — **tesseract | paddle | none**. Upload reads - engine once per request so a UI flip applies to the next photo without restart. - Settings `ocr_engine` wins over YAML when present. - -### Primary: Tesseract, CPU - -- Dependency: system `tesseract-ocr` + data (`eng` minimum; `ind` if available), - Python `pytesseract` in `requirements.txt`. -- Preprocess: OpenCV grayscale → Otsu → optional deskew (minAreaRect on text mask). -- Extract with regexes (inspired by pfm-ocr patterns, simplified): - - DO number: `No.\s*DO\s*[:#]?\s*([A-Z0-9\-/]+)` and loose 10-digit fallback. - - Plate: Indonesian plate pattern + uppercase normalize. - - Table lines: token scan for unit words (sack-like / box-like table in §4) - + adjacent integers → sums. -- Response is **draft only** — operator review is mandatory UI step. -- If `tesseract` binary missing (`engine: tesseract`) or `engine: none` → empty - draft, UI banner “OCR tidak aktif — isi manual”; upload still works. - Missing paddle deps with `engine: paddle` → explicit error, no silent fallback. -- Inference off GPU; **must not** run inside `predict.py` loop — dashboard-side - only, on upload request. - -### Backup: PaddleOCR-level (`engine: paddle`) - -- **Available as a first-class runtime value**, not a deferred redesign: - `ocr_engine: paddle` (settings file or YAML seed) selects the same - `extract_do_fields()` path with a PaddleOCR (PP-DocLayout / - PaddleOCR-VL-class) implementation behind it. -- Purpose: higher field accuracy when Tesseract fails acceptance - (≤30 s operator correction budget) or DO print quality is poor. -- Deploy note: heavier install (Python wheels / optional GPU); document in - `docs/deployment.md` as **optional package**, default image stays Tesseract-only. -- Swap = **UI toggle** (office monitoring **and** operator DO panel) writing - `do_settings.json` `ocr_engine` — both ports, same file, next `GET` syncs the - other page. **No process restart; no API contract change.** -- Missing paddle deps with selected engine → explicit error on upload, no - silent fallback; settings write still 200 (so UI can show engine, upload - banner explains missing package). -- Phase 2 ships the dispatch + Tesseract + both-port `ocr_engine` toggle; - Paddle backend module lands when binary/deps validated on Jetson (stub - raises clear error if engine selected but package missing). - -Acceptance for keeping Tesseract: operator completes a real DO in **≤30s** -including corrections. If not → flip engine to paddle from either UI. - ---- - -## 9. UI - -### 9.0 Mode display + switch (split by port) - -| Surface | Mode control | -|---|---| -| **`monitoring.html` (office 5721)** | **3-way switcher** (Otomatis / DO Manual / Manual legacy) → `POST /api/batch/mode {mode}`; handles 403/409 toasts. Primary place to enter `do_manual`. Default UI state **`auto`** (today’s `currentBatchMode = 'manual'` JS default must change). | -| **`operator.html` (5000)** | **Read-only badge** current mode. `GET` only. | -| Operator auto→manual banner | **Remove** `switchToManual()` POST (would 403). Replace with info: “Mode diatur dari monitoring/kantor” + dismiss. | -| Operator start/stop | Per mode: `auto` hidden/disabled; `manual` legacy buttons; `do_manual` DO panel + buttons. | -| Operator model A–D select | **Remove** (locked #11b) — office monitoring only. | -| OCR engine toggle | **Both** monitoring + operator: `POST /api/do/settings {ocr_engine}` allowed both ports; sync via settings poll. | - -- `GET /api/batch/mode` → `{mode, model_mode, mode_editable, model_mode_editable}` - (`*_editable=true` only on office). -- **Operator model-mode selector removed** (locked #11b): no `#modelModeSelect` - / `setModelMode()` POST from `operator.html`; show read-only active model mode - if desired (optional badge from same GET / `/api/model-modes`). -- Office monitoring owns **batch mode + model mode** switches; **both** pages own `ocr_engine` (§9.1.8). -- Switch blocked while batch active (409 → toast) for batch `mode`. - -### 9.1 DO panel (only when operator sees `mode === 'do_manual'`) - -1. **Ambil Foto DO (smartphone)** — ``. - - Primary device: **operator’s phone** opening `http://…:5000/operator` (same LAN - or reachable host). HTML5 camera capture; multiple selects supported where the - mobile browser allows, else sequential single captures into the same strip. - - Landscape preferred for table rows; UI accepts portrait (preview crop hint only). - - Photo size: downscale server-side to max edge ~1600–2048 px before save/OCR - (phone cameras are large; keeps 7-day disk + upload time small). - - Works on mobile Safari/Chrome via the web page — **not** a Flutter/native client. -2. **Thumbnail strip** + per-DO review card: - - No. DO, Plat, Ekspektasi karung, Ekspektasi box - - Save / Delete -3. **Grup preview** — bucket by plate; chips “B 1234 · 2 DO · 80 karung · 10 box”; - warnings: mixed plates, missing plate, missing DO#. -4. **Start modal** — table of DOs to attach + expected totals; disable confirm with reason list. -5. **Active batch card** — plate, DO numbers, live: - - Karung: `net / expected_sack` - - Box: `net_box / expected_box` -6. **Stop modal** — if `stop-preview.zone_busy`: extra force button styling. -7. **Office-only `require_plate`** — show when `GET /api/do/settings` → - `{editable: true}` (office); hide when `editable: false` (403 on POST from 5000). -8. **OCR engine toggle (`ocr_engine`)** — 3-way (Tesseract / Paddle / Off): - - **Both** `operator.html` (DO panel, near capture) **and** - `monitoring.html` (office DO/settings block) get the same control. - - Write: `POST /api/do/settings {"ocr_engine": ...}` — **allowed on both - ports** (unlike `require_plate`). - - Sync: both pages poll `GET /api/do/settings` (existing settings poll or - same DO-panel interval) → flip on one device appears on the other without - reload. No websocket needed. - - Badge/label shows effective engine; upload failure for missing paddle - package shows inline error on whichever page captured the photo. - -History (`history.html`): columns plate, DO list, expected, net, variance when present. - ---- - -## 10. `predict.py` touchpoints (minimal) - -| Location | Change | -|---|---| -| Mode read each frame (`batch_mode.json`) | Default missing/invalid → **`auto`** (was hard-coded `"manual"`). Accept `do_manual` as a third value; treat **any non-`auto`** as “operator-driven” for start/stop file polling (same as today’s manual branch). | -| Manual/do_manual batch active loop (`count` / `box_count`) | Also persist `unloading` (sack) next to `box_unloading` for net-at-stop. Prefer JSON fields. | -| `do_manual` DO gates | **None in predict.py** — gates live in dashboard start API only. | -| Startup load of state file | No change (opaque dict). | -| Auto mode | **No change** to FSM when `mode == auto`, except `finalize_batch` discard aligns to both-net rule (§7.3). | -| Truck detect interval / ROI | **No change** (all modes). | - -If `unloading` already tracked only in memory as `counter.unloading_count`, -mirror it into `active_batch_info` wherever `count` is written (2 places in -operator-driven branch). - -`predict.py` mode check becomes three-way: - -```python -mode = bm_data.get("mode", "auto") # after config default_mode seed -current_batch_mode = mode if mode in ("auto", "do_manual", "manual") else "auto" -# branches: if current_batch_mode == "auto": FSM else: state-file manual/do_manual -``` - ---- - -## 11. Retention - -- On dashboard start + hourly `threading.Timer`/`loop`: - - cutoff = today − `retention_days` (calendar dates under `do_photos/`). - - `DELETE FROM delivery_orders WHERE counting_date < cutoff AND status != 'attached'` - (attached rows keep photo path optional — **also purge photos** for cutoff - dates always; keep DB row for audit with `photo_path` cleared or left dangling → - prefer `UPDATE ... photo_path=''` after file delete). - - `shutil.rmtree` old date folders. -- Log summary counts at `print`/`logging`. -- Config: `do.retention_days: 7`. - -Disk context (this Jetson): root `/` 27G free; photos are small JPEGs — 7 days -negligible. Prefer photos under `/opt/jetson-counter/do_photos` (same volume as DB). - ---- - -## 12. Testing - -CI stays pure-Python (no cv2/tesseract required): - -| Test file | Covers | -|---|---| -| `tests/test_do_batch.py` | start gates: wrong mode, no DO, missing plate when required, mixed plates, plate optional; group-by-plate helper; net formula; discard only when both nets 0 (dashboard stop + `finalize_batch` helper) — **sack and box**; sack/box unit token classify (`dus`/`kardus` → box); retention date cutoff; mode value validation (`auto`/`do_manual`/`manual`, default auto); **mode POST 403 from operator port, 200 from office**; **model_mode POST 403 from operator, 200 from office**; **`ocr_engine` POST 200 from operator *and* office, 400 unknown value**; **`require_plate` POST still 403 from operator** (Flask test client + Host header) | -| `tests/test_config_loader.py` | extend: `do:` block defaults + overrides; `batch.default_mode` | -| Existing counting/batch tests | must stay green (no regression) | - -Manual / device checks (not CI): - -1. Fresh deploy / missing `batch_mode.json` → mode reads **`auto`**; FSM counts; DO panel hidden. -2. **Office** switches to `do_manual` (no active batch) → operator panel shows DO UI after poll. -3. **Operator** `POST /api/batch/mode` → **403**; banner no longer posts (info only). -4. Office switches to `manual` → legacy start/stop, no DO panel. -5. Mode switch while batch active → 409 / blocked in UI. -6. `do_manual`: stage 2 DOs same plate → start → count → stop → row has plate + do_numbers + expected. -7. Different plates → start blocked. -8. Office port toggles `require_plate`; operator port 403. -9. Stop while “zone busy” → force path. -10. Stop with both nets 0 → no batches row; boxes-only (net_box>0, net_sack=0) → row written. -11. Purge: backdate folder → restart → gone. -12. `POST /api/batch/start` in `auto` mode → 409. -13. Operator `POST` `model_mode` from port 5000 → **403**; no selector on operator page; office monitoring can change A–D. -14. **Smartphone** on site Wi‑Fi: open `:5000/operator`, capture ≥2 DO photos, review, stage, start in `do_manual`. -15. After Phase 4–5: `docs/do-erd.md` Mermaid renders; docs/CHANGELOG updated; push to origin visible in `git log`. -16. Toggle `ocr_engine` from operator `:5000` → office monitoring shows same value on next poll (and reverse); `require_plate` still 403 from operator. - -Commands: - -```bash -python -m pytest tests/ -q -python -m compileall predict.py counter_dashboard.py src -``` - ---- - -## 13. Deployment - -- `deploy_to_jetson.py`: already syncs `counter_dashboard.py`, `operator.html`, - `config.yaml` — **also sync** `templates/monitoring.html` (office mode switcher); - no new service. -- Jetson packages (primary): `sudo apt install tesseract-ocr tesseract-ocr-ind` - (document; CI does not install it). -- Optional backup OCR: PaddleOCR stack when UI sets `ocr_engine: paddle` — - separate install notes in `docs/deployment.md`; not required for default deploy. -- Restart: `sudo systemctl restart karung-counter-dashboard` (and counter if - `predict.py` changed). -- `.gitignore`: ensure `do_photos/` under output dir only (already outside repo - when `output.dir=/opt/jetson-counter`). - ---- - -## 14. Phases & file list - -### Phase 1 — usable end-to-end (no OCR accuracy dependency) - -- [x] `src/config_loader.py` — `DoConfig`; `batch.default_mode` default `"auto"` -- [x] `config.yaml` — `do:` block + `batch.default_mode: auto` -- [x] `batch_mode.json` semantics — 3 modes, default **auto**; `POST /api/batch/mode`: - office-only for `mode` **and** `model_mode`, 409-when-active, 400 unknown -- [x] `predict.py` — mode default **auto**; accept `do_manual`; persist `unloading` - in operator-driven state JSON; **`finalize_batch` discard = both nets 0** - (sack **and** box) -- [x] `counter_dashboard.py` — migrations, DO CRUD, start gates (do_manual only), stop-preview, - settings API: office-only `require_plate`/`require_do`, **both-port `ocr_engine`**; - extended start/stop/current-batch, retention job; start/stop 409 in auto; - **403 model_mode POST from operator**; photo accept/resize for **smartphone** uploads -- [x] `templates/monitoring.html` — office 3-way **batch** mode switcher + **model mode A–D** (primary); - **`ocr_engine` toggle** (syncs with operator) -- [x] `templates/operator.html` — read-only mode badge; **remove** `switchToManual` POST - **and** `#modelModeSelect` / `setModelMode()`; DO panel only in do_manual; - **smartphone capture** input + mobile-friendly review strip; - **`ocr_engine` toggle** (same settings poll as office) -- [x] `templates/history.html` + export columns -- [x] `tests/test_do_batch.py` + mode tests -- [ ] Manual check: **real phone** → `:5000/operator` → multi-photo DO flow on Wi‑Fi - -### Phase 2 — OCR assist - -- [x] `src/do_ocr.py` — `extract_do_fields()` + **tesseract** and **paddle** - dispatch (`none` short-circuit); paddle raises clear error if deps missing; - engine from `do_settings.json` → YAML fallback per upload request -- [x] `requirements.txt` — `pytesseract` -- [x] Wire into `/api/do/upload` (read effective `ocr_engine` each request) -- [x] UI: confirm **both** pages’ `ocr_engine` toggle wired to settings POST/GET - (control markup can land Phase 1; behavior verified here) -- [x] Unit tests with fixture image **optional** (skip if no binary) - -### Phase 3 — documentation update - -- [x] `docs/configuration.md` — `do:` / `batch.default_mode`, office-only mode + model_mode, - both-port `ocr_engine` runtime override -- [x] `docs/deployment.md` — tesseract packages, paddle optional, smartphone operator URL, - photo dir + 7-day retention, restart steps -- [x] `docs/architecture.md` — three batch modes; DO pipeline branch vs auto FSM -- [x] `README.md` — short DO manual flow + pointer to plan/ERD -- [x] `AGENTS.md` — mode matrix, office-only POST rules (mode/model_mode/require_*), - both-port `ocr_engine`, discard both-nets -- [x] `CHANGELOG.md` — dated entry (Keep-a-Changelog style) -- [x] `docs/do-batch-implementation-plan.md` — mark phases done / status line when shipping - -### Phase 4 — entity–relationship diagram (Mermaid) - -- [x] Create **`docs/do-erd.md`** (new file) with Mermaid `erDiagram` covering: - - `batches` (+ plate, do_numbers, expected_sack/box, net_sack/box, count, box_*) - - `delivery_orders` (status, photo_path, expected_*, no_do, plate) - - `daily_summaries` - - logical link: `delivery_orders` N—1 batch via `do_numbers` on stop / - state-file `do_ids` while active (document as **soft** FK — no hard FK required) -- [x] Note JSON sidecars: `current_batch.json`, `batch_mode.json`, `do_settings.json`, - photo tree `{output.dir}/do_photos/YYYY-MM-DD/` -- [x] Link ERD from `docs/architecture.md` + plan §5 - -### Phase 5 — commit and push - -- [x] `python -m pytest tests/ -q` green; `python -m compileall` clean -- [ ] Review `git status` / `git diff` — **no** state junk (`.env`, `*.db`, `*.jpg`, - `*.engine`, `batch_mode.json`, photos) — follow `.gitignore` / AGENTS “don’t commit state” -- [ ] Stage only intended files (code, tests, `config.yaml`, templates, docs, plan, ERD) -- [ ] Commit message(s) conventional/repo style, e.g. - `feat: DO-gated manual batch mode (auto default, office-only switches)` - — split docs-only vs feat if cleaner -- [ ] `git pull --rebase` if needed; `git push` to `origin` main - (`https://git.proit.id/andrew/karung-counting-feedmill-semarang`) -- [ ] If deploy follows: `python deploy_to_jetson.py` then systemctl restart - (services listed in §13) — **only** after explicit deploy request - -### Phase 6 — hardening (post-ship) - -- [ ] PaddleOCR backend validated on Jetson (if Tesseract acceptance fails or - field requests it) — flip `ocr_engine: paddle` from either UI -- [ ] Soft-warn polish from field feedback -- [ ] Variance coloring on history/analytics - -**Deferred / not in v1:** DB purge of old `batches` rows, multi-camera, auto -plate from ROI (impossible without ALPR), native mobile app. - ---- - -## 15. Risks & mitigations - -| Risk | Mitigation | -|---|---| -| Tesseract accuracy poor on feedmill DO | Review UI mandatory; flip `ocr_engine: paddle` from office **or** operator UI (shared `do_settings.json`); fields always editable | -| GPU contention | Tesseract path never in `predict.py`; CPU only. Paddle path also dashboard-only | -| Operator skips photos (`require_do` false) | Default `require_do: true` in production YAML (applies only in `do_manual`) | -| Mode left on `do_manual` overnight | Default seed **`auto`**; UI shows current mode badge; office flips back to auto | -| Accidental start/stop in auto | Manual APIs return 409; buttons hidden | -| Operator POSTs mode or model_mode (old UI/scripts) | **403** on operator port; remove `switchToManual()` **and** operator model selector | -| `ocr_engine` flipped wrong / paddle deps missing | Settings write still 200 (UI shows choice); upload returns clear error — no silent fallback | -| Box-only batch dropped by sack-only finalize check | Locked #1/#3: both nets required for discard; counts store sack **and** box | -| Mixed plates forced by mistake | Hard block + clear message; office can temporarily disable plate req | -| Photo disk fill | 7-day purge on `/opt/jetson-counter`; server-side downscale on phone upload | -| Phone camera too large / slow upload | Max-edge resize + JPEG quality cap before store/OCR | -| Both nets 0 drops audit | Accepted decision #1; boxes-only batches **kept**; monitor logs if frequent | -| `unloading` missing at stop | Phase 1 writes it every counter update | - ---- - -## 16. Acceptance criteria - -1. Fresh start with no `batch_mode.json` → mode is **`auto`**; FSM behaves as today. -2. **Office** can switch **auto ↔ do_manual ↔ manual** when no batch is active; - **operator POST mode → 403**; blocked while active (409). -3. In `do_manual`: operator stages ≥1 DO photo, edits fields, starts batch only when gates pass. -4. Two DOs same plate → one batch; both `do_numbers` stored on stop. -5. Two different plates → start rejected. -6. Plate required toggle and model mode A–D change only from office port; - **`ocr_engine` toggle allowed on both ports** and stays in sync on next poll. -7. Live panel shows net sack/box vs expected during batch (`do_manual`). -8. Stop with zone activity asks confirm; force completes. -9. Stop with `net_sack=0` **and** `net_box=0` → no `batches` row; either net - non-zero → row written with DO columns. -10. Photos older than 7 days removed automatically. -11. `manual` (legacy) start/stop works without DO requirements. -12. `auto` mode: manual start/stop APIs 409; DO panel hidden; counting unchanged. -13. Operator **cannot** change model mode A–D (no selector; POST 403); office can. -14. Discard rule same for dashboard stop **and** `predict.py` `finalize_batch` - (auto): keep row if **either** sack net or box net ≠ 0. -15. **Smartphone** can open operator page, capture multi DO photos, review, upload. -16. `docs/do-erd.md` exists with Mermaid ERD; docs/CHANGELOG updated (Phase 3). -17. Changes committed and pushed to origin (Phase 5) after green tests. -18. `python -m pytest tests/ -q` green in CI (no new hard deps). -19. `ocr_engine` POST **200 from operator and office**; other page shows new value on next `GET`; `require_plate` still 403 from operator. diff --git a/docs/operator-tiles-history-plan.md b/docs/operator-tiles-history-plan.md new file mode 100644 index 0000000..3156eba --- /dev/null +++ b/docs/operator-tiles-history-plan.md @@ -0,0 +1,137 @@ +# Implementation Plan — Operator Tiles + History Cleanup (DO Expected Labels) + +> Status: **implemented** (UI-only; manual device checks pending). +> Scope: **UI labels/columns only** — no API/DB/predict semantics change. +> Ports (locked): **5721 = office** · **5000 = operator** (DO manual uploads, legacy manual, live counts). +> Operator does **not** need history; history/analytics stay **office-only** (`/history`, `/analytics` already gated in `counter_dashboard.py`). + +--- + +## 1. Why + +Current operator tiles in `do_manual` show **Net / Target** next to **Karung Masuk / Box Masuk**. When unloading is 0 (normal load-out), net == gross → redundant and confusing. + +History has a **Net** column that duplicates sacks/boxes when net ≈ count, and **Expected** has no unit hint (sack vs box). + +**Goal:** operator compares **counted** vs **DO paper qty** in four clear tiles. History drops Net; Expected is labeled as DO sacks/boxes. + +--- + +## 2. Locked decisions + +| # | Topic | Decision | +|---|---|---| +| 1 | Port split | 5721 office (mode/model switches, history, analytics, plate require) · 5000 operator (start/stop, DO photo upload, live counts). Unchanged. | +| 2 | Operator history | Operator does not consume history/count analytics; no history link/panel on `operator.html`. History page stays office-only. | +| 3 | Operator tiles | Four tiles only (see §3). **No “Net …” tiles** on operator UI. | +| 4 | DO expected naming | UI name = **DO Sacks** / **DO Boxes** (paper expected). Server fields stay `expected_sack` / `expected_box`. | +| 5 | History columns | Cleaner set (§4). **Remove Net column.** Expected labeled as DO sacks/boxes. | +| 6 | Backend NET | **Keep** `net_sack` / `net_box` in API, `current_batch.json`, DB, discard rule (`both nets 0`), exports if still needed for audit. **UI stop showing net on operator.** | + +--- + +## 3. Operator tiles (`templates/operator.html`) + +Replace current info grid rows with: + +| Tile label | Value | Source (`GET /api/current-batch`) | +|---|---|---| +| **Sacks** | counted sacks | `count` (same as today’s “Karung Masuk”) | +| **Boxes** | counted boxes | `box_count` (same as “Box Masuk”) | +| **DO Sacks** | expected sacks | `expected_sack` (0 if no batch/no DO) | +| **DO Boxes** | expected boxes | `expected_box` | + +Render rules: + +- **All four tiles always visible** on the operator card (not only in `do_manual`). +- Non-`do_manual`: DO Sacks / DO Boxes show `0` or `--` (no staged expectation); still show label so layout is stable. +- `do_manual` + active batch: DO tiles = values from batch state (start sum of staged DOs). +- Remove rows/IDs: `opNetSackItem`, `opNetBoxItem`, and JS that writes `net_sack` / `net_box` into those tiles. +- Keep: Batch Berjalan, Waktu Mulai, Plat / No. DO (batch active), mode badge, DO panel. + +DO card form labels (same vocabulary): + +- `Ekspektasi karung` → **`DO Sacks`** (or `DO Sacks (karung)` if Indonesian preferred — pick one at implement; default English matches tiles). +- `Ekspektasi box` → **`DO Boxes`**. + +Group preview chips unchanged (already “N karung · M box”). + +Optional polish (same file, if trivial): under Sacks show `of DO Sacks` only in `do_manual` — **not** a second Net row. + +--- + +## 4. History (`templates/history.html`) — office only + +Current header: + +`Batch # | Plate | No. DO | Sacks | Boxes | Expected | Net | Start | End | Duration` + +**New header (no Net, clearer Expected):** + +| Column | Content | +|---|---| +| Batch # | `batch_number` | +| Plate | `plate` or `—` | +| No. DO | `do_numbers` joined or `—` | +| Sacks | `count` (gross loaded; stored as `count`) | +| Boxes | `box_loading` | +| DO Sacks / Boxes | `expected_sack / expected_box` e.g. `100 / 12` | +| Start | time | +| End | time | +| Duration | minutes | + +- Remove cell that prints `net_sack / net_box`. +- API: keep returning nets if already present; **UI ignores them** (or stop emitting in `day-detail` later — not required). +- colspan updates (loading, empty, error rows). +- Export day XLSX: drop Net columns if present; rename Expected headers to `DO Sacks`, `DO Boxes` (match UI). + +No changes to operator gate: `GET /history` still office-only. + +--- + +## 5. Non-goals + +- No change to `net_counts` / discard-both-nets / `finalize_batch`. +- No change to ports, office-only POSTs, OCR engine toggle. +- No history page for operator. +- No API rename of `expected_*` (JSON keys stable). + +--- + +## 6. Files + +| File | Change | +|---|---| +| `templates/operator.html` | Four tiles Sacks/Boxes/DO Sacks/DO Boxes; remove Net tiles + JS | +| `templates/history.html` | Drop Net column; Expected → `DO Sacks / DO Boxes` | +| `counter_dashboard.py` | Optional: export headers only (if export shows Net/Expected) | +| `docs/do-expected-tiles-plan.md` | This plan | +| Old plan | Delete `docs/do-batch-implementation-plan.md` (superseded by shipped feature + this UI plan) | + +--- + +## 7. Test / verify + +```bash +python -m pytest tests/ -q +python -m compileall predict.py counter_dashboard.py src tests +``` + +Manual (office vs operator): + +1. `:5000/operator` — tiles labeled Sacks, Boxes, DO Sacks, DO Boxes; no “Net …” labels. +2. `:5000/operator` in `do_manual` — start with DO → DO Sacks/Boxes = sum of DO fields; Sacks/Boxes track camera. +3. `:5721/history` — columns as §4; no Net; Expected shows `n / m`. +4. Operator browser → `/history` → still operator page (office gate). +5. Mode/model POST from `:5000` still 403 (unchanged). + +--- + +## 8. Acceptance + +1. Operator live card shows exactly: Sacks, Boxes, DO Sacks, DO Boxes (+ batch meta). +2. No operator-visible string “Net Karung”, “Net Box”, “Net / Target”. +3. History table has no Net column; Expected header communicates DO sacks/boxes. +4. History remains office-only. +5. Discard rule and DB `net_*` columns unchanged. +6. pytest + compileall green. diff --git a/templates/history.html b/templates/history.html index 2de67ce..1907d5b 100644 --- a/templates/history.html +++ b/templates/history.html @@ -265,8 +265,7 @@ No. DO Sacks Boxes - Expected - Net + DO Sacks / Boxes Start Time End Time Duration (Min) @@ -274,7 +273,7 @@ - + Silakan pilih tanggal dari daftar sebelah kiri untuk memuat detail batch. @@ -358,7 +357,7 @@ const tableBody = document.getElementById('batchesTableBody'); tableBody.innerHTML = ` - + Memuat data batch untuk tanggal ${date}... @@ -384,7 +383,7 @@ if (!data.batches || data.batches.length === 0) { tableBody.innerHTML = ` - + Tidak ada batch tercatat pada tanggal ${date}. @@ -401,7 +400,6 @@ const dos = (b.do_numbers || []).join(', ') || '—'; const plate = b.plate || '—'; const expected = (b.expected_sack || 0) + ' / ' + (b.expected_box || 0); - const net = (b.net_sack ?? b.count) + ' / ' + (b.net_box ?? b.box_loading ?? 0); tr.innerHTML = ` Batch #${b.batch_number} @@ -410,7 +408,6 @@ ${b.count.toLocaleString()} ${(b.box_loading || 0).toLocaleString()} ${expected} - ${net} ${startStr} ${endStr} ${b.duration_minutes || 0} min @@ -422,7 +419,7 @@ console.error('Failed to load date details:', err); tableBody.innerHTML = ` - + Gagal memuat detail data: ${err.message} diff --git a/templates/operator.html b/templates/operator.html index 121f3b4..f2eb9a4 100644 --- a/templates/operator.html +++ b/templates/operator.html @@ -366,20 +366,20 @@
--:--:--
-
Karung Masuk
+
Sacks
0
-
Box Masuk
+
Boxes
0
-