De staging database bevat nu een veel grotere en realistischer dataset, met extra klanten, sites en toestellen. Daardoor kunnen de volgende versies testen hoe een technieker op dezelfde klant extra toestellen en extra opdrachten ziet zonder opnieuw basisdata te moeten uitvinden.
De klanten die vandaag al in de planning zitten zijn bewust het sterkst uitgebreid. Dat maakt het mogelijk om in de volgende versie realistisch te testen hoe je op een klantsite een extra opdracht voor een ander toestel kan openen.
Voor de belangrijkste bestaande klanten en toestellen zijn oudere afgewerkte werkorders toegevoegd in de database. Die zijn nog niet zichtbaar in de huidige UI, maar vormen wel het fundament voor de toestelgeschiedenis die we in de volgende versie willen tonen.
Omdat v1.11 niet eerst nog eens de database moet verbouwen voor testdata. De data ligt al klaar zodat we dan puur kunnen focussen op wat de technieker te zien krijgt: meerdere toestellen per klant, extra opdrachten op dezelfde site en historiek per toestel.
staging.bossuyt.fixassistant.com draait nu op een echte PostgreSQL-laag voor techniekers, klanten, sites, toestellen, werkorders en toewijzingen. De demo-site bossuyt-service.fixassistant.com is bewust onaangeraakt gebleven en gebruikt nog steeds de oude mock-flow.
Staging mag evolueren, breken en migreren. De demo moet stabiel blijven zolang Bossuyt die gebruikt. Daarom heeft staging nu zijn eigen app-container, eigen database en eigen volume.
Dat maakt de latere verhuis naar een andere server ook eenvoudiger: je exporteert de staging app + database als een aparte stack, zonder de demo mee te sleuren.
/api/sync/today
De ontbrekende sync-route is gebouwd en leest nu echte records uit PostgreSQL. Het dagoverzicht krijgt opnieuw max. 6 geplande jobs en 4 open pool jobs, maar nu via een serverlaag in plaats van rechtstreeks uit lib/mock-data.ts.
curl 'https://staging.bossuyt.fixassistant.com/api/sync/today?technicianId=u1&date=2026-04-11'
# β planned: 6 jobs, open: 1 job
Het homescreen leest nu eerst uit IndexedDB en synchroniseert daarna met de server. De interventiedetailpagina probeert eerst de offline cache en valt pas daarna terug op een serverroute per interventie. Daardoor begint de service app eindelijk een echte offline-first read path te krijgen.
De envs zijn opgesplitst tussen host tooling en Docker runtime. In de container praat de app met db-staging; buiten Docker kunnen tools zoals Drizzle of seed-scripts naar localhost:5433 connecteren. Dat voorkomt dat migratie- en seed-commando's per ongeluk naar de verkeerde host wijzen.
Je kan nu een vrij adres intypen als vertrek- of eindpunt. De app zoekt automatisch de GPS-coΓΆrdinaten op via Nominatim (OpenStreetMap), zodat je niet meer aan een vaste lijst vastzit.
De timeline herberekent nu niet alleen bij drag & drop, maar ook wanneer je startadres, eindadres of pauzepositie verandert. Een debounce van 350 ms voorkomt dat de route bij elke toetsaanslag meteen vuurt.
De actieve versie staat nu op één plaats in de service-app repo. Daardoor blijven de badge rechtsonder en /changenotes synchroon, in plaats van dat ze los van elkaar divergeren.
Wanneer Android de geolocatie-popup blokkeert door overlays of zwevende vensters, toont de app nu een explicietere uitleg in plaats van een vage fout. Dat maakt het probleem begrijpelijker voor de technieker op mobiel.
De route-timeline toont nu echte rijtijden en afstanden via OpenRouteService. Zonder ORS-key valt de app in development nog altijd terug op mock-waarden, maar staging kan nu echte routeberekeningen tonen.
Alle locaties kregen lat/lon-coΓΆrdinaten en nieuwe adressen kunnen via Nominatim automatisch naar GPS vertaald worden. Daardoor zit de route-engine niet meer vast op placeholders.
Er kwamen extra demo-klanten en een geloofwaardigere route door Oost-Vlaanderen, zodat de service-app dichter bij de uiteindelijke Bossuyt-flow komt.
Het dagoverzicht toont sindsdien niet langer alleen een lijst, maar een verticale route: start, rijtijd, job, pauze, volgende job en einde. Zo ziet een technieker sneller hoe de dag eruitziet.
Je kan het depot vervangen door een ander vertrekpunt en eventueel ook een ander eindpunt kiezen, bijvoorbeeld thuis in plaats van het depot.
Een middagpauze van 30 minuten wordt automatisch ingevoegd en kan mee verplaatst worden in de route. Bovenaan de timeline zie je sindsdien live het aantal jobs, werkuren en rijtijd.
Aparte Docker Compose file die een staging versie opstart op staging.bossuyt.fixassistant.com. Eigen database zodat productiedata nooit geriskeerd wordt. Container herstart niet automatisch β staging is bewust tijdelijk.
Opstarten: docker compose -f docker-compose.staging.yml up --build -d
Stoppen na testen: docker compose -f docker-compose.staging.yml down
De -f flag vertelt Docker Compose welk bestand te gebruiken. Zonder die flag gebruikt het altijd docker-compose.yml (productie). Zo kunnen beide naast elkaar draaien zonder conflict β andere container naam, andere subdomain, andere Traefik router.
# 1. Code schrijven β geen impact op productie
# 2. Staging opstarten en testen
docker compose -f docker-compose.staging.yml up --build -d
# β https://staging.bossuyt.fixassistant.com
# 3. Alles ok? Naar productie
docker compose up --build -d
# 4. Staging opruimen
docker compose -f docker-compose.staging.yml down
Nieuwe fase toegevoegd aan het masterplan (post Week 40): volledige verhuizing van de Hetzner server naar een Bossuyt-eigen server. Inclusief database backup strategie, Docker image inventarisatie, DNS switch procedure en rollback plan.
Een server migratie heeft drie gevaarlijke momenten:
1. Database: als de dump niet compleet is of de restore mislukt, verlies je data. Oplossing: restore altijd eerst testen op de nieuwe server vΓ³Γ³r de echte migratie.
2. DNS: DNS wijzigingen propageren traag (soms uren). Oplossing: TTL vooraf verlagen naar 5 minuten, zodat een rollback snel effectief is.
3. Environment variables: .env bestanden zijn nooit in git. Oplossing: inventariseer ze expliciet vΓ³Γ³r de migratie.
# Backup (op oude server)
docker exec bossuyt-db pg_dump -U bossuyt -Fc bossuyt > bossuyt.dump
docker exec keycloak-db pg_dump -U keycloak -Fc keycloak > keycloak.dump
# Overbrengen naar nieuwe server
scp bossuyt.dump user@nieuwe-server:/backups/
# Restore (op nieuwe server)
docker exec -i bossuyt-db pg_restore -U bossuyt -d bossuyt < bossuyt.dump
Stap-voor-stap staging workflow toegevoegd aan de "Waar starten" sectie van het masterplan: code schrijven β staging bouwen β testen β productie deployen β staging stoppen.
Nieuwe TypeScript types die de plannningslogica beschrijven: WorkOrder (een werkbon met type planned of open), RemovedReason (waarom een technieker een job uit zijn planning haalt), RouteStep (rijtijd tussen twee stops) en TechnicianDayCache (alles wat gecached wordt bij dagstart).
Types zijn als afspraken: je beschrijft de vorm van je data zodat TypeScript je waarschuwt als je iets vergeet of een typfout maakt. Het zijn geen echte objecten β ze bestaan enkel tijdens het compileren.
// RemovedReason is een "union type" β één van deze exacte strings
type RemovedReason =
| 'cant_finish' // Lukt niet meer
| 'missing_material' // Materiaal ontbreekt
| 'impossible_timing' // Onmogelijke timing voor klant
| 'other' // Andere (verplicht vrij tekstveld)
// Als je nu ergens removedReason = 'verkeerd' schrijft β TypeScript fout
// Als je removedReason vergeet in te vullen β TypeScript fout
Een IRoutingService interface plus een OrsRoutingService implementatie. De interface definieert wat de service doet, de implementatie definieert hoe. Later swap je ORS naar TomTom zonder de rest van de app te wijzigen.
Een interface is een contract. Het zegt: "alles wat dit interface implementeert, moet deze functies hebben." De OrsRoutingService tekent dit contract en levert de echte code. Morgen maak je TomTomRoutingService β zelfde contract, andere code.
// Het contract β geen implementatie, enkel de signatuur
export interface IRoutingService {
getETA(from: Coordinates, to: Coordinates): Promise<RouteResult>
}
// ORS tekent het contract
export class OrsRoutingService implements IRoutingService {
async getETA(from, to) { /* echte ORS code */ }
}
// Gebruik: je kent enkel het contract β niet welke provider
const eta = await routingService.getETA(depot, klant)
// β werkt met ORS Γ©n TomTom zonder aanpassing
De browser-database van de Service App. Slaat interventies, werkbon-formulierdata, offline acties en sync-metadata op zodat alles werkt zonder internet.
IndexedDB is een database ingebouwd in elke browser. Je kan er grote hoeveelheden data in opslaan β ook als je offline bent. We gebruiken het idb pakket dat de moeilijke native API omzet naar gewone async functies.
4 stores (tabellen):
// interventions β de werkbonnen van de dag (planned + open)
// werkbonnen β ingevulde formulieren per werkbon
// pendingWrites β acties gedaan offline, wachten op sync
// dayMeta β wanneer was de laatste sync?
// Voorbeeld: werkbon opslaan (elke keystroke)
await saveWerkbon({
interventionId: 'int-42',
parts: [{ articleId: 'art-1', qty: 2 }],
notes: 'Filter vervangen',
followUpRequired: false,
// ...
})
// β opgeslagen in IndexedDB, overleeft refresh en offline
Transaction: wanneer we de dagcache vervangen, gebruiken we een transaction. Dat garandeert dat Γ³f alles lukt Γ³f niets verandert β nooit halfweg.
Haalt elke ochtend max 10 werkbonnen op (6 planned + 4 open pool) en slaat ze offline op. Vraagt ook rijtijden op voor de geplande stops. Als de sync mislukt door geen internet β geen probleem, gisteren's cache blijft bruikbaar.
// 1. Controleer of sync nodig is (slechts 1x per dag)
const nodig = await shouldSync(technicianId)
if (!nodig) return // cache is nog geldig
// 2. Haal data op van de server
const res = await fetch(`/api/sync/today?technicianId=...`)
const { planned, open } = await res.json()
// 3. Beperk tot max 6 planned + 4 open
const beperkt = [...planned.slice(0, 6), ...open.slice(0, 4)]
// 4. Sla op in IndexedDB (vervangt vorige dag)
await cacheInterventions(beperkt)
// 5. Haal rijtijden op (ORS matrix call)
const route = await fetchDailyRoute(planned)
// 6. Klaar β app werkt nu volledig offline
Pending writes: als je offline een status wijzigt, gaat die actie in de pendingWrites store. Zodra je terug online bent roept syncPendingWrites() ze allemaal op de server β in volgorde.
Twee Next.js API routes: /api/route voor een enkele hop, /api/route/daily voor de volledige dagroute. De ORS API key zit server-side β nooit zichtbaar in de browser. Werkt ook zonder key (geeft mock-data terug in development).
Je kan ORS niet rechtstreeks vanuit de browser aanroepen met je API key β dan is de key zichtbaar voor iedereen. De Next.js route handler draait op de server, leest de key uit process.env.ORS_API_KEY (een geheime omgevingsvariabele) en stuurt de route terug naar de browser.
// Server-side: key is veilig
const routingService = new OrsRoutingService(
process.env.ORS_API_KEY ?? '' // uit .env, nooit in de code
)
export async function POST(req: NextRequest) {
const { from, to } = await req.json()
const result = await routingService.getETA(from, to)
return NextResponse.json(result)
// β { distanceKm: 12.4, travelMinutes: 22, provider: 'ors' }
}
De Service App krijgt 10 gecachte werkbonnen per dag: max 6 geplande items (dispatcher-assigned met deadline) en max 4 open pool items (technieker kiest zelf). Technieker kan een gepland item verwijderen met een reden. Drag & drop om volgorde aan te passen. Optionele collega-view.
Provider-agnostisch routingService.getETA(from, to). Fase 1: OpenRouteService (gratis, account.heigit.org). Fase 2: TomTom swap met traffic-aware ETA β enkel server.ts wijzigt. UI toont disclaimer "zonder file-rekening" op basis van provider veld in response.
Nieuwe les in de cursus op /coreapi: IRoutingService interface, ORS implementatie, dependency injection, beide endpoints, en UI disclaimer logica.
service-app-planning.md en routing-service.md aangemaakt β volledige TypeScript code en uitleg voor toekomstige sessies.
Aparte pagina op /coreapi met 8 lessen: Project setup β Fastify β Docker + PostgreSQL β Drizzle schema β Keycloak JWT β Endpoints β NAV sync β Routing service. Sidebar navigatie, highlight.js syntax highlighting, callout boxes, progress bar.
Vaste top bar op mobiel met huidige les-titel en hamburger menu. Code blocks scrollen horizontaal. Kleinere typografie op kleine schermen. Prev/next navigatie stapelt verticaal.
Pill-knop in de sticky nav: π₯ Full (brede layout, Gantt chart) en π± Compact (mobiele layout, fase-progress bars, gestapelde kolommen). Auto-detecteert telefoon bij eerste bezoek. Keuze opgeslagen in localStorage.
In compact mode: gestapelde versie van het architectuurdiagram i.p.v. het brede 5-koloms grid.
De 40-koloms Gantt chart is onleesbaar op mobiel. In compact mode vervangen door 4 gekleurde progress bars per fase met week-indicaties.
Gebaseerd op Gemini brainstorm sessie, uitgewerkt met Claude + OMC. 40 weken (1 dag/week), 4 fases: Foundation β NAV Bridge β Admin Portal β App Modularisatie. Gantt chart, architectuurdiagram, tech stack, risico register, analyse sectie.
nginx:alpine container geserveerd via Traefik op plan.bossuyt.fixassistant.com. Directory volume mount zodat bewerkingen direct zichtbaar zijn zonder container restart.
Service App strategie: 10 werkbonnen cachen bij dagstart (later verhoogd van 5), klant/toesteldata ad-hoc + gecached, wagenstock (~1000 lijnen) enkel bij verbinding, Service Worker als offline fallback.