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.