This commit is contained in:
1 parent
0efcfa45b8
commit
1e4dc4201c
7 files changed
+172
-729
No files matched your search
@@ -102,7 +102,6 @@ POST). **DO manual flow:** open `http://<jetson>: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
|
||||
+10
-13
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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/<id> (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_<uuid>.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/<id>` | 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/<id>` | operator | Edit fields; set `status=staged` when saved |
|
||||
| `DELETE` | `/api/do/<id>` | 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)** — `<input type="file" accept="image/*" capture="environment" multiple>`.
|
||||
- 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.
|
||||
@@ -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.
|
||||
@@ -265,8 +265,7 @@
|
||||
<th>No. DO</th>
|
||||
<th>Sacks</th>
|
||||
<th>Boxes</th>
|
||||
<th>Expected</th>
|
||||
<th>Net</th>
|
||||
<th>DO Sacks / Boxes</th>
|
||||
<th>Start Time</th>
|
||||
<th>End Time</th>
|
||||
<th>Duration (Min)</th>
|
||||
@@ -274,7 +273,7 @@
|
||||
</thead>
|
||||
<tbody id="batchesTableBody">
|
||||
<tr>
|
||||
<td colspan="10" class="no-data">
|
||||
<td colspan="9" class="no-data">
|
||||
<i class="fa-solid fa-arrow-left"></i>
|
||||
Silakan pilih tanggal dari daftar sebelah kiri untuk memuat detail batch.
|
||||
</td>
|
||||
@@ -358,7 +357,7 @@
|
||||
const tableBody = document.getElementById('batchesTableBody');
|
||||
tableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="10" class="no-data">
|
||||
<td colspan="9" class="no-data">
|
||||
<i class="fa-solid fa-circle-notch fa-spin"></i>
|
||||
Memuat data batch untuk tanggal ${date}...
|
||||
</td>
|
||||
@@ -384,7 +383,7 @@
|
||||
if (!data.batches || data.batches.length === 0) {
|
||||
tableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="10" class="no-data">
|
||||
<td colspan="9" class="no-data">
|
||||
<i class="fa-solid fa-inbox"></i>
|
||||
Tidak ada batch tercatat pada tanggal ${date}.
|
||||
</td>
|
||||
@@ -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 = `
|
||||
<td style="font-weight: 600; color: var(--accent-blue);">Batch #${b.batch_number}</td>
|
||||
@@ -410,7 +408,6 @@
|
||||
<td style="font-weight: 500;">${b.count.toLocaleString()}</td>
|
||||
<td style="font-weight: 500;">${(b.box_loading || 0).toLocaleString()}</td>
|
||||
<td style="font-size:12px;">${expected}</td>
|
||||
<td style="font-size:12px;">${net}</td>
|
||||
<td>${startStr}</td>
|
||||
<td>${endStr}</td>
|
||||
<td>${b.duration_minutes || 0} min</td>
|
||||
@@ -422,7 +419,7 @@
|
||||
console.error('Failed to load date details:', err);
|
||||
tableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="10" class="no-data" style="color: var(--accent-red);">
|
||||
<td colspan="9" class="no-data" style="color: var(--accent-red);">
|
||||
<i class="fa-solid fa-circle-exclamation"></i>
|
||||
Gagal memuat detail data: ${err.message}
|
||||
</td>
|
||||
|
||||
+18
-21
@@ -366,20 +366,20 @@
|
||||
<div class="info-val" id="opStartTime">--:--:--</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Karung Masuk</div>
|
||||
<div class="info-label">Sacks</div>
|
||||
<div class="info-val" id="opSackCount">0</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Box Masuk</div>
|
||||
<div class="info-label">Boxes</div>
|
||||
<div class="info-val" id="opBoxCount">0</div>
|
||||
</div>
|
||||
<div class="info-item" id="opNetSackItem" style="display:none">
|
||||
<div class="info-label">Net Karung / Target</div>
|
||||
<div class="info-val" id="opNetSack">--</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">DO Sacks</div>
|
||||
<div class="info-val" id="opDoSack">0</div>
|
||||
</div>
|
||||
<div class="info-item" id="opNetBoxItem" style="display:none">
|
||||
<div class="info-label">Net Box / Target</div>
|
||||
<div class="info-val" id="opNetBox">--</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">DO Boxes</div>
|
||||
<div class="info-val" id="opDoBox">0</div>
|
||||
</div>
|
||||
<div class="info-item" id="opPlateItem" style="display:none; grid-column: 1 / -1;">
|
||||
<div class="info-label">Plat / No. DO</div>
|
||||
@@ -646,9 +646,9 @@
|
||||
<input data-id="${d.id}" data-k="no_do" value="${escapeHtml(d.no_do || '')}"></div>
|
||||
<div><label>Plat</label>
|
||||
<input data-id="${d.id}" data-k="plate" value="${escapeHtml(d.plate || '')}"></div>
|
||||
<div><label>Ekspektasi karung</label>
|
||||
<div><label>DO Sacks</label>
|
||||
<input data-id="${d.id}" data-k="expected_sack" type="number" min="0" value="${d.expected_sack || 0}"></div>
|
||||
<div><label>Ekspektasi box</label>
|
||||
<div><label>DO Boxes</label>
|
||||
<input data-id="${d.id}" data-k="expected_box" type="number" min="0" value="${d.expected_box || 0}"></div>
|
||||
<div class="row-actions">
|
||||
<button class="btn btn-primary btn-sm" data-act="save" data-id="${d.id}">Simpan</button>
|
||||
@@ -856,19 +856,16 @@
|
||||
sackCount.textContent = data.count || 0;
|
||||
boxCount.textContent = data.box_count || 0;
|
||||
|
||||
const netSack = document.getElementById('opNetSack');
|
||||
const netBox = document.getElementById('opNetBox');
|
||||
const netSackItem = document.getElementById('opNetSackItem');
|
||||
const netBoxItem = document.getElementById('opNetBoxItem');
|
||||
const doSack = document.getElementById('opDoSack');
|
||||
const doBox = document.getElementById('opDoBox');
|
||||
const plateItem = document.getElementById('opPlateItem');
|
||||
const plateDos = document.getElementById('opPlateDos');
|
||||
const showNet = currentMode === 'do_manual' && data.success && data.batch_number;
|
||||
if (netSackItem) netSackItem.style.display = showNet ? 'block' : 'none';
|
||||
if (netBoxItem) netBoxItem.style.display = showNet ? 'block' : 'none';
|
||||
if (plateItem) plateItem.style.display = showNet ? 'block' : 'none';
|
||||
if (showNet) {
|
||||
netSack.textContent = `${data.net_sack ?? data.count} / ${data.expected_sack ?? '-'}`;
|
||||
netBox.textContent = `${data.net_box ?? data.box_count} / ${data.expected_box ?? '-'}`;
|
||||
const batchActive = !!(data.success && data.batch_number);
|
||||
const showPlate = currentMode === 'do_manual' && batchActive;
|
||||
if (doSack) doSack.textContent = data.expected_sack || 0;
|
||||
if (doBox) doBox.textContent = data.expected_box || 0;
|
||||
if (plateItem) plateItem.style.display = showPlate ? 'block' : 'none';
|
||||
if (showPlate) {
|
||||
plateDos.textContent = (data.plate || 'tanpa plat') +
|
||||
(data.do_numbers && data.do_numbers.length ? ' · ' + data.do_numbers.join(', ') : '');
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user