Bug fixes
- The audit log recorded the wrong IP address. Depending on which part of the product wrote
the entry, the IP column held one of three things. Every document action — viewing, signing,
downloading, sending — recorded
172.18.0.6, an internal address belonging to our own infrastructure. Sign-ins, leave changes and settings changes recorded two addresses joined by a comma: the device and the network edge it passed through. Only a handful of entries recorded the device alone. Every entry now records one address: the public address of the device that made the request. This includes the IP on the e-signature audit trail and signing certificate. - The recorded IP could be set by the person using the app. Where two addresses were stored, the first was taken from a header the browser sends, so anyone could put any address into the audit entries for their own actions, including two-factor and password-policy changes. The address is now read only from hops we operate or trust, and a header from anywhere else is ignored.
- Security violations could be written into another organization. The endpoint the browser calls when it detects a tampered session took the organization and the user from the request itself, so any signed-in user could add violation records — naming anyone — to any organization's security log. They now come from the signed-in session only, and a browser can report only the kinds of tampering a browser can actually see.
- Connection errors could show any text. When connecting QuickBooks, Google Calendar or Outlook failed, the settings page displayed whatever the link said, so a crafted link could show an administrator any sentence as if it came from the product. These pages now show only their own messages, and the provider's error text is no longer put in the address bar.
- Personal data in the server log. Each load of the Dashboard wrote every employee's name and leave balance to the application log, along with step-by-step detail of leave calculations. The log now records identifiers and counts, never names, balances, emails or document details.
- The Time report opened empty from May onwards. It defaulted to "this year", and the time entry list serves at most 120 days at a time, so once the year was past 120 days old every visit showed "No time entries found". It now opens on the last 30 days, offers All time, 30 days and This quarter, and a longer custom range says so instead of showing an empty table.
- "View Project" in the Projects report led to a page that does not exist. It now opens the project.
- Report dates were a day early west of UTC. Leave start and end dates, expense dates and the date range shown on PDF exports were printed in the browser's timezone, so a leave starting 17 August read 16 August in Vancouver. They now show the stored day, as the calendar and approvals do.
- Quick date ranges picked the wrong day. "30 days", "This year", "This quarter", each report's default range and the Leave Distribution card worked from the UTC day, or from local midnight read back in UTC, so in the evening west of UTC — or any time east of it — they could start or end a day out, and "This year" could start on 31 December. They now follow the viewer's own timezone, or the organization's when they have not set one.
- Confirmations and error messages were never shown. Hundreds of places in the app raise a message — "Leave request approved", "Could not save", "You can only create leave requests for yourself" — but nothing drew them, so every one was silent. They now appear in the corner of the screen, errors stay a little longer, and hovering a message keeps it open.
- Report CSV files could break or run formulas. A reason or note containing a double quote
split its row, and a value starting with
=,+,-or@was run as a formula when the file was opened in a spreadsheet. Every report CSV, including the export preview grids, now keeps such values as plain text. - An employee opening a report address briefly loaded it. The page sent its report requests (which the server refused) before redirecting. It now redirects without loading anything.
- "Export in Excel" on Projects led to a page that does not exist. It now opens the projects export preview, whose Back link returns to Projects.
- Export preview filters did nothing. The column filters on the leave, time, expense, project and scheduling export previews asked for a filter only the paid edition of the grid provides, so the grid dropped them. They now filter.
- The export preview kept working after you left it. It went on resizing a grid that no longer existed, filling the console with warnings.
- Employees could open the report export previews by address. The leave, time, expense and scheduling previews now send an employee to the dashboard before loading anything, as the reports themselves do. What the server returned was always limited to their own records.
- Trip dates on an expense claim were a day early west of UTC. A claim's date range was read in the browser's timezone, so a trip from 6 to 9 April read 5 to 8 April in Vancouver. It now shows the stored days.
- A receipt dropped onto the scan area was ignored if it was a PDF. Picking the same PDF with the file browser worked. Dropping now accepts every format the file browser does: JPEG, PNG, GIF, WebP and PDF.
- "View all" on a claim's Overview left you looking at blank space. The links sit below the fold, and the shorter tab they opened kept the old scroll position. The tab bar is now brought back into view, and a press on the tab bar itself still keeps the page still.
- Opening a claim at a particular tab showed the Overview first. A link to a claim's Purchases, Per Diem, Travel or Receipts tab drew the Overview for a moment before switching. It now opens straight on the tab asked for.
- The approver had no photo. The approver on a claim was shown with initials even when they had a profile photo. Their photo now appears, as the submitter's does.
- "You" marked the wrong recipient on a document. On a document's page, the badge went to whoever's copy was open rather than the person viewing it, so an administrator opening Noah's copy saw "Noah Brown · You". It now marks the signed-in person.
- People still showed as initials in several places. A profile's Manager card and Direct Reports (both in the side panel and on the Job tab), its 1:1s and goal comments, department managers on the Departments settings page, project task assignees and the Billing breakdown, document recipients in the list and on the document page, and the recipient search when adding a document all drew initials even for people with a photo. They now show the photo. Where the circle's colour meant something (a recipient's signed, declined or pending state), it is kept as a ring around the photo.
- Document names showed "&" and "'". Document titles and descriptions, template names, signing messages and decline reasons were saved with symbols turned into HTML codes, so "Smith & Co - O'Brien Offer" read as "Smith & Co - O'Brien Offer" in email subjects and plain-text emails, notifications in the app and on phones, Slack, the signing certificate and audit-trail PDFs, and the document lists on the web and in the mobile app. Renaming a document added another layer each time. Text is now saved as typed, and existing documents and notifications are corrected.
- The account-deletion email's plain-text version showed HTML codes for names or organization names containing an ampersand or quotes. It now reads as written.
- A "$&" in a title or message could break an email. Text containing it was read as a replacement instruction when the email was assembled. Every email now inserts its content exactly.
- Names and titles in some emails were read as page markup. Onboarding and offboarding, training, certification, visa, carry-over, domain-expiry, one-on-one and review-cycle emails put names, course and task titles, task descriptions, agenda items and cycle names into the email without escaping them, so text containing markup — a link, say — became live markup in a message sent from our domain. Those values are now shown exactly as typed.
- Document list exports kept spreadsheet formulas live. A title beginning with "=" was run as a formula when the CSV was opened. The Documents list, Action Required and the document export preview now keep such values as plain text, as the report exports do.
- Overdue signing reminders showed raw code. The due date in an overdue document reminder
email read
<span style="color:#dc2626;font-weight:600;">Thursday, October 8, 2026 (OVERDUE)</span>instead of the date in red. It now shows the date, marked overdue, in red. Due-soon and follow-up reminders were not affected. - A document could be seen before it reached you. When a document was sent for signature in order — management signing a bonus notice or salary letter first, then the person it is about — that person's home page listed it under "Waiting on someone else", with a link, before anyone ahead of them had signed. The document's file and its signing layout could also be opened by that link, because being the person a document is about let them through. A signer whose turn has not come now sees nothing of the document — not on the home page, not in their documents or the expiring list, not in the download-everything archive, not by link — until it reaches them. Administrators and executives, who can see every document, still see it.
- A department head could tick off a colleague's onboarding task. On a new hire's checklist, the department head could mark complete a task assigned to someone else — "Alarm Code", say, assigned to HR — recording the work as done by somebody who never did it. A task assigned to a person is now theirs to complete (administrators and executives can still complete any task); the department head completes only the unassigned tasks and their own, whatever else the request changes at the same time. The button no longer appears for anyone else, who instead sees who the task belongs to.
- A new joiner with no Reports To sat at the top of the org chart. They now appear under the head of their department until Reports To is filled in — the Department Head set for the department in Settings, or, when none is set there, the one person in the department with the Department Head role. Every Reports To line is still drawn first, so the head of the organization stays at the top even when they belong to a department run by one of their own reports.
- Anyone could read anyone's equipment list by address. The assets on a profile were returned to any signed-in member of the organization who asked for them. They are now visible to the person themselves, their department head, and administrators and executives.
- A department head by role was not the department's manager. The Department Head role (on a person) and the department's Manager (Settings → Departments) were set in different places and never linked, so a department could have a head by role and no manager — and every email and reminder addressed to "the department head" (leave, expenses, time entries, onboarding, probation) went to nobody. The first Department Head added to a department with no manager now becomes its manager, and a one-off update fills existing departments that have no manager and exactly one Department Head. An existing manager is never replaced automatically.
- Department heads could add users through the API. The Add User button was only ever shown to administrators and executives, but the API also accepted department heads — at any non-admin role, in any department. Only administrators and executives can add users now.
- A user could be filed under another organization's department. The department sent with Add User or Edit User, and the manager sent with Edit User, were not checked; both must now belong to your organization.
- Every department head was told about every time entry. Time-entry notifications went to all department heads in the organization, whoever the employee was. They now go to the heads of the employee's own department, plus administrators and executives.
- Department heads could reach people outside their departments in a few places. Each is now
limited to the departments they act for:
- the clock-in status of any employee (
/time-tracking); - re-sending the signing emails of any document;
- schedules: copying a hand-picked list of anyone's shifts, reassigning a shift to someone outside their departments, editing or deleting another department's open shifts, and — for a head with no department — reading or editing anyone's schedule;
- the monthly birthday email, which sent a head with no department the whole organization's birthdays;
- the Dashboard under the "department only" privacy setting, which showed such a head everyone.
- the clock-in status of any employee (
- Pending approvals could show an approver their own request. The cached list was shared between approvers who see the same departments, although each list leaves out the viewer's own requests; it is now cached per viewer.
- A leave balance or booking could name another organization's employee. The person was looked up by id alone; it must now be in your organization.
- Leftover department managers. Under the old rule a manager was always a member of the department they managed, and nothing cleared the Manager field when they later moved. Because a Manager now extends a department head's access, every leftover — a manager who is no longer a member of that department — is cleared once on upgrade, and the department falls to its single Department Head if it has one. Check Settings → Departments afterwards and set any manager you want. A department's manager must now be an administrator, executive or department head.
Improvements
- A birthday greeting. On their birthday, each person receives a short greeting — in the app and, on the mobile app, as a push notification — once their own local time reaches 9:00, so everyone is greeted on their own morning. It uses the timezone on their profile, or the organization's when they have not set one, is sent once a year, and is signed by BookYourPTO or by the organization's own name under white-label branding. It can be turned off under Settings → Notifications.
- One structured server log. Every line is a single JSON record with a level, a request id,
the route, and the signed-in user and organization ids, so one request's activity can be found
and followed. Each response returns its request id in an
X-Request-Idheader, so a support conversation can quote it. A mistyped password is no longer logged as a crash with a stack trace, health checks no longer fill the log, and the level and format are set withLOG_LEVELandLOG_FORMATwithout a redeploy. - Tokens never reach the access log. Query strings are not logged, and the public signing, verification and calendar-feed links are masked.
- nginx records the real visitor. Behind Cloudflare, nginx now resolves the visitor's address from Cloudflare's own header, accepted only from Cloudflare's published ranges.
- The public demo explains itself. Settings pages whose changes the demo refuses now carry a "Disabled in demo mode" label. Receipt scanning stays available on the demo, limited to five scans per visitor per day. Connecting a personal calendar to the shared demo login is refused.
- Provider errors stay out of the log. QuickBooks explains a failed sync in words that can name employees and vendors. Administrators still see that explanation in the sync history; the server log records only the error code and status.
- Details at a glance in Reports. Clicking a row in the Leave, Time or Scheduling report — or choosing View Details — opens a read-only panel on the right with everything about that leave request, time entry or shift, and a link to where it is reviewed or edited. Previously "View Details" left the report for the calendar, and "View Entry" / "View Schedule" opened the general timesheets or schedule page with no trace of the row clicked.
- Pages no longer jump. Switching tabs or views (an expense's tabs, a profile's tabs, the documents sections, report views) keeps the page where it is instead of jumping to the top or shaking as content loads. Pages that loaded in stages — Reports, Expenses, Home, Calendar, Documents, Add a document — now draw in place instead of shifting down as each part arrives.
- Paging returns you to the list. Pressing Next or Previous at the bottom of a list (People, approvals queues, reports, expenses, documents, settings logs and more) brings the new page into view instead of leaving you at the bottom of it. Page buttons are now labelled for screen readers.
- Phone layouts. Report filters sit in tidy equal-width cells on a phone, and the Documents section buttons scroll sideways rather than wrapping onto a second line.
- A redesigned expense claim page. The claim opens on a single header card: the title and
claim number, a large status stamp with one line saying where the claim stands ("Awaiting
...", "Approved by ... · date", "Paid on date by payment method"), the claim's details (submitter, dates, purpose,
departure and return times, duration, currency) in one panel, and the balance due — or total
paid — beside the actions. Less common actions (Edit Details, Export Excel, Cancel Claim,
Delete Claim) sit under More, and the buttons match the size used in toolbars across the
app.
- Tabs. Overview, Purchases, Per Diem, Travel and Receipts are shown as a sliding pill bar, the same control used on Security & compliance and Add a document, with the number of items on each tab.
- Overview. A strip of section totals (purchases, per diem, travel), each opening its tab; the claim's details beside its activity; and the most recent items, with View all.
- Purchases, Per Diem and Travel are tables laid out like the expense claims list, with a totals row. Per Diem shows one row per day with breakfast, lunch, dinner and incidentals in their own columns. Each row's actions are in the same ... menu as the claims list.
- Item details open in a panel on the right instead of a dialog in the middle of the screen, with an Edit button for anyone who may change the item.
- Receipts (administrators and executives) open as a table by default, with a preview of each receipt on hover or keyboard focus. The thumbnail grid is one click away, and the choice is remembered.
- Loading. While a claim loads, the page shows a placeholder shaped like the claim and the tab being opened — a table for Purchases, Per Diem and Travel, a grid for Receipts — so nothing jumps when it arrives.
- The page now uses the full width of the window.
- Choose the columns on the expense claims list. A Columns menu beside Date Range shows or hides any column in the table. Expense No. is hidden by default; every other column is shown. The choice is remembered in the browser across refreshes, Reset to default restores it, and the last visible column cannot be hidden. A search still finds a claim by its number while the column is hidden.
- Choose exactly who holds a company card. Company card claims used to be open either to administrators and executives only, or to everyone. With Allow All Roles off, Settings → Expenses now has a Card Holders list: add the specific people who carry a card, and they — alongside administrators and executives — can create company card claims while everyone else sees only reimbursement claims. Only active people in the organization can be added, removing someone stops new company card claims without touching ones already filed, and every change to the list is recorded in the audit log. The mobile app follows the same list.
- Company Card as a payment method. Recording a company card claim used to offer only payout methods (Direct Deposit, Cheque, Added to Payroll), so there was nothing true to choose. Company Card is now an option, and it is selected for you on a company card claim — in the claim's Mark as Recorded dialog, in Process Claims when every selected claim is on the card, and from the mobile app. The reference is optional and suggests a statement date or the card's last four digits. A recorded claim is stamped Recorded rather than Paid, its total reads Total Recorded, and payment methods read as words ("by Direct Deposit") instead of codes ("by DIRECT_DEPOSIT").
- Add a document: the help button sits beside the wording. The (?) explaining each mode now follows the last word of the description rather than the end of the longest one.
- Health checks you can rely on. The status URL monitors use,
https://app.bookyourpto.com/api/health, is unchanged, but it now answers only{"status":"ok"}or{"status":"unavailable"}with a matching 200 or 503, is never cached, and no longer names the services behind it. It is served from a cached check, so calling it cannot put load on the database, and it reports unavailable while the server is restarting during a release rather than claiming to be healthy. - Releases are traceable and reversible. Every release runs an image tagged with the exact commit it was built from, the release confirms the server is running that commit before it finishes, and any earlier release can be restored in one step from GitHub Actions (Rollback production).
- Scheduled reminders are watched. A reminder or digest job that fails is now recorded as a failure, marks its container unhealthy, and can alert through a Better Stack heartbeat — previously a job could fail every hour for weeks unnoticed.
- Department heads manage their team's equipment and training. On the profile of someone in their own department, a department head can now add, edit and remove assets, and record, complete, remove or dismiss training — before, both tabs showed them nothing they could act on. Their own record stays with an administrator, and defining training courses for the organization remains an administrator setting. Recording a training under a new name adds it as an optional one-time course, so it appears only for the people who hold it.
- More than one Department Head per department. Adding a Department Head to a department that already has a manager is allowed, and the Add User and Edit User forms say who the manager is before you save: the new person gets department-head access, and the manager keeps the department's emails and reminders.
- One person can manage several departments. A department's Manager (Settings → Departments) can now be someone who already manages another department — a director over Software and IT Operations, or a CEO who also runs a small team. Choosing them no longer removes them from the department they already managed or moves them out of their own department; the Manager list shows what each person already manages. A Department Head's access — approvals, people, onboarding, assets, training, documents, schedules, reports — now covers their own department and every department they manage. Each request still reaches each person once, however many departments or roles connect them to it.
- Administrators and executives choose which requests reach them. Leave, expense and time-entry requests have always gone to every administrator and executive. Each can now choose in Settings → Notifications between every request in the organization (the default) and only requests routed to them — from people or departments they manage, or anything nobody else would be told about. The organization can set the default. Every request still appears in the approval queues.