From 7a0bd67fc07fdba978bce73e414426bbfcf954c1 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sat, 23 May 2026 21:44:24 -0400 Subject: [PATCH] docs(shopfloor): implementation plan for tablet lock-screen redesign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 6 tasks covering the visual + interaction redesign: Task 1 — Backend: 3 module-level helpers in tablet_controller.py (_initials_from, _avatar_gradient_for, _lock_company_payload) + extended /fp/tablet/tiles payload + 3 test classes (TDD) Task 2 — Create _tablet_lock_tokens.scss design tokens (light + dark branches via $o-webclient-color-scheme) Task 3 — Full rewrite of tablet_lock.scss (gradient bg, glassmorphic tiles, 4 entrance keyframes, hover lift, click press, clocked-in pulse, prefers-reduced-motion gate) Task 4 — Extend tablet_lock.xml with logo + clock + prompt blocks wrapping the existing tile loop Task 5 — Extend tablet_lock.js with state.clockText / state.dateText / state.company + setInterval clock tick + _formatTime / _formatDate / tileStyle / avatarClass helpers (all per project rule 20 — coercion lives in JS, not in templates) Task 6 — Register the new tokens SCSS in manifest BEFORE tablet_lock.scss (per rule 8), bump version 19.0.32.0.0, deploy + verify Each task has TDD-style steps with full code blocks. Self-review confirms 1-to-1 coverage of every spec section + correct deferral of every §12 Phase 2 item. Plan: docs/superpowers/plans/2026-05-24-tablet-lock-screen-redesign-plan.md Spec: docs/superpowers/specs/2026-05-24-tablet-lock-screen-redesign-design.md Co-Authored-By: Claude Opus 4.7 (1M context) --- ...-05-24-tablet-lock-screen-redesign-plan.md | 1107 +++++++++++++++++ 1 file changed, 1107 insertions(+) create mode 100644 fusion_plating/docs/superpowers/plans/2026-05-24-tablet-lock-screen-redesign-plan.md diff --git a/fusion_plating/docs/superpowers/plans/2026-05-24-tablet-lock-screen-redesign-plan.md b/fusion_plating/docs/superpowers/plans/2026-05-24-tablet-lock-screen-redesign-plan.md new file mode 100644 index 00000000..ed5bf1d2 --- /dev/null +++ b/fusion_plating/docs/superpowers/plans/2026-05-24-tablet-lock-screen-redesign-plan.md @@ -0,0 +1,1107 @@ +# Tablet Lock Screen Redesign — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Redesign the FpTabletLock OWL component's tile screen with company branding, real-time clock, glassmorphic tiles, dual dark/light mode, and a 7-animation catalogue per the spec at `docs/superpowers/specs/2026-05-24-tablet-lock-screen-redesign-design.md`. + +**Architecture:** No DB migration. Extend the existing `/fp/tablet/tiles` endpoint payload with a `company` block + per-tile metadata (initials, gradient, has_photo). Rewrite `tablet_lock.scss` with compile-time dark/light branches via `$o-webclient-color-scheme`. New `_tablet_lock_tokens.scss` partial loads first per project rule 8. Extend XML and JS with logo, clock, and prompt blocks; existing PIN gate / lockout / unlock RPC unchanged. + +**Tech Stack:** Odoo 19 (Python 3.11, OWL 2), JSONRPC, PostgreSQL, native odoo on entech (LXC 111 on pve-worker5). SCSS bundle pipeline auto-compiles for both light and dark. + +--- + +## Critical patterns to follow + +Already documented in `K:/Github/Odoo-Modules/fusion_plating/CLAUDE.md` — re-stating so the implementer doesn't context-switch: + +- **OWL template scope (rule 20):** only `Math` is exposed as a JS global. NEVER call `String(x)`, `Number(x)`, `padStart(x)`, `toLocaleDateString()`, etc. directly inside `t-on-click` / `t-att-*` / `t-out` expressions. The clock and date formatting MUST happen in JS-side methods; templates just `t-esc` the prepared strings. +- **SCSS @import forbidden (rule 8):** every SCSS file is registered separately in the manifest. The new `_tablet_lock_tokens.scss` MUST appear BEFORE `tablet_lock.scss` in `web.assets_backend` so the `$_lock-*` tokens are visible to the consumer. +- **Dark mode at compile time:** branch via `@if $o-webclient-color-scheme == dark { ... }` with `!global` token overrides. No JS-side theme code. No `.o_dark_mode` class selectors. No `@media (prefers-color-scheme)`. +- **End every git commit with:** `Co-Authored-By: Claude Opus 4.7 (1M context) ` +- **Existing PIN flow stays:** FpPinPad component, unlock RPC, lockout, idle warning are out of scope. Don't touch them. + +--- + +## File structure map + +### Backend + +| File | Change | Why | +|---|---|---| +| `fusion_plating_shopfloor/controllers/tablet_controller.py` | modify | Add 3 module-level helpers (`_initials_from`, `_avatar_gradient_for`, `_lock_company_payload`) and extend the `/fp/tablet/tiles` route to emit company block + per-tile metadata | +| `fusion_plating_shopfloor/tests/test_tablet_lock_payload.py` | **create** | Cover the new payload shape + helper edge cases | +| `fusion_plating_shopfloor/__manifest__.py` | modify | Bump version to `19.0.32.0.0`, register the new tokens SCSS BEFORE the consumer scss | + +### Frontend + +| File | Change | Why | +|---|---|---| +| `fusion_plating_shopfloor/static/src/scss/_tablet_lock_tokens.scss` | **create** | Design tokens (light defaults + dark overrides) per spec §5; loads first | +| `fusion_plating_shopfloor/static/src/scss/tablet_lock.scss` | full rewrite | New visual structure + 7 animations + reduced-motion gate + dark/light parity | +| `fusion_plating_shopfloor/static/src/xml/tablet_lock.xml` | modify | Wrap existing tile loop with new logo + clock + prompt blocks; tile inner shape updated | +| `fusion_plating_shopfloor/static/src/js/tablet_lock.js` | modify | Add `state.clockText`, `state.dateText`, `state.company`, `_tickClock` interval, `_formatTime` / `_formatDate` helpers, `animDelay` decoration in `_loadTiles` | + +--- + +## Task 1 — Backend: helpers + extended tiles payload + +**Files:** +- Modify: `fusion_plating_shopfloor/controllers/tablet_controller.py` (the `tiles()` route around line 144, see Step 1) +- Create: `fusion_plating_shopfloor/tests/test_tablet_lock_payload.py` + +- [ ] **Step 1: Locate the existing route + endpoint helpers** + +```bash +grep -n "^def \|@http.route.*tiles\|def tiles" K:/Github/Odoo-Modules/fusion_plating/fusion_plating_shopfloor/controllers/tablet_controller.py | head +``` + +Confirm the existing `tiles()` route lives between lines ~140 and ~194. The new module-level helpers go above the controller class. + +- [ ] **Step 2: Write the failing test first** + +Create `fusion_plating_shopfloor/tests/test_tablet_lock_payload.py`: + +```python +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.addons.fusion_plating_shopfloor.controllers.tablet_controller import ( + _initials_from, + _avatar_gradient_for, + _AVATAR_GRADIENTS, +) + + +class TestInitialsFrom(TransactionCase): + + def test_empty_returns_question_mark(self): + self.assertEqual(_initials_from(''), '?') + self.assertEqual(_initials_from(None), '?') + + def test_single_word_returns_first_two_chars_upper(self): + self.assertEqual(_initials_from('Garry'), 'GA') + self.assertEqual(_initials_from('ab'), 'AB') + self.assertEqual(_initials_from('z'), 'Z') + + def test_multi_word_returns_first_and_last_initial(self): + self.assertEqual(_initials_from('Garry Singh'), 'GS') + self.assertEqual(_initials_from('Johnny Matloub'), 'JM') + self.assertEqual(_initials_from('Mary Anne Smith'), 'MS') + self.assertEqual(_initials_from('EN Technologies'), 'ET') + + def test_extra_whitespace_handled(self): + self.assertEqual(_initials_from(' Garry Singh '), 'GS') + self.assertEqual(_initials_from('Garry\tSingh'), 'GS') + + +class TestAvatarGradientFor(TransactionCase): + + def test_deterministic_per_user_id(self): + # Same id → same gradient across calls + self.assertEqual( + _avatar_gradient_for(5), + _avatar_gradient_for(5), + ) + + def test_modulo_distribution(self): + # Wrapping wraps cleanly: id 0 and id len(gradients) get the same colour + n = len(_AVATAR_GRADIENTS) + self.assertEqual(_avatar_gradient_for(0), _avatar_gradient_for(n)) + self.assertEqual(_avatar_gradient_for(3), _avatar_gradient_for(n + 3)) + + def test_returns_a_known_gradient(self): + # Every output is one of the documented gradients + for uid in range(50): + self.assertIn(_avatar_gradient_for(uid), _AVATAR_GRADIENTS) + + +class TestTilesPayload(TransactionCase): + """End-to-end test of the /fp/tablet/tiles endpoint payload shape. + Doesn't drive HTTP — calls the controller method directly via a + light request stand-in. + """ + + def test_payload_has_company_block(self): + from odoo.addons.fusion_plating_shopfloor.controllers.tablet_controller import ( + _lock_company_payload, + ) + payload = _lock_company_payload(self.env) + self.assertIn('id', payload) + self.assertIn('name', payload) + self.assertIn('tagline', payload) + self.assertIn('logo_url', payload) + self.assertIn('has_logo', payload) + self.assertIn('initials', payload) + self.assertTrue(payload['logo_url'].startswith('/web/image/res.company/')) + self.assertEqual(payload['id'], self.env.company.id) + + def test_tagline_default_when_empty_report_header(self): + from odoo.addons.fusion_plating_shopfloor.controllers.tablet_controller import ( + _lock_company_payload, + ) + # Force report_header empty for this test + self.env.company.report_header = False + payload = _lock_company_payload(self.env) + # When empty, falls back to the i18n-friendly default. + self.assertTrue(payload['tagline']) + self.assertNotEqual(payload['tagline'], False) +``` + +- [ ] **Step 3: Run tests — expect ImportError** + +```bash +ssh pve-worker5 "pct exec 111 -- su - odoo -s /bin/bash -c '/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin --test-enable --stop-after-init --log-level=test -u fusion_plating_shopfloor --test-tags /fusion_plating_shopfloor:TestInitialsFrom,/fusion_plating_shopfloor:TestAvatarGradientFor,/fusion_plating_shopfloor:TestTilesPayload' 2>&1 | tail -30" +``` + +Expected: failures with `ImportError: cannot import name '_initials_from' from ...`. + +- [ ] **Step 4: Add the helpers + extend the route** + +At the top of `fusion_plating_shopfloor/controllers/tablet_controller.py`, after the existing imports (around line 10), add: + +```python +# ===== 2026-05-24 lock-screen redesign helpers ========================= + +_AVATAR_GRADIENTS = [ + 'linear-gradient(135deg, #ef4444, #dc2626)', # red + 'linear-gradient(135deg, #f59e0b, #d97706)', # amber + 'linear-gradient(135deg, #10b981, #059669)', # emerald + 'linear-gradient(135deg, #3b82f6, #2563eb)', # blue + 'linear-gradient(135deg, #8b5cf6, #7c3aed)', # violet + 'linear-gradient(135deg, #ec4899, #db2777)', # pink + 'linear-gradient(135deg, #14b8a6, #0d9488)', # teal + 'linear-gradient(135deg, #f97316, #ea580c)', # orange +] + + +def _initials_from(name): + """First letter of first + last word, capped at 2 chars uppercase. + Single-word names return their first two chars. Empty returns '?'. + """ + if not name: + return '?' + words = name.strip().split() + if not words: + return '?' + if len(words) == 1: + return words[0][:2].upper() + return (words[0][0] + words[-1][0]).upper() + + +def _avatar_gradient_for(user_id): + """Deterministic gradient per user id. Same operator gets the same + color across sessions so they learn their tile.""" + return _AVATAR_GRADIENTS[user_id % len(_AVATAR_GRADIENTS)] + + +def _lock_company_payload(env): + """Returns the company info block for the lock screen. + Reuses res.company.report_header as the tagline (the same field + used by invoice letterheads) with a sensible fallback when empty. + """ + co = env.company + return { + 'id': co.id, + 'name': co.name or '', + 'tagline': co.report_header or 'Shop Floor Terminal', + 'logo_url': f'/web/image/res.company/{co.id}/logo', + 'has_logo': bool(co.logo), + 'initials': _initials_from(co.name), + } +``` + +Then in the `tiles()` route body, replace the per-user dict to include the new fields, and add the company block to the return: + +```python + tiles = [] + for u, u_sudo in zip(users_sorted, users_sudo): + tiles.append({ + 'user_id': u.id, + 'name': u.name, + 'initials': _initials_from(u.name), + 'avatar_url': f'/web/image/res.users/{u.id}/avatar_128', + 'has_photo': bool(u_sudo.image_128), + 'avatar_gradient': _avatar_gradient_for(u.id), + 'is_clocked_in': u.id in clocked_ids, + 'has_pin': bool(u_sudo.x_fc_tablet_pin_hash), + }) + # Clocked-in first, then alphabetical within bucket + tiles.sort(key=lambda t: (not t['is_clocked_in'], t['name'])) + return { + 'ok': True, + 'company': _lock_company_payload(request.env), + 'tiles': tiles, + } +``` + +- [ ] **Step 5: Add the test file to the tests package init (if explicit)** + +```bash +ls K:/Github/Odoo-Modules/fusion_plating/fusion_plating_shopfloor/tests/__init__.py 2>/dev/null && cat K:/Github/Odoo-Modules/fusion_plating/fusion_plating_shopfloor/tests/__init__.py +``` + +If the file exists and explicitly imports each test module, add `from . import test_tablet_lock_payload`. If it uses a glob or doesn't exist, create it with that single import: + +```python +# fusion_plating_shopfloor/tests/__init__.py +from . import test_tablet_lock_payload +``` + +- [ ] **Step 6: Deploy + run tests — expect PASS** + +```bash +cat "K:/Github/Odoo-Modules/fusion_plating/fusion_plating_shopfloor/controllers/tablet_controller.py" | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_shopfloor/controllers/tablet_controller.py'" +cat "K:/Github/Odoo-Modules/fusion_plating/fusion_plating_shopfloor/tests/test_tablet_lock_payload.py" | ssh pve-worker5 "pct exec 111 -- bash -c 'mkdir -p /mnt/extra-addons/custom/fusion_plating_shopfloor/tests && cat > /mnt/extra-addons/custom/fusion_plating_shopfloor/tests/test_tablet_lock_payload.py'" +ssh pve-worker5 "pct exec 111 -- su - odoo -s /bin/bash -c '/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin --test-enable --stop-after-init --log-level=test -u fusion_plating_shopfloor --test-tags /fusion_plating_shopfloor:TestInitialsFrom,/fusion_plating_shopfloor:TestAvatarGradientFor,/fusion_plating_shopfloor:TestTilesPayload' 2>&1 | tail -25" +``` + +Expected: all tests pass. + +- [ ] **Step 7: Commit** + +```bash +cd K:/Github/Odoo-Modules/fusion_plating && git add fusion_plating_shopfloor/controllers/tablet_controller.py fusion_plating_shopfloor/tests/ && git commit -m "feat(shopfloor): extend /fp/tablet/tiles payload with company block + +Adds 3 module-level helpers in tablet_controller.py: + _initials_from(name) — first/last initials for letter-mark fallback + _avatar_gradient_for(uid) — deterministic per-user color gradient + _lock_company_payload(env) — company name + tagline + logo URL + +Endpoint /fp/tablet/tiles now returns: + {ok, company:{id,name,tagline,logo_url,has_logo,initials}, + tiles:[{user_id, name, initials, avatar_url, has_photo, + avatar_gradient, is_clocked_in, has_pin}, ...]} + +8 avatar gradients keyed by user.id modulo — operators learn their +own color. has_photo is sudo-read of res.users.image_128 (avoids +the 1×1 default-image flash on the frontend). + +Tagline reuses res.company.report_header (the invoice-letterhead +field) so no new model field; defaults to 'Shop Floor Terminal'. + +Tested with 3 test classes covering initials edge cases, gradient +determinism, and payload shape. + +Part of the 2026-05-24 tablet lock-screen redesign (spec at +docs/superpowers/specs/2026-05-24-tablet-lock-screen-redesign-design.md). + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +## Task 2 — SCSS design tokens + +**Files:** +- Create: `fusion_plating_shopfloor/static/src/scss/_tablet_lock_tokens.scss` + +- [ ] **Step 1: Create the tokens file** + +```scss +// ===================================================================== +// Tablet lock-screen design tokens (2026-05-24 redesign). +// MUST load before tablet_lock.scss. Per project rule 8 (SCSS @import +// forbidden in Odoo 19 custom code), the manifest registers each SCSS +// file separately; ordering IS the variable scope. +// ===================================================================== + +$o-webclient-color-scheme: bright !default; + +// === Light-mode defaults === +$_lock-bg-top-hex: #fafafa; +$_lock-bg-bottom-hex: #f0f0f3; +$_lock-accent-1-rgba: rgba(240,165,0,0.12); +$_lock-accent-2-rgba: rgba(99,102,241,0.06); +$_lock-text-hex: #1d1f1e; +$_lock-muted-hex: #71717a; +$_lock-prompt-hex: #b45309; +$_lock-prompt-bg-rgba: rgba(240,165,0,0.10); +$_lock-prompt-border-rgba: rgba(240,165,0,0.25); +$_lock-tile-bg-rgba: rgba(255,255,255,0.7); +$_lock-tile-border-rgba: rgba(0,0,0,0.05); +$_lock-tile-hover-bg-rgba: rgba(255,255,255,0.95); +$_lock-tile-hover-border-rgba: rgba(240,165,0,0.5); +$_lock-tile-hover-shadow: (0 12px 24px rgba(240,165,0,0.18)); +$_lock-frame-bg-rgba: rgba(255,255,255,0.85); +$_lock-frame-border-rgba: rgba(0,0,0,0.05); +$_lock-frame-shadow: (0 8px 24px rgba(0,0,0,0.06), inset 0 1px 0 rgba(255,255,255,0.8)); +$_lock-status-clocked-hex: #16a34a; +$_lock-status-pin-hex: #d97706; +$_lock-pulse-dot-border-hex: #ffffff; + +@if $o-webclient-color-scheme == dark { + $_lock-bg-top-hex: #1a1d21 !global; + $_lock-bg-bottom-hex: #2d3138 !global; + $_lock-accent-1-rgba: rgba(240,165,0,0.08) !global; + $_lock-accent-2-rgba: rgba(99,102,241,0.06) !global; + $_lock-text-hex: #f5f5f7 !global; + $_lock-muted-hex: #adb5bd !global; + $_lock-prompt-hex: #f0a500 !global; + $_lock-prompt-bg-rgba: rgba(240,165,0,0.08) !global; + $_lock-prompt-border-rgba: rgba(240,165,0,0.20) !global; + $_lock-tile-bg-rgba: rgba(255,255,255,0.06) !global; + $_lock-tile-border-rgba: rgba(255,255,255,0.08) !global; + $_lock-tile-hover-bg-rgba: rgba(240,165,0,0.10) !global; + $_lock-tile-hover-border-rgba: rgba(240,165,0,0.4) !global; + $_lock-tile-hover-shadow: (0 12px 24px rgba(240,165,0,0.15), 0 0 0 1px rgba(240,165,0,0.2)) !global; + $_lock-frame-bg-rgba: rgba(255,255,255,0.08) !global; + $_lock-frame-border-rgba: rgba(255,255,255,0.10) !global; + $_lock-frame-shadow: (0 8px 24px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.08)) !global; + $_lock-status-clocked-hex: #34c759 !global; + $_lock-status-pin-hex: #ff9f0a !global; + $_lock-pulse-dot-border-hex: #2d3138 !global; +} + +// === CSS-custom-property wrappers so future themes can override === +$lock-bg-top: var(--fp-lock-bg-top, $_lock-bg-top-hex); +$lock-bg-bottom: var(--fp-lock-bg-bottom, $_lock-bg-bottom-hex); +$lock-accent-1: $_lock-accent-1-rgba; +$lock-accent-2: $_lock-accent-2-rgba; +$lock-text: var(--fp-lock-text, $_lock-text-hex); +$lock-muted: var(--fp-lock-muted, $_lock-muted-hex); +$lock-prompt: var(--fp-lock-prompt, $_lock-prompt-hex); +$lock-prompt-bg: $_lock-prompt-bg-rgba; +$lock-prompt-border: $_lock-prompt-border-rgba; +$lock-tile-bg: $_lock-tile-bg-rgba; +$lock-tile-border: $_lock-tile-border-rgba; +$lock-tile-hover-bg: $_lock-tile-hover-bg-rgba; +$lock-tile-hover-border: $_lock-tile-hover-border-rgba; +$lock-tile-hover-shadow: $_lock-tile-hover-shadow; +$lock-frame-bg: $_lock-frame-bg-rgba; +$lock-frame-border: $_lock-frame-border-rgba; +$lock-frame-shadow: $_lock-frame-shadow; +$lock-status-clocked: var(--fp-lock-status-clocked, $_lock-status-clocked-hex); +$lock-status-pin: var(--fp-lock-status-pin, $_lock-status-pin-hex); +$lock-pulse-dot-border: $_lock-pulse-dot-border-hex; +``` + +- [ ] **Step 2: Commit (tokens land in one commit, registered in manifest in Task 6)** + +```bash +git add fusion_plating_shopfloor/static/src/scss/_tablet_lock_tokens.scss && git commit -m "feat(shopfloor): tablet lock-screen design tokens + +Light defaults + dark overrides via \$o-webclient-color-scheme +compile-time branch (project rule for Odoo 19 dark mode — no class +selectors / media queries). Wrapped in CSS custom properties for +future per-tenant theming. + +Per project rule 8 this file MUST load before tablet_lock.scss in +the manifest's web.assets_backend list. Manifest update lands with +the SCSS rewrite in a later commit. + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +## Task 3 — SCSS full rewrite + +**Files:** +- Modify: `fusion_plating_shopfloor/static/src/scss/tablet_lock.scss` (replace entirely) + +- [ ] **Step 1: Replace the file** + +The new file is ~210 lines. Replace contents entirely: + +```scss +// ===================================================================== +// FpTabletLock — lock screen with tile grid + PIN pad overlay +// 2026-05-24 redesign: hybrid Industrial Bold + Premium Glassmorphism +// Spec: docs/superpowers/specs/2026-05-24-tablet-lock-screen-redesign-design.md +// Depends on _tablet_lock_tokens.scss being loaded first. +// ===================================================================== + +.o_fp_tablet_lock { + position: fixed; + inset: 0; + color: $lock-text; + display: flex; + flex-direction: column; + align-items: center; + padding: 28px 20px; + gap: 22px; + z-index: 9000; + overflow-y: auto; + background: + radial-gradient(ellipse at top, $lock-accent-1, transparent 50%), + radial-gradient(ellipse at bottom, $lock-accent-2, transparent 50%), + linear-gradient(135deg, $lock-bg-top 0%, $lock-bg-bottom 100%); +} + +// === Logo block ===================================================== +.o_fp_lock_logo_block { + text-align: center; + animation: lockLogoEnter 0.5s ease-out; +} + +.o_fp_lock_logo_frame { + display: inline-flex; align-items: center; justify-content: center; + width: 84px; height: 84px; + border-radius: 20px; + margin-bottom: 12px; + padding: 14px; + box-sizing: border-box; + background: $lock-frame-bg; + border: 1px solid $lock-frame-border; + box-shadow: $lock-frame-shadow; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + + img { + max-width: 100%; max-height: 100%; + object-fit: contain; + } +} + +.o_fp_lock_logo_placeholder { + width: 100%; height: 100%; border-radius: 14px; + background: linear-gradient(135deg, #f0a500, #ff6b00); + display: inline-flex; align-items: center; justify-content: center; + font-size: 32px; font-weight: 900; color: #1a1d21; +} + +.o_fp_lock_logo_text { + font-size: 19px; font-weight: 700; letter-spacing: -0.01em; + color: $lock-text; +} + +.o_fp_lock_logo_sub { + font-size: 11px; + text-transform: uppercase; letter-spacing: 0.15em; + margin-top: 4px; + color: $lock-muted; +} + +// === Clock block ==================================================== +.o_fp_lock_clock_block { + text-align: center; + font-variant-numeric: tabular-nums; + animation: lockClockEnter 0.5s ease-out 0.1s both; +} + +.o_fp_lock_clock { + font-size: 40px; font-weight: 800; + letter-spacing: -0.03em; line-height: 1; + color: $lock-text; +} + +.o_fp_lock_clock_date { + font-size: 12px; + text-transform: uppercase; letter-spacing: 0.12em; + margin-top: 4px; + color: $lock-muted; +} + +// === Prompt pill ==================================================== +.o_fp_lock_prompt { + font-size: 13px; font-weight: 700; + text-transform: uppercase; letter-spacing: 0.18em; + color: $lock-prompt; + display: inline-flex; align-items: center; gap: 8px; + padding: 6px 16px; + border-radius: 999px; + background: $lock-prompt-bg; + border: 1px solid $lock-prompt-border; + animation: lockClockEnter 0.5s ease-out 0.2s both; +} + +// === Tile grid ====================================================== +.o_fp_lock_tiles { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + width: 100%; + max-width: 480px; +} + +.o_fp_lock_tile { + background: $lock-tile-bg; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid $lock-tile-border; + border-radius: 14px; + padding: 14px 8px 12px; + text-align: center; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + animation: lockTileEnter 0.4s cubic-bezier(0.4, 0, 0.2, 1) both; + box-shadow: 0 1px 3px rgba(0,0,0,0.04); + color: inherit; + + &:hover, &:focus-visible { + background: $lock-tile-hover-bg; + border-color: $lock-tile-hover-border; + transform: translateY(-3px); + box-shadow: $lock-tile-hover-shadow; + outline: none; + } + &:focus-visible { + outline: 2px solid $lock-tile-hover-border; + outline-offset: 2px; + } + &:active { + transform: scale(0.97); + transition: transform 0.05s; + } +} + +.o_fp_lock_avatar { + width: 52px; height: 52px; border-radius: 50%; + display: inline-flex; align-items: center; justify-content: center; + font-size: 21px; font-weight: 700; color: #fff; + margin-bottom: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + position: relative; + overflow: hidden; + + img { + width: 100%; height: 100%; + object-fit: cover; + border-radius: 50%; + } + + &.is-clocked::after { + content: ''; position: absolute; + width: 12px; height: 12px; border-radius: 50%; + background: $lock-status-clocked; + bottom: 0; right: 0; + border: 2px solid $lock-pulse-dot-border; + animation: lockPulseDot 2s ease-in-out infinite; + } +} + +.o_fp_lock_name { + font-size: 12px; font-weight: 600; + line-height: 1.3; + color: $lock-text; +} + +.o_fp_lock_status { + font-size: 10px; + margin-top: 4px; + font-weight: 500; + + &.status-clocked { color: $lock-status-clocked; } + &.status-pin { color: $lock-status-pin; } +} + +// === Empty / loading states ========================================= +.o_fp_lock_loading, +.o_fp_lock_empty { + margin: 2rem auto; + color: $lock-muted; + font-size: 14px; +} + +// === PIN pad wrap =================================================== +.o_fp_lock_pinwrap { + margin-top: 8px; + animation: lockClockEnter 0.3s ease-out; +} + +// === Animations ===================================================== +@keyframes lockLogoEnter { + from { opacity: 0; transform: translateY(-12px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes lockClockEnter { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes lockTileEnter { + from { opacity: 0; transform: translateY(16px) scale(0.96); } + to { opacity: 1; transform: translateY(0) scale(1); } +} +@keyframes lockPulseDot { + 0%, 100% { box-shadow: 0 0 0 0 rgba(52,199,89,0.6); } + 50% { box-shadow: 0 0 0 6px rgba(52,199,89,0); } +} + +// === Reduced-motion override (accessibility) ======================== +@media (prefers-reduced-motion: reduce) { + .o_fp_tablet_lock, + .o_fp_lock_logo_block, + .o_fp_lock_clock_block, + .o_fp_lock_prompt, + .o_fp_lock_tile, + .o_fp_lock_avatar.is-clocked::after, + .o_fp_lock_pinwrap { + animation: none !important; + transition: none !important; + } +} +``` + +- [ ] **Step 2: Commit (will compile after manifest update in Task 6)** + +```bash +git add fusion_plating_shopfloor/static/src/scss/tablet_lock.scss && git commit -m "feat(shopfloor): rewrite tablet_lock.scss for the 2026-05-24 redesign + +Full rewrite per spec §4–§6: + - Full-viewport gradient bg (two radial ambient glows over linear) + - Glassmorphic logo frame (84px rounded-20, backdrop-blur) + - 40px tabular-nums clock + uppercase tracked date + - Amber prompt pill + - 3-column tile grid (max 480px) with frosted-glass tiles + - 52px circular avatars with status pulse-dot overlay + - 4 entrance keyframes + hover lift + click press + clocked-in pulse + - prefers-reduced-motion override disables all animation + transition + +Dark / light parity via the new _tablet_lock_tokens.scss compile-time +branch (project rule). No JS-side theme code. + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +## Task 4 — XML template extension + +**Files:** +- Modify: `fusion_plating_shopfloor/static/src/xml/tablet_lock.xml` (replace contents) + +- [ ] **Step 1: Replace the file** + +```xml + + + + + +
+ + +
+
+ +
+
+
+
+
+ + +
+
+
+
+ + +
+ Tap your name +
+ + +
+ Loading… +
+
+ +
+ No operators configured. +
+
+ + + +
+
+ +
+
+ + + + + + + + +``` + +**Template scope check (project rule 20):** No `String()` / `Number()` / `parseInt()` calls inside `t-on-click`, `t-att-*`, or `t-out`. The two computed values — `tileStyle(tile)` for the animation-delay inline style and `avatarClass(tile)` for the clocked-in modifier — are getter methods on the component (defined in Task 5). Both return strings, so the template just `t-att-style`s / `t-att-class`es them directly. + +- [ ] **Step 2: Commit** + +```bash +git add fusion_plating_shopfloor/static/src/xml/tablet_lock.xml && git commit -m "feat(shopfloor): extend tablet_lock.xml with logo, clock, prompt blocks + +Wraps the existing tile loop with three new blocks per spec §4: + - .o_fp_lock_logo_block — company logo + name + tagline + - .o_fp_lock_clock_block — HH:MM clock + date + - .o_fp_lock_prompt — amber pill 'Tap your name' (hidden while + PIN pad is open via !state.selectedTileUserId guard) + +Tile inner structure updated: + - Avatar background uses the per-tile gradient string from server + - Falls back to initials when has_photo is False + - is_clocked_in adds the .is-clocked modifier (drives the SCSS + pulse-dot overlay) + +All formatting (clock text, date, animation delay, modifier classes) +is computed in JS-side getter methods per project rule 20 — templates +just render strings via t-esc / t-att-*. + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +## Task 5 — JS extensions: clock + company state + helpers + +**Files:** +- Modify: `fusion_plating_shopfloor/static/src/js/tablet_lock.js` + +- [ ] **Step 1: Read the current file to confirm structure** + +```bash +grep -n "useState\|setup()\|onMounted\|onWillUnmount\|_loadTiles" K:/Github/Odoo-Modules/fusion_plating/fusion_plating_shopfloor/static/src/js/tablet_lock.js | head +``` + +Confirm `setup()` has the `useState({...})` block; `onMounted` exists; `_loadTiles` is around line 67. + +- [ ] **Step 2: Extend `setup()` — add clock and company state** + +In `setup()`, change the `useState` block to include three new keys: + +```javascript + this.state = useState({ + tiles: [], + selectedTileUserId: null, + idleSecondsRemaining: null, + loadingTiles: false, + // 2026-05-24 redesign — clock + company branding + clockText: this._formatTime(new Date()), + dateText: this._formatDate(new Date()), + company: null, + }); +``` + +Note `clockText` / `dateText` are seeded synchronously so there's no flash of empty content on first render. + +- [ ] **Step 3: Extend `onMounted` — start the clock tick interval** + +In the existing `onMounted(async () => { ... })` block, add a tick interval AFTER the existing setup: + +```javascript + onMounted(async () => { + await this._loadTiles(); + this._tick = setInterval(() => this._checkIdle(), 1000); + // Heartbeat ping every 60s — for forensic visibility + this._ping = setInterval(() => { + if (this.techStore.currentTechId) { + rpc("/fp/tablet/ping", { current_tech_id: this.techStore.currentTechId }) + .catch(() => {}); + } + }, 60000); + // Clock tick — update visible HH:MM and date label every 60s. + // 60s is enough; minute boundaries align naturally. + this._clockInterval = setInterval(() => { + const now = new Date(); + this.state.clockText = this._formatTime(now); + this.state.dateText = this._formatDate(now); + }, 60000); + }); +``` + +- [ ] **Step 4: Extend `onWillUnmount` — clear the new interval** + +```javascript + onWillUnmount(() => { + if (this._tick) clearInterval(this._tick); + if (this._ping) clearInterval(this._ping); + if (this._clockInterval) clearInterval(this._clockInterval); + }); +``` + +- [ ] **Step 5: Replace `_loadTiles` to consume the new payload** + +```javascript + async _loadTiles() { + this.state.loadingTiles = true; + try { + const stationId = parseInt(localStorage.getItem("fp_landing_station_id")) || null; + const res = await rpc("/fp/tablet/tiles", { station_id: stationId }); + if (res && res.ok) { + this.state.company = res.company || null; + // Decorate each tile with an animation-delay (50ms staggered, + // capped at 300ms so the screen doesn't take 3s to settle on + // shops with 20+ operators). + this.state.tiles = (res.tiles || []).map((tile, idx) => ({ + ...tile, + animDelay: Math.min(idx * 50, 300), + })); + } + } catch (err) { + // Quiet fail — tile grid stays empty; user gets prompted + } finally { + this.state.loadingTiles = false; + } + } +``` + +- [ ] **Step 6: Add the formatting + getter methods at the bottom of the class** + +```javascript + // === 2026-05-24 redesign helpers ==================================== + + _formatTime(d) { + // 24-hour HH:MM with leading zeros. Per project rule 20 this MUST + // live in JS, not in the template (padStart isn't in OWL scope). + const hh = String(d.getHours()).padStart(2, '0'); + const mm = String(d.getMinutes()).padStart(2, '0'); + return hh + ':' + mm; + } + + _formatDate(d) { + // 'SATURDAY · MAY 23' style label. Uses Intl for locale-correct + // weekday + month abbreviations, then upcases for visual tracking. + const weekday = d.toLocaleDateString(undefined, { weekday: 'long' }); + const month = d.toLocaleDateString(undefined, { month: 'short' }); + const day = d.getDate(); + return (weekday + ' · ' + month + ' ' + day).toUpperCase(); + } + + tileStyle(tile) { + // Inline animation-delay so each tile's entrance staggers. + // Returned as a string per project rule 20 — templates can't call + // String() inside t-att-* expressions. + return 'animation-delay: ' + tile.animDelay + 'ms'; + } + + avatarClass(tile) { + return tile.is_clocked_in + ? 'o_fp_lock_avatar is-clocked' + : 'o_fp_lock_avatar'; + } +``` + +- [ ] **Step 7: Commit** + +```bash +git add fusion_plating_shopfloor/static/src/js/tablet_lock.js && git commit -m "feat(shopfloor): tablet_lock.js — clock + company + tile-decoration + +Adds three new state keys (clockText, dateText, company) seeded +synchronously in setup() so the first render shows the real time and +company info — no flash of empty content. + +onMounted starts a 60s setInterval that re-formats time + date. +onWillUnmount clears it. Minute-boundary precision is enough — the +clock isn't a seconds display. + +_loadTiles now reads res.company from the extended endpoint, and +decorates each tile with a staggered animDelay (Math.min(idx * 50, 300)) +so the entrance ripple settles in ~300ms even with 20+ tiles. + +Four new helpers — _formatTime, _formatDate, tileStyle(tile), +avatarClass(tile) — all live in JS per project rule 20 (templates +only get Math as a global; String/Number/padStart fail with +'v2 is not a function' at click time). + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +## Task 6 — Manifest registration + deploy + verify + +**Files:** +- Modify: `fusion_plating_shopfloor/__manifest__.py` + +- [ ] **Step 1: Find the lock-screen SCSS block in the manifest** + +```bash +grep -n "tablet_lock\|tablet_lock.scss" K:/Github/Odoo-Modules/fusion_plating/fusion_plating_shopfloor/__manifest__.py +``` + +Confirm the existing line is `'fusion_plating_shopfloor/static/src/scss/tablet_lock.scss'` (around line 98 from earlier readings). + +- [ ] **Step 2: Insert the tokens file BEFORE `tablet_lock.scss`** + +Replace: +```python + 'fusion_plating_shopfloor/static/src/scss/components/_idle_warning.scss', + 'fusion_plating_shopfloor/static/src/xml/components/idle_warning.xml', + 'fusion_plating_shopfloor/static/src/js/components/idle_warning.js', + 'fusion_plating_shopfloor/static/src/scss/tablet_lock.scss', +``` + +With: +```python + 'fusion_plating_shopfloor/static/src/scss/components/_idle_warning.scss', + 'fusion_plating_shopfloor/static/src/xml/components/idle_warning.xml', + 'fusion_plating_shopfloor/static/src/js/components/idle_warning.js', + # 2026-05-24 lock-screen redesign — tokens MUST precede tablet_lock.scss + 'fusion_plating_shopfloor/static/src/scss/_tablet_lock_tokens.scss', + 'fusion_plating_shopfloor/static/src/scss/tablet_lock.scss', +``` + +- [ ] **Step 3: Bump the manifest version** + +Change `'version': '19.0.31.0.0'` → `'version': '19.0.32.0.0'`. + +- [ ] **Step 4: Deploy all changed files to entech** + +```bash +cd K:/Github/Odoo-Modules/fusion_plating && for f in \ + fusion_plating_shopfloor/__manifest__.py \ + fusion_plating_shopfloor/static/src/scss/_tablet_lock_tokens.scss \ + fusion_plating_shopfloor/static/src/scss/tablet_lock.scss \ + fusion_plating_shopfloor/static/src/xml/tablet_lock.xml \ + fusion_plating_shopfloor/static/src/js/tablet_lock.js \ + fusion_plating_shopfloor/controllers/tablet_controller.py ; do + cat "$f" | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/$f'" +done && echo "All files copied." +``` + +- [ ] **Step 5: Upgrade the module + bust the asset cache** + +```bash +ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_shopfloor --stop-after-init\" 2>&1 | grep -iE \"(ERROR|loaded fusion_plating_shopfloor|Registry loaded)\" | tail -8 && systemctl start odoo && systemctl is-active odoo'" +ssh pve-worker5 'pct exec 111 -- su - postgres -c "psql -d admin -c \"DELETE FROM ir_attachment WHERE url LIKE '"'"'/web/assets/%'"'"';\""' +``` + +Expected: "Module fusion_plating_shopfloor loaded ... active". A small number of asset attachments deleted (3-10). + +- [ ] **Step 6: Verify the endpoint returns the new payload shape** + +```bash +echo "from odoo.addons.fusion_plating_shopfloor.controllers.tablet_controller import _lock_company_payload, _initials_from, _avatar_gradient_for +co = _lock_company_payload(env) +print('=== Company payload ===') +print(co) +print() +print('=== Sample initials ===') +print('Garry Singh:', _initials_from('Garry Singh')) +print('EN Technologies:', _initials_from('EN Technologies')) +print() +print('=== Sample gradient ===') +print('uid=5:', _avatar_gradient_for(5))" | ssh pve-worker5 "pct exec 111 -- su - odoo -s /bin/bash -c '/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http' 2>&1" | grep -E "^(Company|uid|Garry|EN|===|{)" | head -20 +``` + +Expected: +- Company payload prints with `id`, `name`, `tagline`, `logo_url`, `has_logo`, `initials` keys. +- `_initials_from('Garry Singh')` → `'GS'`. +- `_avatar_gradient_for(5)` → one of the 8 documented gradients. + +- [ ] **Step 7: Smoke-test in a browser** + +Open `https://enplating.com` in a browser. The Plating app should land on the Shop Floor plant kanban. Log out (or use Hand Off) to trigger the lock screen. Verify: +- Company logo / letter-mark appears centered top +- Company name + tagline below it +- Real-time clock + date +- Amber "Tap your name" pill +- Tiles laid out 3 per row with frosted-glass styling +- Hover on a tile lifts it + glows amber +- Clocked-in tiles show a pulsing green dot on the avatar +- Hard-refresh once (Ctrl+Shift+R) to clear browser cache + +- [ ] **Step 8: Commit the manifest + final tag** + +```bash +cd K:/Github/Odoo-Modules/fusion_plating && git add fusion_plating_shopfloor/__manifest__.py && git commit -m "chore(shopfloor): register _tablet_lock_tokens.scss + bump to 19.0.32.0.0 + +Final commit for the 2026-05-24 tablet lock-screen redesign. + +Registers the new tokens partial in the manifest's web.assets_backend +list BEFORE tablet_lock.scss — per project rule 8, SCSS file order +in the manifest IS the variable scope (no @import allowed in Odoo +19 custom SCSS). + +Verified end-to-end on entech: + - Module loaded clean on -u + - Endpoint /fp/tablet/tiles returns the new company block + per- + tile initials/gradient/has_photo fields + - 3 helper unit tests pass + - Lock screen renders with company branding, clock, glassmorphic + tiles, animations in both dark and light bundles + - Asset cache cleared so the new bundles compile fresh + +Closes the 6-task plan in + docs/superpowers/plans/2026-05-24-tablet-lock-screen-redesign-plan.md +Spec: docs/superpowers/specs/2026-05-24-tablet-lock-screen-redesign-design.md + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +## Self-review + +**1. Spec coverage** — Cross-checking spec sections against the 6 tasks: + +| Spec section | Implementing task | +|---|---| +| §3 D1 — Hybrid A+B vibe | Task 3 (SCSS sets gradient bg + glassmorphic tiles + bezier hover) | +| §3 D2 — Company logo from res.company.logo + letter-mark fallback | Task 1 (`_lock_company_payload` + `_initials_from`) + Task 4 (XML conditionally renders `` or placeholder) | +| §3 D3 — Tagline from res.company.report_header | Task 1 (`tagline = co.report_header or 'Shop Floor Terminal'`) | +| §3 D4 — 3-column tile grid max 480px | Task 3 (`.o_fp_lock_tiles { grid-template-columns: repeat(3, 1fr); max-width: 480px }`) | +| §3 D5 — Dark + light parity via compile-time branch | Task 2 (tokens with `@if $o-webclient-color-scheme == dark` overrides) | +| §3 D6 — 7-animation catalogue + reduced-motion gate | Task 3 (4 `@keyframes` + hover/active transitions + `prefers-reduced-motion: reduce` override) | +| §3 D7 — Clocked-in first, then alphabetical | Existing behaviour preserved (Task 1 doesn't change the sort) | +| §3 D8 — No search box | Out of scope (correctly not in any task) | +| §4 Layout | Tasks 3 + 4 implement the vertical structure | +| §5 Color tokens | Task 2 | +| §6 Animation catalogue + reduced-motion gate | Task 3 | +| §7.1 Extended payload | Task 1 | +| §7.2 `_lock_company_payload` helper | Task 1 | +| §7.3 `_avatar_gradient_for` helper | Task 1 (`_AVATAR_GRADIENTS` + the function) | +| §8.1 Files modified table | Tasks 1–6 each handle one or two files | +| §8.2 Clock interval setup/teardown | Task 5 (Step 3 + Step 4) | +| §8.3 animDelay computation | Task 5 (Step 5) | +| §8.4 Manifest registration order | Task 6 | +| §9 Accessibility (touch target, focus ring, contrast, alt text, reduced motion) | Task 3 (focus-visible outline + `prefers-reduced-motion`) + Task 4 (`state.company.name`) — note touch-target sizing is met by avatar 52px + tile padding (~140×110 px). | +| §10 Testing strategy | Task 1 includes 3 unit-test classes covering initials, gradient determinism, and payload shape | +| §11 Migration & rollout | Tasks 4-6 deploy via the existing `-u` pattern; no DB migration | +| §12 Open questions | Correctly deferred — no task implements search, multi-tenant theming, weather widget, etc. | + +All spec sections map to at least one task. No gaps. + +**2. Placeholder scan** — Searched for `TBD`, `TODO`, `implement later`, `add appropriate`, `similar to Task`. None found. Every code block contains the actual content. + +**3. Type consistency**: +- Function name `_initials_from` used the same way in Task 1's helpers, the test file, and Task 6's verification. +- `_avatar_gradient_for` consistent. +- State key names `clockText`, `dateText`, `company` consistent between Task 5's `useState`, the XML in Task 4, and the verification in Task 6. +- CSS class name `o_fp_lock_*` prefix consistent across Tasks 3 and 4 (Task 3 defines, Task 4 references). +- Endpoint payload field names (`avatar_gradient`, `has_photo`, `initials`, `is_clocked_in`, `has_pin`) consistent between Task 1's Python emitter and Task 4's XML consumer. + +No issues found. + +--- + +## Execution Handoff + +Plan complete and saved to `docs/superpowers/plans/2026-05-24-tablet-lock-screen-redesign-plan.md`. Two execution options: + +**1. Subagent-Driven (recommended)** — I dispatch a fresh subagent per task, review between tasks, fast iteration + +**2. Inline Execution** — Execute tasks in this session using executing-plans, batch execution with checkpoints + +Which approach?