From ff51035494b47cc08a656a92a1fe666530dda6b9 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Mon, 25 May 2026 12:07:38 -0400 Subject: [PATCH] =?UTF-8?q?docs(brainstorm):=20quality=20dashboard=20redes?= =?UTF-8?q?ign=20=E2=80=94=20action=20surface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User goal: 'all quality related updates at glance, all the flagged tasks need to show right here so the manager can quickly follow up and complete the task'. Current dashboard is a tab-router (6 numeric tiles + click-to-drill) — flagged tasks aren't visible without navigation. Design (Hybrid layout, approved): - Red 'Needs Attention Today' banner on top (up to 6 items, 2x3 grid) showing items that are overdue OR from critical customers (x_fc_rush / x_fc_vip / aerospace). Green 'all caught up' when zero. - Per-type sections below in QM-urgency order: Certs / Holds / NCRs / RMAs / CAPAs / Checks. Each shows top 5 items inline + Open all link to the existing kanban. - Single 'Open ->' button per row -> opens record form via act_window. No one-click action shortcuts (cert form is where Fischerscope + sign-off prereqs are validated). - Drop the existing 'Quality Overview' header strip entirely. - 60s poll cadence preserved. - ?tab=certificates deep-link from awaiting-cert notification email preserved as scrollIntoView on the certs section. Backend: replace /fp/quality/dashboard/counts with /snapshot. New helper class FpQualityDashboardSnapshot builds banner + 6 sections in one response. Cross-module reads sudo'd per Rule 13m; missing fields gracefully degrade per Rule 13j defensive pattern. Frontend: rewrite the OWL component. BannerCard + 6 SectionCards as sub-components in the same JS file (not reused elsewhere yet). Existing per-model kanbans untouched. Self-review fixed 4 issues: - _critical_customer_domain made per-type (was contradictory) - OVERDUE_THRESHOLDS gained explicit use_due_date flag (CAPA branch) - Template requirement called out: id='section-' on each card for the deep-link scrollIntoView to work - doAction call shape disambiguated for xmlid vs full dict Spec: docs/superpowers/specs/2026-05-25-quality-dashboard-redesign-design.md Co-Authored-By: Claude Opus 4.7 (1M context) --- ...05-25-quality-dashboard-redesign-design.md | 514 ++++++++++++++++++ 1 file changed, 514 insertions(+) create mode 100644 fusion_plating/docs/superpowers/specs/2026-05-25-quality-dashboard-redesign-design.md diff --git a/fusion_plating/docs/superpowers/specs/2026-05-25-quality-dashboard-redesign-design.md b/fusion_plating/docs/superpowers/specs/2026-05-25-quality-dashboard-redesign-design.md new file mode 100644 index 00000000..31626754 --- /dev/null +++ b/fusion_plating/docs/superpowers/specs/2026-05-25-quality-dashboard-redesign-design.md @@ -0,0 +1,514 @@ +# Quality Dashboard Redesign — Action Surface + +**Date:** 2026-05-25 +**Status:** Approved for implementation (brainstorming gate) +**Author:** Brainstorming session (gsinghpal) +**Triggering incident:** The current Quality Dashboard is a tab-router — it surfaces 6 numeric tiles and forces the QM to click into per-model kanbans to actually see and act on items. Two complaints: (1) lots of empty whitespace below the tile row, (2) flagged tasks that need QM attention aren't visible at a glance. The Quality Manager wants "all quality related updates at glance, all the flagged tasks need to show right here so the manager can quickly follow up and complete the task" (verbatim from session 2026-05-25). + +## Goal + +Turn the Quality Dashboard from a **router** ("click here to see N records") into an **action surface** ("here are the records that need attention, click Open to act"). Specifically: + +1. **Surface flagged items directly on the page** — no extra navigation for the things that need the QM's eyes today. +2. **Distinguish urgent from routine** — red "Needs Attention Today" banner at the top draws the eye to overdue + critical-customer items across all types; per-type sections below hold the routine queue. +3. **Preserve the existing per-model kanbans** — "Open all →" links route to them unchanged. We're rebuilding the dashboard surface, not the underlying record management. +4. **Keep the existing notification deep-link working** — the `?tab=certificates` URL param from the awaiting-cert email (spec 2026-05-25-post-shop-cert-shipping-job-states) still lands the QM on the right section. + +## Out of scope (deferred to follow-on work) + +- **Real-time push** (bus.bus / WebSocket). 60-second poll is the cadence; instant updates would add infrastructure complexity for marginal benefit on a QM dashboard. +- **Filter chips on the dashboard** (e.g. "show only my customer"). Sections themselves are the filter; per-section search lives inside the per-model kanban. +- **Per-QM saved layout** (drag-reorder sections). Fixed order in v1 — Settings field is YAGNI for one user role. +- **Export / print of the dashboard**. The underlying kanbans support print. +- **"My quality day" personalization** ("Hi Lisa — 4 critical items…"). Fixed shared view in v1. +- **Inline action buttons that fire actions directly** (e.g. one-click "Issue" on the cert row). Considered and rejected — the cert form is where Fischerscope review + sign-off prereqs are validated; bypassing the form risks issuing a cert before checks complete. Every row uses a single `Open →` button that navigates to the form. + +## Decisions reached during brainstorming + +| # | Decision | Rationale | +|---|---|---| +| D1 | **Hybrid layout** — red "Needs Attention Today" banner on top + grouped sections per type below | Banner forces urgent items to the front of the eye; sections preserve type semantics for routine work. User picked from a 4-option visual mockup over Unified Inbox / Grouped Only / Priority Stacks. | +| D2 | **Banner rule** = (overdue per type) OR (critical customer + open) | Catches the high-stakes Boeing CoC at 6h before it crosses the 24h overdue line. Simple enough to predict, more responsive than overdue-only. | +| D3 | **Critical customer** = `partner.x_fc_rush` OR `partner.x_fc_vip` OR aerospace/regulated indicator (`part_catalog_id.name ILIKE '%aerospace%'` OR `customer_spec_id.code ILIKE 'AS9100%'` OR `'NADCAP%'`) | Reuses existing partner flags (no new field to maintain). Aerospace/regulated catches high-stakes regulatory work even on non-rush/VIP customers. | +| D4 | **Banner size** = up to 6 items in a 2×3 grid; if zero qualify, render a green "✓ All caught up" card instead of hiding | Predictable layout, positive reinforcement when the queue is clear. "Showing 6 of N" footer when more than 6 qualify. | +| D5 | **Inline row action** = single `Open →` button per row that opens the record form | Safe (no one-click bypasses of in-form checks), consistent across all 6 types, easiest to implement. The verb-specific button alternative (Issue / Disposition / Review / etc.) was rejected as visual clutter without functional value. | +| D6 | **Drop the existing "Quality Overview" header strip** (Open across all 6 / Overdue / 6 tab tiles) | Redundant after the redesign — banner shows urgent, each section shows its own count + overdue subtotal. Recovered vertical space goes to actual content. | +| D7 | **Section order** = Certificates → Holds → NCRs → RMAs → CAPAs → QC Checks | QM-urgency order: Certs first because they block shipment + are time-sensitive post-shop; Holds second because they block production; then NCRs / RMAs / CAPAs / Checks in decreasing time-pressure. NOT alphabetical, NOT the existing tab order. | +| D8 | **Section content** = top 5 items inline + "Open all →" link to the existing per-model kanban | Top 5 covers the daily attention budget; deeper drill uses the kanban the QM already knows. | +| D9 | **Section header shows count + overdue subtotal** | Section is its own micro-summary — no need for the dropped header strip to repeat this. | +| D10 | **Critical-customer items get a badge in the banner** (`[RUSH]`, `[VIP]`, `[AS9100]`) | Tells the QM WHY the item is in the banner when it's not yet overdue. No badge = banner reason is overdue. | +| D11 | **Section that shows zero items still renders** with an italic "No open items" row | Predictable layout. The QM trusts the dashboard isn't lying about types being absent. | +| D12 | **An item may appear in BOTH the banner and its section** | Intentional. The banner is "urgent across all types"; the section is "your queue per type". Visual reinforcement of urgency, not duplication. | +| D13 | **Refresh cadence** = keep the existing 60-second JS poll | No bus.bus / WebSocket. 60s matches the existing pattern + is fine for a QM surface. | +| D14 | **`?tab=certificates` deep-link preserved as scroll-into-view** | The notification email from spec 2026-05-25-post-shop-cert-shipping-job-states links here. Translate `?tab=` to `document.getElementById('section-').scrollIntoView()` on mount. | +| D15 | **ACL enforced at click time, not pre-filtered in snapshot** | Pre-filtering would 4x the query cost. Odoo's standard ACL fires when `act_window` navigates to the record — user gets the usual access error if blocked. Acceptable since dashboard is QM-facing and QMs have read on all 6 models. | + +## Architecture + +``` +┌─ PAGE LAYOUT (top → bottom) ─────────────────────────────────┐ +│ │ +│ ┌─ NEEDS ATTENTION TODAY · N ────────────────────────────┐ │ +│ │ │ │ +│ │ [ITEM] [ITEM] [ITEM] ← 2 × 3 grid, up to 6 items │ │ +│ │ [ITEM] [ITEM] [ITEM] │ │ +│ │ │ │ +│ │ (or green "✓ All caught up" card when zero) │ │ +│ │ (or "Showing 6 of N — see sections" when overflow) │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ 🏷️ CERTIFICATES · X open · Y overdue ── Open all → ───┐ │ +│ │ ROW · ROW · ROW · ROW · ROW ← top 5 │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ ┌─ 🛑 HOLDS · X open ───────────────────── Open all → ───┐ │ +│ │ ROW · ROW · ROW │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ ┌─ 🔬 NCRs · X open · Y overdue ──────── Open all → ──┐ │ +│ │ ROW · ROW │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ ┌─ ↩️ RMAs · X open ────────────────── Open all → ────┐ │ +│ │ ROW │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ ┌─ 📋 CAPAs · X open ─────────────────── Open all → ──┐ │ +│ │ ROW │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ ┌─ ✓ QC CHECKS · X open ─────────────── Open all → ──┐ │ +│ │ ROW │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +└───────────────────────────────────────────────────────────────┘ + +┌─ DATA FLOW (one endpoint, one render) ───────────────────────┐ +│ │ +│ browser POST /fp/quality/dashboard/snapshot │ +│ │ │ +│ ▼ │ +│ FpQualityDashboardSnapshot._build() │ +│ │ │ +│ ├── _critical_customer_domain() → reusable filter clause │ +│ ├── _overdue_filter(type) → per-type thresholds │ +│ │ │ +│ ├── _build_section('cert') ─┐ │ +│ ├── _build_section('hold') │ │ +│ ├── _build_section('ncr') ├─ per-type queries │ +│ ├── _build_section('rma') │ (sequential, ~50ms each) │ +│ ├── _build_section('capa') │ │ +│ └── _build_section('check') ─┘ │ +│ │ │ +│ ▼ │ +│ _build_banner(candidates) → ranked top 6 │ +│ │ │ +│ ▼ │ +│ { banner: {...}, sections: [...] } returned as JSON │ +│ │ │ +│ ▼ │ +│ OWL component renders BannerCard + 6 SectionCards in order │ +│ │ +└───────────────────────────────────────────────────────────────┘ +``` + +## Backend — snapshot endpoint + +### Replace `/fp/quality/dashboard/counts` with `/fp/quality/dashboard/snapshot` + +**Why replace, not extend:** the counts endpoint returned only numbers; the new endpoint returns numbers + actual records. The shape is incompatible. Grep confirms no other module calls the counts endpoint — it's only used by the dashboard JS we're rewriting. + +### Response shape + +```jsonc +{ + "banner": { + "items": [ + { + "type": "cert", // cert|hold|ncr|rma|capa|check + "id": 123, + "name": "CoC-30058", + "customer": "ABC Manufactoring", + "subtitle": "14h overdue · awaiting issuance", + "urgency": "overdue", // "overdue" or "critical_customer" + "critical_badge": null, // "RUSH" | "VIP" | "AS9100" | null + "open_action": { + "res_model": "fp.certificate", + "res_id": 123 + } + } + // ... up to 6 items + ], + "all_clear": false, // true when items list is empty + "total_matching": 9 // count BEFORE the top-6 truncation + }, + "sections": [ + { + "type": "cert", + "label": "Certificates", + "icon": "🏷️", + "open": 5, + "overdue": 3, + "items": [ // top 5 by urgency + { + "id": 123, + "name": "CoC-30058", + "customer": "ABC Manufactoring", + "subtitle": "14h overdue", + "urgency": "overdue", + "open_action": { + "res_model": "fp.certificate", + "res_id": 123 + } + } + // ... up to 5 + ], + "open_kanban_xmlid": "fusion_plating_certificates.action_fp_certificate" + } + // ... 6 sections in this order: cert, hold, ncr, rma, capa, check + ], + "computed_at": "2026-05-25T16:42:00" +} +``` + +### Algorithm + +```python +# fusion_plating_quality/controllers/fp_quality_dashboard.py + +# Per-type "overdue" thresholds (reused from existing counts endpoint — +# battle-tested): +# CAPA is the special case — its overdue rule is due_date < today, +# not create_date < cutoff. Marked with use_due_date=True so the +# overdue-filter dispatcher branches correctly. +OVERDUE_THRESHOLDS = { + 'cert': {'days': 1, 'use_due_date': False, 'domain': [('state', '=', 'draft')]}, + 'hold': {'days': 3, 'use_due_date': False, 'domain': [('state', 'in', ('on_hold', 'under_review'))]}, + 'ncr': {'days': 7, 'use_due_date': False, 'domain': [('state', 'in', ('open', 'containment', 'disposition'))]}, + 'rma': {'days': 5, 'use_due_date': False, 'domain': [('state', '=', 'received')]}, + 'capa': {'days': None, 'use_due_date': True, 'domain': [('state', 'not in', ('closed', 'effective'))]}, + 'check': {'days': 1, 'use_due_date': False, 'domain': [('state', '=', 'pending')]}, +} + +# Per-type config for the snapshot builder +TYPE_CONFIG = { + 'cert': {'label': 'Certificates', 'icon': '🏷️', + 'model': 'fp.certificate', + 'kanban_xmlid': 'fusion_plating_certificates.action_fp_certificate', + 'partner_field': 'partner_id', 'name_field': 'name'}, + # ... etc +} + +# Canonical order +SECTION_ORDER = ['cert', 'hold', 'ncr', 'rma', 'capa', 'check'] + + +class FpQualityDashboardSnapshot: + def __init__(self, env): + self.env = env + self.now = fields.Datetime.now() + + def build(self): + candidates_for_banner = [] + sections = [] + for type_code in SECTION_ORDER: + section = self._build_section(type_code) + if section is None: + continue # model not installed (e.g. fp.certificate) + sections.append(section) + # Pull banner candidates: overdue OR critical-customer + open + banner_candidates = self._fetch_banner_candidates(type_code) + candidates_for_banner.extend(banner_candidates) + banner = self._build_banner(candidates_for_banner) + return { + 'banner': banner, + 'sections': sections, + 'computed_at': self.now.isoformat(), + } + + def _critical_customer_domain(self, type_code): + """Per-type domain fragment that matches records with critical- + customer signals. ORed via |. Per D3 — uses existing partner + flags + aerospace/regulated indicators. The per-type field + names differ (e.g. cert uses partner_id directly; check uses + job_id.partner_id), so the method dispatches per type. + + Returns a list-of-clauses ready to be combined with the + type's overdue filter via Odoo OR domain prefix notation. + Returns empty list when none of the optional fields exist. + """ + # cert → partner_id.x_fc_rush | partner_id.x_fc_vip | + # part_catalog_id.name ILIKE '%aerospace%' | + # customer_spec_id.code ILIKE 'AS9100%' | 'NADCAP%' + # hold → partner_id.x_fc_rush | x_fc_vip (no part/spec on hold) + # ncr → partner_id.x_fc_rush | x_fc_vip + part via job_id link + # rma → partner_id.x_fc_rush | x_fc_vip + # capa → partner_id.x_fc_rush | x_fc_vip (via linked ncr/rma) + # check → job_id.partner_id.x_fc_rush | x_fc_vip + part via job_id + ... + + def _overdue_filter(self, type_code): + """Build overdue domain for the type. CAPA uses due_date < today; + all others use create_date < (now - threshold_days).""" + cfg = OVERDUE_THRESHOLDS[type_code] + base = list(cfg['domain']) + if type_code == 'capa': + base += [('due_date', '<', self.now.date()), + ('due_date', '!=', False)] + else: + cutoff = self.now - timedelta(days=cfg['days']) + base += [('create_date', '<', cutoff)] + return base + + def _fetch_banner_candidates(self, type_code): + """Per-type pull of records that qualify for the banner: + (overdue) OR (critical-customer AND still-open). + Returns list of dicts in the shape that _build_banner can sort. + """ + # ... runs two searches per type (overdue + critical-customer-open), + # dedupes by id, returns shaped dicts with urgency + critical_badge. + ... + + def _build_section(self, type_code): + """Return the section dict with top-5 items + counts. + Returns None when the model isn't installed.""" + ... + + def _build_banner(self, candidates): + """Rank candidates: overdue first (oldest first), then + critical-customer-non-overdue (oldest first). Take top 6. + Returns {items: [...], all_clear: bool, total_matching: int}.""" + ... +``` + +**Sudo strategy** — per Rule 13m, the snapshot reads cross-module fields (`partner_id.x_fc_rush`, `part_catalog_id.name`, `customer_spec_id.code`) that low-privilege roles might not have read on. The controller does `request.env['fp.certificate'].sudo()` etc. for the snapshot build. The `open_action` payload navigates via standard `act_window` which re-enforces ACL on click. + +**Defensive field-existence checks** — the cross-module field reads are guarded: +```python +partner = rec.partner_id +is_rush = bool(getattr(partner, 'x_fc_rush', False)) +is_vip = bool(getattr(partner, 'x_fc_vip', False)) +``` +If a field doesn't exist (module uninstalled), that signal is unavailable — the item can still qualify via the overdue path. + +## Frontend — OWL component tree + +``` +FpQualityDashboard // top-level client action +├── BannerCard // one card; shows items or all-clear +│ └── BannerItem[] // 0-6 grid cells +└── SectionCard[] // 6 in fixed order + └── SectionRow[] // up to 5 rows, or 1 italic "no open items" +``` + +**Sub-components live in the same JS file** (`fp_quality_dashboard.js`) — they're not reused anywhere else. If `BannerItem` later moves to a manager dashboard, *then* it migrates to `components/`. + +### Component state + +```javascript +this.state = useState({ + loading: true, + snapshot: null, // the whole response shape + error: null, // shown if RPC fails +}); + +// On mount: +// 1. await rpc('/fp/quality/dashboard/snapshot') → state.snapshot +// 2. if action.params.tab → scroll to section after render +// 3. start 60s setInterval to re-poll +// +// On row click — build the act_window dict explicitly. doAction +// accepts either a full action dict OR an xmlid string; we use the +// dict shape here because we're synthesising a form view from the +// snapshot payload (no act_window xmlid exists for "this specific +// record's form"). +// this.action.doAction({ +// type: 'ir.actions.act_window', +// res_model: item.open_action.res_model, +// res_id: item.open_action.res_id, +// view_mode: 'form', +// views: [[false, 'form']], +// target: 'current', +// }); +// +// On "Open all →" click — pass the xmlid string directly. The +// action service in Odoo 19 resolves it via the registry. Confirmed +// pattern used by the existing dashboard's openTab() method which +// already passes a full dict; we use the xmlid form here because +// the snapshot ships pre-resolved xmlids and we don't want to +// re-encode the kanban view config in the snapshot payload. +// this.action.doAction(section.open_kanban_xmlid); +// +// Note: if `doAction(xmlid_string)` ever stops working in a future +// Odoo version, the fallback is to ship the full act_window dict in +// the snapshot instead of just the xmlid string — change is local +// to the snapshot builder. +``` + +### Deep-link preservation + +The notification email from spec 2026-05-25-post-shop-cert-shipping-job-states links to `/odoo/action-fp_quality_dashboard?tab=certificates`. The new dashboard reads `this.props.action.context.params.tab` on mount and, after first render, calls `document.getElementById('section-').scrollIntoView({behavior: 'smooth'})`. + +**Template requirement (don't forget at implementation):** each `` MUST render `
`. Without the IDs the scrollIntoView call no-ops silently and the deep-link still lands on the dashboard but doesn't focus the right section. The tab → section_id mapping is direct (`'certificates'` from the URL maps to `'cert'` from the type code — the email's `?tab=certificates` arrives as `params.tab='certificates'` so the JS needs a one-line normalize: `const sectionType = tab.startsWith('cert') ? 'cert' : tab;`). + +No change to the email body needed. + +## SCSS — file structure + dark-mode + +**Single file**: `fusion_plating_quality/static/src/scss/fp_quality_dashboard.scss`. No partials — the dashboard is small enough. + +**Tokens**: reuse `$plant-card-bg`, `$plant-bg`, `$plant-card-border`, `$plant-text`, `$plant-muted` from `_plant_tokens.scss` (loaded earlier in the manifest). Dark mode auto-flips via the existing `@if $o-webclient-color-scheme == dark` global override. + +**New scoped tokens** (defined locally with light + dark variants): + +```scss +$_qd-urgent-bg-light: #fee2e2; +$_qd-urgent-bg-dark: #3a1818; +$_qd-urgent-border: #dc2626; + +$_qd-good-bg-light: #d1fae5; +$_qd-good-bg-dark: #14281a; +$_qd-good-border: #22c55e; + +$_qd-section-head-bg-light: #fef3c7; +$_qd-section-head-bg-dark: #3a2f15; + +@if $o-webclient-color-scheme == dark { + $_qd-urgent-bg-light: $_qd-urgent-bg-dark !global; + $_qd-good-bg-light: $_qd-good-bg-dark !global; + $_qd-section-head-bg-light: $_qd-section-head-bg-dark !global; +} +``` + +**Banner styling**: +- When items present: `background: linear-gradient(135deg, $_qd-urgent-bg-light, $plant-card-bg);` + `border: 1px solid $_qd-urgent-border;` +- When zero items (all-clear): swap to green tokens. +- Grid: `display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;` collapsing to 1fr below 900px. + +**Section styling**: +- Card pattern with header strip + body, gradients matching the existing plant kanban polish (Rule 9, no hardcoded hex outside the dark-branch defs). +- Section header bg uses `$_qd-section-head-bg-light` (amber gradient). +- Row hover: subtle `background: rgba(0,0,0,0.03)` lift. + +## File inventory + +### Modify + +| Path | Change | +|---|---| +| `fusion_plating_quality/controllers/fp_quality_dashboard.py` | **Rewrite.** Delete `counts()` route. Add `snapshot()` route + `FpQualityDashboardSnapshot` helper class. Same file, expanded. | +| `fusion_plating_quality/static/src/js/fp_quality_dashboard.js` | **Rewrite.** Drop `TABS` array, `selectTab`, `openTab`. New shape: `setup` fetches snapshot, `onOpenItem` / `onOpenSection` actions. Add BannerCard + BannerItem + SectionCard + SectionRow sub-components in same file. Keep the `?tab=` param parsing but translate to scrollIntoView. | +| `fusion_plating_quality/static/src/xml/fp_quality_dashboard.xml` | **Rewrite.** New template structure: outer wrapper + banner card + 6 section cards via `t-foreach="snapshot.sections"`. No tab row. | +| `fusion_plating_quality/static/src/scss/fp_quality_dashboard.scss` | **Rewrite.** New token block, banner styles, section styles, row styles, mobile breakpoint. Reuse `$plant-*` base tokens. | +| `fusion_plating_quality/__manifest__.py` | Version bump `19.0.7.0.0` → `19.0.8.0.0`. | + +### Create + +| Path | Purpose | +|---|---| +| `fusion_plating_quality/tests/test_dashboard_snapshot.py` | NEW — unit tests for the snapshot endpoint (algorithms, edge cases, missing-module guards) | +| `fusion_plating_quality/scripts/bt_quality_dashboard_redesign.py` | NEW — entech smoke script (RPC call, response-shape assertions, click-through smoke) | + +### Untouched + +- All per-model kanban views — `fusion.plating.quality.hold`, `fusion.plating.quality.check`, `fusion.plating.ncr`, `fusion.plating.capa`, `fusion.plating.rma`, `fp.certificate` +- All per-model form views + their action_* methods (Issue, Disposition, etc.) +- The cert authority ACL guard (Rule 24 / spec 2026-05-25-post-shop) — fires unchanged from the cert form +- The `fp.notification.template` + `mail.activity` infrastructure +- The existing menu entry — same `ir.actions.client` xmlid, same menu entry, just different template/JS/CSS/controller behind it + +## Edge cases + defensive design + +| Case | Behavior | +|---|---| +| Zero banner items | Banner renders green `✓ All caught up — no critical items right now` card. Sections still render below. | +| > 6 banner-eligible items | Top 6 by urgency rank shown; footer line `Showing 6 of N urgent items — see sections below` | +| Section with zero open items | Renders the card with one italic row `No open items` — predictable layout, no hidden states | +| Item appears in BOTH banner and section | Intentional — banner is across-types urgency, section is per-type queue. Visual reinforcement. | +| `fp.certificate` (or any type's model) not installed | `_build_section('cert')` returns None; section omitted from response. Banner skips cert candidates. | +| Cross-module field missing (e.g. `partner.x_fc_rush` not defined) | `getattr(partner, 'x_fc_rush', False)` falls back to False — item only qualifies via overdue path | +| Cert created in the second BEFORE snapshot fires | Negligible — 60s poll catches it next refresh. No real-time correctness requirement. | +| User lacks ACL on a record in their snapshot | `Open →` opens the record via `act_window`; Odoo's standard ACL fires at navigation; user gets the standard access error. Not pre-filtered in the snapshot (would 4x query cost). | +| Snapshot RPC fails (network blip, DB lock) | Frontend shows `Couldn't refresh dashboard — retry in 60s` banner. Last-known snapshot stays on screen. Same pattern as existing `_refreshCounts`. | +| Mobile / tablet < 900px | Banner grid collapses 3-col → 1-col. Sections stay full-width. Row buttons keep ≥32px tap target. | +| Banner item's source section is below the fold | No special handling — banner is its own surface. Click navigates via `act_window` regardless of section visibility. | +| Dark mode toggle mid-session | Browser reload required (Odoo standard behavior). Tokens flip automatically via SCSS compile-time branch. | + +## Testing strategy + +### Unit tests — `fusion_plating_quality/tests/test_dashboard_snapshot.py` (NEW) + +| Test | Asserts | +|---|---| +| `test_empty_db_returns_all_clear` | Empty DB → `banner.all_clear == True`, all 6 sections present with `open == 0` | +| `test_overdue_cert_in_banner` | Cert created 25h ago → `banner.items[0].type == 'cert'`, `urgency == 'overdue'` | +| `test_vip_cert_in_banner_before_overdue` | Cert created 1h ago, customer.x_fc_vip=True → in banner with `urgency == 'critical_customer'` and `critical_badge == 'VIP'` | +| `test_rush_partner_banner_badge` | partner.x_fc_rush=True → `critical_badge == 'RUSH'` | +| `test_aerospace_part_banner_badge` | part.name='Aerospace Bracket' → `critical_badge == 'AS9100'` (or similar) | +| `test_banner_caps_at_6_with_overflow_count` | 8 overdue items → `banner.items` length 6, `total_matching == 8` | +| `test_banner_ranks_overdue_before_critical_customer` | Mix of 3 overdue + 5 VIP-non-overdue → first 3 are overdue, next 3 are VIP | +| `test_section_order_is_canonical` | Response `sections` list ordered: cert, hold, ncr, rma, capa, check | +| `test_section_top_5_only` | 8 items of one type → section.items length is 5; section.open == 8 | +| `test_missing_certificate_model_omits_section` | Mock `fp.certificate` not in env → no cert section, no traceback | +| `test_missing_partner_field_falls_through` | Partner without `x_fc_rush` field → item evaluated via overdue path only, no AttributeError | +| `test_snapshot_includes_computed_at_iso` | Response has `computed_at` as parseable ISO timestamp | + +### Entech smoke script — `fusion_plating_quality/scripts/bt_quality_dashboard_redesign.py` (NEW) + +Steps: +1. Hit `/fp/quality/dashboard/snapshot` via odoo-shell-style RPC +2. Assert response shape (banner + sections present, all keys present) +3. Assert section order is canonical +4. Assert each section has `open_kanban_xmlid` that resolves to a real `ir.actions.act_window` +5. Pick first banner item → build the equivalent `act_window` from `open_action` → verify it resolves +6. Print summary: open/overdue per section + banner item count + +### Manual QA on entech + +1. `/odoo/action-fp_quality_dashboard` as admin — verify banner + sections render in dark mode +2. As a Sales Rep (lower ACL) — verify items render; click an item → expect ACL error if blocked +3. Issue a draft cert via the cert form → reload dashboard → cert disappears from Certificates section +4. Flag a partner `x_fc_rush=True` → reload → their open items get `[RUSH]` badge in banner +5. Dark mode toggle in user prefs → reload → confirm gradient + green/red flips correctly +6. Resize browser to < 900px → confirm banner collapses to 1 column, sections stay readable + +## Migration / rollback + +- **No DB migration** — purely a UI replacement. No schema changes. +- **No data backfill** — the snapshot is computed at request time from existing data. +- **Rollback**: `git revert` the implementation commits, `-u fusion_plating_quality`, asset cache bust. Affected users see the old tab-router until the revert deploys. + +## Files touched summary + +``` +fusion_plating_quality/ +├── controllers/ +│ └── fp_quality_dashboard.py MODIFY (rewrite endpoint + helper class) +├── static/src/ +│ ├── js/fp_quality_dashboard.js MODIFY (rewrite component + sub-components) +│ ├── xml/fp_quality_dashboard.xml MODIFY (rewrite template) +│ └── scss/fp_quality_dashboard.scss MODIFY (rewrite styles) +├── tests/ +│ └── test_dashboard_snapshot.py CREATE (unit tests) +├── scripts/ +│ └── bt_quality_dashboard_redesign.py CREATE (entech smoke) +└── __manifest__.py MODIFY (version 19.0.7.0.0 → 19.0.8.0.0) +``` + +5 files modified, 2 created. + +## Open questions for implementation phase + +1. **`subtitle` text** per type — what's the second line on each row? For certs `"14h overdue · awaiting issuance"` is obvious; for NCRs `"7d · disposition pending"` works; CAPAs `"due 5/30"`; RMAs `"received 2d ago"`. Settle the exact format during implementation. Not a design decision. +2. **Icon choice** — emojis (🏷️ 🛑 🔬 ↩️ 📋 ✓) vs Font Awesome (`fa-certificate`, `fa-stop-circle`, etc.). Plant kanban uses emojis; consistency argues for emojis. Trivially swappable later. +3. **Per-section search/sort** within the dashboard — out of scope for v1 (the per-model kanban has these via the standard search bar). Revisit if QM asks. +4. **Polling pause when tab is hidden** — `document.visibilityState` check could pause the poll when the user is on another tab. Nice-to-have, not in v1. + +## Status & deployment notes + +Target version bump: `fusion_plating_quality` 19.0.7.0.0 → **19.0.8.0.0**. + +Deploy steps (mirrors the post-shop redesign flow): +1. Sync the 5 modified files + 2 new files to entech `/mnt/extra-addons/custom/fusion_plating_quality/` +2. `-u fusion_plating_quality` (no other modules — this is self-contained) +3. Asset cache bust: `DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';` +4. Restart odoo +5. Run the smoke script via odoo-shell +6. Manual browser verification at `/odoo/action-fp_quality_dashboard` + +No coordination with other modules required — the dashboard's only callers are humans clicking the menu entry.