Files
Odoo-Modules/fusion_plating/docs/superpowers/specs/2026-05-24-tablet-pin-session-redesign-design.md
gsinghpal cfaf4657ce docs(plating): tablet PIN session redesign spec
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>
2026-05-24 11:42:00 -04:00

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_id in 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_id kwarg and use env_for_tablet_tech to attribute the write to the tech via env(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_id sits 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.

  1. PIN unlock endpoint calls request.session.authenticate(request.db, {'type': 'fp_tablet_pin', 'login': tech.login, 'pin': pin}).
  2. Odoo's standard auth flow takes over: _check_credentials is invoked, sees type='fp_tablet_pin', calls our handler.
  3. Our handler hashes the PIN and compares against tech.x_fc_tablet_pin_hash. Validates tech.active and that the tech holds any shop-branch group.
  4. 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_session takes a DB row lock on res.users(id=tech_id) for the duration of verify_tablet_pin + failed_count write. Prevents double-counting failed attempts.
  • fp.tablet.session.event writes are sudo'd and append-only. Race conditions produce two adjacent audit rows (sortable by create_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'. No write() or unlink() ACL granted to any group except base.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_id cleanly 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

  1. 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.
  2. Smart button on res.users form (Owner-only) — "Tablet Events" with last-7-days count, opens audit list filtered to that user (user_id OR attempted_user_id).
  3. 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_events exists but is DISABLED. Configurable via ir.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_uid on every model — that's the primary audit.
  • Chatter authorship — still primary on individual records.
  • This log catches what create_uid can'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_session and /fp/tablet/lock_session
  • fp.tablet.session.event model + audit views
  • new OWL tablet_session_manager service
  • custom fp_tablet_pin auth manager

OLD endpoints stay:

  • /fp/tablet/unlock returns current_tech_id (no session swap)
  • env_for_tablet_tech still routes endpoints
  • OWL fp_shopfloor_tech_store still 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:
    1. Reboot or hard-refresh browser
    2. Sysadmin enters kiosk credential ONCE (stored in 1Password)
    3. Bookmark /odoo/action-fusion_plating_shopfloor.action_fp_plant_kanban
    4. Lock screen renders under kiosk user
    5. Test: tech taps tile, enters PIN, full flow works as them
    6. Track in spreadsheet (2-3 tablets total)

Step 3 — Day 14 cleanup commits

  • Sweep tablet_tech_id kwargs out of all ~15 endpoints
  • Delete _tablet_audit.env_for_tablet_tech (becomes import error → forces final cleanup)
  • Remove OLD /fp/tablet/unlock endpoint
  • Remove OLD fp_shopfloor_tech_store OWL service
  • Strip auto-injection from services/fp_rpc.js
  • Archive the legacy "shopfloor service" user

Auto-login pattern for kiosk

Three options, cheapest first:

  1. Browser-stored cookie + long session_lifetime (recommended for entech) — set session_db.session_lifetime to 90 days for kiosk. Sysadmin logs in once, cookie lasts 3 months. Cheap, manual.
  2. Kiosk browser extension — KioWare or Chromium kiosk-mode auto-fills credential. Auto-recovers from reboots.
  3. 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, same verify_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

  1. Identity — pick any create_uid on fp.job.step change in last 30 days. Cross-reference fp.tablet.session.event for active session at that timestamp under that user. Match rate target: 100% of tablet-originated writes.
  2. 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.
  3. Session length — longest session ≤ 8hr (within 5-min cron grace).
  4. Gap detection — adjacent lock → next unlock delta computable; "was anyone on the floor at 2pm?" answerable.
  5. No silent attribution — post-Step 3, action endpoint without tablet_tech_id runs under request.env.user which IS the tech. create_uid = tech.id.
  6. Kiosk privilege check — log in as fp_tablet_kiosk in private browser. Try to navigate any plating URL. Result: access denied / blank menu.
  7. Browser navigation under tech — as the unlocked tech, hit /odoo main menu. They see ONLY menus their role allows.
  8. Idle lockout fires — PIN-unlock, wait 11 min touching nothing. Auto-lock. event_type='idle_lock' row appears.
  9. Hard ceiling fires — backdate session_started_at past 8 hrs. Run cron. session_ended_at populates, event_type='force_lock'.
  10. 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_at could 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-plans to create the implementation plan
  • Execute the plan per subagent-driven-development
  • Deploy + validate on entech

End of design document.