feat(shopfloor): force-lock cron for stale tablet sessions
Every 5 minutes, find active unlock events past 8-hour ceiling and mark them force-locked. SQL bypass of the model's read-only ACL is the only path that can update existing rows (no Python write() works because the model override blocks even sudo writes without the explicit fp_tablet_audit_admin_write context flag). Ceiling configurable via ir.config_parameter[fp.tablet.session_ceiling_hours]. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,14 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<!-- Placeholder for cron records added in Phase C task C4
|
<data noupdate="1">
|
||||||
(force-lock stale tablet sessions). File registered in manifest
|
<record id="ir_cron_force_lock_stale_sessions" model="ir.cron">
|
||||||
to keep the data list stable across phase boundaries. -->
|
<field name="name">Tablet: Force-lock Stale PIN Sessions</field>
|
||||||
|
<field name="model_id" ref="model_fp_tablet_session_event"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model._cron_force_lock_stale_sessions()</field>
|
||||||
|
<field name="interval_number">5</field>
|
||||||
|
<field name="interval_type">minutes</field>
|
||||||
|
<field name="active" eval="True"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@@ -113,3 +113,45 @@ class FpTabletSessionEvent(models.Model):
|
|||||||
"admin-purge context flag."
|
"admin-purge context flag."
|
||||||
))
|
))
|
||||||
return super().unlink()
|
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),
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user