Skip to content

Feature: Employee attendance via QR clock-in (LAN check-in, photo capture, optional face-match & cloud/geofenced remote check-in) #428

Description

@itsbkm

Context

FloCafe has no employee clock-in/clock-out tracking today. The only employee-shaped data is the
users table (main/db.ts:4224-4238id, name, role, pin/pin_hash, is_active), used
for login/PIN auth and KDS station assignment (station_users, main/db.ts:4182). There is no
attendance or staff_clock table, and no existing doc in docs/ covers staff clock-in.

This issue proposes an Employee Attendance feature: a fourth standalone server on port 3004,
following the exact bootstrap pattern already used for the API (:3001), KDS (:3002), and
Server App (:3003) in main/index.ts's initialize() (lines 648-684) — each with its own
start*() / stop*() / get*Port() / is*Running() exports and an entry in the shutdown
array (main/index.ts:811-814).

Related Existing Infrastructure (verified in code)

These precedents directly answer the open questions raised when scoping this feature:

  • QR + local-network pairing: main/routes/kds-info.ts (52 lines) generates a QR via the
    qrcode package containing a plain http://{ip}:{port} URL, and an mdns_url of
    http://flo.local:{port}. The IP-changes-on-DHCP-renewal problem is already solved for KDS —
    not by the QR/token, but by startMdns() in main/index.ts:457-474, which publishes a
    bonjour-service (package.json dep ^1.4.4) mDNS record advertising the service as
    flo.local with each server's port in the TXT record. Devices resolve flo.local:3002 instead
    of a raw IP; the per-IP QR list (ips_data in kds-info.ts) is only a fallback for networks
    where multicast/mDNS is blocked (some corporate/guest Wi-Fi).
  • Employee identity: users table (main/db.ts:4224) — id, name, role, pin_hash.
  • Photo/image handling precedent: product images are stored as Base64 data: URIs directly in
    a SQLite column (main/routes/products.ts:491-550), not as files on disk. There is no
    uploads/ directory anywhere in the repo and no multer/multipart-to-disk middleware.
  • On-disk file convention (when SQLite-blob storage isn't right): Electron userData path,
    main/index.ts:224 (~/.config/flo-desktop equivalent), used today for flo.db, DB backups,
    the WhatsApp auth directory (main/services/whatsapp.ts:283), and encrypted tokens
    (main/services/google-drive.ts:170, main/services/master-pin.ts:17).
  • No ML/face-recognition dependency exists anywhere in package.json or main/ today
    (confirmed by grep for face, tensorflow, onnx, opencv, ml-, vision).
  • Optional/cloud-adjacent feature pattern: main/services/google-drive.ts,
    main/services/whatsapp.ts, main/services/cloud-sync.ts — all singleton services with
    start()/stop(), gated by a DB-backed enabled flag, wired into main/index.ts's startup/
    shutdown sequence. Feature-level enable gating precedent: requireKdsEnabled middleware
    (main/middleware/security.ts:340) backed by isKdsEnabled() (main/db.ts:701).
  • Existing cloud + mobile pairing rail: docs/cloud-v2-plan.md documents that FloCafe already
    registers with a cloud backend (FloAdmin) via main/services/cloud-sync.ts and issues a
    short-lived numeric pairing code, shown as both digits and a QR, that a companion mobile app
    ("RevFlo") scans to pair (POST /api/pairing/redeem). This is the natural rail for any
    cloud/mobile-app variant of attendance — it should not be reinvented.
  • License: repo is plain MIT (LICENSE), no dual-licensing or paid-tier precedent in this
    codebase. docs/cloud-v2-plan.md and AGENTS.md's "optional network features" invariant are the
    only precedent for cloud-gated (as opposed to fully local) functionality.

Problem

Stores need a way to record when employees start/end their shift ("attendance", distinct from POS
cash-drawer shifts — see Disambiguation below), ideally by having the employee scan a QR code on
arrival, the way KDS stations are paired today. Several open design questions block a clean
implementation:

  1. A QR code that encodes a raw IP breaks whenever DHCP reassigns the host's local IP, but store
    staff want something they can print once and stick on the wall.
  2. Where do check-in photos live, given there's no existing "files on disk, served statically"
    convention in this codebase — only DB-blob storage for the much lower-volume case of product
    images?
  3. Should check-in photos be automatically matched against a reference photo (on-device face
    recognition), or just captured for a human to review?
  4. Is any part of this a paid/cloud feature, given the codebase is plain MIT with no existing
    commercial tier?
  5. Should there be a remote/mobile check-in path (phone shares GPS, matched against the store's
    registered location), and if so, does it need new cloud infrastructure or can it reuse the
    existing FloAdmin/RevFlo pairing rail?

Architectural Decision

1. QR code / changing IP — Reuse the KDS pattern exactly, not a variant of it. Add
main/routes/attendance-info.ts (mirrors kds-info.ts) exposing mdns_url
(http://flo.local:3004/attendance/checkin), a per-interface ip_url fallback list, and a
qr_data_url PNG. Add attendance's port to the existing mDNS TXT record in startMdns()
(main/index.ts:465) alongside kds_port/server_app_port. Printed on paper, the QR stays valid
across IP changes as long as the network supports multicast DNS — which is the same assumption
the KDS pairing screen already makes today, so this introduces no new risk. On networks that block
multicast, fall back to the same per-IP QR list KDS already shows, refreshed from the settings
screen rather than printed.

2. Where check-in photos are saved — Do not reuse the products.ts Base64-in-SQLite pattern.
That pattern fits a handful of rarely-changing product images; attendance photos accumulate
per-employee, per-check-in, indefinitely, and would bloat flo.db and slow every backup/restore.
Store check-in photos as JPEG files under a new userData/attendance-photos/<user_id>/ directory
(following the existing userData-rooted convention used for WhatsApp auth and DB backups), with
only the relative path + a SHA-256 hash in a new attendance_events table row. This needs an
explicit retention policy (e.g. configurable days-to-keep, default TBD) so photo storage
doesn't grow unbounded — per AGENTS.md's data-safety invariant, any automatic purge must be a
deliberate, documented, owner-configurable policy, not an ad hoc deletion.

3. On-device AI face match — Ship as a separate, later phase, not MVP. MVP captures the
photo as an audit artifact for manager review only (no automated matching) — this ships real value
immediately with zero new runtime dependencies, consistent with AGENTS.md's "reuse before adding"
invariant. A later phase can add local, on-device face-embedding matching (e.g. a small ONNX
model bundled and run fully offline, never a cloud face-recognition API) as an opt-in enhancement.
Flag: this would be FloCafe's first ML dependency and has real bundle-size implications — coordinate
with the bundle-size work in #417 before committing to a specific model/runtime.

4. Open-source vs. paid — The LAN QR check-in, local photo capture, and manager review MVP
should stay fully open-source and 100% offline, matching every other core-POS invariant in this
repo (no reason to treat attendance differently from orders/billing/KDS). Any commercial
packaging question only plausibly applies to the cloud variant (item 5) — and even there, the
monetizable surface is FloAdmin cloud hosting/verification, not the FloCafe client code, which
stays MIT like everything else in this repo. This is a business decision outside a code issue's
scope
— flagged under Open Questions below rather than decided here.

5. Cloud-supplied remote check-in (mobile app + geofencing) — Do not build new cloud
infrastructure for this. Reuse the existing FloAdmin/RevFlo pairing rail described in
docs/cloud-v2-plan.md (main/services/cloud-sync.ts, /api/mobile/pairing-code,
/api/pairing/redeem) so a store's already-paired RevFlo mobile app can submit GPS-tagged
check-ins without a second pairing flow. Per AGENTS.md's private-specs boundary, this repo issue
can only cover FloCafe's client-side contribution (submitting/consuming attendance events over
the existing paired-device channel, storing the store's registered lat/long + geofence radius
locally, surfacing results in the UI) — the FloAdmin-side geofence verification service and any
RevFlo UI work are out of scope here and belong in the private specs repo / FloAdmin's own
tracker. This phase must degrade gracefully offline exactly like every other optional network
feature (README.md:77-87): if the cloud is unreachable, remote check-in queues or fails
visibly, and on-site QR check-in keeps working regardless.

Disambiguation: Attendance vs. Shift

"Attendance" (this issue — employee clock-in/out) is a different concept from the open
"Shift" feature in #279 (cash-drawer open/close, cash float, business-day reconciliation — a POS
session, not a person's work hours). Naming risk: both could reasonably be called "shift." Use
"attendance" / "clock-in" / "clock-out" terminology throughout this feature to keep it distinct.
A future integration (e.g., a cashier can't open a POS shift before clocking in) is a reasonable
follow-up but is explicitly out of scope for both issues today.

Phases

  • Phase 1 (MVP, open-source, fully offline): attendance-server.ts on port 3004 following the
    KDS bootstrap pattern; attendance-info.ts QR/mDNS route; new attendance_events table
    (user_id, type [in/out], timestamp, photo_path, device_info); employee-facing
    check-in screen (scan own PIN or badge + camera capture); manager-facing review/report screen;
    on-disk photo storage with configurable retention.
  • Phase 2 (opt-in, still fully local): on-device face-embedding match against a reference photo
    captured at employee setup, surfaced as a confidence score for the manager rather than an
    auto-accept/reject gate. Coordinate model/runtime choice with perf(packaging): reduce production bundle size by scoping googleapis / @googleapis/drive #417.
  • Phase 3 (opt-in, cloud-gated): remote check-in via the paired RevFlo mobile app, GPS-tagged
    and matched against the store's registered geofence, over the existing FloAdmin pairing channel.
    Requires FloAdmin-side design work tracked outside this repo.

Non-Goals / What NOT to Do

  • Do not require internet connectivity for any Phase 1 functionality — core check-in must work
    fully offline like the rest of the POS.
  • Do not store attendance photos as Base64 blobs in flo.db (see Architectural Decision fix(windows): add Express path-rewriting middleware for Next.js static exports #2).
  • Do not send raw face images or biometric templates to any cloud service; Phase 2 face matching
    must run entirely on-device.
  • Do not conflate this feature with the cash-drawer "Shift" concept in Feature: Shift lifecycle management, cash drawer reconciliation, & business day close #279.
  • Do not design or implement any FloAdmin-side (server) geofencing logic in this repo — client
    only, per the private-specs boundary in AGENTS.md.
  • Do not decide the open-source-vs-paid question inside this issue's implementation — surface it
    to maintainers first (see Open Questions).

Open Questions for Maintainers (non-technical / business decisions)

  • Is Phase 3 (cloud/geofenced remote check-in) something FloAdmin should meter or gate behind
    a paid store plan, given it depends on FloAdmin-hosted verification rather than local compute?
  • Does RevFlo (existing companion mobile app) already have a natural home for an "attendance"
    tab, or does this need new RevFlo screens designed by that app's owners?
  • What's the default/configurable retention window for on-disk check-in photos?
  • Should Phase 2 face-match ship as a core feature or an optional downloadable model package
    (to avoid inflating the base installer for stores that never enable it)?

Acceptance Criteria

Phase 1

  • main/attendance-server.ts exports startAttendanceServer() / stopAttendanceServer() /
    getAttendancePort() / isAttendanceServerRunning(), wired into main/index.ts startup and
    the shutdown array, default port 3004 (env-overridable, matching KDS_PORT convention).
  • main/routes/attendance-info.ts returns mdns_url, ip_url, per-interface qr_data_url,
    gated by a new requireAttendanceEnabled middleware / isAttendanceEnabled() DB flag.
  • mDNS TXT record in startMdns() includes the attendance port.
  • New attendance_events table (migration) with user_id, type, timestamp, photo_path,
    device_info; photos stored under userData/attendance-photos/<user_id>/, never in SQLite.
  • Configurable retention policy purges photos/rows older than N days (owner-configurable,
    disabled/unlimited by default until an explicit decision is made).
  • Check-in/check-out works with zero network connectivity.
  • Manager-facing report screen lists check-ins/check-outs with photo review.

Phase 2 / 3: acceptance criteria to be defined in follow-up issues once Phase 1 ships and the
Open Questions above are resolved.

Verification

npm run lint
npm run build
npm test

Plus a manual offline check: disable networking, confirm Phase 1 check-in/check-out and photo
capture still work end-to-end.

Dependencies

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions