Staff
Anyone who signs into Helm to operate the shop — owner, manager, sales staff, mechanic, junior. Each staff member has identity, a role, and per-staff permission overrides.
Table: staff
| Column | Type | Notes |
|---|---|---|
id | INTEGER PK | |
first_name, last_name | TEXT | PII |
email | TEXT | PII |
phone | TEXT | PII |
role_id | INTEGER FK → roles | Determines default permissions |
pin_hash | TEXT | PBKDF2-SHA256, base64; see ADR-0013 |
pin_salt | TEXT | base64 |
pin_set_at | TEXT | Timestamp of last PIN change |
lockout_until | TEXT | NULL when not locked; set on 5 wrong attempts |
failed_attempts | INTEGER | Resets on success or expiry of lockout_until |
last_sign_in_at | TEXT | |
is_active | INTEGER (0/1) | Soft-delete; inactive staff cannot sign in |
wage_cents_per_hour | INTEGER | For cost-of-service reporting; not payroll |
created_at, updated_at | TEXT |
Related tables
roles— Sys Admin, Owner, Sales, Mechanic, Junior, Service Lead, etc.role_permissions—(role_id, screen_id, can_see)defaultsstaff_screen_permissions—(staff_id, screen_id, can_see)per-staff overrides; wins over rolestaff_sessions— active sign-in tokens; one row per active session
See ADR-0014: per-staff permission overrides.
Behaviors
Sign-in
POST /api/auth/login:
- Body:
{ pin } - Hashes input PIN with each staff's salt, compares to
pin_hash - On match: creates
staff_sessionsrow, setshelm_sessioncookie - On miss: increments
failed_attempts; if ≥ 5 in 60s, setslockout_until= now+5min
The admin reset code (466687) is a special case — it signs in as the Sys Admin staff row directly (without per-staff PIN match). See security model.
PIN management
PUT /api/staff/{id}/pin:
- Body:
{ new_pin }(5 digits) - Generates new salt, hashes, updates
pin_hash+pin_salt+pin_set_at - Audit-logged (the new PIN value is never logged)
DELETE /api/staff/{id}/pin:
- Clears
pin_hash+pin_salt - Staff cannot sign in until PIN is set again
- Audit-logged
Lockout reset
The admin reset code unlocks any locked staff (clears lockout_until and failed_attempts). Also accessible via the PIN-reset modal at the sign-in overlay.
Edit
PUT /api/staff/{id} for name, email, phone, role, wage, is_active. Editable in-situ on the staff card in Settings → Staff & Permissions.
Permission resolution
For a given staff + screen, the resolved permission is:
COALESCE(
(SELECT can_see FROM staff_screen_permissions WHERE staff_id=? AND screen_id=?),
(SELECT can_see FROM role_permissions WHERE role_id=staff.role_id AND screen_id=?)
)
The Staff & Permissions UI shows a checkbox per screen per staff; toggling writes to staff_screen_permissions (per-staff), not role_permissions.
A "Reset to role defaults" button clears all override rows for that staff.
Idle timeout
The operator app tracks pointer/key events. After 60 seconds idle (configurable in shop_config.idle_timeout_seconds), the sign-in overlay re-appears. The current page state is preserved; signing back in restores it.
In-situ editing surface
On the Staff & Permissions card in Settings:
- Each staff row: click name to edit; role dropdown; per-screen visibility checkboxes; Select/Deselect All
- PIN management: a "Set PIN" / "Clear PIN" small button
- "Reset to role defaults" button per staff
-
- Add staff opens the new-staff form
Migrated from AIM
For Swicked: 9 staff records from AIM's stf table:
stf.stf_pk→ internal mapping forstaff.idassignmentstf.fname/lname→staff.first_name/last_name- AIM's role concept (string) → mapped to Helm role IDs
- PINs are NOT migrated; each staff member sets a fresh PIN on first sign-in (a Sys Admin sets initial PINs via the staff card)