Teaching briefing · Spotter take-home

Django from zero, and the HOS algorithm

The assignment says Django and React. It never says Vite. Use Next.js on Vercel as the UI, keep every planning rule in Django, and treat hours-of-service as four clocks that forbid the next driving minute.

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.

NEXT_PUBLIC_API_URL localhost:3000 CORS rewrites /api → Django in dev

Django for a TypeScript person

It is NestJS plus Prisma, with a batteries-included folder layout

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 already think in TypeScript: Django the project is the Next/Nest repo. A Django app is a feature folder. settings.py is next.config.ts plus env plus module registration. urls.py is the router. Django Ninja endpoints are tRPC procedures or Hono routes. manage.py is package.json scripts. A virtualenv is a local Node: one interpreter and a private package set, instead of repo-level node_modules.

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
src/features/trips python manage.py startapp trips One app. api.py, routing.py, hos.py, tests
Prisma schema + client models.py + migrations Skip. Stateless POST. No User table.
tRPC / Route Handler Django Ninja path operation One write: plan a trip
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 the runtime (settings, root urls). startapp is a plugin you then list in INSTALLED_APPS. Until the app is installed and its urls are included, Django does not know your endpoints exist. That is the same as writing a Nest module and forgetting to import it in AppModule.

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 TypeScript 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 import the router in the root layout 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:

  1. Week full (cycle ≥ 70) → 34 hours off duty, cycle back to 0.
  2. Else 11-hour or 14-hour spent → 10 hours sleeper. Do not put a 30-min break in front of it.
  3. 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.