- Rework /fusion_clock/dashboard_data into a personal block (everyone)
plus a team block (team lead = direct reports, manager = org-wide).
A regular employee's payload never contains another employee's data.
- New OWL stacked layout: gradient KPI cards (Today/Week/OT/Streak),
Today's Shift, Recent Activity, Upcoming Leave, Recent Penalties; team
band adds Present/Absent/Late/Pending, roster, and Needs Attention.
- Dark/light via compile-time $o-webclient-color-scheme branching;
drop the old runtime html.o_dark dashboard block.
- Open the Dashboard menu to group_fusion_clock_user (lead/manager imply).
- Add HttpCase permission/no-leak tests. Bump 3.13.2 -> 3.14.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Attendance now groups the operational records: All Attendances, Leave
Requests, Correction Requests, Penalties (Leaves + Penalties moved in from
top level).
- Scheduling groups all schedule-building: Shift Planner, Scheduled Shifts,
Shifts (templates, moved from Configuration), Schedule Audit.
- Configuration: Settings, Locations, Enroll NFC Card (the NFC wizard moved in
from top level).
- Removed the duplicate top-level Locations menu (kept the one under Config).
Only parent/sequence changed; no actions/views touched. Live on entech 3.13.2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
iOS Safari date inputs have a large intrinsic min-width that can break a flex
row; switch .fclk-leave-daterange to grid 1fr 1fr + min-width:0 on the inputs
so the two fields always share the row and shrink. Also changes the bundle hash
to force iOS to drop the cached CSS. Live on entech 19.0.3.13.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Request Leave now takes a From/To date range instead of a single day (the To
field is optional -> single-day). Added date_to to fusion.clock.leave.request
(start kept as leave_date), with overlap detection on submit and a date_to >=
leave_date constraint. The absence check and reports now treat a leave as
covering its whole span. The form shows two date inputs; the controller accepts
date_from/date_to (the old single leave_date payload is still honoured). A
migration backfills date_to = leave_date for existing rows.
Live and verified on entech 19.0.3.13.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verified from the live DOM that fusion_plating_portal wraps the app in
#wrapwrap > main > .o_fp_portal_shell > .o_fp_portal_main > #wrap.o_portal_wrap
> .container. The white frame was .o_fp_portal_shell (+ .container max-width),
which my earlier wrapper-neutralisation didn't target. Add the shell + inner
main + force all wrappers transparent/full-width/no-padding under
body:has(.fclk-app). Live on entech 19.0.3.12.4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Odoo 19 silently ignores the legacy `_sql_constraints` list (repo CLAUDE.md
rule 9), so it never created a DB constraint — two employees could be assigned
the same x_fclk_nfc_card_uid and the NFC tap's search(limit=1) then picked an
arbitrary one. Replace it with a declarative models.UniqueIndex carrying a
partial WHERE predicate, so uniqueness is enforced only when a UID is set;
employees without a card keep sharing a blank/NULL value.
Makes test_nfc_models.TestNfcModels.test_card_uid_is_unique_when_set pass.
Verified on entech (DB admin): 0 pre-existing duplicate UIDs, full upgrade +
61/61 fusion_clock tests green, and the unique partial index
hr_employee_fclk_nfc_card_uid_unique now exists.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- White border on every portal page: the .fclk-app full-bleed relied on exact
negative margins to cancel the portal layout's container padding; when it
didn't match, the white page chrome showed through. Match the PAGE background
to the app (light #f3f4f6 / dark #0f1117, via body:has(.fclk-app)) so the
gutter is invisible, and clip horizontal overflow.
- Timesheets not responsive: the 6-column table crammed/wrapped on phones.
Replaced the table with stacked cards (date + net up top, in -> out, then
break / location / Correct) that read cleanly at any width. Correction-link
data attributes preserved; the xpath-inherited .fclk-nav-bar untouched.
Live on entech 19.0.3.12.2 (both rules verified in the served frontend bundle).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reminders, absence detection, late/early penalties, and auto-clock-out are now
driven by each employee's real schedule (posted planner entry -> recurring
shift), never the global 9-5 default. Employees who aren't scheduled get no
reminders/absence. Overtime past the scheduled end is never cut off — auto
clock-out only fires at a max-shift safety cap (default raised 12 -> 16h). Team
leads build the planner in draft and Post it (publishes + emails employees).
- hr.employee._get_fclk_day_plan: explicit `scheduled` flag; posted-only planner
entries (drafts ignored), else recurring shift covering that weekday, else
not-scheduled; sources 'schedule'/'shift'/'none'.
- fusion.clock.shift: day_mon..day_sun weekday pattern + covers_weekday().
- fusion.clock.schedule: draft/posted state + posted_date; planner edits reset
to draft; fclk_email_posted_week notification.
- Rewrote the reminder / absence / auto-clock-out crons: schedule-gated,
per-employee savepoints, OT-aware cap, weekend hardcode removed.
- Penalties + all three clock-in paths skip days the employee isn't scheduled.
- shift_planner: Post Week route + planner Post button + draft count.
- Migration backfills pre-existing schedule entries to 'posted' so they keep
driving automation after upgrade.
- Tests: resolver matrix, cron gating, OT cap; fixed the existing planner test
for the new state/source semantics.
Design: docs/superpowers/specs/2026-05-30-schedule-driven-attendance-design.md
Frontend footprint kept at zero to avoid colliding with the concurrent
employee-portal (payslips) work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The website module injects a fixed "frontend->backend" nav
(.o_frontend_to_backend_nav — the floating apps-grid/edit button) on every
frontend page for any internal user. Since the kiosk account is an internal
user, that button let a kiosk user tap through to the Odoo backend.
Hide it with a page-scoped inline style in the kiosk template head, so it's
suppressed only on /fusion_clock/kiosk/nfc and the real website keeps its nav.
Live as 19.0.3.11.8 (verified the rule is in the rendered template).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Privacy/space housekeeping for the kiosk verification selfies. A new daily cron
(_cron_fusion_wipe_old_photos) deletes the photo attachments on attendances
whose clock-in is older than fusion_clock.photo_retention_days (default 60).
Only the images are removed — attendance records, worked hours and penalties
are kept. Clearing the attachment-backed binary reclaims filestore space.
- Configurable in Settings → Fusion Clock → NFC Kiosk ("Auto-Wipe Photos After
(days)"); set 0 to disable.
- Wipes all three photo fields (NFC check-in/out + legacy portal photo),
batched with per-batch savepoints.
- tests/test_photo_retention.py covers wipe-old / keep-recent / retention=0.
Verified live on entech (19.0.3.11.7) via a rollback-only dry run: a 70-day
shift's photos were wiped (record + 8h hours preserved) while a 5-day shift's
photo was kept; nothing persisted. 0 attendances currently exceed 60 days.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The kiosk captures and stores a photo on every tap (x_fclk_check_in_photo /
x_fclk_check_out_photo on hr.attendance), but no view displayed those — the
form only showed the legacy portal field x_fclk_checkin_photo, so the NFC
photos were invisible in the UI. Add a "Verification Photos" group showing the
clock-in and clock-out photos (plus the legacy portal photo), each hidden when
empty. (The activity log has no image field — photos live on the attendance.)
Live as 19.0.3.11.6.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The result card showed x_fclk_net_hours = worked_hours − break − early-out
penalty minutes. Tapping out before the scheduled end adds a 15-min early-out
penalty to the break field, so short shifts clamped to 0 → "Worked 0h 0m".
Show GROSS attendance.worked_hours (the actual clock-in → clock-out elapsed
time) instead, and format adaptively (Xh Ym / Ym / Ys) so brief shifts and
quick tests don't all read 0. Net-of-deductions stays in the payroll reports.
Live as 19.0.3.11.5 (verified worked_hours computes correctly in the DB).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root-caused on live entech (not guessed):
- The kiosk runs as a non-HR operator (uid 141) who gets AccessError reading
hr.employee images, so /web/image served a placeholder. Point the result-card
avatar at hr.employee.public/avatar_128 — verified readable as the operator,
returns the real photo. (Odoo's own UI uses .public for employee images.)
- The Odoo profile/preferences avatar is res.users → res.partner.image_1920,
which the capture never wrote. Propagate the captured photo to the linked
user's partner image so the profile updates too.
- Enlarge the capture oval (it was small): stage 62vh/520px, guide width 64%.
Live as 19.0.3.11.4. Also backfilled the existing test photo to the user's
partner image so the profile shows it without re-capturing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Profile photo DID save (verified: image_1920 attachment persists); the
"doesn't update" was a browser-cache miss. Add ?unique=<write_date> to the
result-card avatar URL so a freshly-captured photo shows on clock in/out.
- Capture now starts a 10-second countdown (time to get into frame) then
auto-snaps; the button toggles to Cancel while counting.
- Face guide is now a VERTICAL oval (aspect-ratio 3/4) over a portrait stage —
it was rendering horizontal. Faces are taller than wide.
Deployed live to entech (LXC 111) as 19.0.3.11.3; frontend bundle verified to
compile clean and contain the new rules.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Odoo's Sass compiler evaluates the built-in min() function and errors with
"Incompatible units: 'px' and 'vw'" on `width: min(86vw, 380px)`, which broke
the entire web.assets_frontend bundle (kiosk + all portal pages unstyled).
Equivalent, compiler-safe: `width: 86vw; max-width: 380px;`.
Verified: forced a fresh frontend bundle compile on entech — no Incompatible
-units error, served CSS contains the compiled --pin rule. Live as 19.0.3.11.2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The --pin panel used width:auto, so in the centred flex overlay it
collapsed to its content width and crushed the 3-column numpad. Give it
a definite width (min(86vw, 380px)) and make the keys proper tappable
squares (min-height 60px, 1.6rem font).
Deployed live to entech (LXC 111) as 19.0.3.11.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NFC kiosk:
- Add "📷 Photo" action to every Manage-page employee row and to the
post-enroll result card, so a manager can set/replace a profile photo
at any time (previously only surfaced when the employee had no image).
- Slim the Manager PIN pad: dedicated --pin panel variant (max-width 360px,
reduced padding) with a tighter numpad, removing the oversized whitespace.
Deployed live to entech (LXC 111) as 19.0.3.11.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kiosk work across this session (19.0.3.6.0 -> 19.0.3.10.0):
- Program-from-unknown-tap: amber prompt -> Manager PIN -> pick/create employee
-> binds the captured UID (no re-tap). Reassign moves a card between employees.
- Manager page (gear, when unlocked): search employees + tag status; assign/re-tag,
clear tag, archive employee, + new employee. Server-gated by the enroll password.
- Screen lock: kiosk starts locked (tap-only); Unlock -> Manager PIN, Lock button;
PIN remembered for the session so the gear never re-prompts.
- Sounds: pleasant + loud sine chimes (rising in / descending out) + a low "denied"
tone for wrong/unknown taps. Gated by fusion_clock.enable_sounds.
- Guided profile-photo capture for employees with no picture (clock-in or enroll):
live camera + oval face guide -> capture -> preview -> save to hr.employee.
- PIN no longer re-renders per digit; centered result card; 12h time; clock-out shows
"Worked Xh Ym this shift"; modern clock idle icon; faster animations/result timers;
session keep-alive so the kiosk login never expires.
- New endpoints: create_employee, clear_tag, delete_employee (archive), verify_pin,
save_profile_photo; enroll gains force-reassign.
- Docs: fusion_clock is now developed in Claude Code (dropped Cursor references).
Spec/plan under fusion_clock/docs/superpowers/. Deployed live on entech
(odoo-entech / LXC 111 on pve-worker5), v19.0.3.10.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- PWA manifest on the NFC kiosk page so it installs as a full-screen
home-screen app (Chrome "Install" / Safari "Add to Home Screen").
- Dedicated "Kiosk Operator" permission + gated "Fusion Clock Kiosk"
top-level app (act_url -> /fusion_clock/kiosk/nfc). Kiosk controllers
accept Manager OR Kiosk Operator; all kiosk data ops already run sudo.
- Fix 403: read the company kiosk location via sudo on page-load and tap
(Kiosk Operator has no fusion.clock.location ACL).
- Odoo 19 permissions UX: ir.module.category + res.groups.privilege so
User/Team Lead/Manager and Kiosk Operator appear as application-access
dropdowns on the user form (no developer mode). Short group display names.
- Docs: note res.groups.privilege as the Odoo 19 category_id replacement.
Deployed live to entech (odoo-entech / LXC 111 on pve-worker5). v19.0.3.6.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
macOS keystroke injection from a CLI-launched Python hits multiple
TCC permission walls (Accessibility AND Automation, both attaching
to identities macOS often can't resolve cleanly). After bouncing
through Quartz, AppleScript, and pyautogui fallbacks, none of them
worked reliably in our test environment.
Switch to a proper IPC channel instead of pretending to be a
keyboard.
Daemon (wedge.py):
- Adds a ThreadingHTTPServer on 127.0.0.1:8765 exposing /events
- SSE stream pushes each detected UID as one event
- 30s keep-alive comments to keep idle connections open
- CORS: Access-Control-Allow-Origin: * (kiosk page may be on any
client-domain HTTPS origin; SSE source is always localhost)
- Keystroke injection kept as best-effort fallback for non-SSE
clients
Kiosk JS (fusion_clock_nfc_kiosk.js):
- Adds startWedgeSseListener() that opens EventSource to
http://localhost:8765/events on setup
- On message: same handleTap()/_onEnrollTap() flow as Web NFC + HID
- EventSource auto-reconnects; first error is logged then silenced
- http://localhost is a "potentially trustworthy origin" so this
works from https:// pages without mixed-content blocking
Result: ACR122U + wedge.py daemon now drives the kiosk with zero
macOS permission prompts and no focused-window dependency. Same
input plumbing as Web NFC and HID — penalty/photo/activity log
fire identically.
Bump fusion_clock to 19.0.3.3.0.
The NFC kiosk previously required Web NFC, which is Android-Chrome-only.
This blocked desktop testing and locked us to a single hardware path.
Add a keyboard-wedge listener that captures keystrokes from USB HID NFC
readers (the standard Sycreader/Yanzeo class). The listener buffers hex
chars + separators, flushes on Enter (or 600ms idle as fallback for
readers without a terminator), and routes the UID through the same
handleTap()/_onEnrollTap() codepath as Web NFC. Photo verification,
penalty calc, and activity logging all fire identically.
Make the setup button tolerant: try Web NFC, but treat its absence as
non-fatal. USB HID always activates. Only hard-fail when photoRequired
is True AND the camera is unavailable.
Result: same kiosk page now works on Android Chrome (Web NFC), desktop
Chrome with a USB reader, or both at once.
Bump manifest to 19.0.3.2.0.
Wizard was deployed without an entry in security/ir.model.access.csv,
so ANY user (including managers) got a permission error when opening
the menu. The model is registered but has no group access rules,
so Odoo's ORM blocks read/create on it.
Grant full CRUD on fusion.clock.nfc.enrollment.wizard to
group_fusion_clock_manager (the same group the menu is gated to).
Bump manifest to 19.0.3.1.1.
The Enroll NFC Card menu item references action_fusion_clock_nfc_enrollment_wizard,
which is defined in wizard/clock_nfc_enrollment_views.xml. With the wizard file
listed AFTER clock_menus.xml in the manifest, the menu load failed with
"External ID not found in the system" on first upgrade.
Move the wizard view above clock_menus.xml so the action XMLID exists by the
time the menu references it.
Verified on odoo-entech: fusion_clock upgraded cleanly to 19.0.3.1.0, all
wizard XMLIDs registered.
Adds a tap-driven enrollment workflow so managers can pair NFC/RFID
cards to employees using a USB HID reader at their desk:
- New wizard model fusion.clock.nfc.enrollment.wizard with auto-focused
Card UID field, employee picker, and reassignment warning if the
card is already held by someone else.
- Two actions: 'Enroll Card' (single) and 'Enroll & Next' (bulk).
- Menu entry under Fusion Clock root, manager-gated.
- Exposes x_fclk_nfc_card_uid on the Employee form Clock Settings
section (next to Kiosk PIN) so it can be inspected/edited directly.
- Bumps manifest to 19.0.3.1.0 for asset cache bust.
Wizard reuses FusionClockNfcKiosk._normalize_uid so stored format
matches what the kiosk /tap endpoint looks up later. Reassignment
clears the UID from the previous holder and logs both events to the
activity log under 'card_enrollment'.