feat(shopfloor): fp.tablet.session.event append-only audit log
Captures unlock / failed_unlock / manual_lock / idle_lock / ceiling_lock / force_lock / admin_reset events with session hash, ip, user-agent, duration, failure reason, acting uid. Read-only ACL granted to Owner in Phase A; no write/unlink anywhere. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,3 +10,4 @@ from . import fp_operator_queue
|
|||||||
from . import fp_tank
|
from . import fp_tank
|
||||||
from . import res_users
|
from . import res_users
|
||||||
from . import res_config_settings
|
from . import res_config_settings
|
||||||
|
from . import fp_tablet_session_event
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Append-only audit log of tablet session lifecycle events.
|
||||||
|
|
||||||
|
Every PIN unlock / lock-back / failed attempt / cron-force-lock
|
||||||
|
writes a row here. The model has read-only ACL (Owner only); even
|
||||||
|
write/unlink/edit are forbidden to anyone except root via direct SQL.
|
||||||
|
|
||||||
|
Spec section 4: docs/superpowers/specs/2026-05-24-tablet-pin-session-redesign-design.md
|
||||||
|
"""
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class FpTabletSessionEvent(models.Model):
|
||||||
|
_name = 'fp.tablet.session.event'
|
||||||
|
_description = 'Tablet Session Event (audit log)'
|
||||||
|
_order = 'create_date desc'
|
||||||
|
_rec_name = 'event_type'
|
||||||
|
|
||||||
|
event_type = fields.Selection(
|
||||||
|
[
|
||||||
|
('unlock', 'Unlock (PIN success)'),
|
||||||
|
('failed_unlock', 'Failed PIN attempt'),
|
||||||
|
('manual_lock', 'Manual lock (Hand-Off button)'),
|
||||||
|
('idle_lock', 'Idle timeout lock'),
|
||||||
|
('ceiling_lock', '8-hour ceiling lock'),
|
||||||
|
('force_lock', 'Force lock (cron, stale session)'),
|
||||||
|
('admin_reset', 'Admin force-reset PIN'),
|
||||||
|
],
|
||||||
|
required=True,
|
||||||
|
readonly=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_id = fields.Many2one(
|
||||||
|
'res.users',
|
||||||
|
string='Tech',
|
||||||
|
readonly=True,
|
||||||
|
ondelete='restrict',
|
||||||
|
help='The tech whose session was unlocked/locked. Empty for failed '
|
||||||
|
'attempts where the tile was tapped but unlock never succeeded.',
|
||||||
|
)
|
||||||
|
attempted_user_id = fields.Many2one(
|
||||||
|
'res.users',
|
||||||
|
string='Attempted Tech',
|
||||||
|
readonly=True,
|
||||||
|
ondelete='restrict',
|
||||||
|
help='For failed_unlock: which tile was tapped. user_id stays empty.',
|
||||||
|
)
|
||||||
|
|
||||||
|
session_id_hash = fields.Char(
|
||||||
|
string='Session ID (hashed)',
|
||||||
|
readonly=True,
|
||||||
|
help='sha256 hash of the Odoo session sid. Correlates events for '
|
||||||
|
'the same session without storing the raw token.',
|
||||||
|
)
|
||||||
|
session_started_at = fields.Datetime(readonly=True)
|
||||||
|
session_ended_at = fields.Datetime(readonly=True)
|
||||||
|
duration_seconds = fields.Integer(
|
||||||
|
string='Duration (s)',
|
||||||
|
readonly=True,
|
||||||
|
help='Filled on lock events from session_started_at delta.',
|
||||||
|
)
|
||||||
|
|
||||||
|
ip_address = fields.Char(readonly=True)
|
||||||
|
user_agent = fields.Char(
|
||||||
|
readonly=True,
|
||||||
|
help='Trimmed to 256 chars to prevent oversize logs.',
|
||||||
|
)
|
||||||
|
|
||||||
|
failure_reason = fields.Selection(
|
||||||
|
[
|
||||||
|
('wrong_pin', 'Wrong PIN'),
|
||||||
|
('locked_out', 'Locked out (too many failures)'),
|
||||||
|
('no_pin_set', 'No PIN configured'),
|
||||||
|
('user_inactive', 'User archived or disabled'),
|
||||||
|
('no_role', 'User has no shop-branch role'),
|
||||||
|
],
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
acting_uid = fields.Many2one(
|
||||||
|
'res.users',
|
||||||
|
string='Acting User',
|
||||||
|
readonly=True,
|
||||||
|
help='The user the SERVER saw at request time. Usually '
|
||||||
|
'fp_tablet_kiosk for unlocks; the manager for admin_reset; '
|
||||||
|
'base.user_root for cron-driven events.',
|
||||||
|
)
|
||||||
|
|
||||||
|
notes = fields.Text(readonly=True)
|
||||||
Reference in New Issue
Block a user