At some point during this project I stepped back and looked at what we’d built.
Three files. A working web application. Deployable for $7 a month. Zero web development experience going in.
That felt worth writing down.

The Three Files
The whole thing runs on three files. That’s not minimalism for its own sake — it’s what the problem actually required.
scheduler_engine.py is the brain. Pure Python. No Flask, no web server, no file paths baked in. You give it a file-like object — the uploaded Excel workbook — and it gives you back a dictionary: status, schedule, staff load, total assignments. That’s it. It doesn’t know it’s running inside a web app. It doesn’t care.
app.py is the wrapper. Flask handles the HTTP part: serve the frontend, receive the uploaded file, pass it to the engine, hand back the results as JSON. Three routes. One hundred lines. Nothing fancy.
templates/index.html is the face. A single-page frontend with a dark theme, drag-and-drop file upload, a spinner while the solver runs, and a pivot table showing the results. Staff, day, time slot, client, course. Filterable. Downloadable as CSV. Vanilla JavaScript only — no React, no Vue, no framework overhead.
Why the Separation Matters
Keeping the engine separate from Flask wasn’t an architectural philosophy. It was a practical decision that turned out to be essential.
During the debugging phase, I needed to run the engine against the actual Excel file and see what was happening — without spinning up a web server, without clicking through a browser interface, without uploading a file over HTTP. Just: here’s the workbook, show me the output.
Because scheduler_engine.py has no Flask dependency, I could do exactly that. One Python call, direct output, full error messages. The three bugs described in the previous post? I found all of them that way. If the engine had been tangled up with Flask, the debugging loop would have been much slower.
The lesson generalizes: if you’re building something with a computational core — an optimizer, a model, a parser — separate the computation from the delivery mechanism. The computation should be testable in isolation. The delivery layer is just plumbing.
The Solver Decision
One detail worth documenting: the solver.
PuLP can use several solvers to actually solve the MILP. The original Excel setup used SolverStudio, which calls the CBC solver. When I converted to standalone Python, I initially tried COIN_CMD — the same CBC solver, but called as an external binary that has to be installed separately on the system.
That caused problems. On different machines, on Linux, on the deployment server — finding and calling an external binary adds friction. The thing might not be on the PATH. The version might be wrong. It becomes one more thing that can go wrong on deployment.
PuLP ships with its own bundled CBC binary, accessible as PULP_CBC_CMD. Same solver, same results, zero installation. You pip install pulp and the solver comes with it. That’s what the app uses now, and it’s the right call for any project like this. One less dependency to manage.
The Stack
For the record, here’s everything that went into this:
- Python 3
- PuLP — the optimization modeling library
- pandas — for reading the Excel sheets into dataframes
- openpyxl — for the Excel file support (pandas needs it for .xlsx)
- Flask — the web framework
- Gunicorn — the production WSGI server (Flask’s built-in server isn’t for production)
- CBC solver, via PULP_CBC_CMD
- Claude as a coding partner — mostly for the Flask scaffolding, the HTML layout, and talking through deployment steps
The optimization model — the math, the constraints, the objective function, the data structures — that came from 20 years of doing this work in Excel. The web parts were new territory. Having help with the web parts was useful. The parts that required knowing Operations Research couldn’t be delegated to anyone or anything.
What the App Actually Does
From a user’s perspective: you go to a URL, you upload an Excel file in the required format, you wait a few seconds, you get a schedule.
The schedule comes out as a pivot table — time slots on the vertical axis, days of the week on the horizontal, cells showing the Staff / Client / Course for each assigned session. You can filter by staff member, by client, or by course. You can download the full assignment list as a CSV.
There’s a sample workbook available for download so you can see exactly what format the input file needs to be in. There are instructions on the page explaining the data structure. If you submit a file that can’t produce a feasible schedule, the app tells you that clearly — it doesn’t just crash or return empty results.
Is It Production-Ready?
Depends what you mean. It runs. It solves the problem it was built to solve. Multiple people can use it simultaneously from different locations with different workbooks — it’s stateless, each upload is independent.
It doesn’t have user accounts, data persistence, usage analytics, or access controls. If you wanted to put this behind a paywall or track who’s using it, that would require additional work. But as a functional tool for the problem it addresses — scheduling staff for a training company — it works exactly as intended.
The live app is at staff-scheduling.kindoflost.com. The sample workbook is available for download on that page.

Next up: getting this from a laptop to the internet — Git, GitHub, and Render.
Rebuilding it surfaced problems the spreadsheet had been hiding — I covered three bugs that would have stayed hidden in Excel.
Things that I use, like, and am affiliated with:
Mint Mobile offers great cell phone service for $15 flat, get $15 off using the link. Get discounted phones with service activation and no contract.
I never spend money before I check Mr Rebates or Rakuten to get cashbacks, rebates, discounts, coupons or cheaper gift cards.
