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) <noreply@anthropic.com>
1108 lines
46 KiB
Markdown
1108 lines
46 KiB
Markdown
# 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) <noreply@anthropic.com>`
|
||
- **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) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## 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) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## 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) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4 — XML template extension
|
||
|
||
**Files:**
|
||
- Modify: `fusion_plating_shopfloor/static/src/xml/tablet_lock.xml` (replace contents)
|
||
|
||
- [ ] **Step 1: Replace the file**
|
||
|
||
```xml
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<templates xml:space="preserve">
|
||
|
||
<t t-name="fusion_plating_shopfloor.TabletLock">
|
||
<t t-if="isLocked">
|
||
<div class="o_fp_tablet_lock">
|
||
|
||
<!-- ============== LOGO BLOCK ============== -->
|
||
<div class="o_fp_lock_logo_block" t-if="state.company">
|
||
<div class="o_fp_lock_logo_frame">
|
||
<img t-if="state.company.has_logo"
|
||
t-att-src="state.company.logo_url"
|
||
t-att-alt="state.company.name"/>
|
||
<div t-else=""
|
||
class="o_fp_lock_logo_placeholder"
|
||
t-esc="state.company.initials"/>
|
||
</div>
|
||
<div class="o_fp_lock_logo_text" t-esc="state.company.name"/>
|
||
<div class="o_fp_lock_logo_sub" t-esc="state.company.tagline"/>
|
||
</div>
|
||
|
||
<!-- ============== CLOCK BLOCK ============== -->
|
||
<div class="o_fp_lock_clock_block">
|
||
<div class="o_fp_lock_clock" t-esc="state.clockText"/>
|
||
<div class="o_fp_lock_clock_date" t-esc="state.dateText"/>
|
||
</div>
|
||
|
||
<!-- ============== PROMPT PILL ============== -->
|
||
<div t-if="!state.selectedTileUserId" class="o_fp_lock_prompt">
|
||
<i class="fa fa-lock"/> Tap your name
|
||
</div>
|
||
|
||
<!-- ============== TILES OR PIN PAD ============== -->
|
||
<div t-if="state.loadingTiles" class="o_fp_lock_loading">
|
||
<i class="fa fa-spinner fa-spin"/> Loading…
|
||
</div>
|
||
<div t-elif="!state.selectedTileUserId" class="o_fp_lock_tiles">
|
||
<t t-if="!state.tiles.length">
|
||
<div class="o_fp_lock_empty">
|
||
No operators configured.
|
||
</div>
|
||
</t>
|
||
<t t-foreach="state.tiles" t-as="tile" t-key="tile.user_id">
|
||
<button class="o_fp_lock_tile"
|
||
t-att-style="tileStyle(tile)"
|
||
t-on-click="() => this.onTileClick(tile.user_id)">
|
||
<div t-att-class="avatarClass(tile)"
|
||
t-att-style="'background: ' + tile.avatar_gradient">
|
||
<img t-if="tile.has_photo"
|
||
t-att-src="tile.avatar_url"
|
||
t-att-alt="tile.name"/>
|
||
<span t-else="" t-esc="tile.initials"/>
|
||
</div>
|
||
<div class="o_fp_lock_name" t-esc="tile.name"/>
|
||
<div t-if="tile.is_clocked_in"
|
||
class="o_fp_lock_status status-clocked">
|
||
Clocked in
|
||
</div>
|
||
<div t-elif="!tile.has_pin"
|
||
class="o_fp_lock_status status-pin">
|
||
PIN required
|
||
</div>
|
||
</button>
|
||
</t>
|
||
</div>
|
||
<div t-else="" class="o_fp_lock_pinwrap">
|
||
<FpPinPad onSubmit.bind="unlock"
|
||
title="_selectedTileName()"
|
||
subtitle="'Enter your 4-digit PIN'"
|
||
onCancel.bind="onPinCancel"/>
|
||
</div>
|
||
</div>
|
||
</t>
|
||
<t t-else="">
|
||
<t t-slot="default"/>
|
||
<FpIdleWarning t-if="state.idleSecondsRemaining !== null"
|
||
secondsRemaining="state.idleSecondsRemaining"/>
|
||
</t>
|
||
</t>
|
||
|
||
</templates>
|
||
```
|
||
|
||
**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) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## 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) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## 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) <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## 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 `<img>` 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 (`<img alt="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?
|