feat(fusion_plating_shopfloor): /fp/tablet/tiles + /fp/tablet/ping endpoints (P6.1.4-P6.1.5)

Tiles returns the lock-screen grid: operator-group users, sorted
clocked-in-first then alphabetical, with avatar URL + has_pin flag.
Honours station.x_fc_authorised_user_ids when non-empty (Phase 6.1.6
adds that field). Ping is a lightweight ack used by FpTabletLock as
a heartbeat — logs current_tech_id at DEBUG for forensic visibility.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-23 00:15:40 -04:00
parent a594431eb6
commit 59ad77839a
2 changed files with 95 additions and 0 deletions

View File

@@ -147,3 +147,65 @@ class FpTabletController(http.Controller):
'error': _('Incorrect PIN.'),
'attempts_remaining': threshold - new_count,
}
# ======================================================================
# /fp/tablet/tiles — lock-screen tile grid
# ======================================================================
@http.route('/fp/tablet/tiles', type='jsonrpc', auth='user')
def tiles(self, station_id=None):
env = request.env
op_group = env.ref(
'fusion_plating.group_fusion_plating_operator',
raise_if_not_found=False,
)
if not op_group:
return {'ok': False, 'error': 'operator group missing'}
# Determine candidate users — station roster wins if non-empty
users = op_group.user_ids
if station_id:
Station = env['fusion.plating.shopfloor.station']
station = Station.browse(int(station_id))
if (station.exists()
and 'x_fc_authorised_user_ids' in station._fields
and station.x_fc_authorised_user_ids):
users = station.x_fc_authorised_user_ids
# has_pin needs sudo-read on the hash field
clocked_ids = set()
if 'hr.employee' in env and hasattr(
env['hr.employee'], '_fp_clocked_in_user_ids',
):
clocked_ids = env['hr.employee']._fp_clocked_in_user_ids() or set()
users_sorted = users.sorted('name')
users_sudo = users_sorted.sudo()
tiles = []
for u, u_sudo in zip(users_sorted, users_sudo):
tiles.append({
'user_id': u.id,
'name': u.name,
'avatar_url': f'/web/image/res.users/{u.id}/avatar_128',
'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, 'tiles': tiles}
# ======================================================================
# /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):
"""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.
"""
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()}

View File

@@ -181,3 +181,36 @@ class TestTabletUnlock(HttpCase):
res = self._unlock('1234')
self.assertFalse(res['ok'])
self.assertTrue(res.get('needs_setup'))
@tagged('-at_install', 'post_install', 'fp_shopfloor', 'fp_tablet_pin')
class TestTabletTiles(HttpCase):
"""P6.1.4 — /fp/tablet/tiles endpoint."""
def setUp(self):
super().setUp()
self.authenticate("admin", "admin")
self.op_group = self.env.ref('fusion_plating.group_fusion_plating_operator')
self.alice = self.env['res.users'].create({
'name': 'Alice Tile', 'login': 'alice_tile@example.com',
'group_ids': [(6, 0, [self.op_group.id])],
})
self.bob = self.env['res.users'].create({
'name': 'Bob Tile', 'login': 'bob_tile@example.com',
'group_ids': [(6, 0, [self.op_group.id])],
})
self.alice.sudo().set_tablet_pin('1111')
def test_tiles_returns_all_operators_without_station(self):
res = _rpc(self, '/fp/tablet/tiles')
self.assertTrue(res['ok'])
names = [t['name'] for t in res['tiles']]
self.assertIn('Alice Tile', names)
self.assertIn('Bob Tile', names)
def test_tile_has_pin_flag(self):
res = _rpc(self, '/fp/tablet/tiles')
alice_tile = next(t for t in res['tiles'] if t['name'] == 'Alice Tile')
bob_tile = next(t for t in res['tiles'] if t['name'] == 'Bob Tile')
self.assertTrue(alice_tile['has_pin'])
self.assertFalse(bob_tile['has_pin'])