Customer feedback: rows 2 (description) and 3 (serials) in the Part
cell rendered as read-only spans. User wanted to edit directly.
New writable computed fields on fp.direct.order.line:
- part_name_editable: compute reads part_catalog_id.name, inverse
writes back to part.name on the linked catalog record
- serials_text: compute joins serial_ids names with commas; inverse
parses the typed string and find-or-creates fp.serial records,
updates the line's serial_ids M2M
Removed the redundant rev separator (display_name already includes
'(Rev X)' so showing it twice was clutter). Rev edits happen by
editing the part record directly via the OPEN button.
OWL widget templates updated:
- Row 2: <input> bound to part_name_editable, t-on-change saves
- Row 3: <input> bound to serials_text, t-on-change parses + saves
SCSS:
- Row 2 input: italic, transparent border, focus tints background yellow
- Row 3 input: small grey text, comma-separated friendly placeholder
- Both disabled-look when no part is picked
Both inputs trigger the inverse method on blur. The G4 sync chain
takes over from there to push line.line_description etc. back to
the part as before — so editing in the line keeps the part defaults
fresh for future orders.
Four customer-feedback fixes (G1-G4):
G1 — Part cell display redundancy. fp.part.catalog.display_name was
showing 'PART (Rev X) — Name' which duplicated with my Part cell widget's
separately-rendered revision + name rows. Added @api.depends_context
('fp_express_part_picker') to _compute_display_name: when the context
flag is True, display_name returns JUST the part_number. The Express
view passes the flag on the part_catalog_id field, so the picker shows
'9876699373' and the widget's row 2/3 show the rev + name.
G2 — Material/Process Tag is now the order's RECIPE, not a free-text
shop tag. Converted material_process from Char to Many2One(fusion.
plating.process.node) with domain [('node_type','=','recipe')] on both
fp.direct.order.wizard AND sale.order. Pre-migration (19.0.22.1.0/
pre-migrate.py) drops the old VARCHAR column so Odoo recreates as
INTEGER FK. Per dev-stage policy, old tag data is dropped.
G3 — Auto-apply order recipe to every line. New onchange
_onchange_material_process_apply_to_lines on the wizard: when the
header recipe is picked / changed, propagate to every line's
process_variant_id (unless the line has an explicit per-line override
that doesn't match the previous header value).
Plus an override on fp.direct.order.line.create that seeds new lines'
process_variant_id from wizard.material_process. So a newly-added
line auto-inherits the order's recipe.
G4 — Auto-sync line edits back to the part catalog. New
_fp_sync_to_part method called from create() + write() on
fp.direct.order.line. Tracked fields:
- line_description → part.default_specification_text
- bake_instructions → part.default_bake_instructions
- thickness_range → part.x_fc_default_thickness_range
- masking_enabled → part.default_masking_enabled
- process_variant_id → part.default_process_variant_id
Future orders for the same part will auto-pull these updated defaults
via the existing _onchange_part_default_thickness chain. Last-write-
wins semantics across concurrent edits (acceptable per dev-stage).
Wasted-space audit revealed:
1. PO Block occupied a wide 2-col span but its inner inputs didn't
fill the available width (130px label column + narrow input area)
2. Customer Job # + Job Sorting placed in row 2 cols 3-4 next to the
tall PO block left empty vertical space below them
3. Row 2 col 3-4 was sparse because the PO block forced row 2 to be tall
Reorganized:
- PO Block now spans 2 grid ROWS (.row-span-2 class → grid-row: span 2)
- Customer Job # / Job Sorting flow into row 2 cols 3-4 (alongside PO top)
- Material/Process Tag / Lead Time flow into row 3 cols 3-4 (alongside
PO bottom) — filling the previously-empty space next to the PO block
- Row 4 (after PO ends): Payment Terms / Delivery Method / Pricelist /
Quote Validity — full 4-col width back
- Row 5: Blanket Sales Order + Invoice Strategy + conditional Deposit % /
Progress Initial % (only show when relevant invoice strategy picked)
Inside the PO block:
- Label column tightened 130px → 110px so the input takes more width
- Inputs + Many2One wrappers now have width: 100% propagated, so PO #
and Expected By inputs fill the available row width
- Upload button restyled with the accent colour (was the green default)
Net effect: same field count but tighter packing, no empty vertical
or horizontal space next to the PO block.
Root cause for column widths: Odoo 19's column_width_hook.js dynamically
sets inline widths on every cell at render time, overriding any CSS
width on td/th selectors. Confirmed by reading the hook source on
entech: 'A width can also be hardcoded in the arch (width="60px").'
Fix: set width='Npx' as an ARCH ATTRIBUTE on each <field> in the line
list:
- Part Number 230px, Line Job # 80px, Thickness 100px, Mask 55px,
Bake 120px, Qty 55px, Price 80px, Subtotal 90px, Action stack 60px
- Specification + Internal Notes get NO width → take remaining flex
space (responsive: layout adapts to viewport)
Root cause for missing checkbox: my SCSS underline-style override
selected ALL .o_field_widget input including type=checkbox, rendering
checkboxes as 30px-tall full-width transparent text inputs.
Fix: exclude type=checkbox/radio/file from the underline rule, and
add explicit rendering for type=checkbox (18px square, accent-coloured)
inside .o_fp_xpr_cell. The Blanket Sales Order checkbox + the inline
Block partial shipments checkbox are now both visible.
1. Blanket Sales Order — match legacy field shape. Renamed label from
'Blanket SO' to 'Blanket Sales Order' (matches legacy view), removed
the boolean_toggle widget (defaults to checkbox), and added the
sibling 'block_partial_shipments' field inline (only visible when
blanket is checked, with 'Block partial shipments' helper text).
2. Column widths — give roomier columns where data needs space, tighten
numeric columns. Part Number 230px, Specification min 220px,
Internal Notes min 140px, Qty 60px, Price 80px, Subtotal 90px,
Mask 55px, Bake 130px, Action stack 60px.
3. Stacked DWG / OPEN buttons — new OWL widget FpExpressActionBtns
(express_action_btns.js + .xml) renders both buttons vertically in
ONE column to save horizontal space. Widget binds to a new
action_btns_anchor field (related from part_catalog_id) on the
line. Each button shows tooltip + disabled state when no part is
picked; DWG triggers the native file picker, OPEN navigates to the
part record.
4. Field activation — clicking the cell anywhere now focuses the
input, not just clicking the label. Achieved via:
- cursor: text on .o_fp_xpr_cell
- cursor: pointer on labels
- min-height: 30px on all inputs (larger click target)
- width: 100% propagated through Many2One wrappers (.o-dropdown,
.o-autocomplete) so the input genuinely fills the cell
- box-sizing: border-box so widths are predictable
- Background tint on focus for visual feedback
A bad replacement in the previous commit left an extra '}' that
prematurely closed the .o_fp_xpr block, dumping all the legend bar /
PO status pill / part cell / bake pill styles OUTSIDE the namespace.
SCSS compile silently produced an unusable bundle and the form
rendered without any of the new visual treatment.
Brace balance now verified at 0.
NEW OWL widgets:
- FpExpressPartCell (static/src/js/express_part_cell.js + .xml) — multi-row
Part cell. Wraps Many2OneField for the part picker (row 1: part# / rev,
bold). Below it: row 2 part description (italic muted), row 3 serial #s
joined + '+ bulk' button that triggers the existing bulk-add wizard.
- FpExpressBakePill (static/src/js/express_bake_pill.js + .xml) — click-
to-edit Bake pill. Renders amber pill when set, italic muted 'no bake'
when empty. Click swaps to inline textarea + Save / Clear / Cancel.
NEW fields:
- fp.direct.order.line.part_number_display / part_revision_display /
part_name_display (related Char from fp.part.catalog) — fed to the
Part cell widget so it can render multi-row without RPC.
- fp.direct.order.wizard.tooling_charge (Monetary) — surfaced in the
Totals card on the Express form.
- fp.direct.order.wizard.po_status (computed Selection
received/pending/missing) — drives the PO Block status badge.
- sale.order.x_fc_tooling_charge (Monetary) — receives wizard.tooling_charge
at confirm.
View updates (fp_express_order_views.xml):
- PO block header now shows the PO status pill (green Received,
amber Pending, red Missing)
- Order Lines legend bar (Mask / Bake pill / DWG / OPEN explainers)
- Part Number column uses widget='fp_express_part_cell' — single cell
with 3 internal rows
- Bake column uses widget='fp_express_bake_pill' — interactive pill
- Totals card now has Subtotal / Tooling Charge / Total Lines / Total
Quantity / Grand Total + currency pill
SCSS adds:
- Multi-row part cell styles (internal borders, bold/italic/muted rows)
- Bake pill (has-bake amber, no-bake italic muted) + inline editor
- Legend bar (section background, gap-spaced explainer chips)
- PO status pill colour scheme
- Bulk button styling
Wizard's action_create_order now carries tooling_charge to the SO at
confirm so it persists on the resulting sale.order.
Previous rebuild used Odoo's <group col='4'> which renders as an HTML
table — colspan+nesting broke into a vertical stack. Replaced entirely
with raw <div> + CSS Grid (display: grid; grid-template-columns:
repeat(4, 1fr)) so the header layout matches the mockup exactly:
- Row 1: Customer (span 2) + Shipping Address (span 2)
- Row 2: PO block (span 2, accent-bordered card with PO#/PDF/Pending
toggle/Expected date stacked + chase warning) + Customer Job # + Job Sorting
- Row 3: Material/Process Tag + Lead Time (inline X to Y) + Payment
Terms + Delivery Method
- Row 4: Blanket SO + Currency/Pricelist + Quote Validity + Invoice Strategy
Footer also rebuilt as CSS Grid (1fr 320px) — Notes/Terms cards
stacked in the left column, Totals card with Grand Total + currency
pill in the right column. Each card has a title + subtitle + body
matching the mockup's card chrome.
SCSS overrides Odoo's default field chrome inside .o_fp_xpr_cell so
inputs render with the mockup's underline style (no Bootstrap form-
control border, just a 1px bottom-border that thickens on focus).
Restructures the Express form to align with the brainstorming mockup:
Header (4-column grid via <group col='4'>):
- Row 1: Customer (colspan=2) + Shipping Address (colspan=2)
- Row 2: Consolidated PO Block (colspan=2 with PO#/PDF/Pending toggle/
Expected date stacked + chase warning inline) + Customer Job # + Job Sorting
- Row 3: Material/Process Tag + Lead Time (X to Y inline) + Payment Terms + Delivery Method
- Row 4: Blanket SO + Currency/Pricelist + Quote Validity + Invoice Strategy
Lines: 13 inline columns including the Express-specific Line Job #,
masking toggle, bake text, plus per-line action buttons (DWG, OPEN,
+ bulk) wired to the Phase B helpers.
Footer: side-by-side cards — Notes + Terms stacked in the left card,
Totals card on the right with Total Lines / Total Qty / Grand Total
+ currency pill.
SCSS adds:
- PO block: accent-bordered card-within-card
- Lines: tight spreadsheet borders, hover row highlight
- Bake column: amber pill style, italic 'no bake' for empty
- Customer Line Job #: bold, uppercase, narrow column
- Inline action buttons: small uppercase bordered chips
- Footer cards with prominent Grand Total + currency pill
OWL multi-row Part cell (FpExpressPartCell) and click-to-edit Bake
pill (FpExpressBakePill) are still deferred — they need real OWL
components, separate pass.
C1: product.pricelist._compute_display_name override gated by the
'fp_express_currency_picker' context flag (set on the Express form's
pricelist_id field). When active, prefixes the dropdown label with
the currency code: 'CAD — Public Pricelist (CAD)'. Elsewhere the
standard display name is unchanged.
C3: SCSS tokens + base styles for the Express form. Tokens use the
compile-time @if $o-webclient-color-scheme branch per the project's
'Dark Mode' rule — same SCSS compiles into both bundles with different
hex values. Token vars wrapped in CSS custom properties so downstream
modules can override for per-shop branding without recompiling.
Base styles: spreadsheet-feel table borders, bake-cell inset-pill,
customer line ref bold/uppercase, accent section markers.
D1: action_open_draft method routes drafts-list click to the matching
form view by view_source. view_source badge column added to the drafts
list (Express=blue, Legacy=muted).
D2: Deprecation banner on the legacy direct-order form pointing operators
to the new Express view, plus a Switch-to-Express header button. Legacy
action context defaults new drafts to view_source='legacy' (Express
action defaults to 'express') so newly-created drafts open in the right
view automatically.
Consolidates the brainstorming session into a single design spec. Covers:
header layout + field-to-model mapping, line widget (multi-row Part cell,
masking + bake pills, serial bulk-add trigger), masking/baking override
flow at SO confirm, currency/pricelist picker mechanics, inline part
create + drawing upload + open-part buttons, and phase-out path for the
legacy Direct Order view.
Reuses fp.direct.order.wizard model end-to-end (Q1=D). New fields:
material_process, customer_line_ref, masking_enabled, bake_instructions,
default_specification_text, default_bake_instructions,
default_masking_enabled, x_fc_internal_notes, x_fc_print_terms.
Rename: wizard.notes → terms_and_conditions. Retire: wizard.currency_id.
Mockup at .claude/mockups/express_orders.html (interactive, light + dark).
User is on Mac via Tailscale into this Windows host. Browser previews
bound to Windows localhost are unreachable from the Mac browser. Default
to text-based design discussion on this host instead of spinning up the
brainstorming visual companion. Has bitten three times now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures all clarifying-question answers + exploration findings so a
fresh Claude Code session on Mac can resume at 'propose architectural
approaches' without re-running the discovery work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs caught by entech battle test on first deploy:
1. Manifest entry landed in the 'demo' list instead of 'data' because
my anchor (fp_demo_shopfloor_data.xml) was already in 'demo' —
the entry pattern-matched into the wrong section. Demo data
doesn't load on entech (no --load demo), so the mail.template
never existed. Moved fp_tablet_pin_reset_template.xml to 'data'.
2. The fp.notification.template wrapper record referenced a model
that doesn't exist until fusion_plating_notifications loads;
fusion_plating_shopfloor doesn't depend on notifications, so
the data load ParseError'd. Removed the wrapper — the controller
calls mail_template.send_mail() directly anyway, not via the
notification dispatcher. Added an inline comment explaining why
the wrapper isn't here.
Battle test updated to drop the (now removed) wrapper xmlid check.
Battle test ALL PASS on entech after fixes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 new state-machine modes on FpTabletLock, reusing the existing
FpPinPad 4-cell component:
- request_code : 'Send temp PIN' button screen (no-PIN tile OR
after 3-fail Forgot button)
- enter_temp_code : 4-cell pad for the emailed code
- set_new_pin : 4-cell pad — choose new PIN
- confirm_new_pin : 4-cell pad — confirm new PIN
Trigger paths (per D1 + D2):
- Tap no-PIN tile -> goes straight to request_code mode
(onTileClick dispatches via tile.has_pin)
- Wrong PIN 3 times -> 'Forgot? Reset PIN via email' button appears
below the pad (gated by state.failedAttempts >= 3)
Client-side failedAttempts counter (resets on tile re-select per D14).
Server-side x_fc_tablet_pin_failed_count keeps incrementing to the
existing 5-fail lockout per D13.
After Confirm New PIN succeeds, auto-login fires unlock_session with
the new PIN. If unlock_session fails for any reason, falls back to
'PIN set, tap your tile to log in.' status.
SCSS reuses $lock-* tokens from _tablet_lock_tokens.scss — light +
dark handled by the existing token system (no new tokens needed).
Hand-Off gold gradient repeated for the primary 'Send temporary PIN'
button to match the existing tablet visual language.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mail template renders the 4-digit code in both subject (mobile
notification glance) and body (big bold display). Per Rule 25 only
core res.users fields referenced; the code itself comes from ctx.
fp.notification.template wrapper enables admin UI customization of
the body without touching code. tablet_pin_reset_requested added to
TRIGGER_EVENTS selection.
Daily ir.cron purges used/expired rows > 7 days old (audit trail
lives in fp.tablet.session.event, not here, so aggressive cleanup
is safe).
Manifest bump 19.0.34.2.0 -> 19.0.35.0.0 (triggers asset cache
invalidation on -u so the new template + SCSS load cleanly).
Phase 1 backend complete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three controller changes in one commit (tight code coupling):
1. /fp/tablet/request_reset_code (Task 2) — generates 4-digit code,
emails it, returns masked_email. Specific error codes for the
frontend to switch on (no_email + manager_name, rate_limited +
wait_minutes, user_not_found, no_role, inactive). Shop-branch
role check matches existing _check_credentials per Rule 13l + 23
(all_group_ids transitive — Owners reach Technician through
implication).
2. /fp/tablet/verify_reset_code (Task 3) — verifies the emailed
code, on success mints a 5-min HMAC reset_token. Error responses
are specific (no_active_code / expired / too_many_attempts /
wrong_code with attempts_left).
3. set_pin extended to accept reset_token (Task 4) — three auth
paths now: old_pin (existing), reset_token (new), or neither
(existing — only for users with no current hash). reset_token
path is the only one that operates on a user OTHER than env.user;
token proves the legit user just verified their email.
Failure audit reuses existing failed_unlock event_type with a notes
field describing the reset-code-specific reason. Success audit uses
the new pin_reset_requested / pin_reset_code_verified /
pin_set_after_reset event_type values.
_mask_email helper added for the no-email-on-file edge case.
3 more tests cover: valid token roundtrip + set_pin, expired token
rejection, and lockout-cleared-on-reset.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fp.tablet.pin.reset stores hashed 4-digit codes emailed for self-
service PIN create/reset. Per CLAUDE.md Rule 24 + Rule 13l it follows
the defensive patterns established elsewhere in the shopfloor module:
- PBKDF2-SHA256 hashing (200k iterations, matches ResUsers PIN)
- 72h TTL per D4
- 5 wrong-attempt cap per D5 (invalidates code, used_at set)
- 3 requests/60min rate limit per D6 (raises UserError)
- SQL EXCLUDE constraint enforces one-active-row-per-user per D7
- HMAC-SHA256 reset_token (300s TTL, single-use) for step 3 of
the flow (set_pin via reset_token alternative to old_pin)
Audit event_type extended with 3 new values (pin_reset_requested,
pin_reset_code_verified, pin_set_after_reset). Manager-only ACL on
the new model; sudo when endpoints need access.
10 model-level tests cover generate / replace-active / rate-limit /
verify-correct / verify-wrong / 5-attempt-cap / expired / token sign
roundtrip / tampered-sig / purpose-mismatch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User goal: from the Shop Floor Terminal lock screen, a user with no
PIN (or who forgot their PIN) should be able to set / reset their
own PIN without a manager's help. Today, FpPinSetup runs only from
Preferences which requires being logged in — there's no path from
the lock screen.
Design (approved, with user-picked defaults):
- Tap tile of no-PIN user -> 'Send temporary PIN' button -> email
4-digit code, valid 72 hours -> enter code -> choose new PIN ->
auto-login.
- For existing-PIN users: 3 failed PIN entries -> 'Forgot? Reset
PIN via email' button appears below keypad -> same email flow.
- Both flows merge at: enter temp code -> set new PIN.
- Email goes to res.users.login (or partner_id.email fallback).
No-email-on-file -> 'Contact your manager: <owner>' message.
- Rate limit: 3 requests per user per rolling 60 min.
- Per-code cap: 5 wrong attempts invalidates the code.
- New model fp.tablet.pin.reset stores hashed code + expires_at
with SQL constraint enforcing one-active-row-per-user.
- 2 new endpoints (request_reset_code, verify_reset_code) + extend
existing /fp/tablet/set_pin to accept reset_token alternative
to old_pin.
- Audit: 3 new event_type values on fp.tablet.session.event.
- Reuses existing PBKDF2 helpers, FpPinPad component (mode prop),
fp.notification.template dispatch, mail.template pattern.
Per CLAUDE.md Rule 25 the mail template references ONLY core
res.users fields (object.name, object.email, object.login,
object.company_id) — ctx.code is dispatched as extra_context, not
a model field. Safe at parse-time.
Self-review fixed 2 issues:
- event_kind -> event_type (real field name on fp.tablet.session.event)
- Listed existing event_type values explicitly for context
Spec: docs/superpowers/specs/2026-05-25-tablet-pin-self-service-design.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan-time xmlids were wrong — entech battle test caught all 5
non-cert kanban xmlids missing. Real xmlids (queried via
ir.model.data on entech):
hold: action_fp_quality_hold (was action_fusion_plating_quality_hold)
ncr: action_fp_ncr (was action_fusion_plating_ncr)
rma: action_fp_rma (was action_fusion_plating_rma)
capa: action_fp_capa (was action_fusion_plating_capa)
check: action_fp_quality_check (was action_fusion_plating_quality_check)
cert stays unchanged — action_fp_certificate was already correct.
After fix: battle test ALL PASS — 6 sections in canonical order,
all xmlids resolve, 3 banner items pulled from real entech data
(5 draft certs, 3 of them overdue past 24h).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Version 19.0.7.0.0 → 19.0.8.0.0 (triggers asset cache invalidation
on -u so the new template + SCSS load cleanly).
Battle test script: 6-check entech smoke. Validates snapshot shape,
canonical section order, required section keys, open_kanban_xmlid
resolves to act_window, banner item shape when items exist. Summary
prints per-section counts so you can eyeball the entech state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JS: single FpQualityDashboard component + BannerCard / BannerItem /
SectionCard / SectionRow sibling sub-components in the same file.
Fetches /fp/quality/dashboard/snapshot, 60s poll, deep-link
?tab=certificates scrolls to section-cert via scrollIntoView.
XML: outer wrapper + banner + 6 sections (t-foreach over
state.snapshot.sections). Each section has id='section-<type>' so
the deep-link target works. SectionRow has overdue-conditional
class for red subtitle highlight.
SCSS: local tokens for urgent/good/section-head with light+dark via
$o-webclient-color-scheme branch. 135deg gradients matching the
plant kanban polish. Mobile breakpoint at 900px collapses banner
grid to 1 col and stacks row Open button.
OLD TABS array, selectTab, openTab, totalOpen, totalOverdue all
deleted. Old template's tab tiles + per-tab panels deleted. Existing
per-model kanbans untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers: missing-field critical-customer check returns empty without
crashing; computed_at is a valid ISO timestamp; every section ships
a non-empty open_kanban_xmlid in module.xmlid format.
(missing-model test from the plan omitted — patching env.__contains__
was unsafe; the in-self.env guard is already exercised by Tasks 2-4
in production behavior. The other 3 defensive tests still cover the
missing-field path, which is the more common scenario.)
Phase 1 backend complete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
_fetch_banner_candidates collects (overdue) OR (critical-customer +
open) records per type. _critical_customer_ids reuses partner.x_fc_rush
and partner.x_fc_vip flags when defined (gracefully no-ops when
absent). _critical_badge returns RUSH/VIP/AEROSPACE/AS9100 label
when the banner reason is critical-customer (no badge when overdue).
_build_banner ranks: overdue first by oldest, then critical-customer
by oldest, takes top 6, reports total_matching.
build() now collects banner candidates from every section in one
pass + invokes _build_banner once.
Tests cover overdue hold pickup, 6-cap with overflow count, and
all_clear when DB is empty.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
_fetch_section_items pulls top-5 open records per type, ranked
overdue-first by oldest create_date. _build_item shapes each row
with id/name/customer/subtitle/urgency/open_action. _resolve_partner
defensively walks partner_id -> job_id.partner_id -> ncr_id.partner_id
per type. _build_subtitle generates the human-readable second line.
Tests cover empty list, 5-cap on 8-record set, and required item
keys (id/name/customer/subtitle/urgency/open_action).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces /counts with /snapshot. Helper class FpQualityDashboardSnapshot
returns response with correct shape — banner placeholder + per-type
sections with open/overdue counts (reuses old counts endpoint
thresholds). Items + critical-customer banner come in Tasks 3-5.
Per CLAUDE.md Rule 13m, Model.sudo() on cross-module reads. Per
Rule 24 the in-self.env check guards missing-model paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tests for empty-DB all-clear, canonical section order, and required
keys on each section. All fail until Task 2 lands the snapshot helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>