Fair rotas for small teams. Describe the team and the hours you need covered, and it builds a schedule that respects the rules and spreads the load evenly.
One HTML file, no dependencies, no build step needed to use it, no server. Nothing you type ever leaves the browser.
You give it:
- The team — names, how many days a week each person may work, and each person's weekdays marked as available, wants the day off, or unavailable.
- The coverage — which weekdays need cover, the opening window (say 06:00–22:00), the shift length, and how many people must be on duty at all times.
- The rules — minimum rest between shifts and the maximum run of consecutive days.
It gives you a roster, three ways to read it (by person, by shift, as a calendar), a workload balance table, and CSV / iCalendar exports.
Before scheduling anything, the planner compares what you are asking for against what the team can actually supply.
7 days × 16 hours with 8-hour shifts is 14 shifts a week. Two people on duty at all times makes 28 person-shifts. A team working 5 days a week supplies 5 each — so you need 6 people, and with 5 you are short no matter how the rota is arranged.
When the numbers don't work, it says so and tells you how many more people would close the gap, instead of quietly producing a schedule with holes in it. When they do work, every shift is filled.
| Rule | Default |
|---|---|
| One shift per person per day | always |
| Minimum rest between two shifts | 11 hours (EU daily rest) |
| Maximum consecutive working days | 5 |
| Weekly cap on days worked | per person |
| Declared unavailability | never scheduled |
The rest rule is the one that does the most work. With a 06:00–22:00 window it makes "close at 22:00, open at 06:00" impossible — eight hours is not eleven — which is the scheduling mistake that gets made most often in retail and hospitality.
Within those rules it tries to equalise, in order of weight:
- Total shifts, relative to each person's own availability — someone contracted for three days a week is not compared against someone on five.
- Night and weekend shifts, so the unpopular ones don't land on the same people.
- Requested days off.
- Pattern stability: shifts of the same kind cluster, and a late shift is not followed by an earlier one the next day. Forward rotation is easier on the body clock.
- No single day off wedged between two working days.
Rostering is a constraint problem, and finding a provably optimal roster is NP-hard. At the scale this tool is for — tens of people, weeks not years — the practical answer is good enough, fast:
- Feasibility check — capacity against demand, before anything else.
- Greedy construction — hardest shifts first (the ones fewest people can work), each seat going to whoever is furthest below their fair share.
- Simulated annealing — tens of thousands of moves, swaps and replacements, each one checked against the hard rules and scored by the cost function above.
Hard rules are enforced when a move is applied, never merely penalised, so a returned roster is always legal. If a seat cannot be filled legally it is left visibly empty and reported.
The whole thing is deterministic: the same inputs and the same variant number always produce the same roster. Try another just increments the seed.
Shift colours are not arbitrary. Hue is derived from the shift's position on the clock, so evenly spaced shifts are automatically evenly spaced in colour, and a glance at the grid tells you the time of day. The offset is picked so that the common two-shift day lands on amber and cyan, avoiding the red/green pair.
index.html built, single file — this is what GitHub Pages serves
src/app.html markup, styles and UI logic
src/solver.js the scheduler, with no DOM dependency
test/verify.js headless checks over a set of scenarios
build.js inlines the solver into a single file
Editing index.html directly will be overwritten. Edit src/, then:
node build.js # regenerate index.html
node test/verify.js # audit the solver against the hard rulesverify.js re-checks every returned roster with an independent constraint checker that
shares no code with the solver, so a bug in the solver cannot hide itself.
- Roles and skills ("at least one senior on every shift")
- Shifts of different lengths within the same day
- Multi-week repeating patterns and carry-over between horizons
MIT.