all builds >> N. GUGLIELMI
>> nicola@guglielmi ~ / labs / vigil

Vigìl

A control-room console for a private guarding company, built to be walked through rather than sold: the operational screens run on staged data, and I would rather say so here than let a demo imply otherwise. One module is not staged at all. The shift planner is a real multi-objective optimiser that builds a week of guard duty against seven competing goals and hard rest rules, in the browser, in under a second.

Next.js 14 TypeScript Constraint scheduling Gini index Seeded PRNG Zero backend Cloud Run
>> THE_PROBLEM

Rostering guards is a problem where every good answer is somebody's bad week. Cover every post and the cost runs away. Cut the cost and the same four people take every night shift. Keep the same guard on the same site, which clients want, and you have quietly built a rota nobody else can cover when that guard is sick. Add the rules that are not negotiable, one shift a day, no morning after a night, a ceiling on the week, and the search space stops being something a planner holds in their head.

So the planner does not output a schedule and call it optimal. It exposes the seven objectives as dials, and shows what each setting costs against the others: what happens to overtime when you push proximity, what fairness does when you chase cost. The answer is a trade-off made visible, not a black box with a number on it.

>> HOW_A_WEEK_GETS_BUILT
Slotssite x day x shiftsorted by criticality Candidatesqualification | zone indexradius widens if thin Hard rulesrest, night eligibilityweekly ceiling Score 0 to 1seven objectivesweights from the dials Assignbest candidate winsstate updates, next slot Plan metricscost, coverage, overtimeGini equity, continuity vs baselinethe manual rota,computed the same way loop: one slot at a time Nothing leaves the browser. The same seed produces the same workforce, so two settings can be compared without the ground moving underneath them.

> scales from 250 to 6,000 employees, with the search deliberately narrowed at the top end to keep the run interactive.

>> DECISIONS_THAT_MATTERED
[ 01 ]

Every number is relative to the rota it replaces

An absolute cost per week means nothing to someone who already runs this company. So a baseline plan is generated first, configured the way manual scheduling actually behaves, continuity above everything, cost and fairness barely considered, and scored by the identical code. Every figure on screen is then a delta against that. It doubles the computation on each change, and it turns "€X per week" into the only version of the claim that is arguable.

[ 02 ]

Greedy on purpose, because it has to explain itself

A proper solver would return better rotas. It would also take the run out of the browser and make every assignment unanswerable except by rerunning it. The planner instead walks the slots hardest-to-fill first and picks the top-scoring eligible person for each, which means any single choice can be opened and read: these were the candidates, this is why that one won. No optimality guarantee, and the result depends on the order slots are visited. That was the trade accepted for a plan you can argue with.

[ 03 ]

The search widens when the roster is small

The instinct is the opposite: small company, tight radius, short journeys. It produced uncovered posts, and the coverage figure was measuring the wrong thing. With few hundred people split across zones and qualifications, a bucket can simply be empty, so the gap was an artefact of slicing rather than a scheduling decision. The radius now opens up as headcount falls. Longer commutes on small rosters, in exchange for a metric that reports the optimiser's judgement instead of its sampling.

[ 04 ]

Fairness needed a number, and the number is honest about being arbitrary

"Spread the nights fairly" has to become arithmetic before it can be optimised. Burden is weighted, nights counting heaviest, then run through a Gini coefficient over everyone working, so the index reads zero when the load is perfectly even. The shape of that metric is defensible. Its calibration, the exact weights and the constant that rescales Gini onto a nicer range, is a judgement call with no evidence behind it, and it is labelled as one rather than dressed up.

[ 05 ]

Same seed, same workforce, or the comparison is theatre

The demo workforce is generated, not sampled from a client, so it could have been random. It is not: a seeded generator builds the identical staff, sites and slots on every run. Otherwise two scenarios differ by both the settings and the underlying people, and nothing shown is attributable. The same reasoning drives a harsher rule: change the company size and the saved comparisons are discarded, because a 250-person plan and a 6,000-person plan were never on the same axis.

[ 06 ]

No backend, and therefore no data to lose

The whole thing runs client-side: the optimisation, the metrics, even the branded PDF, generated in the page and pulled in only when someone actually asks for one. For a tool you hand to a prospective client and leave running, staff rosters never reaching a server is a feature rather than a shortcut. The bill is real too, the run blocks the main thread rather than sitting in a worker, which is fine at a second and would not be at ten.

>> WHY_THIS_MATTERS_TO_YOU

Most planning problems are not short of algorithms, they are short of an agreed definition of better. The work here was turning cost, fairness, continuity and fatigue into things that can be measured against each other, then refusing to hide the calibration behind a score. If you have a scheduling or allocation problem where the hard part is the disagreement about what to optimise, that is the conversation I like having. Let's talk.