Highlights
- A part-finished week no longer looks like a saving - Insights was comparing every shift in the period against only the hours already clocked, so mid-week always read as a large underspend, coloured green. It now compares against the shifts that have actually ended and says the period is still running.
- Past weeks keep the pay they were run at - scheduled cost used to be recalculated from today's hourly rate, so a pay rise quietly re-priced every rota you had already paid. Worked shifts now use the rate on their timesheet, and shifts are stamped with their rate when published.
- Late-night shifts land in the right week - a shift starting at 23:00 and clocked in after midnight used to count as scheduled in one week and worked in the next. Both sides now follow the shift, and every date on the page is read in your company's timezone rather than the server's.
- Open shifts are no longer invisible - unassigned shifts have no rate to cost, so they were silently missing from Insights while the rota still counted them. The page now says how many there are and how many hours they represent.
- Shift reminders run on a schedule - the "your shift starts soon" and "don't forget to clock in" nudges used to fire only while a manager happened to have the app open. They now run every five minutes on their own.
- A notification that fails is retried - a failed send used to disappear with no trace. It is now retried with a backoff and, if it still cannot be delivered, recorded and reported instead of lost.
- Turn off the duplicate email - if you get push notifications you can now stop the matching email from your profile. If you have no push notifications you keep receiving email, so you never miss a shift change.
- A phone layout that fits the screen - a tab bar at the bottom of every page, filters behind a single button, wide tables replaced by card lists on Timesheets and Insights, Back moved into the header, and empty dashboard cards collapsed to a single line.
Changes
Features
- feat(mobile): bottom tab bar with the sections for your role, a bottom-sheet filter control reused across Timesheets, Insights and Company costs, and Back moved into the header on a phone.
- feat(timesheets): card list instead of a 980px-wide table below
sm, with the same row actions. - feat(insights): card list with a sort control, a compact summary strip in place of three stacked cards, and labels explaining the saved range, the four-week trend and what "include pending" counts.
- feat(insights): staff with no activity in the period are listed as zero rows instead of being absent, and hours clocked against no shift are marked as unscheduled.
- feat(rota): shifts are stamped with the hourly rate they were published at, and re-stamped if a published shift moves to another day.
- feat(notifications): per-user email preference with a toggle on the profile page, retries with a backoff for failed sends, and republishing a week no longer re-notifies everyone who was already told.
- feat(dashboard): empty cards collapse to a single line that expands on tap, and a refresh control sits next to the title on a phone.
Fixes
- fix(insights): variance is measured against shifts that have ended while a period is still running; range boundaries, week numbers and labels use the company timezone; scheduled and actual hours are windowed on the same timestamp; open shifts are reported separately.
- fix(notifications): both scheduled jobs are registered, so reminders and queued notifications no longer depend on someone using the app; push to an unconfigured device is recorded as a stub rather than a delivery; emails escape the text typed into shift notes and cancellation reasons, and render times in the company timezone.
- fix(notifications): a notification that cannot be recorded no longer fails the action that raised it - creating a shift succeeds even if the notification does not.
- fix(mobile): touch targets meet the 44px minimum on touch screens, and a destructive Delete no longer sits a thumb-width from Edit on the costs list.
Maintenance
- chore(notifications): read and long-dead notification events are cleaned up on a schedule, unused push subscriptions are pruned, and the cron secret is compared in constant time.
- chore(db): repaired the migration snapshot chain, which had been unable to generate a migration since 0029.