Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
aa8da20
Time & pay foundation: PINs, breaks, trusted clock devices, schedule …
techmore Nov 6, 2026
48c302e
Shared glue for time & pay: TimeNotifier, push delivery stub, worker …
techmore Nov 6, 2026
0de4f54
Push notifications: APNs and Web Push delivery, device registration, …
techmore Nov 6, 2026
677f2ac
iPhone app 1.1.0: Time tab first, push registration with a soft ask, …
techmore Nov 6, 2026
ba9bc45
App Store 1.1.0 package, Spanish locale base, a11y audit script
techmore Nov 6, 2026
310fd6b
ios/README: Xcode project, Simulator run, UI test and push notes
techmore Nov 6, 2026
81fe307
DEPLOYS.md: PR #642 deploy receipt and live smoke test
techmore Nov 6, 2026
5a3af75
Merge branch 'tp/ios' into feat/time-scheduling-pay
techmore Nov 6, 2026
ff44086
Fix iOS merge: keep the locale methods, add the ios allowlist entries…
techmore Nov 6, 2026
307b935
iOS merge follow-ups: push subscribe lives on the notifications page;…
techmore Nov 6, 2026
3a751d6
Staff layout: hide the web brand row and build strip inside the iPhon…
techmore Nov 6, 2026
1e2c311
Time-off calendar: list semantics instead of a broken ARIA table (axe…
techmore Nov 6, 2026
693c42b
Screenshots: App Store set and final staff/manager captures (light an…
techmore Nov 6, 2026
886eef6
Changelog entry for time, scheduling and pay; App Store checklist: sc…
techmore Nov 6, 2026
a2e375e
REPORT.md: production status and what's left
techmore Nov 6, 2026
e7257a0
Staff pages: Sign out in the footer (reachable inside the iPhone app)…
techmore Nov 6, 2026
1ec5401
iPhone Simulator screenshots of the merged Time tab; UI test walks th…
techmore Nov 6, 2026
7cc626f
DEPLOYS.md: host space rules and backup policy from the owner
techmore Nov 6, 2026
e34446b
CI: document RUPERT_DEV_ORIGIN, exempt the UI-test runner variables; …
techmore Nov 6, 2026
138bdb3
Merge remote-tracking branch 'origin/main' into feat/time-scheduling-pay
techmore Nov 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- **Time, scheduling and pay for hourly staff (#640, #642, #644).** Staff clock in at the register or the counter tablet with a **4-digit PIN** (`/clock` kiosk and the Clock button on the POS strip; registered store devices only, or a signed-in phone), take **breaks and meals**, fix a forgotten clock-out in two taps, and see their day on **My time** (`/employee`): today's shift, this period's hours, Schedule, Time off and Pay. **Schedule**: managers build the week by drag and drop with conflict chips (time off, availability, hours, rest, minors, blackouts) and a labour-cost-vs-sales line, save templates, post the week to staff; staff pick up open shifts, give away or swap shifts with guardrails and manager approval. **Time off** in four taps with balances, accrual, blackout dates and notice. **Pay** shows hours net of unpaid breaks, estimated gross, **card tips** attributed from the Dejavoo terminal and settled against Authorize.net batches (owner's pool rule: shared evenly among everyone clocked in, admins excluded), declared **cash tips**, bonuses and the next payday; English and Spanish. Managers get **timesheets with exceptions first** and bulk approve, one **Approvals** inbox (punch fixes, time off, trades, timesheets), a **Tips** report with reconciliation, **Pay periods** with lock and a provider-agnostic payroll CSV, per-store **Time rules**, **Clock devices**, store-scoped roles, leave accrual and offboarding. **Push notifications** (APNs for the iPhone app, Web Push for the installed web app) for posted or changed schedules, decisions, trade offers, missed clock-outs and breaks, once `APNS_*`/`VAPID_*` keys are set. The iPhone app 1.1.0 opens on a **Time** tab and is packaged for App Store resubmission (`ios/AppStore/checklist.md`). Docs: `docs/time-and-pay/`.
- **Learning center: a reusable LMS, and its first course, “How our card payments work”.** **Me → Learning center** (`/learning-center`, every signed-in account, including staff in their own shell) lists each person's courses with a progress ring, required-by dates and earned badges. A course is lessons → quiz questions as workspace data (`lms_*` tables, seeded from `db/seeds/lms/*.yml` on first visit or `bin/rails lms:seed`), so the next course (HR policies, POS training) is a YAML file. Lessons are 2–4 minutes, with tap-to-define glossary words, callouts, step cards and responsive HTML diagrams (the life of a tap, who is who, where the fees go, tips, void vs refund, a dispute timeline, PCI scope, first moves when the terminal misbehaves). Each lesson ends with a quick check; every answer must be right, wrong ones show why, and attempts are kept. Finishing every lesson awards a printable badge. Progress saves on every view and answer. **Who takes it**: rules by role (required, due within N days) enrol people now and as accounts are added; managers with `hr.write` also assign named people, edit lessons and quizzes (with preview) and keep the **support contacts** shown inside the course current; **Team completion** (`hr.read`) shows everyone against every course with filters and a CSV. English UI strings have a Spanish translation file; content carries a locale. See `docs/lms/`.
- **Extra Dejavoo terminals, one per register or event.** Checkout setup → **Extra Dejavoo terminals**: add a terminal's TPN and SPIn auth key for a register name, **Check** it (a status lookup that cannot charge), and that register's card sales, refunds and batch close use it; other registers keep the main TPN. Each checkout remembers its terminal, so refunds go back to the same one. Retired terminals keep their keys for refunds of past sales. **Close terminal batch** closes every terminal's batch.
- **Dejavoo FEED: every Dejavoo transaction and batch reaches Rupert.** Point iPOSpays Settings → FEED at the store's POS webhook URL with Basic Authentication (API key `rupert`, secret = the **Dejavoo FEED key** in checkout setup). Rupert stores each sale, refund, void and closed batch (amounts, batch, card brand and last four; never card number, name, email, phone or card token), matches it to the Rupert sale, refund or online payment it belongs to, and lists the ones run outside Rupert in checkout setup. HMAC signatures are also accepted under either of Dejavoo's published field orders.
Expand Down
4 changes: 4 additions & 0 deletions deploy/ser8/tests/test_environment_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
# RUPERT_* identifiers that are not environment variables.
NOT_ENV = {
'RUPERT_SOURCES': 'Ruby constant in web/app/services/swipesimple_tender_match.rb',
# xcodebuild test-runner inputs for the iPhone UI tests (ios/RupertApp/UITests), never server settings.
'RUPERT_UI_EMAIL': 'ios/RupertApp/UITests/TimeFlowsUITests.swift',
'RUPERT_UI_PASSWORD': 'ios/RupertApp/UITests/TimeFlowsUITests.swift',
'RUPERT_UI_SHOTS': 'ios/RupertApp/UITests/TimeFlowsUITests.swift',
}
SKIP_SUFFIXES = ('.md', '.html', '.png', '.jpg', '.svg', '.ico', '.lock', '.pdf')

Expand Down
3 changes: 3 additions & 0 deletions docs/ops/environment-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ Two kinds of configuration exist. **Boot-time variables** live in `/etc/rupert.e
| --- | --- | --- |
| `RUPERT_BUZZ_ENABLED` | off | `1` turns on the optional Buzz module ([web/engines/buzz/README.md](../../web/engines/buzz/README.md)) |
| `RUPERT_EMPLOYEE_APP_ENABLED` | `0` in production, `1` elsewhere | `1` allows employee-app (staff mobile) sign-in; see [staff-app.md](../module-guides/staff-app.md) |
| `APNS_KEY_ID`, `APNS_TEAM_ID`, `APNS_KEY_P8`, `APNS_TOPIC`, `APNS_ENVIRONMENT` | unset (push off) | Push notifications to the Rupert iPhone app (`PushNotifier`, token-based APNs auth). `APNS_KEY_P8` is the contents of the App Store Connect `.p8` key (Keys → Apple Push Notifications service), `APNS_TOPIC` the bundle id `com.rupertpos.Rupert`, `APNS_ENVIRONMENT` `sandbox` for TestFlight/dev builds or `production`. All four of the first keys must be set for APNs to be used; the bell works without them. Platform-level: boot-time only, never a workspace setting |
| `RUPERT_DEV_ORIGIN` | unset | Development only, read by the iPhone app's DEBUG builds (`ios/Sources/Rupert/Instance.swift`): points the whole app at a local Rupert (`http://127.0.0.1:3005` in the `RupertApp (local)` Xcode scheme and the Simulator UI tests). Never set on a server; release builds ignore it |
| `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY`, `VAPID_SUBJECT` | unset (push off) | Web Push for the installed staff web app (`/employee`). Generate once with `bin/rails runner 'k = WebPush.generate_key; puts k.public_key, k.private_key'`; `VAPID_SUBJECT` is `mailto:` the support mailbox. Platform-level, boot-time only |
| `RUPERT_GOOGLE_OAUTH_HOST` | `HOST` in production; any host elsewhere | The one host whose Google callback is registered. Every Google sign-in and sign-up is sent there, so other hostnames and the iPhone app need no callback of their own. A bare hostname or a URL |
| `RUPERT_SWIPESIMPLE_INBOX` | unset (off) | Absolute directory that `SwipesimpleInboxJob` imports reports from; only for an explicit trusted producer, not for webpage sync. See [swipesimple-reporting.md](swipesimple-reporting.md) |
| `RUPERT_BACKUP_STATUS_DIR` | `web/storage/backups` | Where System → Backups reads the status JSON written by the backup scripts; must match their `STATUS_DIR` / `STATUS_PATH`. See [backups.md](backups.md) |
Expand Down
28 changes: 28 additions & 0 deletions docs/time-and-pay/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ to Square.
| Time off | 7 days notice requested (warning); blackout dates flagged, not blocked | Managers decide; the system informs. |
| Sick accrual | Off (0 per hour) | Most state laws use 1 h per 30 h worked; the state presets set that. |
| Vacation | 80 h/year grant; sick 40 h/year; carry-over cap 40 h | Matches the existing `LeaveBalance` defaults. The grant lands on **January 1** (the first `LeaveAccrualJob` run of the year), not the hire anniversary, so every balance resets on the same day as the leave year. On that run last year's unused vacation and sick hours carry over up to the cap; the excess is written to the ledger as a `carryover_cap` line so nothing disappears silently. Sick hours earned on hours worked are credited daily from paid clock hours since the last run (`employees.sick_accrued_through`). Every change is a `People::LeaveAccrual` line visible on the staff member's page. |

| Tips | Pool, admins excluded, per store, cash tips declared by the worker at clock-out | Owner rule plus a declared-cash step so the record is complete for tax reporting. |

## Card tip attribution (`Tips::Attribution`)
Expand Down Expand Up @@ -111,3 +112,30 @@ All new screens: 44 px targets, Dynamic Type friendly (rem sizes), VoiceOver lab
contrast checked with `bin/check-palette-contrast`, dark mode via tokens. Spanish strings for the
employee-facing clock, time-off and pay screens live in `config/locales/es.yml`; a worker's `locale`
is set from their profile.

## App Store (iPhone app 1.1.0)

- **Time is the first tab.** Hourly staff open the app to clock in; `/employee` is the tab's root.
Search left the tab bar (five is iOS's limit before a "More" tab) and stays in every page's top bar.
- **Push notifications are opt-in with a soft ask.** After the first signed-in page the app explains
the five kinds in its own words, then shows the system prompt. "Not now" is remembered; iOS
Settings owns the switch afterwards. Every kind can be muted at `/employee/notifications/settings`
(`User#notification_prefs`), which `TimeNotifier` honours for the bell and push alike.
- **APNs and VAPID keys are platform secrets**, read from `/etc/rupert.env` only (never a
workspace's managed settings): one app bundle serves every workspace. Until they are set, the
bell carries every message and nothing is enqueued (`PushNotifier.configured?`).
- **A push payload is data.** The app opens only the `url` field, and only when it resolves to a
Rupert host (`PushRegistration.url(in:)`); nothing else in the payload is acted on.
- **Sign in with Apple is not added.** Guideline 4.8 applies to apps that use a third-party login
to *set up or authenticate the primary account*. Rupert accounts are created by the business
(email/password, Rupert's own system); Google is an alternative way to sign in to that same
account and sign-up is hidden in the app. Apple's stated exceptions ("exclusively uses your
company's own account setup and sign-in systems", enterprise accounts) cover this. If App Review
disagrees, the fallback is to add Sign in with Apple as a second alternative login rather than
argue twice; this is recorded in `ios/AppStore/checklist.md` item 19.
- **Account deletion covers staff accounts.** The `employee` role could not reach
`/account/delete` before (allowlist); it can now, and deletion also removes push tokens and the
clock PIN while keeping the worker's hours for the business (FLSA retention).
- **Session awareness in every layout.** The app watches `<meta name="rupert-session">` to restart
its tabs after a sign-in; the employee and POS layouts now carry it, so a sign-in on the Time tab
no longer leaves the other tabs on the sign-in page.
24 changes: 24 additions & 0 deletions docs/time-and-pay/DEPLOYS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,27 @@ Production: Incus container `rupert` on SER8 (`ssh ser8`, then `incus exec ruper
- Post-deploy: checkout clean (no schema.rb drift this time), `rupert-logs errors` empty for the window, `rupert-web`
and `rupert-jobs` active, `/login` and `/up` 200 (staff pages redirect to sign-in as expected). No user-facing
change in this deploy.

## 2026-11-06 14:12 UTC — PR #642 clock, scheduling, tips, manager (`eb246e60`) — deployed, healthy

- Pre-deploy: manual `sqlite3 .backup` to `/var/backups/rupert-manual/production-20261106-140603-pre-pr642.sqlite3`
(sha256 `756b98a1…`), integrity ok; deployer backup `production-20261106-141146-b7fdbf63-09c7fa9af6f4b569.sqlite3`.
- Install: autodeploy, 1 additive migration (`pay_period_adjustments`, `leave_accruals`, two employee columns), `/up` healthy,
smoke passed, `X-Rupert-Build: eb246e60`, checkout clean afterwards (the FEED index name in `schema.rb` now matches what
migrate generates, so the Nov 3 drift cannot recur).
- Live smoke test (Playwright against https://herbalhealers.rupertpos.com) with two temporary accounts created in the
workspace for the test (`smoke-staff@`, `smoke-manager@rupertpos.com`): staff **clock in → break → end break → clock out**
on `/employee` (EST times shown), `/employee/schedule`, `/employee/time-off`, `/employee/pay`, `/time_clock` all render;
manager `/people/approvals`, `/people/timesheets`, `/people/schedule` render. `/people/tips`, `/people/pay_periods`,
`/settings/time_rules`, `/people/clock_devices` refused the `manager` role as designed (payroll/settings/hr.write are
admin permissions). `rupert-logs errors` empty for the window. The smoke punch and break were deleted, the smoke worker
terminated and both accounts deactivated (owner may delete them from Accounts).
- Host disk: 99% used (15 GB free) on SER8's root volume; the deployer keeps five 300 MB backups. Worth a cleanup soon.

## 2026-11-06 15:00 UTC — host note (owner)

The owner's session freed 49 GB on SER8 (root volume now 93%, ~64 GB free) and set a keep-newest-10 retention on
`rupert-model-audit`. Rules from here on: check `df -h /` before any deploy or backup and do not deploy under ~25 GB free;
create no large artifacts on SER8; rely on the deployer's own pre-deploy backup (verified after the install with
`PRAGMA integrity_check`) instead of extra manual copies. The two manual copies taken earlier today were removed. Deploys
are serialized by the updater's own `flock` on `/run/rupert-autodeploy.lock` and always come from `origin/main`.
37 changes: 34 additions & 3 deletions docs/time-and-pay/REPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,27 @@ clocked-in staff at that store/time; owner's pool rule, admins excluded, whole c
Authorize.net settled transactions and Dejavoo batch close, mirrors refunds/voids/adjustments as negative rows and carries money that
settles after a lock into the next open period.

## Production status
## Production status (SER8, herbalhealers.rupertpos.com)

_(see DEPLOYS.md)_
Three PRs merged to `main` and installed by the autodeployer, each preceded by a verified SQLite backup and
followed by a live smoke test; details and receipts in [DEPLOYS.md](DEPLOYS.md).

| PR | Build | What | Result |
| --- | --- | --- | --- |
| #640 | `b7fdbf63` | Foundation (3 additive migrations) | Healthy |
| #642 | `eb246e60` | Clock, scheduling, tips, manager pages (1 additive migration) | Healthy; live clock in → break → clock out verified with a temporary worker, then cleaned up |
| #644 | see DEPLOYS.md | Push notifications, iPhone 1.1.0, App Store package | see DEPLOYS.md |

Operational findings the owner should act on:

1. **Encrypted backups have been failing** since at least Nov 1 (no active backup key). Create the key at
**Settings → Backups** and keep the recovery kit. Until then only the deployer's pre-deploy copies and the
manual copies in `/var/backups/rupert-manual` exist.
2. **Push needs keys**: `APNS_KEY_ID`, `APNS_TEAM_ID`, `APNS_KEY_P8`, `APNS_TOPIC`, `APNS_ENVIRONMENT` (and optionally
`VAPID_*`) in `/etc/rupert.env`; the bell works without them.
3. **SER8 root volume is 99% full** (15 GB free).
4. Pick the stores' **state preset** at Settings → Time rules (defaults are federal) and register each register browser
or counter tablet at **Team → Clock devices** so PIN punches work without a sign-in.

## Payroll provider recommendation

Expand Down Expand Up @@ -99,4 +117,17 @@ Sources: [Homebase: best payroll providers 2026](https://wf.joinhomebase.com/blo

## What's left

_(filled in at the end)_
- **Owner clicks**: App Store Connect submission (checklist item 22), Push Notifications capability and APNs key,
backup key, time-rules preset, clock-device registration, payroll provider sign-up.
- **Live tip attribution has no real data yet**: the Dejavoo FEED and Authorize.net reporting connectors are configured
per the payments docs but the terminal programme is still in sandbox (`docs/integrations/dejavoo-spin.md`); the first
live batches will exercise `Tips::Reconciliation` for real. Watch **Team → Tips** for unassigned rows that first week.
- **Clerk ids on the terminal**: for the `performedBy` evidence path, staff must key the same number on the terminal as
`employees.employee_number`; otherwise attribution falls back to the checkout cashier or clocked-in staff (still correct
under the pool rule).
- **Photo capture at the clock** is designed but off (privacy); GPS is deliberately not used.
- **Expo staff app** (`mobile/`) keeps working against the extended API but gets no new screens; the iPhone app is the one
being shipped.
- Pre-existing flaky test `TeamChatRestartTest#SIGKILL…` fails under full-suite load and passes alone; unrelated to this work.
- Nice-to-haves noted during review: a plain `‹ period ›` stepper on the Pay page (today it is a date field + Apply, which
works but is one tap more), and a "Next thing to do" item for pending approvals (NextThings only reads MCP tools today).
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/time-and-pay/screenshots/ios-08-pay.png
1 change: 1 addition & 0 deletions ios/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ xtool/
*.ipa
.bsp/
.swiftpm/
RupertApp/RupertApp.xcodeproj/
Loading
Loading