docs(fusion_plating_shopfloor): brainstorm spec for tablet redesign

Multi-section design covering:

- 3 OWL client actions: fp_shopfloor_landing (replaces fp_shopfloor_tablet
  + folds in fp_plant_overview), fp_job_workspace (NEW full-screen WO
  surface), fp_manager_dashboard (refactored — 4 sibling tabs incl.
  Workflow Funnel, Approval Inbox, At-Risk).

- 5 shared OWL services: WorkflowChip, GateViz, SignaturePad,
  HoldComposer, KanbanCard — reused across all three client actions to
  enforce one-widget-one-place and prevent terminology drift.

- Backend additions: 8 new RPC endpoints, blocker_kind/reason computes
  on fp.job.step, display_wo_name + late_risk_ratio + active_step_id on
  fp.job, bottleneck_score on fp.work.centre, auto-pause cron (fixes
  411h ghost timer), ACL lift for operator group per "techs wear
  multiple hats" rule.

- Terminology pass: WO # 00001 (display only, sequence rename deferred),
  Shop Floor / Up Next / Embrittlement Bakes / etc.

- 5-phase deploy sequence, each phase independently shippable.

- Out of scope (deferred to v2): cost roll-up, cycle time, per-tech
  throughput, system-wide sequence rename.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-22 21:31:15 -04:00
parent f661724c72
commit fb5da1e3cd

View File

@@ -0,0 +1,487 @@
# Shop Floor Tablet Redesign — Design Spec
**Date:** 2026-05-22
**Status:** Brainstorm complete, awaiting user review
**Authors:** Garry Singh + Claude
**Module owners:** `fusion_plating_shopfloor`, `fusion_plating_jobs`
**Target client:** EN Technologies (Fusion Plating)
---
## 1. Context
The current Shop Floor tablet view (client action `fp_shopfloor_tablet`, OWL component `ShopfloorTablet`) was built during the initial Fusion Plating implementation. Since then the underlying models — `fp.job`, `fp.job.step`, `fp.job.workflow.state`, `fp.certificate`, `fp.thickness.reading`, `fp.job.consumption`, `fp.job.node.override`, `fp.racking.inspection` and friends — have grown substantially. Many of those new fields, actions, and workflows are not surfaced on the tablet.
Symptoms observed on a live development instance:
- Step name shows "Active: Blasting" with no WO/customer context
- Qty rendered as "Qty 17/1" — ambiguous direction
- Step position shown as "step 1.1/11" (sequence divided by 10)
- Active timer reading **411:52:16** — a stale start that never finished and was never auto-paused
- "SIGN-OFF REQUIRED" chip is informational only; no actual sign-off control
- Customer spec, drawings, recipe overrides, milestone progress, holds, and most lifecycle actions are invisible
Three roles operate the system: **Owner**, **Manager**, **Technician**. Technicians wear multiple hats (receiving, plating, QC, shipping) — the client explicitly wants minimal gating between roles.
## 2. Goals
- A technician can manage a WO end-to-end from a single full-screen workspace, without typing into search bars or jumping to the back-office.
- A manager can see at a glance: where every WO is in the workflow, what needs their decision right now, what's trending late, and where the bottlenecks are.
- All recent additions to `fp.job` / `fp.job.step` (workflow milestones, blocker reasons, recipe overrides, customer spec, etc.) are surfaced on the tablet and manager dashboard.
- Terminology matches how techs talk on the shop floor — "WO # 00001" not "WH/JOB/00001".
- The system never displays a 411-hour ghost timer.
## 3. Non-goals (v1)
- Multi-tablet pairing per technician
- Offline-first / PWA mode
- Voice input
- Performance optimization beyond ~500 active jobs (current scale: ~50)
- Webhooks to external dashboards
- Cost roll-up per job, cycle time per recipe, per-tech throughput (P2 — deferred to v2)
- System-wide rename of `fp.job` sequence (`WH/JOB/...``WO ...`) — display-only on tablet for now; back-office/reports/emails keep current sequence until a separate decision is made
## 4. Terminology decisions
All approved in brainstorm:
| Element | Was | Is now |
|---|---|---|
| Page title | "Tablet Station" | **Shop Floor** (with station chip e.g. "@ EN Plating Tank") |
| Document number | `WH/JOB/00001` | **`WO # 00001`** (display only) |
| Active step header | "Active: Blasting" | **"WO # 00001 — Blasting"** (Step 1 of 11) |
| Qty | "Qty 17/1" | **"1 / 17 done"** + scrap subtext + mini progress bar |
| Step position | "step 1.1/11" | **"Step 1 of 11"** |
| Sign-off chip | "SIGN-OFF REQUIRED" | **"Finish & Sign Off"** action button (replaces plain Finish) |
| Queue heading | "My Queue" | **"Up Next"** (at this station) |
| Bath state | "OPERATIONAL / LOG: OUT_OF_SPEC" | **"Operating"** / **"Last log out of spec"** |
| Bake panel | "Bake Windows" | **"Embrittlement Bakes"** |
| Gate panel | "First-Piece Gates" | **"First-Piece Inspections"** |
| Tile set | 6 mixed tiles | **4 tech-relevant tiles**: Ready · Running · Bakes Due · Holds (others move to manager dashboard) |
| Stale timer | "411:52:16" | Auto-pause at 8h (configurable) with chatter audit; display switches to "Started Nd ago" past 24h |
## 5. Architecture — option B (specialized components + shared services)
Three OWL client actions, five shared OWL services, a small set of backend additions. Each client action is independently deployable.
```
┌────────────────────────────────┐ ┌────────────────────────────────┐ ┌────────────────────────────────┐
│ fp_shopfloor_landing │ │ fp_job_workspace │ │ fp_manager_dashboard │
│ (replaces fp_shopfloor_tablet │ │ NEW — full-screen WO surface │ │ refactored — 4 tabs │
│ + folds in fp_plant_overview)│ │ │ │ │
│ • station-scoped kanban │ │ • sticky header + WO chips │ │ • Workflow Funnel (default) │
│ • All-Plant toggle │ │ • workflow milestone bar │ │ • Approval Inbox │
│ • QR scan, station picker │ │ • step list + side panel │ │ • Plant Board (existing) │
│ • tap card → JobWorkspace │ │ • sticky action rail │ │ • At-Risk │
└────────────────────────────────┘ └────────────────────────────────┘ └────────────────────────────────┘
│ │ │
└──────────────────────────────────┴──────────────────────────────────┘
┌────────────┴────────────┐
│ Shared OWL services │
│ WorkflowChip · GateViz │
│ SignaturePad · KanbanCard │
│ HoldComposer │
└─────────────────────────┘
```
### 5.1 Shared OWL services
| Service | Used by | Props | Depends on |
|---|---|---|---|
| **WorkflowChip** | Landing card · Workspace header · Manager funnel | `{ state: {id, name, color}, nextActionLabel? }` | `fp.job.workflow.state` records (already shipped). Reads `color` field. |
| **GateViz** | Workspace step rows · Manager "Needs Worker" cards | `{ canStart, blockerKind, blockerReason, jumpTarget? }` | New `fp.job.step.blocker_kind` + `blocker_reason` computes |
| **SignaturePad** | Workspace (Finish & Sign Off) · Cert issue | `{ title, contextLabel, onSubmit(dataUri), onCancel }` | Odoo `dialog` service; HTML canvas + pointer events |
| **HoldComposer** | Workspace (Hold button) · Manager Approval Inbox | `{ jobId, stepId?, defaultQty, partRef, onCreated(hold) }` | New endpoint `/fp/workspace/hold` (with photo attachment) |
| **KanbanCard** | Landing (station + all-plant) · Manager (Plant Board + Workflow Funnel) | `{ data, density: 'compact'\|'normal', showWorkflowChip, showWorkcenter, showAssignedTo, onTap }` | Embeds `WorkflowChip` + `GateViz` badge |
Each service is its own file under `fusion_plating_shopfloor/static/src/js/components/`. Roughly 80200 lines OWL + 3080 lines SCSS per service.
### 5.2 Landing component (`fp_shopfloor_landing`)
Replaces today's `fp_shopfloor_tablet`, folds in `fp_plant_overview`. Single entry surface for technicians.
**Layout regions** (top-to-bottom):
1. **Header strip** — "Shop Floor" title, station chip, station picker, mode toggle (`Station``All Plant`), QR scan controls (Code + Camera), refresh indicator.
2. **KPI tile row (4 tiles)** — Ready · Running · Bakes Due · Holds. Holds turns red when > 0.
3. **Kanban board** — columns = work centres; cards = `KanbanCard` (one per WO at that work centre); urgency-sorted within column (existing logic in `plant_overview.py` carries over). Drag-and-drop between columns keeps current behaviour.
4. **Optional left filter rail** (collapsed by default) — search box, priority, customer, due-by, blocker filter. Promote the existing plant_overview search bar.
5. **Footer** — auto-refresh indicator + "Last sync HH:MM:SS".
**Mode behaviour:**
| Mode | Columns shown | Default when |
|---|---|---|
| **Station** | Paired work centre + Unassigned + next 12 work centres in the recipe flow | A station is paired (via QR scan or picker) |
| **All Plant** | Every active work centre, recipe-flow order | No station paired, OR user toggles |
Toggle persists in `localStorage` per tablet (same pattern as `fp_tablet_station_id`).
**Card tap behaviour:**
```js
action.doAction({
type: 'ir.actions.client',
tag: 'fp_job_workspace',
params: { job_id: card.job_id, focus_step_id: card.current_step_id }
});
```
Browser back returns to Landing with kanban scroll/mode preserved.
**QR scan dispatch** (existing `/fp/shopfloor/scan` endpoint, unchanged):
| Scanned | Behaviour |
|---|---|
| `FP-STATION:<code>` | Pair tablet, switch to Station mode |
| `FP-JOB:<name>` | Open JobWorkspace for that WO |
| `FP-STEP:<id>` | Open JobWorkspace, focus that step |
| `FP-TANK:<code>` / `FP-BATH:<name>` | Chemistry quick-log dialog (existing endpoint) |
| `FP-OVEN:<code>` | Jump to next bake awaiting that oven |
**Auto-refresh** — every 15s.
**Files:**
```
fusion_plating_shopfloor/
controllers/landing_controller.py ← NEW (~250 lines)
static/src/js/shopfloor_landing.js ← OWL (~600 lines)
static/src/xml/shopfloor_landing.xml
static/src/scss/shopfloor_landing.scss
```
### 5.3 Job Workspace component (`fp_job_workspace`)
The heart of the redesign. Full-screen surface a tech opens by tapping a kanban card.
**Layout regions** (sticky top → scrollable middle → sticky bottom):
| Region | Sticky | Data | Behaviour |
|---|---|---|---|
| **Back** | top | — | `doAction` back to Landing, preserves kanban scroll/mode |
| **WO header** | top | `display_wo_name`, `partner_id`, `part_catalog_id` + rev, qty/qty_done/qty_scrapped, `date_deadline`, `workflow_state_id`, `quality_hold_count`, `customer_spec_id` | `+1 Done` / `1 Done` / `+1 Scrap` quick bumps inline. Holds count → opens Holds drawer. |
| **Workflow milestone bar** | top | All `fp.job.workflow.state` records ordered by sequence; current = `workflow_state_id`; `next_milestone_action` + `next_milestone_label` | Dots: passed (●), current (filled), pending (○). "Next" button on right fires `/fp/workspace/advance_milestone`. Disabled until preconditions met. |
| **Step list** (left/center, scrolls) | scrolls | `fp.job.step_ids` sorted by `sequence` | Each row uses step-row template (see below). Active step auto-scrolled and auto-expanded if `focus_step_id` param set. |
| **Side panel** (collapsible right) | scrolls | `customer_spec_id` (PDF), attachments, chatter | Three sub-cards: **Spec** (inline via `fusion_pdf_preview`), **Drawings**, **Notes** (chatter — read + quick-add). Collapses to icon strip on narrow screens. |
| **Action rail** | bottom | — | Always: Create Hold (`HoldComposer`), Add Note, Photo. Conditional: Issue Cert (when `_fp_has_draft_required_certs()`), Mark Done / Schedule Delivery / Mark Shipped (per `next_milestone_action`). |
**Step row anatomy:**
- **Collapsed** (default for done/pending/paused): one line — icon + Step N · Name + assigned tech + duration + state badge
- **Expanded active** (auto for `state == 'in_progress'`): recipe chips + instructions + primary + secondary actions
- **Expanded by tap** (any step): same shape, action buttons gated by `can_start`
**Per-step actions:**
| Button | Visible when | Calls |
|---|---|---|
| Start | `state in ('ready','paused')` AND `can_start` | `/fp/shopfloor/start_wo` |
| Finish (or Finish & Sign Off) | `state == 'in_progress'` | `/fp/shopfloor/stop_wo` (finish=true) OR `/fp/workspace/sign_off` if `requires_signoff` |
| Pause | `state == 'in_progress'` | `step.button_pause()` |
| Skip | `state in ('ready','paused')` AND user is supervisor+ | `step.button_skip()` |
| Move Parts | always | `FpMovePartsDialog` (existing) |
| Move Rack | when `kind == 'rack'` | `FpMoveRackDialog` (existing) |
| Quick QC | when `quick_look_prompt_ids` non-empty | `step.action_open_quick_look()` |
| Operator Inputs | when step has unrecorded inputs | `step.action_open_input_wizard()` (existing wizard) |
| Photo | always | inline camera → attach to step |
| Stop Timer (correction) | when duration looks wrong | `FpStopTimerDialog` (existing) |
| Open in backend | always (small icon) | `doAction` to fp.job.step form (escape hatch) |
When step is **blocked** (`can_start == False`), action button row is replaced by `GateViz` block.
When step is **opted out** (`override_ids` says excluded), row shows ✕ icon + "Skipped per recipe override" + supervisor-only "Re-include" button.
**Auto-pause integration** — if `_cron_autopause_stale_steps` flips a step to paused, the row's chatter reflects "Auto-paused after Nh idle". Tech can tap Resume.
**Auto-refresh** — every 15s.
**Files:**
```
fusion_plating_shopfloor/
controllers/workspace_controller.py ← NEW (~400 lines)
static/src/js/job_workspace.js ← OWL (~800 lines)
static/src/xml/job_workspace.xml
static/src/scss/job_workspace.scss
static/src/js/components/{workflow_chip,gate_viz,signature_pad,hold_composer,kanban_card}.js
```
### 5.4 Manager Dashboard refactor (`fp_manager_dashboard`)
Same client action, **four sibling tabs** under a shared header + KPI strip.
**KPI strip (extended):** keep existing 4 always-on (Unassigned Steps · In Progress · Ready to Ship · Awaiting Assignment) + existing conditional reds (Missed Bakes · Open Holds · Stale Steps · Predecessor Locked) + **2 new: Pending Cert · At-Risk**.
**Tabs:**
| Tab | Default? | Content |
|---|---|---|
| **Workflow Funnel** | yes | Vertical stack of `fp.job.workflow.state` records. Each row shows stage chip + count badge + first ~5 `KanbanCard`s + "+ N more" drawer. Bar chart bar behind the row scaled to count. Tap card → JobWorkspace. |
| **Approval Inbox** | no | 4 grouped strips: **Holds to Release** (`state in ('on_hold','under_review')`), **Certs to Issue** (`all_steps_terminal` + draft required cert), **Scrap to Review** (recent `qty_scrapped` bumps with operator reason), **Override Requests** (deferred — placeholder). Per-row inline action buttons + bulk-action ("Release all"). |
| **Plant Board** | no | Today's existing 3-column "Needs Worker / In Progress / Team" view — unchanged behaviour. Becomes one tab among four. |
| **At-Risk** | no | 3 sub-panels: **Trending Late** (sorted by `late_risk_ratio` desc, top 20), **Hold Reasons** (open holds grouped by `hold_reason`), **Bottleneck Heatmap** (work centres ranked by `bottleneck_score`). |
**Cross-tab features:** live 8s refresh (existing cadence), QR scan in header, "Take Over Tablet" supervisor handover.
**Permissions:** dashboard already gated to `group_fusion_plating_supervisor`+. That stays. Owner + Manager hit this; Technicians don't.
**Files:**
```
fusion_plating_shopfloor/
controllers/manager_controller.py ← add 3 endpoints (funnel, approval_inbox, at_risk)
static/src/js/manager_dashboard.js ← refactor: extract Plant Board, add 3 sibling tabs
static/src/xml/manager_dashboard.xml
static/src/scss/manager_dashboard.scss
```
## 6. Backend support
### 6.1 HTTP endpoints
All `type='jsonrpc'`, `auth='user'`.
**NEW** — added by this work:
| Endpoint | Lives in | Purpose |
|---|---|---|
| `POST /fp/landing/kanban` | `landing_controller.py` | Station OR all-plant kanban data |
| `POST /fp/workspace/load` | `workspace_controller.py` | Full Job Workspace payload |
| `POST /fp/workspace/hold` | `workspace_controller.py` | HoldComposer create (with photo) |
| `POST /fp/workspace/sign_off` | `workspace_controller.py` | Signature + finish step atomically |
| `POST /fp/workspace/advance_milestone` | `workspace_controller.py` | Fire `next_milestone_action` |
| `POST /fp/manager/funnel` | `manager_controller.py` (add) | Workflow funnel data |
| `POST /fp/manager/approval_inbox` | `manager_controller.py` (add) | Holds + draft certs + scrap to review |
| `POST /fp/manager/at_risk` | `manager_controller.py` (add) | Late-risk + hold reasons + bottlenecks |
**KEPT** — unchanged, used by new components via wrappers: `/fp/shopfloor/scan`, `start_wo`, `stop_wo`, `start_bake`, `end_bake`, `log_chemistry`, `log_thickness_reading`, `bump_qty_done`, `bump_qty_scrapped`, `mark_gate`, `pair_station`.
**DEPRECATED** — kept as stubs for 1 release, then removed:
- `/fp/shopfloor/tablet_overview` → calls `/fp/landing/kanban` internally
- `/fp/shopfloor/plant_overview` → calls `/fp/landing/kanban?mode=all_plant`
- `/fp/shopfloor/queue` → removed (no replacement)
### 6.2 Model fields / computes
On `fp.job` (`fusion_plating_jobs/models/fp_job.py`):
| Field | Type | Purpose |
|---|---|---|
| `display_wo_name` | computed Char | "WO # 00001" formatter from `name` |
| `late_risk_ratio` | computed Float, stored | `remaining_planned_minutes / minutes_to_deadline` |
| `active_step_id` | computed Many2one→fp.job.step | Current `in_progress` step (Workspace landing focus) |
On `fp.job.step` (`fusion_plating_jobs/models/fp_job_step.py`):
| Field | Type | Purpose |
|---|---|---|
| `blocker_kind` | computed Selection | `predecessor` · `contract_review` · `parts_not_received` · `racking_required` · `manager_input` · `none` |
| `blocker_reason` | computed Char | Human reason (e.g. "Waiting on Step 3: Activation") |
| `blocker_jump_target_model` | computed Char | Optional tap-to-jump target model |
| `blocker_jump_target_id` | computed Integer | Optional tap-to-jump target id |
On `fp.work.centre` (`fusion_plating/models/fp_work_centre.py`):
| Field | Type | Purpose |
|---|---|---|
| `bottleneck_score` | computed Float, non-stored | `active_step_count × avg_wait_minutes` |
| `avg_wait_minutes` | computed Float, non-stored | Rolling 7-day avg ready→start wait |
On `fusion.plating.process.node` (recipe node):
| Field | Type | Purpose |
|---|---|---|
| `long_running` | Boolean | Opt out of auto-pause (24h bakes etc.) |
### 6.3 Auto-pause cron
```xml
<!-- fusion_plating_jobs/data/fp_cron_data.xml -->
<record id="ir_cron_autopause_stale_steps" model="ir.cron">
<field name="name">FP Jobs: auto-pause stale in-progress steps</field>
<field name="model_id" ref="model_fp_job_step"/>
<field name="state">code</field>
<field name="code">model._cron_autopause_stale_steps()</field>
<field name="interval_number">30</field>
<field name="interval_type">minutes</field>
<field name="active" eval="True"/>
</record>
```
Method (`fp_job_step.py`):
```python
@api.model
def _cron_autopause_stale_steps(self):
threshold = float(self.env['ir.config_parameter'].sudo()
.get_param('fp.shopfloor.autopause_threshold_hours', 8))
deadline = fields.Datetime.now() - timedelta(hours=threshold)
stale = self.search([
('state', '=', 'in_progress'),
('date_started', '<', deadline),
('recipe_node_id.long_running', '=', False),
])
for step in stale:
step.button_pause()
step.message_post(body=Markup(
"<b>Auto-paused</b> after %.1fh idle. "
"Resume from the tablet when work continues."
) % threshold)
_logger.info("Auto-paused step %s after %.1fh idle", step.id, threshold)
```
`ir.config_parameter` key: `fp.shopfloor.autopause_threshold_hours` (default `8`).
### 6.4 ACL changes (operator group)
Per "techs wear multiple hats" rule — minimal new gates.
| Model | Read | Write | Create | Unlink | Notes |
|---|---|---|---|---|---|
| `fp.certificate` | ✓ existing | **NEW ✓** | — | — | Flip draft → issued from tablet "Issue Cert" |
| `fp.thickness.reading` | **NEW ✓** | **NEW ✓** | **NEW ✓** | — | Capture Fischerscope readings from tablet |
| `fp.job.node.override` | **NEW ✓** | — | — | — | Read-only — tech sees opt-out badge |
Supervisor-only operations enforced in `workspace_controller.py` (not via ACL):
- Step Skip (`button_skip`)
- Hold Release (state transition `on_hold``released`)
- Override Re-include
### 6.5 Terminology — `display_wo_name`
`fp.job.display_wo_name` is a computed Char that formats `name` as `WO # 00001`. All new tablet/dashboard payloads use this field. The underlying `fp.job.name` (`WH/JOB/00001`) stays unchanged — reports, emails, back-office forms continue using `name`.
System-wide sequence rename is **out of scope** for this work. If pursued separately, it requires: (a) updating `ir.sequence` prefix to `WO `, (b) backfill script for existing records, (c) coordination with any external integrations that grep on the old prefix.
## 7. Build & deploy sequence
Each phase is independently deployable. Rollback is per-phase, not all-or-nothing.
| Phase | Ships | Independently deployable? |
|---|---|---|
| **1** | Shared OWL services + JobWorkspace + workspace_controller + fp.job.step blocker_* computes + display_wo_name. Opens from existing `fp.job` form smart button. | Yes — works before Landing refactor |
| **2** | Auto-pause cron + ACL lift + `late_risk_ratio` + `active_step_id` computes | Yes — silent infra |
| **3** | landing_controller + `fp_shopfloor_landing` component. Old `fp_shopfloor_tablet` menu redirected. PlantOverview menu hidden. | Yes — Workspace already works via smart button |
| **4** | 3 new manager endpoints + manager dashboard refactor (4 tabs) + `bottleneck_score` compute | Yes |
| **5** | Cleanup: remove deprecated endpoint stubs, retire fp_plant_overview module dir | Last |
## 8. Testing strategy
### 8.1 Python tests (`fusion_plating_shopfloor/tests/`)
| Test | Verifies |
|---|---|
| `test_display_wo_name` | Formatter handles various `name` shapes |
| `test_late_risk_ratio` | Correct ratio with deadline / no deadline / overdue / not started |
| `test_active_step_id` | Sole in_progress step; empty when none; first-by-sequence when multiple |
| `test_blocker_kind_and_reason` | Each kind returns correct enum + human string + jump target |
| `test_autopause_cron` | Stale flips; chatter posted; respects `long_running`; idempotent |
| `test_workspace_load_payload` | Full payload shape — keys, types, opted-out marked |
| `test_workspace_sign_off` | Signature captured, step finished, empty-sig rejected |
| `test_workspace_advance_milestone` | Fires only when preconditions met; friendly error otherwise |
| `test_hold_composer_create` | Hold + photo + qty split; rollback on validation error |
| `test_acl_operator_permissions` | Operator can issue cert, cannot skip step (controller gate) |
| `test_funnel_and_inbox` | Funnel grouping correct; inbox returns all 4 buckets |
### 8.2 OWL tests (light)
- `WorkflowChip` renders correct color per state
- `GateViz` renders correct copy per blocker_kind
- `SignaturePad` returns non-empty data URI after stroke
- `HoldComposer` validates qty ≤ remaining before submit
- `KanbanCard` collapses chips at compact density
### 8.3 Manual QA checklist
Lives at `docs/qa/shopfloor-redesign-qa.md`. 10-step walkthrough covering: pairing → Landing modes → tap card → Workspace → Finish & Sign Off → Create Hold → Manager Funnel → Approval Inbox release → auto-pause test → dark mode.
## 9. Observability
- `_logger.INFO` on milestone advance, hold create from tablet, sign-off, auto-pause
- `_logger.WARNING` on workspace_load with bad job_id, sign_off with empty data URI
- `_logger.EXCEPTION` on controller failures (existing pattern)
- Chatter audit on auto-pause, hold create, milestone advance, sign-off
Future metrics (flagged, no infra now): tablet refresh frequency, time-to-Start from Workspace open, auto-pause rate, hold creation rate.
## 10. Edge cases
| Case | Handling |
|---|---|
| Job has zero steps | "Recipe not generated" placeholder + back-office link |
| Job has 50+ steps | Standard scroll, no virtualization in v1 |
| All steps in_progress (defensive) | `active_step_id` picks first by sequence; logs warning |
| No workflow states defined | Bar hides; Next button disabled |
| Step state changed during 15s gap | Refresh corrects; no error toast |
| Two techs tap Start simultaneously | `button_start` idempotent; second call returns state |
| Wrong station scanned | Header "Unpair" link; localStorage cleared |
| Network drop mid-action | Toast "Saving failed — tap to retry"; UI state preserved |
| 24h-bake step | `recipe_node.long_running=True` skips auto-pause |
| Customer spec PDF missing | Side panel: "No customer spec attached" |
| Funnel with 200+ jobs in stage | Top 5 cards + "View all (N)" drawer |
| Operator with no facility | Landing prompts pick-or-scan station |
| HoldComposer fails after photo upload | Photo cleaned in `except` block — no orphans |
| Manual step (no recipe_node_id) | Chips/instructions empty — OK |
| Sign-off step finished from back-office | Workspace re-renders without re-prompting |
| Tech opens Workspace for unassigned job | Allowed — read+write per "many hats" rule |
## 11. Performance
| Surface | Load shape | Mitigation |
|---|---|---|
| Landing kanban (All Plant, ~400 cards) | Existing `plant_overview.py` batch prefetch carries over | None new |
| Workspace load (1 job × ~11 steps) | Trivial | None |
| Manager funnel (~50 jobs × 9 stages) | Trivial | None |
| Manager At-Risk (7-day step state scan) | Potentially heavy on large plants | Cache 60s per facility |
| Auto-pause cron | Filtered query, no N+1 | None |
| `late_risk_ratio` (stored) | Recomputed on step state change | `@api.depends` triggers |
## 12. Rollback strategy
| Phase | Rollback |
|---|---|
| 1 (Workspace) | Hide smart-button entry in fp.job form. Workspace becomes orphan, harmless. |
| 2 (Cron + ACL) | Disable cron via UI. ACL changes are CSV-line edits. |
| 3 (Landing) | Re-enable old `fp_shopfloor_tablet` menu. Old endpoint stub still active. |
| 4 (Manager) | Revert manager_dashboard.xml to single Plant Board tab. |
| 5 (Cleanup) | Defer if issues — leave stubs longer. |
Only stored field added is `late_risk_ratio` Float — additive `ALTER TABLE`, safe to drop.
## 13. Backwards compatibility
- All existing QR codes keep working — `/fp/shopfloor/scan` unchanged
- Existing `fp.job` form smart buttons → Workspace opens via doAction
- Existing `fp.job.step` form view stays as "Open in backend" escape hatch
- Old reports/emails keep showing `WH/JOB/00001` (sequence rename deferred)
## 14. Decisions log
| Decision | Rationale |
|---|---|
| Hybrid mental model (queue → workspace), not pure queue or pure job-first | Queue is the natural entry; full workspace solves "manage the whole job" goal |
| One tablet, no role-segmentation per persona | Client said techs wear multiple hats; minimal gating |
| Architecture B (specialized components + shared services) over mega-component | Each surface has its own lifecycle; shared services enforce consistency |
| Station-scoped kanban as default landing, All Plant as toggle | Matches physical reality (tablet at station) + provides escape hatch |
| Approval Inbox + Workflow Funnel as new manager tabs, Plant Board stays | Tactical (assignment) and strategic (where is everything) views coexist |
| Auto-pause stale timers at 8h default | Solves the 411-hour ghost timer permanently; protects cost/cycle-time math |
| WO # display only; sequence rename deferred | Lower risk; user can choose system-wide rename separately |
| ACL lift for operator group on cert / thickness reading / override read | Per "techs wear many hats" rule; supervisor-only ops enforced in controller, not ACL |
## 15. Out of scope for v1
- Multi-tablet pairing per tech
- Offline-first / PWA mode
- Voice input
- Performance optimisation beyond ~500 active jobs
- Webhooks to external dashboards
- Cost roll-up per job (P2)
- Cycle time per recipe (P2)
- Notification audit feed (P2)
- Per-tech throughput (P2)
- System-wide `fp.job` sequence rename to `WO ` prefix
---
**Next step:** user reviews this spec; once approved, transition to `superpowers:writing-plans` skill to produce the phased implementation plan.