- Not Vite Assignment text Only “Django and React”. Vite was our shortcut.
- Next.js New UI pick App Router, UI only. No Next API routes for HOS.
- Django ≈ Express, not Next We use it as an HTTP server. No Mongo.
- 4 clocks The whole algorithm 8h break · 11h drive · 14h window · 70h week
Frontend
Vite is not required. Switch to Next.js.
The brief says “Build a Full-stack app using Django and React” and “can use Vercel.app”. That is the entire frontend constraint. Vite, TanStack Start, and Next.js are all legal React. Vite was picked earlier only because it is a static SPA with no extra server, so it cannot accidentally steal work from Django.
TanStack Start is a React framework that still uses Vite under the hood. Choosing Start is not “instead of Vite”; it is Vite plus router plus optional SSR. Next.js is the thing the assignment’s Vercel hint actually points at, and the thing a grader already knows. That is the new lock: Next.js App Router as a UI-only frontend on Vercel, talking to Django on Render.
| Option | What it is | Use here? |
|---|---|---|
| Vite SPA | Bundler + React. No framework. npm run dev serves the client. Closest to Create React App. |
Fine. We dropped it because you want a real React framework. |
| TanStack Start | File routing and SSR on Vite. Server functions want to be the API. | Allowed if you insist. Do not put trip planning in server functions. |
| Next.js App Router | React framework Vercel is built for. Route Handlers and Server Actions also want to be the API. | Yes. Client components fetch Django. No app/api/plan route. |
Hard rule
Django must own the algorithm
If HOS math lives in a Next Route Handler, Django becomes a prop and the take-home fails the stack requirement. Leaflet, the SVG log, and the form stay in React. Geocoding, routing, and every clock stay in Python.
Django for a MERN person
It is Express plus a loud folder layout. Mongo stays home.
Django is a Python web framework from 2005. The famous parts are an ORM, an admin UI, and HTML templates. This assignment barely uses those. We need Django because the brief named it: it is the process that listens on a port, receives POST /api/trips/plan, and returns JSON. The interesting file is ordinary Python, trips/hos.py, which does not know HTTP exists.
If you think in MERN: Django is Express, not Next. A Django app is routes/trips.js plus its controller, not the Next app/ folder. settings.py is .env plus the cors setup in server.js. urls.py is app.use("/api", tripRoutes). Django Ninja is router.post. manage.py is npm scripts. A virtualenv is a private Node, not a node_modules folder. Full mapping: django-mern.plan.abstergo.fyi.
| You already know | Django name | In this take-home |
|---|---|---|
package.json + npm scripts |
manage.py + requirements.txt |
runserver, migrate, later gunicorn |
node_modules + lockfile |
.venv/ + pinned pip deps |
Activate the venv before every command or nothing imports |
npx create-next-app |
django-admin startproject config . |
Creates settings, urls, wsgi/asgi, manage.py |
| routes/trips.js + controller | python manage.py startapp trips |
One Express-style router bundle. api.py, routing.py, hos.py, tests |
| Mongoose models/User.js | models.py + migrations | Skip. Stateless POST. No Mongo. |
router.post / Express controller |
Django Ninja path operation | One write: plan a trip. Not a Next Route Handler. |
app/layout.tsx templates |
Django templates + admin | Unused. React draws the UI. |
process.env |
settings.py reading os.environ | SECRET_KEY, ALLOWED_HOSTS, CORS, Nominatim UA |
Express listen() |
WSGI/ASGI + gunicorn | Dev: runserver. Prod: gunicorn on Render |
| Jest / Vitest | pytest | HOS goldens with no network |
Project vs app is the one concept that bites JS people. startproject is server.js (settings, root urls). startapp is routes/trips.js. Until the app is listed in INSTALLED_APPS and mounted in urls.py, Django does not know your endpoints exist. That is creating a router file and forgetting app.use("/api", tripRoutes).
A request is boring: gunicorn receives HTTP → root urls.py matches /api/ → Ninja finds POST /trips/plan → that function geocodes, routes, calls plan_trip(), returns a dict that Ninja JSON-encodes. There is no magic React-style render on the server.
Setup
Virtualenv first, then a project, then one app
Python does not install packages into the repo the way npm does. You create a folder that contains a copy of the interpreter. Every terminal must source .venv/bin/activate or you are talking to system Python, which will not see Django.
| Step | MERN / Next reflex | What we run |
|---|---|---|
| Isolate deps | pnpm install into node_modules |
python3 -m venv .venv && source .venv/bin/activate |
| Install framework | pnpm add next react |
pip install django django-ninja django-cors-headers httpx pytest |
| Scaffold | create-next-app frontend |
In backend/: django-admin startproject config . then python manage.py startapp trips |
| Register the feature | app.use("/api", tripRoutes) in server.js |
Add trips and corsheaders to INSTALLED_APPS; mount Ninja at /api/ |
| Dev server | pnpm dev on :3000 |
python manage.py runserver on :8000, Next on :3000 |
| Prod | Vercel builds Next | Render runs gunicorn config.wsgi:application |
Files that will actually matter: config/settings.py (CORS, allowed hosts), config/urls.py (mount /api), trips/api.py (the POST), trips/routing.py (Nominatim + OSRM), trips/hos.py (clocks). You can ignore models, admin, and templates until some later assignment needs them.
Algorithm
Not a calendar. A game with four stamina bars.
Forget “fill out Tuesday’s log.” The computer first draws a highway (addresses → coordinates → a driving polyline with miles and minutes). That geometry is frozen. Then a tiny simulator walks the line one minute at a time and asks: is the next driving minute legal? If yes, emit Driving. If no, insert the cheapest rest that makes it legal, then try again. Pickup, dropoff, and fuel are on-duty chores that always happen even when driving is illegal.
When the timeline is done, slice it at midnight. Each slice is one paper sheet. The map is the same events with lat/lng. There is no second algorithm for logs.
| Clock | Fills up when | Blocks driving at | Empties when |
|---|---|---|---|
| 8-hour break | The truck is moving | 8 hours of driving since the last ≥30 min not-driving | Any 30+ min stretch of off duty, sleeper, pickup, dropoff, or fuel |
| 11-hour driving | The truck is moving | 11 hours of driving since last long rest | 10 consecutive hours sleeper (or off duty) |
| 14-hour window | Wall clock, even during a 30-min break | 14 hours after the first on-duty or drive of the shift | Same 10-hour sleeper. Then the window is unset until work starts again. |
| 70-hour week | Driving + on-duty (not off, not sleeper) | 70 hours, counting the form’s “cycle used” as already filled | 34 consecutive hours off duty. We do not invent old days rolling off. |
The job list on the frozen road is short: drive to pickup (skip if already there) → 60 min on-duty pickup → drive to dropoff → 60 min on-duty dropoff. Every 1,000 driving miles, pause 30 min on-duty to fuel. Fuel also resets the 8-hour break clock, so we do not add a coffee stop on top of a fuel stop.
When driving is illegal, pick one rest, the strongest needed:
- Week full (cycle ≥ 70) → 34 hours off duty, cycle back to 0.
- Else 11-hour or 14-hour spent → 10 hours sleeper. Do not put a 30-min break in front of it.
- Else 8-hour driving spent → 30 minutes off duty, unless pickup/dropoff/fuel is already sitting here for ≥30 min.
On-duty work is legal after the 14-hour window and after 70 hours. Only driving is banned. That is why dropoff can still take an hour after the driver has no driving time left.
Worked example
Chicago pickup at 06:00, then Dallas, cycle already 12 hours
Current location is the pickup dock in Chicago, so there is no empty drive first. Routed loaded drive is 920 miles, 13.5 hours. Under 1,000 miles, so no fuel. Dispatch is 06:00 America/Chicago. Cycle used = 12 hours, so 58 hours remain in the week. A legal 10-hour off just ended, so the 8/11/14 clocks start empty.
| Clock time | What happens | Why |
|---|---|---|
| Day 1 00:00–06:00 | Off duty on the sheet | Fill the night before dispatch. Not simulated work. |
| 06:00–07:00 | On duty, pickup, Chicago | Assignment: 1 hour. This opens the 14-hour window (ends 20:00). Cycle becomes 13h. Break clock stays 0 because on-duty ≥ 30 min counts as the break. |
| 07:00–15:00 | Driving, 8 hours | First binder is the 8-hour break rule, not 11 or 14. About 546 miles. |
| 15:00–15:30 | Off duty, 30-min break | Break clock full. Window keeps running (now 9.5 hours used). Cycle does not increase. |
| 15:30–18:30 | Driving, 3 hours | Only 3 hours left of the 11-hour driving cap (8 + 3 = 11). Window still has 1.5 hours; we stop for 11, not 14. |
| 18:30 → Day 2 04:30 | 10 hours sleeper | 11-hour cap hit, 2.5 hours of Dallas still to go. Sleeper resets 8, 11, and 14. Cycle stays 24h (12 start + 1 pickup + 11 drive). |
| Day 2 04:30–07:00 | Driving, 2.5 hours | New window. Arrive Dallas. No fuel: 920 < 1,000. |
| 07:00–08:00 | On duty, dropoff | Assignment: 1 hour. Then off duty until midnight so the sheet totals 24. |
Two paper sheets, same timeline:
| Day | Off | Sleeper | Driving | On duty | Sum |
|---|---|---|---|---|---|
| Day 1 | 6.5h | 5.5h | 11h | 1h | 24 |
| Day 2 | 16h | 4.5h | 2.5h | 1h | 24 |
34-hour restart
Same trip, cycle used = 69
Pickup still happens (on-duty after 70 is legal). Cycle becomes 70. The next driving minute is illegal, so the simulator inserts 34 hours off duty, zeros the week, then drives. That is the only extra branch. We never fake seven prior days in the recap box; we print today’s on-duty, cycle at dawn, and what is left of 70.
Takeaway
What to remember when we start coding
Next.js draws. Django decides. hos.py is a loop over minutes and four numbers. If those four numbers never allow an illegal driving minute, the map and the paper logs come out correct for free.