From d9f2983ea789916242bdfb830fc10203ae130d53 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 24 May 2026 14:31:21 -0400 Subject: [PATCH] cleanup(shopfloor): remove legacy /fp/tablet/unlock + _tablet_audit helper Session-swap is now the only flow. Legacy /fp/tablet/unlock endpoint deleted. _tablet_audit.py (env_for_tablet_tech helper) deleted with its last caller gone. /fp/tablet/ping no longer takes current_tech_id (session uid IS the tech). /fp/tablet/tiles drops tablet_session_mode return key (kiosk_uid + current_uid retained for OWL isLocked logic). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../controllers/_tablet_audit.py | 42 -------- .../controllers/tablet_controller.py | 95 ++----------------- 2 files changed, 7 insertions(+), 130 deletions(-) delete mode 100644 fusion_plating/fusion_plating_shopfloor/controllers/_tablet_audit.py diff --git a/fusion_plating/fusion_plating_shopfloor/controllers/_tablet_audit.py b/fusion_plating/fusion_plating_shopfloor/controllers/_tablet_audit.py deleted file mode 100644 index cf73da5d..00000000 --- a/fusion_plating/fusion_plating_shopfloor/controllers/_tablet_audit.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2026 Nexa Systems Inc. -# License OPL-1 (Odoo Proprietary License v1.0) -"""Helper for audit-credit propagation (Phase 6.3 tablet redesign). - -Controllers that accept an optional `tablet_tech_id` kwarg use this -helper to switch their `env` to the tech-of-record before performing -writes. The result: chatter posts + create_uid/write_uid carry the -unlocked tech's identity, not the tablet's persistent session user. -""" -import logging - -_logger = logging.getLogger(__name__) - - -def env_for_tablet_tech(env, tablet_tech_id): - """Return an env scoped to `tablet_tech_id` if it's a valid user; - otherwise return the original env unchanged. - - Validation: the user must exist and be active. We deliberately do - NOT cross-check that they actually unlocked recently — the OWL - component is the source of truth for "who's at the tablet right - now", and the only path that produces a tablet_tech_id is a - successful /fp/tablet/unlock followed by an active session in the - OWL tech_store. - """ - if not tablet_tech_id: - return env - try: - tech_id = int(tablet_tech_id) - except (TypeError, ValueError): - return env - User = env['res.users'].sudo() - tech = User.browse(tech_id) - if not tech.exists() or not tech.active: - _logger.warning( - "tablet_tech_id %s invalid (not found or inactive); " - "falling back to session uid %s", - tablet_tech_id, env.uid, - ) - return env - return env(user=tech_id) diff --git a/fusion_plating/fusion_plating_shopfloor/controllers/tablet_controller.py b/fusion_plating/fusion_plating_shopfloor/controllers/tablet_controller.py index ef67343e..427efaaa 100644 --- a/fusion_plating/fusion_plating_shopfloor/controllers/tablet_controller.py +++ b/fusion_plating/fusion_plating_shopfloor/controllers/tablet_controller.py @@ -141,78 +141,6 @@ class FpTabletController(http.Controller): ) return {'ok': True} - # ====================================================================== - # /fp/tablet/unlock — verify PIN + manage failure counter / lockout - # ====================================================================== - @http.route('/fp/tablet/unlock', type='jsonrpc', auth='user') - def unlock(self, user_id, pin): - env = request.env - Users = env['res.users'].sudo() # need sudo to read hash field - target = Users.browse(int(user_id)) - if not target.exists(): - return {'ok': False, 'error': _('User not found.')} - - # No PIN set yet — caller must set one first - if not target.x_fc_tablet_pin_hash: - return { - 'ok': False, - 'error': _('No PIN set. Set one in Preferences first.'), - 'needs_setup': True, - } - - # Currently locked out? - now = fields.Datetime.now() - if target.x_fc_tablet_locked_until and target.x_fc_tablet_locked_until > now: - return { - 'ok': False, - 'error': _('Account locked. Try again in a few minutes.'), - 'locked_until': target.x_fc_tablet_locked_until.isoformat(), - } - - if target.verify_tablet_pin(pin): - # Reset failure state on success - target.write({ - 'x_fc_tablet_pin_failed_count': 0, - 'x_fc_tablet_locked_until': False, - }) - _logger.info( - "Tablet unlocked by uid %s (session uid %s)", - target.id, env.uid, - ) - return { - 'ok': True, - 'current_tech_id': target.id, - 'current_tech_name': target.name, - } - - # Wrong PIN — increment and check threshold - new_count = (target.x_fc_tablet_pin_failed_count or 0) + 1 - threshold = int(env['ir.config_parameter'].sudo().get_param( - 'fp.shopfloor.tablet_pin_fail_threshold', 5, - )) - lockout_min = int(env['ir.config_parameter'].sudo().get_param( - 'fp.shopfloor.tablet_pin_fail_lockout_minutes', 5, - )) - vals = {'x_fc_tablet_pin_failed_count': new_count} - if new_count >= threshold: - vals['x_fc_tablet_locked_until'] = now + timedelta(minutes=lockout_min) - target.write(vals) - _logger.warning( - "Tablet PIN failure for uid %s (count=%d, locked=%s)", - target.id, new_count, bool(vals.get('x_fc_tablet_locked_until')), - ) - if vals.get('x_fc_tablet_locked_until'): - return { - 'ok': False, - 'error': _('Too many failed attempts. Locked for %d minutes.') % lockout_min, - 'locked_until': vals['x_fc_tablet_locked_until'].isoformat(), - } - return { - 'ok': False, - 'error': _('Incorrect PIN.'), - 'attempts_remaining': threshold - new_count, - } - # ====================================================================== # /fp/tablet/unlock_session — verify PIN + mint REAL Odoo session as tech # ====================================================================== @@ -498,12 +426,10 @@ class FpTabletController(http.Controller): }) # Clocked-in first, then alphabetical within bucket tiles.sort(key=lambda t: (not t['is_clocked_in'], t['name'])) - # Phase D bootstrap: lock screen needs the feature flag + kiosk - # uid so it knows whether to call /unlock (legacy) or - # /unlock_session (new), and whether the current session is - # the kiosk's (= no tech logged in). - ICP = env['ir.config_parameter'].sudo() - session_mode = ICP.get_param('fp.shopfloor.tablet_session_mode', 'legacy') + # Lock screen needs the kiosk uid + current session uid so it can + # tell whether a tech is currently logged in (current_uid != + # kiosk_uid) or the tablet is back on the kiosk (current_uid == + # kiosk_uid). Post-Phase-G session_swap is the only flow. kiosk = env.ref( 'fusion_plating_shopfloor.user_fp_tablet_kiosk', raise_if_not_found=False, @@ -513,7 +439,6 @@ class FpTabletController(http.Controller): 'ok': True, 'company': _lock_company_payload(request.env), 'tiles': tiles, - 'tablet_session_mode': session_mode, 'kiosk_uid': kiosk_uid, 'current_uid': request.env.uid, } @@ -522,15 +447,9 @@ class FpTabletController(http.Controller): # /fp/tablet/ping — heartbeat used by the OWL component on every action # ====================================================================== @http.route('/fp/tablet/ping', type='jsonrpc', auth='user') - def ping(self, current_tech_id=None): + def ping(self): """Lightweight heartbeat. Used by the OWL component to confirm - the server-side session is alive AND to log the tech-of-record - every few minutes so the server has forensic visibility into - which tech was 'driving' the tablet at any moment. + the server-side session is alive. The session uid IS the tech + post-Phase-G — no extra plumbing needed. """ - if current_tech_id: - _logger.debug( - "Tablet ping: session uid %s carrying tablet_tech_id=%s", - request.env.uid, current_tech_id, - ) return {'ok': True, 'server_time': fields.Datetime.now().isoformat()}