06Case study · Working tool

DutyGuard

Can I take this overtime shift — and what does it do to my week?

DutyGuard on a phone: the duty still to be recorded, the week's attendance against a personal cap, and a list of compliance checks with their verdicts
Working app · demo depot
My roleResearch, product design & build — solo
TypeInstallable web app (PWA)
StackNext.js · TypeScript · Tailwind · Web Crypto
StatusWorking tool · in personal use

Independent tool — not affiliated with, or endorsed by, Royal Mail. The public app runs on a fictional depot; every duty code, time and route shown is invented. DutyGuard is a planning assistant, not a compliance guarantee.

The problem

I drive at a mail centre. Overtime there is driver-led: you put your own name in a book for the duties you want, and management may approve them without recalculating everyone’s previous week. The expectation is that you already know whether you are able to work it.

So every driver is doing the same arithmetic in their head, against rules most of us have never been sat down and taught — daily rest, weekly rest, driving limits, working time. Get it wrong and you can end up over a limit you did not know you were near. The safe move is to take less overtime than you could. The tempting move is to take more than you should.

Nobody wants to do this maths at half past midnight to find out whether they can work on Wednesday.

Research

The research was the job. I photographed what is actually on the wall: the late duty roughs, the distribution scheduled attendance, the Saturday and Sunday sheets, and one real duty card. Then I transcribed them into a duty pack — 89 duties at one site.

Three things fell out of that, and all three shaped the product:

Hours on site are not driving hours

One duty card documents eight hours of attendance, forty-five minutes of meal relief and three hours fifty of driving. The rest is loading, collections and travel. Any tool that treats a shift length as a driving figure is wrong before it starts — so attendance, working time and driving time are three separate fields that are never derived from one another.

Half the data does not exist

The late rough publishes start times and no finishes. 43 of the 89 duties have a start and nothing else. A planner that quietly assumed “start plus eight hours” would be confidently wrong — real duties at this site run from 4h25 to 10h30.

Duties are not all the same shape

Some are fixed rounds with a documented route. Some are pooled attendances — twelve identical slots where the number is a slot, not a round, and the work is whatever needs doing. One is a split duty with two blocks in a day and a gap that is not a rest period. Several cross midnight into the following regulatory week.

The design approach

The app answers one question — can I take this? — and a second one behind it: how much more could I take? Everything else is in service of those.

Pick a dutySee the verdictPut your name in the bookRecord what you worked

The decisions that mattered:

Never say legal or illegal

Three things sit on top of each other: the regulations, the employer’s own rules, and what a driver personally wants. Collapsing them into one verdict would be dishonest, so the app reports no conflict identified, check before accepting, or potential conflict — and always says why. Breaching your own preference is amber. It is never red, because it is not that kind of problem.

“I don’t know” is a first-class answer

Where a finish time or a driving figure is missing, the app says so rather than passing. A silent green on missing data is the one outcome that could actually get someone in trouble, because that is the one they would act on. Propose a duty with no published finish and twelve of twenty-two checks return unknown, each naming the gap.

What you were rostered is not what you worked

Every shift holds scheduled and actual times separately. Before the duty, projections use the roster; afterwards the app asks what you really finished, and uses that. A third of my own contracted week is pooled work with no predictable driving figure, which makes recording actuals the main way data gets in, not an afterthought.

The hardest call was a count, not a rule

The availability results used to open with “you could potentially accept 19 of the 19 duties that fit your window”. Logically defensible — those nineteen had no conflict in the checks that completed. But a driver reads a count as a count, and one adverb cannot carry the weight of nineteen unfinished checks. Three separate reviews flagged that sentence independently before I accepted it was wrong. It now leads with how many fit the window, which is a fact, and states separately that none could be checked through. Same data, and it no longer reassures.

A pass should say how much room it passed by

My Saturday finishes 18:00 and my Sunday starts 05:30. That is eleven and a half hours against an eleven hour floor — a pass, with thirty minutes of slack. A green tick alone hides that. So green now carries its margin, and the tightest gap of the week is surfaced on the home screen.

From the depot wall to a decision

Captured from the working app running on the public demo depot — every duty code, time and route shown is invented.

DutyGuard calendar screen: a month grid, Monday-first, with a filled disc on days that carry a duty, and a legend beneath naming each mark in words
01 · See the week, not the shiftEvery row of the month is one regulatory week, so tapping a day opens the week its totals are judged against. Duties, the rest between them and the weekly limits only mean anything together.
DutyGuard showing an unrecorded duty: a Not recorded badge, the prompt 'Finished R7 Monday?', and a week summary stating that driving time is not entered for two duties so the driving limits cannot be checked
02 · Unknown is deliberately visibleA duty nobody has recorded holds the week’s totals at not enough data, and the app says which duty and which figure. Unknown gets its own mark, its own wording and its own colour — never a quiet green.
DutyGuard overtime planner with two modes: Known duty and Availability, above a row of selectable dates
03 · Two different questions“They’ve offered me A14, can I take it?” and “I’m free 11:00–20:30, what can I put my name down for?” are not one feature with a filter. They are separate jobs, so they are separate modes.
DutyGuard availability results: a headline stating how many duties fit the window and that none could be checked all the way through, above a list where each duty carries both its verdict and the figures that could not be verified
04 · Don’t hide the uncertainty in the answerNineteen duties fit the window and not one has a published finish time, so the headline says exactly that. Each row carries its verdict and what could not be looked at.

Technical implementation

The compliance rules are pure TypeScript functions with no knowledge of React, storage or the network, so every rule can be tested against a hand-built week. The regulatory thresholds are versioned configuration rather than code, because which rules govern a given duty is a question the app is not entitled to settle on its own.

Next.jsTypeScriptTailwindVitestWeb Crypto
Static exportOffline-first PWANo backend, no accountData stays on the device

A rules engine, not rules scattered through screens

Daily rest, weekly rest, driving time, working time and the employer or personal layer are separate modules behind one interface. Every verdict is stamped with the rule set and version that produced it, and every threshold ships flagged as needing confirmation — because it does.

Adversarially reviewed, and it needed to be

The suite runs to 379 tests, but the useful pass was an adversarial one hunting for a single failure: green where the app should hesitate. It found two. One returned a pass on a week the same engine called a conflict when asked differently; the other split a Sunday-night-into-Monday driving period in half so a breach was invisible from both weeks. Both were reproduced with runnable tests before anything was changed.

The real duty pack is encrypted, not hidden

The depot’s sheets are internal documents, so the public app ships a fictional depot instead. My own pack travels inside the same build under AES-256-GCM with a key stretched 600,000 times, and decrypts onto my phone when I enter a passphrase. An unlisted URL would not have been enough — certificate transparency logs make those findable.

One depot, behind an interface any depot could use

Nothing in the engine knows about mail. A duty pack is data: codes, categories, times where they are published, and honest nulls where they are not. Another driver at another operator can import their own and get the same checks.

Outcome

DutyGuard is a working, installable app that I use for my own duties. It tells me before I write my name in the overtime book whether a duty conflicts with the rest I have had, what it does to my week, and — just as often — which parts of that it cannot answer yet.

It has not been given to other drivers. The honest position is that it needs its remaining unknowns closed first: which hours regime governs my duties is a question for the depot, not for me, and 43 duties still have no published finish time.

What I learned

The hardest part was not the regulations. It was deciding what to do when the data runs out — and the answer, every time, was to say so. It is tempting to fill a gap with a reasonable assumption, because the screen looks better. But a tool that guesses teaches you to trust it, and then it is worse than nothing.

The other lesson was that domain knowledge is what makes the model right. I knew attendance was not driving time because I had a duty card in my hand showing eight hours and three-fifty. Without doing the job, I would have built something that looked correct and was quietly wrong.