# Who's OOO 0.4.0: Leave Balance History, Auto-Approve Countdown, and Digest Scheduling

> What shipped in Who's OOO 0.4.0: a record of every leave balance change, a countdown for auto-approved requests, a weekly digest you can schedule, and a fix for a double allowance bug.

Who's OOO 0.4.0 is out. Most of this release is about the leave balance. Every change to it is now recorded, and a bug that could add a year's allowance twice is fixed. The rest is smaller: a countdown for auto-approval, a weekly digest you can schedule, and a separate production Docker setup.

Here's what changed.

## A history behind every leave balance

Until now, a leave balance was a single number on the user's profile. Booking leave lowered it, a rejection gave the days back, the yearly reset added the allowance, and an admin could overwrite it in a form. When the number looked wrong, nothing showed how it got there. You had to rebuild it from the leave requests and hope nobody had edited it by hand.

0.4.0 puts a ledger behind that number. Every change writes an entry: leave booked, days returned after a rejection or withdrawal, the annual allowance added on the reset day, or a manual adjustment by an admin. Each entry stores the change, the balance before and after, where it came from (the app, Slack or the scheduler), and for manual changes who made it and why. Entries are grouped into leave years, and the app never updates or deletes them. If a balance needs fixing, the fix is a new entry.

![The Balance History page in Who's OOO for the 2026 leave year, with a summary row showing an opening balance of 30, 11 days used, an adjustment of plus 11 and a current balance of 30, above a table of entries: the opening balance, two Vacation bookings of 6 and 5 days, and a manual adjustment with the reason "Carry-over from last year"](/assets/images/blog/release-0-4-0/balance_history.png)

Employees see their own history on a new Balance History page. Admins can open it for anyone, and managers for their direct reports. Admins also get an Adjust balance action, and it won't save without a reason. The balance field is gone from the edit form, so changing the number by hand always leaves a record.

![The Adjust leave balance form for an employee with a current balance of 17, with a required Change field where positive numbers add days and negative numbers remove them, and a required Reason field that is recorded in the audit trail](/assets/images/blog/release-0-4-0/adjust_balance.png)

After a user's opening balance, every change goes through one method, `UserFacade::recordBalanceChange`. It locks the user row and saves the entry and the new balance in one transaction. A rule like that is easy to break by accident six months later, so I added an architecture test that scans the source code and fails the build if any file outside a short allowlist assigns the balance field. It's a blunt check, but a future change can't quietly go around the ledger.

The ledger also changed two things you'll notice. Rejecting a request from Slack used to delete it. Now the request stays, and the days come back as a ledger entry. And booking leave that would take the balance below zero is refused with an error message.

An entry counts toward the leave year it was recorded in, not the year the absence falls in. If you book a week in January while it's still December, the days come off the leave year that is open in December. The dashboard counts used days by the absence start date, so the two numbers can differ, and the history page explains this.

Existing installs don't start with an empty history. The migration opens a leave year for every user and writes an opening balance entry with their current balance.

## Auto-approve countdown

Who's OOO can approve leave requests automatically after a delay you set in minutes. The problem was that you couldn't see it coming. A request sat as pending, and at some point it was approved.

The leave requests list now has an Auto approved column. For a pending request it shows when auto-approval will happen, with a countdown that updates every second. Once a request is approved, the column says whether that happened automatically or a person did it.

![The Absence Requests list in Who's OOO with an Auto approved column: a pending Vacation request shows a countdown of 00h 29m 25s until it is approved automatically, and an approved request shows No because a manager approved it](/assets/images/blog/release-0-4-0/auto_approve_countdown.png)

The approval itself still runs in a background job every five minutes. So when the countdown reaches zero, the column switches to "Approving shortly", and the request stays pending until the next run picks it up.

## Weekly digest on your schedule

The Slack weekly digest used to go out every Monday at 8:15 server time. That was hard-coded in a cron attribute. If your team starts the week at 9:00 in Warsaw, or doesn't start it on a Monday, there was nothing to change.

Application Settings now has a Weekly Digest Schedule section where you pick the day, the time and the timezone. The scheduler reads these settings every time it works out the next run, so a change applies without restarting the worker. Technically, the digest moved to its own Symfony Scheduler schedule, with a small custom trigger that builds the cron expression from the settings.

![The Weekly Digest Schedule section in Who's OOO Application Settings, with the send day set to Friday, the send time to 19:45 and the timezone to Europe/Berlin](/assets/images/blog/release-0-4-0/weekly_digest_schedule.png)

## Fixes

The most important fix is for leave balances. A user created in the admin panel got a balance reset day of 1 January of the current year. That date had already passed, so the first nightly reset added a second allowance on top of the one the user was created with. The upgrade that introduced the reset day had the same effect on existing users. New users now get 1 January of next year, and the reset skips the allowance when the reset day is on or before the day the account was created.

The production Docker setup also got a lot of attention. There's now a separate `docker-compose.prod.yml` that builds the production image, mounts no source code and runs the background workers. The development compose file drops the database and loads demo data on every start, which is fine on a laptop and a disaster on a server. So `.env.dist` pins `COMPOSE_FILE` to the production file, and the two stacks use separate volumes and container names.

The same work documented `TRUSTED_PROXIES`, `TRUSTED_HOSTS` and `APP_BASE_URL` for running behind a reverse proxy, and password reset links are now built from `APP_BASE_URL`. Admins can resend an invitation. Deactivated users can no longer log in, and an open session ends on their next request. And when no Slack signing secret is configured, the Slack endpoint now rejects every request. Before, it checked signatures against an empty key, which anyone could reproduce.

## Before you upgrade

If you already run Who's OOO, check these before you upgrade:

1. Older installs may have people whose `is_active` flag is still `0`, because the column was added without filling it in. After the upgrade they can't log in. The [upgrade guide](/docs/production-deployment#upgrading-an-existing-install) has a read-only query that lists them.
2. The balance fix stops new cases but doesn't touch balances that are already too high. The [deployment guide for 0.4.0](https://github.com/igornast/who-is-out-of-office/blob/v0.4.0/docs/production-deployment.md#leave-balances-added-by-mistake-before-this-fix) has a query that lists every user with their balance. It says to correct them in the edit form, but that field is gone in 0.4.0, so use Adjust balance instead and the reason ends up in the ledger. Please don't fix them with a single `UPDATE`, because some admins already corrected balances by hand.
3. If you run the workers yourself instead of using the Docker setup, the scheduler worker now has to consume two transports: `messenger:consume scheduler_default scheduler_weekly_digest`.

Then run the migrations as usual.

## In closing

0.4.0 changes how balances are stored more than how the app looks. If a balance looks wrong now, you open the history and see which entry did it.

Who's OOO is open source and free to self-host. The [full changelog](https://github.com/igornast/who-is-out-of-office/releases/tag/v0.4.0) and the code are on GitHub. If it's useful to you, a star on GitHub or a sponsorship helps keep the project going.

