Time Off Schedule™
Give your team the opportunity to book time off that works with your business — not against it. Team leaders are notified via text message when time off has been requested. Team members are notified via text message when their time off has been approved or denied.
-
Nov 5, 2026
Fix account settings not saving on evented accounts
UpdateAccount built each settings event from the folded getters after save. Those getters read the event log, which still held the old value, so every save re-recorded it. Diff the new values against the folded values captured before assign instead. This also…
-
Oct 20, 2026
Replace the Remetric SDK with a first-party tracker
Events now post to the app's own /track endpoint, form-encoded so the request stays a CORS simple request and needs no preflight. The script captures the inbound campaign once per session and sends it on every event, so a conversion stays attributable to the…
-
Oct 20, 2026
Stop reading the beacon's own Referer as the visitor's referrer
On a tracking beacon the Referer header is the page that fired it, never where the visitor came from, so every row recorded as a referral from our own marketing site and the direct bucket never filled. The page already reports the real referrer and already…
-
Oct 20, 2026
Keep marketing analytics in our own table instead of Remetric
Remetric is a nonprofit engagement platform: SMS, email, postcards and contact timelines. It is the wrong shape for the only question the ads budget needs answered, which is which source produced a paying center. That question is a GROUP BY, so this replaces…
-
Oct 20, 2026
Upgrade ESLint from 6 to 10 and clear the peer-dependency warning
ESLint was the last part of the JavaScript toolchain still on 2019 versions, and its dead plugins caused the YN0086 "Some peer dependencies are incorrectly met" warning on every `yarn install` and every Heroku build. ESLint had never actually run here.…
-
Oct 20, 2026
Move the JavaScript toolchain to Yarn 4
Yarn 1.22.22 was the final Yarn Classic release (March 2024), so the version we pinned last is pinned to a dead line. Move to Yarn 4.18.1, resolved from the `packageManager` field by corepack. Keep a real node_modules tree (`nodeLinker: node-modules` in…
-
Oct 20, 2026
Pin the JavaScript toolchain for the Heroku build
Production installed an unpinned Node and Yarn on every build, and warned about both. `engines.node` was a range, so it pinned nothing. Local dev ran Node 22 while production ran Node 24. Pin Node to 24.19.0 and Yarn to 1.22.22. Add `.node-version` so asdf…
-
Oct 20, 2026
Upgrade Ruby to 4.0.7
Heroku flagged 4.0.5 as behind the current patch of the minor release. 4.0.7 carries the security and bug fixes since; no gem versions move with it.
-
Oct 20, 2026
List cancelled requests under the Cancelled chip only
Upcoming and Past applied no state cut, so they listed cancelled requests as if the time off were still being taken. The Approved chip kept them too: approving a cancellation does not clear approved_at, so approved? stays true for the rest of the request's…
-
Oct 20, 2026
Take cancelled requests out of the Pending list
A request cancelled while still pending carried two labels at once: pending? only looked at approved_at/denied_at, so the request listed under both the Cancelled and the Pending filter chips, and the imminent-request reminder still nagged approvers to decide…
-
Sep 9, 2026
Give the ratio calculator its own page and promote both lead magnets
The calculator was buried in one blog post with a single inbound link, and the playbook only reached people already on the site. Both are free, ungated, and the sort of thing directors send each other. - Extract the widget into _includes/ratio-calculator.html…
-
Sep 9, 2026
Add the missing honeypot field to both contact forms
ContactsController#create silently accepts and drops any submission with `website` present, but neither published form posts that field, so the check has never fired. Both forms build their POST body key by key rather than from FormData, so the input alone…
-
Sep 9, 2026
Drop the phone number; support runs in writing
The number is gone from the business. It was still on the support hero, in both meta descriptions, in the privacy contact section, twice in llms.txt, in both forms' network-error message, and in the Organization contactPoint in _includes/schema.html, which…
-
Sep 9, 2026
Price per center, not per organization
$99/month meters a center, so "for your whole team" and "for the entire organisation" both undersell a chain: four centers is $396, not $99. The unit changed; the number did not. Both machine-readable price assertions are hand-written mirrors of the visible…
-
Sep 9, 2026
Learn from Stripe when a trial converts and when a customer lapses
Nothing in the app has ever listened to Stripe, which left three things invisible. Trial-to-paid conversion read a structural 0%, because only signup ever fires 'paid' and a card-free trial never pays at signup. A customer who lapsed after being verified…
-
Sep 9, 2026
Remember where a signup came from, across the Stripe hop
Nothing recorded the source of a signup, so ad spend would have bought traffic and taught nothing. Every query parameter on a marketing link died at the Stripe Checkout redirect: #checkout passed no metadata and built success_url out of session_id alone. GET…
-
Sep 9, 2026
Pause the card-free trial at day 30 instead of cancelling it
A canceled Stripe subscription is terminal: it cannot be reactivated or paid, only replaced. Cancelling at the end of a card-free trial therefore left a director who wanted to buy with nothing to buy — the locked screen sent them to the Billing Portal, which…
-
Sep 9, 2026
Refuse a time-off request that names no real person
An admin files on behalf of a team member, so person_id arrives from the form rather than the session; a member's is forced to their own id. A POST that carried no person_id emitted request.requested with person_id: nil, and every collection read keys on…
-
Sep 8, 2026
Raise instead of denying when an access rule itself breaks
`has_access?` rescued everything to false, so a rule that raised was indistinguishable from a user who legitimately lacked access. That is how the calendar's "+ Add request" gate stayed dead from 2023 to now: the warn line was there, but nobody reads warn…
-
Sep 8, 2026
Show approvers the calendar's "+ Add request" button
The calendar's action buttons live in the layout's `action_name == "calendar"` branch. The request button probed `has_access?(:Person, :show)` — an identity rule (`current_user.id == resource.id`) asked with no record, so `Symbol#id` raised and…
-
Sep 8, 2026
Seed Vacation with one labelled example quota
With no counter rule anywhere, the availability engine returns nil days remaining and a new account's profile page renders no counter at all, so a trial director never sees the accrual tracking that separates this from a spreadsheet. Vacation now ships one…
-
Sep 8, 2026
Give a new account starter leave categories at signup
A time-off request must name a category and a fresh account folded none, so a director who signed up landed in an app that could not take a single request until they hand-built one. Signup now seeds Vacation, Sick leave and Family responsibility before it…
-
Sep 8, 2026
Point an empty account at its next step instead of a bare table
A new tenant lands on the calendar with nothing in the account, and the team and category lists rendered a table header over nothing. Each of the three now names the next action and links to it, gated by the same has_access? probes the surrounding views use,…
-
Sep 8, 2026
Separate the trial signup event from the paid one, and match the copy
A card-free signup was firing Remetric's `paid`, so every trial counted as a conversion. Stripe's checkout payment_status already draws the line, so branch on it: 'paid' keeps meaning money changed hands, a card-free trial gets 'trial_started'. Both now carry…
-
Sep 8, 2026
Give every new account a 30-day trial with no credit card
The marketing site has promised a 30-day free trial with no card since launch, but checkout charged $99 on the spot: the Checkout session asked for no trial, and the return leg rejected anything Stripe did not report as 'paid'. Those two are a double-bind —…