Real per-tech Odoo sessions on PIN unlock (not just attribution). Closes the audit-trail gap from Phase 1 permissions overhaul: today the tablet runs as a persistent 'shopfloor service' user and the PIN is just an OWL overlay — every action is attributed to whoever the session user is, not the tech who tapped their tile. Locked decisions: 1. Real per-tech sessions (impersonation, cookie swap) 2. Idle timeout 10min + manual lock + 8hr hard ceiling 3. Dedicated kiosk user (fp_tablet_kiosk, near-zero ACL) 4. No manager override — Mgr/Owner PIN in as themselves 5. Two-step deploy with 1-week overlap; OLD endpoint removed after successful rollout Audit: fp.tablet.session.event append-only log captures unlock / manual_lock / idle_lock / ceiling_lock / force_lock / failed_unlock / admin_reset events with ip, ua, session hash, duration. Effort: ~4 dev days + 1 week observation. Plan via writing-plans skill next. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
30 KiB
Tablet PIN Session Redesign — Design Document
Date: 2026-05-24
Status: Approved for implementation
Owner module: fusion_plating_shopfloor (with minor changes in fusion_plating for ACL data)
Brainstorm: session with @gsinghpal, 2026-05-24
Linked plan: TBD (writing-plans skill, next step)
Related: docs/superpowers/specs/2026-05-23-permissions-overhaul-design.md (Phase 1 permissions overhaul which surfaced this gap)
Problem Statement
The current tablet PIN system on entech is theatre. It looks like a security/audit gate but doesn't actually enforce attribution:
- The shop-floor tablet PC logs in ONCE as a persistent "shopfloor service" Odoo user. That session never changes.
- When a tech taps their tile and enters the PIN, the OWL frontend stores a
current_tech_idin an in-memory service (fp_shopfloor_tech_store). The underlying Odoo session cookie does NOT change. - ~15 of the ~30 shop-floor write endpoints accept a
tablet_tech_idkwarg and useenv_for_tablet_techto attribute the write to the tech viaenv(user=tech_id). The OTHER ~15 endpoints write under the session user (the shopfloor service user). The audit trail is incomplete. - There is no idle timeout, no re-lock on walk-away. Once unlocked, the
tech_idsits in OWL state until someone manually swaps it. A tech walks away, the next person clicks a job card → Odoo records the work under the prior tech. - A tech (or anyone with browser access) can type any URL —
/odoo/settings,/web/...— and act under the shopfloor service user's full backend privileges. The "lock" is only an OWL overlay over the kanban; it doesn't gate URL navigation.
The whole point of adding a PIN was to enforce "log who did what." Today it doesn't. AS9100/Nadcap audit trails are unreliable because attribution can be wrong.
Locked Design Decisions
| # | Question | Decision |
|---|---|---|
| Q1 | Session model | Real per-tech sessions (impersonation). After PIN unlock the backend creates a REAL Odoo session AS the tech (cookie swap, server-side session row). They literally ARE that user for the duration of the unlock. |
| Q2 | Lock-back trigger | Idle timeout + manual lock + hard ceiling. Default 10 min idle, 8 hr hard ceiling regardless of activity. Manual "Lock" / "Hand-Off" button always available. |
| Q3 | Kiosk identity | Dedicated kiosk user. New user fp_tablet_kiosk@enplating.local in a new group_fp_tablet_kiosk group with near-zero ACL (read res.users for tile grid + read ir.config_parameter for idle settings). |
| Q4 | Manager override / impersonation | No special path. Manager wanting to chip in must PIN in as themselves. Simpler, strongest audit. |
| Q5 | OLD endpoint lifecycle | Remove after successful rollout. Two-step deploy with 1-week overlap, then Step 3 cleanup commits remove the legacy tablet_tech_id plumbing. |
Section 1 — Architecture Overview
Three identities, two state transitions
┌──────────────────────────────────────────────────────────────────────┐
│ STATE: KIOSK │
│ ──────────── │
│ Browser cookie = kiosk session_id │
│ Session uid = fp_tablet_kiosk (new user, near-zero ACL) │
│ Visible = lock screen + tile grid only │
│ Idle timer = OFF │
└──────────────────────────────────────────────────────────────────────┘
│
│ Tech taps tile + enters correct PIN
│ POST /fp/tablet/unlock_session
│ → server: verify hash, mint new session
▼
┌──────────────────────────────────────────────────────────────────────┐
│ STATE: TECH │
│ ─────────── │
│ Browser cookie = tech session_id (fresh Set-Cookie from unlock) │
│ Session uid = tech.id (real Odoo session as the tech) │
│ Visible = full Plating UI per tech's normal ACLs │
│ Idle timer = ON (10 min default) │
│ Hard ceiling = 8 hr from session_started_at │
│ │
│ EVERY Odoo write naturally attributed via session.uid: │
│ create_uid = tech.id │
│ write_uid = tech.id │
│ chatter authorship = tech.partner_id │
│ NO MORE tablet_tech_id plumbing needed │
└──────────────────────────────────────────────────────────────────────┘
│
│ Any of: manual Lock button / 10 min idle /
│ 8 hr ceiling
│ POST /fp/tablet/lock_session
│ → server: destroy session, re-auth as kiosk
▼
back to KIOSK
Why this is fundamentally different from today
Today the tablet has ONE persistent session. The PIN is an OWL overlay; the underlying Odoo session never changes. New design: every PIN unlock creates a NEW Odoo session AS the tech. Lock-back DESTROYS that session and creates a fresh kiosk session.
Result: Odoo's standard create_uid / write_uid / chatter authorship already attributes everything correctly. We drop the tablet_tech_id kwarg + env_for_tablet_tech helper entirely — they become dead code.
Security gain: If the tech navigates to ANY Odoo URL (Settings, Users, anything), they're acting under their own permissions, not the kiosk's. The kiosk user has near-zero ACLs so even if someone hits /fp/... URLs before PIN-ing in, they see nothing.
Section 2 — Components & Files
New files
| Path | Responsibility |
|---|---|
fusion_plating_shopfloor/data/fp_tablet_kiosk_user.xml |
Idempotent create of fp_tablet_kiosk@enplating.local + assignment to group_fp_tablet_kiosk. Default password = random secret stored in ir.config_parameter['fp.tablet.kiosk_password'] (sysadmin can reset). |
fusion_plating_shopfloor/security/fp_tablet_kiosk_security.xml |
New res.groups group_fp_tablet_kiosk. NOT under the Fusion Plating privilege block — orthogonal to the 8-role hierarchy. privilege_id stays empty so it doesn't pollute the role dropdown. |
fusion_plating_shopfloor/security/ir.model.access.csv (rows added) |
2 ACL rows for kiosk: READ res.users (tile grid) + READ ir.config_parameter (idle settings). Nothing else. No fp.job, no sale.order, no anything. |
fusion_plating_shopfloor/models/fp_tablet_session_event.py |
New model fp.tablet.session.event — append-only audit log. Owner-only read; only base.group_system can ever write/unlink (we don't grant that). |
fusion_plating_shopfloor/views/fp_tablet_session_event_views.xml |
List + form views. Owner-only menu under Plating → Configuration → Tablet Audit Log. Smart button on res.users form. |
fusion_plating_shopfloor/models/res_users.py (existing — add auth check) |
Override _check_credentials to handle type='fp_tablet_pin' (custom auth manager). Verifies PIN hash + recordset state; raises AccessDenied on any failure. |
fusion_plating_shopfloor/data/fp_tablet_cron.xml |
New cron _cron_force_lock_stale_sessions (every 5 min). Finds any active fp.tablet.session.event past 8hr ceiling with no session_ended_at, force-marks it ended. |
fusion_plating_shopfloor/static/src/js/services/tablet_session_manager.js |
OWL service. Tracks idle time via DOM event listeners. Fires lock-back at 10min idle or 8hr ceiling. Replaces fp_shopfloor_tech_store. |
Modified files
| Path | Change |
|---|---|
controllers/tablet_controller.py |
Replace /fp/tablet/unlock with two new endpoints: /fp/tablet/unlock_session (verifies PIN → mints new Odoo session via request.session.authenticate(db, {type:'fp_tablet_pin', ...})), /fp/tablet/lock_session (destroys tech session → re-auths as kiosk). OLD /fp/tablet/unlock kept alive during the 1-week overlap. |
controllers/_tablet_audit.py |
env_for_tablet_tech becomes a one-line no-op pass-through. Marked deprecated; deleted in Step 3 cleanup. |
All ~15 endpoints with tablet_tech_id kwarg |
Step 3 (post-overlap) — remove the kwarg. Endpoints run under request.env.user which IS the tech (because session swap). |
OWL FpTabletLock component |
Use new tablet_session_manager service. On successful unlock, window.location.reload() so the entire app re-bootstraps with the tech's session cookie. (Cleanest — no half-state.) |
services/fp_rpc.js |
Stop auto-injecting tablet_tech_id. Becomes a thin wrapper, removable once endpoints drop the kwarg in Step 3. |
Auth path — custom auth manager
Method: Register a new auth type fp_tablet_pin via res.users._check_credentials.
- PIN unlock endpoint calls
request.session.authenticate(request.db, {'type': 'fp_tablet_pin', 'login': tech.login, 'pin': pin}). - Odoo's standard auth flow takes over:
_check_credentialsis invoked, seestype='fp_tablet_pin', calls our handler. - Our handler hashes the PIN and compares against
tech.x_fc_tablet_pin_hash. Validatestech.activeand that the tech holds any shop-branch group. - On success, Odoo issues the session, sets cookie, returns response — same code path Odoo uses for password login. We get session lifecycle hooks, validation chain, and security for free.
Alternative considered: direct request.session.uid = tech.id + manual cookie. Faster to implement but bypasses Odoo's _check_credentials validation chain (2FA, IP gating, future security modules). Picked the slower-to-implement but correct path.
Idle-timer mechanics (OWL service)
// tablet_session_manager.js (sketch)
class TabletSessionManager {
setup(env) {
this.idleMs = 10 * 60 * 1000; // 10 min, configurable via ir.config_parameter
this.ceilingMs = 8 * 60 * 60 * 1000; // 8 hr hard
this.lastActivity = Date.now();
this.sessionStartedAt = ...; // from server on bootstrap
['click', 'touchstart', 'keydown', 'mousemove'].forEach(ev =>
document.addEventListener(ev, () => this.touch(), { passive: true })
);
setInterval(() => this.tick(), 5000);
}
touch() { this.lastActivity = Date.now(); }
tick() {
const now = Date.now();
if (now - this.lastActivity > this.idleMs ||
now - this.sessionStartedAt > this.ceilingMs) {
this.lockBack(now - this.lastActivity > this.idleMs ? 'idle' : 'ceiling');
}
}
async lockBack(reason) {
await rpc("/fp/tablet/lock_session", { reason });
window.location.reload(); // fresh page → fresh kiosk session
}
}
Server-side belt-and-suspenders: cron _cron_force_lock_stale_sessions runs every 5 min and force-destroys any tablet session past the 8-hr ceiling — handles browser crashes, tablet reboots with stale cookie, etc.
Section 3 — Session Lifecycle in Detail
Unlock flow
Tablet OWL Server
────── ─── ──────
Tap tile ──────────────▶ PIN pad opens
Enter 4 digits ────────────▶ collect PIN
POST /fp/tablet/unlock_session
{ tech_id, pin }
cookie: kiosk_session_id
────────────────────▶
1. verify kiosk session active
2. browse(tech_id), exists+active
3. lockout check (failed_count,
locked_until)
4. verify_tablet_pin via hash
│
├─ FAIL → fp.tablet.session.event
│ (failed_unlock,
│ attempted_user_id=tech_id,
│ failure_reason='wrong_pin',
│ ip, ua)
│ + increment failed_count
│ + maybe set locked_until
│ + return {ok:false, error}
│
└─ PASS:
5. session.authenticate(db, {
type:'fp_tablet_pin',
login:tech.login,
pin:pin })
→ Odoo issues new sid,
uid=tech.id
→ response Set-Cookie
6. fp.tablet.session.event
(unlock, user_id=tech_id,
session_id_hash=sha256(sid),
session_started_at=now,
ip, ua)
7. reset failed_count=0
8. return {ok:true, tech_name}
◀────────────────────
window.location.reload()
Lock-back flow
Trigger (any of):
- User taps Lock button
- 10 min no activity (idleMs)
- 8 hr since session_started_at (ceilingMs)
POST /fp/tablet/lock_session { reason: 'manual' | 'idle' | 'ceiling' }
cookie: tech_session_id
──────▶ server:
1. read session.uid (current tech)
2. fp.tablet.session.event
(event_type matches reason,
user_id=tech_id, session_id_hash,
session_ended_at=now,
duration_seconds=now - session_started_at)
3. request.session.logout()
4. response Set-Cookie: clear tech session
5. session.authenticate(db, {type:'password',
login:'fp_tablet_kiosk',
password:KIOSK_SECRET_from_ir_config})
→ response carries new kiosk Set-Cookie
6. return {ok:true, locked_at:now}
◀────── browser receives Set-Cookie (kiosk session)
window.location.reload()
App re-bootstraps as kiosk → lock screen renders
Edge cases (must work)
| Scenario | Behavior |
|---|---|
| Tech walks away, browser crashes | Cron _cron_force_lock_stale_sessions (every 5 min) finds the stale session, writes event_type='force_lock'. Next tablet boot starts as kiosk regardless. |
| Two techs race-tap two tiles simultaneously | First unlock wins. Second tech's POST sent with the FIRST tech's NEW cookie (Set-Cookie applied) → backend sees them as tech 1, returns access-denied to mint another session. UI debounces with spinner. |
| Wrong PIN 5 times | failed_count → 5, locked_until set to now+5min. Subsequent PIN attempts return {ok:false, locked_until} → UI shows countdown. Audit event each attempt. |
| Network drops mid-unlock | OWL gets timeout → retry button. Session NOT created server-side if request never completed. Single DB transaction guarantees atomicity. |
| Browser pre-fills cached cookie from old session | Server validates session row; if invalid, returns 401, OWL forces page reload → re-authenticates as kiosk. |
| Manager force-resets a tech's PIN while tech is unlocked | Tech's current session keeps working (sessions independent of PIN hash). Next PIN entry requires the new PIN. Manager action logged via admin_reset event. |
Tech navigates to /odoo/settings or other URLs |
They're a real Odoo user with their own ACLs. Standard ACLs apply. Technician sees what a Technician would see (mostly nothing — Manager+ only menus). |
| Tablet PC reboots mid-shift | Boots to login page (kiosk session cookie may have expired). Stored kiosk credential auto-fills. Reaches lock screen, ready for PIN. |
Concurrency / race protection
unlock_sessiontakes a DB row lock onres.users(id=tech_id)for the duration ofverify_tablet_pin+failed_countwrite. Prevents double-counting failed attempts.fp.tablet.session.eventwrites are sudo'd and append-only. Race conditions produce two adjacent audit rows (sortable bycreate_date) — never lose data.
Section 4 — Audit Log Model + UI
Model: fp.tablet.session.event
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', readonly=True, ondelete='restrict',
help='The tech whose session was unlocked/locked. NULL for failed '
'attempts where the tile was tapped but unlock never succeeded.')
attempted_user_id = fields.Many2one('res.users', readonly=True, ondelete='restrict',
help='For failed_unlock: which tile was tapped. user_id stays empty.')
session_id_hash = fields.Char(readonly=True,
help='sha256 hash of the Odoo session sid. Lets us correlate '
'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(readonly=True)
ip_address = fields.Char(readonly=True)
user_agent = fields.Char(readonly=True, help='Trimmed to 256 chars.')
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', 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.')
notes = fields.Text(readonly=True)
Design rules:
- Append-only. No
_inherit = 'mail.thread'. Nowrite()orunlink()ACL granted to any group exceptbase.group_system(which we DON'T grant — root SQL access required to tamper). - Hashed session sid, not raw — if DB leaks, attackers can't replay sessions.
attempted_user_idcleanly distinguishes "Carlos's tile was tapped" from "Carlos was authenticated."
Lifecycle: who writes what
| Trigger | Endpoint | event_type | user_id | attempted_user_id | acting_uid |
|---|---|---|---|---|---|
| Successful PIN unlock | /fp/tablet/unlock_session |
unlock | tech | — | kiosk |
| Wrong PIN | /fp/tablet/unlock_session (fail) |
failed_unlock | — | tech | kiosk |
| 5th wrong PIN → lockout | same | failed_unlock (locked_out) |
— | tech | kiosk |
| Lock button | /fp/tablet/lock_session |
manual_lock | tech | — | tech |
| 10 min idle | /fp/tablet/lock_session |
idle_lock | tech | — | tech |
| 8 hr ceiling | /fp/tablet/lock_session or cron |
ceiling_lock | tech | — | tech or cron |
| Cron force-lock | _cron_force_lock_stale_sessions |
force_lock | tech | — | base.user_root |
| Manager resets PIN | /fp/tablet/reset_pin_for |
admin_reset | tech | — | manager |
UI: 3 surfaces
- Owner-only menu — Plating → Configuration → Tablet Audit Log. List view with badges, filters (today/week/month, event_type, user), group-by, default 90-day window.
- Smart button on
res.usersform (Owner-only) — "Tablet Events" with last-7-days count, opens audit list filtered to that user (user_idORattempted_user_id). - Chatter linkback (deferred to follow-up) — tooltip on chatter messages linking to the unlock event. Phase 2, not blocking.
Retention
- Default: indefinite.
_cron_purge_old_session_eventsexists but is DISABLED. Configurable viair.config_parameter['fp.tablet.audit.retention_days'](unset = forever). - AS9100 retention typically 3-7 years. Safe default for small shops.
What this audit log does NOT replace
- Standard
create_uid/write_uidon every model — that's the primary audit. - Chatter authorship — still primary on individual records.
- This log catches what
create_uidcan't: failed attempts, session lengths, idle vs manual locks, gaps between sessions.
Section 5 — Migration & Rollout
Two-step deploy with overlap window
To avoid downtime, OLD and NEW endpoints coexist for ~1 week. Tablets switch over individually as they reboot.
Step 1 — Day 0 deploy (NEW code, OLD still alive)
-u brings up:
- kiosk user + group + ACL
- new
/fp/tablet/unlock_sessionand/fp/tablet/lock_session fp.tablet.session.eventmodel + audit views- new OWL
tablet_session_managerservice - custom
fp_tablet_pinauth manager
OLD endpoints stay:
/fp/tablet/unlockreturnscurrent_tech_id(no session swap)env_for_tablet_techstill routes endpoints- OWL
fp_shopfloor_tech_storestill bypassed when new manager is active
Frontend feature flag ir.config_parameter['fp.shopfloor.tablet_session_mode']:
'legacy'(Day 0 default) → OWL uses old flow'session_swap'→ OWL uses new flow
Step 2 — Days 1-7 cutover, one tablet at a time
- Flip flag to
'session_swap'on entech - Per tablet:
- Reboot or hard-refresh browser
- Sysadmin enters kiosk credential ONCE (stored in 1Password)
- Bookmark
/odoo/action-fusion_plating_shopfloor.action_fp_plant_kanban - Lock screen renders under kiosk user
- Test: tech taps tile, enters PIN, full flow works as them
- Track in spreadsheet (2-3 tablets total)
Step 3 — Day 14 cleanup commits
- Sweep
tablet_tech_idkwargs out of all ~15 endpoints - Delete
_tablet_audit.env_for_tablet_tech(becomes import error → forces final cleanup) - Remove OLD
/fp/tablet/unlockendpoint - Remove OLD
fp_shopfloor_tech_storeOWL service - Strip auto-injection from
services/fp_rpc.js - Archive the legacy "shopfloor service" user
Auto-login pattern for kiosk
Three options, cheapest first:
- Browser-stored cookie + long session_lifetime (recommended for entech) — set
session_db.session_lifetimeto 90 days for kiosk. Sysadmin logs in once, cookie lasts 3 months. Cheap, manual. - Kiosk browser extension — KioWare or Chromium kiosk-mode auto-fills credential. Auto-recovers from reboots.
- Odoo SSO with stored token — overkill for 2-3 tablets.
Rollback plan
- Set
tablet_session_mode = 'legacy'→ all OWL switches back. No redeploy. - If kiosk user is broken, legacy "shopfloor service" still has its permissions and the OLD endpoints — system keeps working.
What WON'T be touched
res.users.x_fc_tablet_pin_hash— same field, same hash format, sameverify_tablet_pin(). PINs don't need to be reset.- Lockout state (
failed_count,locked_until) — preserved. - All other shop-floor functionality (kanban, workspace, recipes) — unaffected.
Timing on entech
| Day | Action |
|---|---|
| 0 (deploy day) | -u fusion_plating_shopfloor + 4 related modules. Verify new endpoints. tablet_session_mode='legacy'. |
| 0 (evening) | Flip flag on one test tablet. Manual test 5 unlock/lock cycles. |
| 1-2 | Roll to second + third tablets. Owner watches audit log. |
| 3-7 | Operators use new flow. Owner reviews audit log daily. |
| 7 | Decision: keep 'session_swap' permanently OR roll back. |
| 14 | If kept: Step 3 cleanup commits. |
Section 6 — Acceptance, Risks, Deferred
Acceptance criteria
- Identity — pick any
create_uidonfp.job.stepchange in last 30 days. Cross-referencefp.tablet.session.eventfor active session at that timestamp under that user. Match rate target: 100% of tablet-originated writes. - Failed attempts — query "every wrong-PIN attempt for user X in last week" returns rows with
attempted_user_id=X,failure_reason='wrong_pin', IP, UA, timestamp. - Session length — longest session ≤ 8hr (within 5-min cron grace).
- Gap detection — adjacent lock → next unlock delta computable; "was anyone on the floor at 2pm?" answerable.
- No silent attribution — post-Step 3, action endpoint without
tablet_tech_idruns underrequest.env.userwhich IS the tech.create_uid = tech.id. - Kiosk privilege check — log in as
fp_tablet_kioskin private browser. Try to navigate any plating URL. Result: access denied / blank menu. - Browser navigation under tech — as the unlocked tech, hit
/odoomain menu. They see ONLY menus their role allows. - Idle lockout fires — PIN-unlock, wait 11 min touching nothing. Auto-lock.
event_type='idle_lock'row appears. - Hard ceiling fires — backdate
session_started_atpast 8 hrs. Run cron.session_ended_atpopulates,event_type='force_lock'. - Audit log append-only — try
event.write({...})as Owner. AccessError. Only root SQL access can tamper.
Open risks
| Risk | Likelihood | Mitigation |
|---|---|---|
| Custom auth manager conflicts with Odoo's session_lifetime / 2FA / IP modules | Medium | Distinctive type name (fp_tablet_pin, not tablet_pin). Tests for pwd + 2FA paths unchanged. Document in CLAUDE.md. |
| Browser cookie misbehavior on rapid lock/unlock | Low | window.location.reload() after every transition kills half-state. |
| Tablet PC reboot mid-shift with stale cookie | Medium | Long-lived kiosk cookie (90-day session_lifetime). Sysadmin re-login if expired (~30s downtime). |
| Backend cron clock drift | Low | All timestamps fields.Datetime.now() (UTC, server). Server cron is source of truth for ceiling. |
| Network drop mid-unlock | Low | One DB transaction — atomic. Either session+audit row both commit, or neither. |
| Operator hits backend URLs | Expected behavior | They're real Odoo users with their own ACLs. Standard ACLs apply. Working as designed. |
| Brute-force PIN attempts as DoS | Low (insider only) | 5-attempt → 5-min lockout. Cron clears failed_count after 1hr of no failures. 10000 possible PINs + lockout → ~3.5 years to brute-force on average. |
Deferred to follow-up
- Chatter linkback (Section 4, surface 3) — useful but not blocking. Phase 2.
- 2FA on lock screen (badge tap, biometric) — out of scope.
- Per-tablet identity — currently every tablet uses same kiosk credential. If you ever want to track which physical tablet did what, add
tablet_device_id. Deferred — small shop doesn't need it. - SAML/SSO integration — out of scope.
- Manager override mode — explicitly killed in Q4. Manager wanting to chip in must PIN in as themselves.
- Time-clock integration — separate concern. The
session_started_atcould feed time-tracking but that integration is its own design. - Mobile (non-tablet) access — Technician on phone uses standard Odoo login. PIN flow is tablet-only.
Estimated effort
- Phase 1: server-side (kiosk user, auth manager, endpoints, audit model, cron) — ~1.5 days
- Phase 2: OWL (session manager, lock-back UI, reload-on-transition) — ~0.5 days
- Phase 3: audit views + Owner menu + smart button — ~0.5 days
- Phase 4: entech rollout (deploy, feature-flag test, per-tablet cutover, validation week) — ~1 day spread over 7 days
- Phase 5: Step 3 cleanup (rip out tablet_tech_id) — ~0.5 days
Total: ~4 development days + 1 calendar week observation.
Status
- ✅ Brainstorm complete (5 locked decisions)
- ✅ Design doc written
- ⏳ Self-review (next)
- ⏳ User reviews this spec
- ⏳ Invoke
writing-plansto create the implementation plan - ⏳ Execute the plan per
subagent-driven-development - ⏳ Deploy + validate on entech
End of design document.