diff --git a/fusion_plating/fusion_plating_shopfloor/data/fp_tablet_cron.xml b/fusion_plating/fusion_plating_shopfloor/data/fp_tablet_cron.xml index 47b82d2a..479fe2a4 100644 --- a/fusion_plating/fusion_plating_shopfloor/data/fp_tablet_cron.xml +++ b/fusion_plating/fusion_plating_shopfloor/data/fp_tablet_cron.xml @@ -1,6 +1,14 @@ - + + + Tablet: Force-lock Stale PIN Sessions + + code + model._cron_force_lock_stale_sessions() + 5 + minutes + + + diff --git a/fusion_plating/fusion_plating_shopfloor/models/fp_tablet_session_event.py b/fusion_plating/fusion_plating_shopfloor/models/fp_tablet_session_event.py index 039c8e0d..d87cbb13 100644 --- a/fusion_plating/fusion_plating_shopfloor/models/fp_tablet_session_event.py +++ b/fusion_plating/fusion_plating_shopfloor/models/fp_tablet_session_event.py @@ -113,3 +113,45 @@ class FpTabletSessionEvent(models.Model): "admin-purge context flag." )) return super().unlink() + + @api.model + def _cron_force_lock_stale_sessions(self): + """Belt-and-suspenders cron: find any active unlock event past + the 8-hour ceiling and mark it force-locked. + + Handles browser crashes, tablet reboots with stale cookies, + and any path that bypasses /fp/tablet/lock_session. + + Runs every 5 minutes per fp_tablet_cron.xml. + """ + from datetime import timedelta + ceiling_hours = int(self.env['ir.config_parameter'].sudo().get_param( + 'fp.tablet.session_ceiling_hours', 8)) + cutoff = fields.Datetime.now() - timedelta(hours=ceiling_hours) + stale = self.search([ + ('event_type', '=', 'unlock'), + ('session_ended_at', '=', False), + ('session_started_at', '<', cutoff), + ]) + now = fields.Datetime.now() + for event in stale: + duration = int((now - event.session_started_at).total_seconds()) + self.sudo().create({ + 'event_type': 'force_lock', + 'user_id': event.user_id.id, + 'session_id_hash': event.session_id_hash, + 'session_started_at': event.session_started_at, + 'session_ended_at': now, + 'duration_seconds': duration, + 'notes': 'Cron force-lock: session exceeded %d-hour ceiling' % ceiling_hours, + }) + # Mark the original unlock event closed so it's not reprocessed + # next tick. write() is blocked by the model override — use + # direct SQL bypass (this is the documented escape hatch for + # the retention/cron path). + self.env.cr.execute( + """UPDATE fp_tablet_session_event + SET session_ended_at = %s, duration_seconds = %s + WHERE id = %s""", + (now, duration, event.id), + )