React SPA
Vite, not Next.js. Root directory in the Vercel project = frontend. Env: VITE_API_URL = the Render origin. No serverless Django. Vercel never runs the API.
Architecture briefing · 18 August 2026
One stack, not a menu. Django API on Render, React SPA on Vercel, free OSM mapping, a pure-Python HOS engine, and SVG paper logs. The workspace collection stays a folder of take-homes; this assignment is its own GitHub repo living in hos/.
Picked stack
Django 5 + Ninja · Vite React
Nominatim + OSRM + Leaflet
Stateless POST /api/trips/plan
Assignment
Hosted app, GitHub, 3–5 minute Loom. Inputs: current location, pickup, dropoff, current cycle used (hours). Outputs: map with route plus stop/rest info, and one or more filled Driver’s Daily Log sheets. Property-carrying, 70 hrs / 8 days, no adverse driving. Fuel at least once every 1,000 miles. One hour each for pickup and drop-off. UI can offset some output inaccuracy — the paper logs have to look real.
Repo and hosting
/home/abstergo/Programming/Projects/Spotter-assignment is a collection for future assignments. Django at that root would poison the next take-home. Create hos/ as a nested git repo whose GitHub remote is the submission. Graders clone a repo whose root is backend/ + frontend/ + README. The parent folder can later grow a thin index README; it is not what they clone.
| Path | Job |
|---|---|
| hos/README.md | Assumptions, sample trips, local make dev, hosted URLs, Loom outline. |
| hos/Makefile | Single command: API + Vite together. |
| hos/backend/ | Django 5.2 project config, app trips, pytest. |
| hos/backend/trips/hos.py | Pure HOS engine. No HTTP. Unit-tested. |
| hos/backend/trips/routing.py | Nominatim + OSRM (+ ORS fallback). User-Agent, cache, 1 req/s. |
| hos/backend/trips/api.py | Django Ninja: POST /api/trips/plan, OpenAPI at /api/docs. |
| hos/frontend/ | Vite + React + TypeScript. Leaflet map, SVG logs. |
| hos/docs/assumptions.md | HOS constants, timezone, recap approximation, map attribution. |
Vite, not Next.js. Root directory in the Vercel project = frontend. Env: VITE_API_URL = the Render origin. No serverless Django. Vercel never runs the API.
Python web service, gunicorn, backend as root. Free tier cold-starts (~30–50s); document that in README. Do not use SQLite for anything a grader must keep — stay stateless.
No cookies, no CSRF dance. django-cors-headers with an allowlist: http://localhost:5173 and the Vercel origin. API is AllowAny. SECRET_KEY, ALLOWED_HOSTS, CORS_ALLOWED_ORIGINS, NOMINATIM_USER_AGENT (must be an identifying string, never the default python-requests), optional ORS_API_KEY.
Local: Vite proxies /api to http://127.0.0.1:8000, so the browser talks same-origin in dev. Production frontend calls VITE_API_URL. One command:
| Command | What it starts |
|---|---|
make dev | concurrently: Django runserver + Vite. Prints both URLs. |
make test | pytest on the HOS engine (no network). |
make seed-check | One live geocode+route against Chicago → Indianapolis → Dallas. |
Mapping
All OSM, no credit card, no Mapbox. Geocode and route from Django only — never from the browser — so User-Agent, rate limits, and caching live in one place.
| Job | Primary | Limit | Fallback |
|---|---|---|---|
| Geocode | Nominatim nominatim.openstreetmap.org/search |
1 req/s absolute; identifying User-Agent; cache results; OSM attribution | Photon photon.komoot.io, then a baked-in city dictionary for demo buttons |
| Route | OSRM public router.project-osrm.org driving, geometries=geojson, overview=full, waypoints = current → pickup → dropoff |
Demo service, no SLA, can 429 | OpenRouteService directions, free key, 2,000/day and 40/min, no card |
| Tiles | Leaflet 1.9 + Carto Positron basemaps.cartocdn.com/light_all |
Fair use; attribution required | OSM default tile URL if Carto is blocked |
One plan = 3 geocodes + 1 route. Sleep 1.1s between Nominatim calls. LRU cache on normalized address strings. Ship three sample trips as buttons so graders never type, and so Nominatim is not hammered. Show “© OpenStreetMap contributors” on the map and in the footer.
Do not use Google Maps, Mapbox, or MapLibre. Leaflet is enough and matches the paper-and-dispatch look.
Backend
/api/docs for the Loom. Single write: POST /api/trips/plan.Auth: none. Persistence: none. Optional save is a time sink and breaks on Render’s ephemeral disk. Graders test the hosted form; they do not create accounts.
| Field | Shape |
|---|---|
| In | current_location, pickup_location, dropoff_location (strings), cycle_used_hours (0–70), optional depart_at (ISO). Default depart: 06:00 America/Chicago today so day-1 logs look like a real dispatch, not “whatever time the grader opened the tab.” |
| Out · route | Miles, OSRM driving hours, GeoJSON coordinates, geocoded labels/latlng for the three inputs. |
| Out · stops | List of pickup, fuel, 30-min break, 10-h rest, 34-h restart, dropoff. Each has kind, label, miles-from-start, latlng interpolated along the polyline by distance, duration hours, clock times. |
| Out · timeline | Duty segments: off_duty | sleeper | driving | on_duty, start/end ISO, remark, place name. |
| Out · logs | One object per calendar day: header fields, segments clipped to midnight–midnight, four row totals (must sum to 24), remarks flags, 70/8 recap numbers. |
| Rule | Engine behavior |
|---|---|
| 11-hour driving | Stop driving at 11 h since last 10-h off/sleeper. Insert 10 h sleeper if still on the route. |
| 14-hour window | No driving after 14 h from first on-duty moment after a 10-h reset, even if driving hours remain. |
| 30-min break | Cannot drive if 8 h of driving have passed without ≥30 min off-duty or sleeper. Insert 30 min off duty (not on-duty). Place at interpolated map point. |
| 70/8 cycle | remaining = 70 − cycle_used. Driving + on-duty burn remaining. If a segment would go negative, insert a 34-h restart and reset remaining to 70. |
| Fuel | Every 1,000 miles, 0.5 h on-duty not driving. Assignment did not specify duration; 30 min is the take-home convention. |
| Pickup / dropoff | 1.0 h on-duty not driving at each, after arriving via the route. |
Walk the route by remaining miles, not by OSRM’s unbroken duration. OSRM time is converted to an average speed on that geometry; HOS inserts add clock time but do not invent extra miles. Overnight rest uses sleeper berth so the grid looks like a trucker log, not a bus schedule.
Cycle recap cannot be a true rolling 8-day total without 8 days of history. Treat cycle_used_hours as hours already on the clock at depart. Recap A = on-duty + driving today (rows 3+4). Recap B = cycle used at start of that day. Recap C = remaining = 70 − (B + A), floor at 0. Document this in docs/assumptions.md. Do not fake a 7-day table of invented prior days.
Frontend
Vite + React + TypeScript. No Next, no Redux, no router beyond a hash if needed. Chrome should feel like a night dispatch desk (ink, cream paper, steel), not a purple SaaS template. The logs themselves are high-contrast black hairlines on cream — they must read as the FMCSA form, not a dashboard widget.
| Component | Does |
|---|---|
TripForm | Four fields + optional depart time. Three sample-trip chips. Submit disabled while in flight. Cycle used as a number input 0–70 with a remaining hint (70 − value). |
RouteMap | Leaflet, Carto Positron, polyline, numbered markers for current / pickup / dropoff / fuel / rest. Popup = kind + clock + duration. Fit bounds after plan. |
StopList | Vertical itinerary next to the map. Same objects as markers. This is the “information regarding stops and rests.” |
DailyLogSheet | One SVG per day. Print-friendly. Pager: Day 1 of N. |
SVG viewBox roughly 1100×780, landscape paper. Coordinate system: x = 0..96 quarter-hours across the 24-h bar; y = four duty rows (off, sleeper, driving, on-duty). Do not use HTML tables for the line graph — tables for the header/recap, SVG for the grid and the duty polyline.
Filled handwriting-style is optional; a clean mono ink on cream already beats 90% of take-homes. Support @media print so “Save as PDF” is free. Do not build a PDF library.
Scope
| Skip | Why |
|---|---|
| Login, JWT, users, email | Graders will not sign up. Hurts the hosted test. |
| ELD hardware / Bluetooth / VIN simulation | This is a planner that draws RODS, not a device. |
| Split sleeper 7/3 or 8/2 pairing UI | FMCSA highlighted it; implementing it correctly is a product. Use 10 consecutive hours. |
| PDF export library | Browser print on SVG is enough. |
| Postgres, Redis, Celery, Docker Compose, PostGIS | One POST, no jobs. |
| 60/7 toggle, short-haul, adverse driving, team drivers, personal conveyance, yard moves | Assignment says property-carrying 70/8, no adverse. |
| Saving trips, shareable URLs, history | Nice; not scored; Render disk is ephemeral. |
| Next.js, shadcn default kit, Mapbox | Wrong host fit, generic look, credit card. |
Sequence
Each slice is a working hosted or local demo. Do not wait until the engine is perfect to draw the paper.
cycle_used_hours inserts a 34-h restart. One more test. Then polish copy, assumptions, attribution, Loom script.Risks
Missing User-Agent or burst geocodes from Vite HMR / graders mashing Submit. Mitigate: identifying UA, 1.1s spacing, LRU + disk-less in-memory cache, sample chips that hit cache first, Photon fallback, baked city coords for the three samples so the demo never depends on OSM being up.
If you dump OSRM’s 14-hour drive onto a log as 14 hours of driving, you violate 11/14 and the sheet looks wrong. Always walk miles at route speed, insert HOS time on top, and clip at midnight. Map polyline stays the geography; logs stay the clock.
HTML divs will look like a spreadsheet. SVG with 15-min ticks, vertical status jumps, remarks triangles, and a totals column that sums to 24 is the whole aesthetic bet. Match the blank form’s four rows and 70/8 recap block.
Without prior days you cannot compute a real rolling 8-day total. Own the approximation in the README. If you silently invent seven prior days, a grader who knows HOS will ding accuracy. Remaining = 70 − used − on-duty-so-far, with a 34-h restart when that hits zero.
First request after idle can take ~45s. Frontend should show a “waking API” state. Mention it in the Loom so it is not mistaken for a hang.
“Springfield” is a trap. Constrain Nominatim to US, take the first result, show the resolved label on the map so the grader sees what you interpreted.
Locked
Nested repo at hos/. Django Ninja on Render. Vite React on Vercel. Nominatim + OSRM + Leaflet. Stateless POST /api/trips/plan. SVG paper logs. No auth. Default depart 06:00 America/Chicago. Sleeper for overnight, off-duty for 30-min breaks. Fuel 30 min every 1,000 miles. Print CSS instead of PDF.