TreeMap
무료 오픈소스 디스크 사용량 시각화 도구로, 사각형 트리맵·중복 파일 탐색·디스크 가득 예측·정리 시 파일을 휴지통으로만 이동한다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Een persoonlijke agenda die niet alleen zegt "je hebt om 09:00 school", maar vooral "je hebt om 09:00 school, je moet om 08:23 vertrekken." Open daarna http://localhost:3000. De app werkt direct, zonder API-sleutel. Stel bij het eerste gebruik je thuislocatie in via Instellingen. Daarna berekent de app voor elke activiteit met een locatie automatisch de reistijd en de vertrektijd.README 전문 보기 ↗
Een persoonlijke agenda die niet alleen zegt "je hebt om 09:00 school", maar vooral "je hebt om 09:00 school, je moet om 08:23 vertrekken."
HOME_LOCATION → DESTINATION → ROUTING API → TRAVEL TIME
VERTREKTIJD = STARTTIJD − REISTIJD − VEILIGHEIDSMARGE
THUISKOMST = EINDTIJD + REISTIJD TERUG
npm install
npm run dev
Open daarna http://localhost:3000. De app werkt direct, zonder API-sleutel.
Stel bij het eerste gebruik je thuislocatie in via Instellingen. Daarna berekent de app voor elke activiteit met een locatie automatisch de reistijd en de vertrektijd.
| Keuze | Waarom |
|---|---|
| Next.js 16 (App Router) + TypeScript | Eén project voor frontend én server. De API-routes draaien op de server, dus geocoding en routing gebeuren nooit vanuit de browser. |
| Tailwind CSS v4 | Snel, klein en consistent. Categoriekleuren gaan via CSS-variabelen, zodat ze dynamisch kunnen zijn zonder gegenereerde klassen. |
| Nominatim + OSRM (standaard) | Gratis, open en zonder API-sleutel, dus de app werkt meteen na npm install. |
| OpenRouteService (optioneel) | Nauwkeuriger en met hogere limieten. Zet TRAVEL_PROVIDER=ors en ORS_API_KEY=… in .env.local; de key blijft server-side. |
| localStorage | Voor deze MVP is een database overbodig. De opslaglaag (src/lib/storage.ts) is bewust klein, zodat er later een echte backend achter kan. |
De browser praat uitsluitend met /api/geocode en /api/travel. Providerkeuze, basis-URL's
en de eventuele API-sleutel staan in .env.local en worden alleen in src/lib/server/*
gelezen (met import "server-only" als extra slot op de deur).
Wat wél in de browser terechtkomt, staat er met opzet: alles met NEXT_PUBLIC_ ervoor
belandt in de bundel die iedere bezoeker kan lezen. Dat zijn er vier, en alle vier zijn ze
openbaar bedoeld: NEXT_PUBLIC_SUPABASE_URL en NEXT_PUBLIC_SUPABASE_ANON_KEY (de anon-sleutel
is geen geheim — waar een gebruiker bij mag, bepaalt Supabase met row level security),
NEXT_PUBLIC_VAPID_PUBLIC_KEY (de publieke helft van het meldingen-sleutelpaar) en
NEXT_PUBLIC_SENTRY_DSN. Zet nooit iets anders achter NEXT_PUBLIC_. De echte geheimen —
SUPABASE_SERVICE_ROLE_KEY, VAPID_PRIVATE_KEY, PUSH_CRON_SECRET — hebben dat voorvoegsel
niet en horen het nooit te krijgen.
Kopieer .env.example naar .env.local als je iets wilt aanpassen:
cp .env.example .env.local
De meldingen kennen geen account. Je telefoon verzint één keer een willekeurig id
(crypto.randomUUID(), 122 bits toeval) en bewaart dat lokaal; daarmee meldt hij zich aan
en zet hij zijn wachtrij klaar. Bewust, want zo hoeft de server niet te weten wíé je bent om
je op tijd te kunnen porren.
De keerzijde hoort er eerlijk bij te staan: dat id ís de sleutel. Wie het in handen krijgt, kan dat apparaat afmelden en er een melding met zelfgekozen tekst voor klaarzetten. Uitlezen lukt niet — de tabellen staan alleen open voor de service-sleutel — en raden ook niet, maar weggeven wel. Dus: niet in een logregel, niet in een foutmelding, niet in een screenshot. Wie het vermoeden heeft dat het gelekt is, zet meldingen in Instellingen uit en weer aan; dan komt er een nieuw id en is het oude niets meer waard.
src/
├─ app/
│ ├─ layout.tsx App-frame + AgendaProvider
│ ├─ page.tsx Dashboard "Vandaag" (eerstvolgende + tijdlijn)
│ ├─ agenda/page.tsx Agenda: Vandaag / Morgen / Week / Maand
│ ├─ schoolwerk/page.tsx Opdrachten en toetsen met status en stappen
│ ├─ instellingen/page.tsx Thuislocatie, locaties, weekplan, back-up
│ ├─ globals.css Designsysteem (licht + donker)
│ └─ api/
│ ├─ geocode/route.ts GET /api/geocode?q=…
│ └─ travel/route.ts POST /api/travel { from, to, mode }
├─ components/
│ ├─ AppShell.tsx Navigatie + globale "+ Activiteit toevoegen"
│ ├─ ActivityForm.tsx Toevoegen/bewerken/verwijderen + herhaling
│ ├─ ActivityCard.tsx Activiteit incl. reistijd en vertrektijd
│ ├─ WeekGrid.tsx Weekraster op tijdas, met reistijd als aanloop
│ ├─ MonthGrid.tsx Maandraster met stippen en dagdetail
│ ├─ DayTimeline.tsx Dagoverzicht met vertrekmomenten als eigen regel
│ ├─ LocationInput.tsx Vrij typen + suggesties uit /api/geocode
│ ├─ NextUpCard.tsx "Eerstvolgende activiteit" met aftelling
│ └─ ui.tsx Spinner, lege staat, foutmelding
├─ hooks/
│ ├─ useAgenda.tsx Store: activiteiten, instellingen, herberekening
│ └─ useNow.ts Klok voor "eerstvolgende" en de aftelling
└─ lib/
├─ types.ts Datamodel
├─ categories.ts Categorieën, kleurenpalet en kleurkeuze
├─ time.ts Datum- en tijdhelpers
├─ travel.ts Vertrektijdberekening + geldigheid reistijd
├─ recurrence.ts Wekelijkse herhaling: weekdagen en uitzonderingen
├─ agenda.ts Selectors: dag, week, tijdlijn, eerstvolgende
├─ places.ts Opgeslagen locaties en vaste plek per categorie
├─ backup.ts Import/export-formaat, validatie en normalisatie
├─ schoolwork.ts Sortering, prioriteitskleuren en "dagen tot"
├─ storage.ts Persistentie (localStorage)
├─ api.ts Client voor /api/*
└─ server/ Alleen server: config, cache, geocoding, routing
Elke berekende reistijd wordt opgeslagen met een sleutel:
thuis-coördinaten → bestemming-coördinaten @ vervoersmiddel. Zodra die sleutel niet meer
klopt (je verandert de locatie van een activiteit, of je thuislocatie in de instellingen),
haalt de store automatisch een nieuwe reistijd op voor alle betrokken activiteiten.
De vertrektijd is afgeleid, niet opgeslagen. Verschuif je Gym van 18:00 naar 19:00, dan schuift de vertrektijd direct mee zonder onnodige API-aanroep.
Activiteiten zonder locatie (bijvoorbeeld koken) tonen geen reistijd en geen vertrektijd.
Voor elke activiteit met een locatie wordt ook de rit terug naar huis berekend, zodat je onder de activiteit ziet hoe laat je thuis bent. Heen en terug worden apart opgehaald, door eenrichtingsverkeer en afslagen verschillen ze in de praktijk (bv. 32 min heen, 31 min terug), dus spiegelen zou een verkeerd antwoord geven.
De veiligheidsmarge telt hier bewust niet mee: die bestaat om op tijd aan te komen, niet om een schatting van je thuiskomst op te rekken.
Let op: de app gaat ervan uit dat je na elke activiteit naar huis gaat. Plan je twee dingen achter elkaar, dan zie je dus zowel de terugreis van de eerste als de heenreis van de tweede.
Het tabblad Reizen is een volwaardige OV-reisplanner: kies van/naar (halte, station, adres, je huidige locatie of thuis), kies "nu", "vertrek om" of "aankomst om", en je krijgt meerdere reisopties met live vertragingen. Bladeren naar eerdere of latere ritten kan met de knoppen boven en onder de lijst. Uitklappen toont de hele rit: lopen naar de halte, welke lijn, richting, spoor, overstappen en aankomsttijden.
Vanuit de agenda kun je doorlinken met
/reizen?toLat=…&toLon=…&toLabel=…&arriveBy=<ISO>.
Wat er bewust niet in zit: ticketprijzen en storingsteksten. Die zitten niet in de open data.
Naast de vijf standaardtypes maak je in het activiteitenformulier je eigen type: klik op ➕ Eigen, geef het een naam en een kleur, en desgewenst een emoji. Een emoji is niet verplicht: laat je het leeg, dan is het icoon de eerste letter van de naam in de kleur van het type. Dat is een bewuste keuze — op een laptop is een emoji typen een sneltoets die je moet kennen, en wie die niet kende typte maar iets.
Elk type bewerk je via ✎ <naam> bewerken onder de tegels: naam, icoon en kleur. Dat geldt
ook voor de vijf standaardtypes — heet "Gym" bij jou "Sporten", dan noem je hem zo. Zo'n
aanpassing komt als Settings.categoryOverrides óver het standaardtype heen en bewaart alleen
wat je werkelijk veranderd hebt: laat je de naam staan, dan blijft die gewoon meelopen met de
taal. Het id verandert nooit, want daar hangen al je activiteiten aan. Standaard herstellen
zet er weer op wat de app levert. Een standaardtype kun je niet weggooien; een eigen type wel. Gooi je een eigen type weg terwijl er nog activiteiten op staan, dan blijven
die gewoon bestaan; de app toont zo'n onbekend type neutraal in plaats van het als iets anders
voor te stellen (zie unknownCategory in src/lib/categories.ts). Eigen types staan in
Settings.customCategories en werken overal in de app.
Per activiteit kies je 🚗 auto, 🚲 fiets, 🚶 lopen of 🚆 OV (of "Standaard", dan geldt de instelling uit Instellingen → Thuis en vertrektijd → Standaard vervoermiddel).
Bij OV werkt de app als een reisplanner: hij zoekt een echte rit die je vóór de starttijd (min de marge) laat aankomen, en toont de hele reis: lopen naar de halte, de lijn, de richting, het spoor, overstappen en aankomsttijden. De vertrektijd komt dan uit de dienstregeling in plaats van uit een rekensom. Ook de terugreis wordt als echte rit gepland, vanaf de eindtijd.
| Vervoermiddel | Bron | Waarom |
|---|---|---|
| 🚗 Auto | OSRM | Snel en betrouwbaar voor autoroutes. |
| 🚲 Fiets / 🚶 Lopen | MOTIS | De publieke OSRM-demo kent alleen het autoprofiel en gaf voor fiets en lopen dezelfde (en dus foute) tijd. |
| 🚆 OV | MOTIS (transitous) | Gratis, zonder sleutel, met wereldwijde GTFS-dekking en dienstregelingdetail. |
MOTIS vraagt om een herkenbare User-Agent; die hergebruiken we uit NOMINATIM_USER_AGENT.
Zie de gebruiksvoorwaarden van transitous.
De looproutes daarin komen uit OpenStreetMap. Klopt de kaart niet, dan klopt de
vertrektijd niet, en daar valt in de app niets aan te doen — een kortere route
verzinnen is precies wat deze app niet hoort te doen. Plekken waar dat speelt,
met bewijs om ze te kunnen melden, staan in KAARTFOUTEN.md.
De looptijden komen niet van de planner maar rekent de app zelf uit: de
afstand van de kaart gedeeld door 5,04 km/h, naar boven op hele minuten
(src/lib/walkTimes.ts). De planner telt daar straftijd bij op voor oversteken,
stoplichten en hoogteverschil — over tien ritten gemeten 4,6 km/h in plaats van
de 5,04 die we vragen, en bij een kapot stuk kaart veel erger. 9292 doet dat
niet, dus zolang wij die straftijd erin lieten stond er bij elke reis een paar
minuten meer dan de gebruiker ernaast zag staan. De marge die je wilt hebben
staat los in je instellingen (standaard tien minuten); die hoort zichtbaar en
zelf te kiezen te zijn, en niet verstopt in elk loopstuk.
Omdat een OV-rit afhangt van het tijdstip, zit de dag en starttijd in de cache-sleutel: verandert de tijd of de dag, dan wordt de rit opnieuw opgezocht. Voor een herhalende activiteit plant de app op de eerstvolgende dag dat hij voorkomt.
Die sleutel zegt welke rit je zoekt, niet hoe laat die vandaag echt rijdt. Vanaf drie uur voor je activiteit tot het einde ervan wordt een OV-rit daarom elke twee minuten opnieuw opgehaald, en meteen zodra je de app weer voor je neus haalt — ook als er al een uitkomst met dezelfde sleutel staat. Anders blijft de vertrektijd van gisteravond staan, met "op tijd" erbij, terwijl je trein een kwartier later rijdt.
Is je vertrektijd toch verstreken en moet je activiteit nog beginnen, dan zoekt de app de rit die je nu nog kunt halen en zet erbij hoe laat je dan aankomt — en hoeveel te laat dat is.
Je hoeft een adres maar één keer te zoeken. Vink bij een activiteit "Onthouden als vaste locatie voor 🏋️ Gym" aan (staat standaard aan bij een nieuwe activiteit) en de app:
Wisselen van categorie wisselt een automatisch ingevulde locatie mee; een locatie die je zelf koos blijft staan. Bij het bewerken van een bestaande activiteit staat het vinkje uit, zodat je vaste locatie niet ongemerkt verandert.
Een locatie verwijderen kan in de instellingen; de categorie die hem als vaste plek gebruikte raakt dan enkel die koppeling kwijt.
Elke categorie heeft een standaardkleur (school blauw, werk grijsblauw, gym groen, koken
oranje, hobby paars). In het formulier kun je onder Kleur een eigen kleur kiezen; die
overschrijft de categoriekleur in alle weergaven. Standaard zet hem terug.
De agenda heeft vier tabbladen:
Het tijdvenster van het weekraster past zich aan: het loopt van een uur vóór je vroegste vertrek tot een uur na je laatste activiteit.
Zet 🔁 Herhalen aan in het formulier en kies de weekdagen, bijvoorbeeld ma t/m do voor werk. De datum wordt dan de startdatum van de reeks; optioneel stel je een einddatum in.
Een reeks wordt éénmaal opgeslagen en per dag uitgeklapt bij het tonen
(src/lib/recurrence.ts). Dat betekent dat de reistijd ook maar één keer wordt opgehaald en
op elke dag van de reeks geldt.
Bij het verwijderen van een dag uit een reeks krijg je de keuze:
exceptions en wordt overgeslagen.Andere wijzigingen (tijd, locatie, categorie) gelden altijd voor de hele reeks; dat staat ook zo in het formulier vermeld.
Het tabblad Schoolwerk toont je opdrachten en toetsen, zoals aangeleverd door je planner (Claude in een aparte chat). De app berekent hier zelf niets; hij leest de data getrouw in.
Bovenaan staat een zoekbalk. Die doorzoekt alles wat er van een opdracht of toets te lezen valt: vak, titel, omschrijving, de stappen, de onderwerpen van een toets, de deadline en de woorden "bezig", "hoog" enzovoort. Juist die stappen, want daar zit het detail dat je je herinnert — "Vergane Glorie" is een stap, geen opdracht. Elk woord dat je typt moet ergens voorkomen, maar niet per se in hetzelfde veld: "bedrijfseconomie casus" vindt de opdracht met dat vak én dat woord in de titel. Hoofdletters, accenten en leestekens doen niet mee, dus "H8.1" vindt ook "H8.1 t/m 8.4". Zoeken werkt samen met de twee filterrijen, en de tellingen op die knoppen volgen je zoekopdracht — een knop die "(3)" belooft terwijl er één overblijft, liegt. Escape of het kruisje wist het veld; de zoekopdracht wordt bewust niet onthouden.
Leer- en werkblokken uit je planner komen binnen als gewone activiteiten met
category: "school" en source: "leerplan"; ze lopen mee in alle agenda-weergaven en krijgen
daar een subtiel 📚 leerplan-label.
Zet je een opdracht of toets op klaar, dan staan de blokken die eraan gekoppeld zijn
(linkedTaskId / linkedExamId) in je agenda doorgestreept, met een groen ✓ af erbij — in
de dag- en weekweergave, het maandraster, het dagoverzicht en op de kaart van je eerstvolgende
activiteit. Het blok blijft staan, want je wilt kunnen zien waar je tijd heen ging, maar je ziet
in één blik dat die twee uur vanavond vrij zijn. Op het dashboard staat er bovendien bij hoeveel
van je geplande leertijd vandaag al af is.
Dat werkt ook per stap. Een opdracht van zes uur plan je zelden in één blok; je hakt hem in
stukken ("samenvatting", "opgaven maken") en vinkt die stuk voor stuk af. Een blok dat bij zo'n
stap hoort is doorgestreept zodra die stap af is — de hele opdracht hoeft nog niet klaar te zijn.
Bij voorkeur staat die koppeling er expliciet in (linkedStepId, de id van een stap uit
steps); staat hij er niet, dan herkent de app de stap aan de titel van het blok, zodat blokken
die er al stonden ook meedoen. "BE – samenvatting H3" hoort bij de stap "Samenvatting H3".
Hele woorden tellen, dus "H3" matcht niet op "H30", en staat er meer dan één stap in de titel dan
wint de langste.
Naast elk blok staat een rondje: tik het aan en het blok is afgestreept. Dat is de andere kant op dan hierboven, en in de praktijk de kant die je gebruikt — je bent klaar met een blok, je kijkt naar je dag, en je wilt het aantikken zonder eerst de bijbehorende stap op te zoeken.
Wat het vinkje afstreept hangt van het blok af (doneTarget in src/lib/schoolwork.ts):
| Het blok hoort bij | Het vinkje zet |
|---|---|
| een stap van een opdracht | die stap op af (en daarmee de opdracht op bezig of klaar) |
| een opdracht zonder stappen | de hele opdracht op klaar |
| een toets | de toets op klaar |
| niets van dat alles | het blok zelf, op die dag |
Dat laatste geval is de helft van een agenda: boodschappen, de was, een leerblok waar geen opdracht
achter zit. Die stand staat in doneDates op de activiteit — een lijst dagen, net als
exceptions, want een herhalend blok streep je af voor vandaag en niet voor elke dinsdag. Zit er
wel schoolwerk aan vast, dan is dat de waarheid en kijkt de app niet naar doneDates: anders kon
je een stap uitvinken terwijl het blok doorgestreept bleef staan.
In het week- en maandraster is een blok te smal voor een rondje; daar zit het vinkje in het
formulier dat opengaat als je het blok aantikt ("Dit is af"). Je planner ziet het ook: blokken die
af zijn krijgen done: true in read_agenda, zodat er niets opnieuw ingepland wordt.
Onder Instellingen → Back-up deel je exact dezelfde data met je planner via één JSON-bestand:
{ "app": "vertrektijd-agenda", "version": 2, "exportedAt": "<ISO>",
"settings": { ... }, "activities": [ ... ], "tasks": [ ... ], "exams": [ ... ] }
vertrektijd-agenda.json.app-veld en de versie worden gecontroleerd,
onbekende velden genegeerd en ontbrekende velden krijgen een veilige standaardwaarde, zodat de
app nooit crasht op een handmatig gemaakt bestand. Twee modi:
id voor activiteiten, taken en toetsen; instellingen
worden alleen overschreven als het bestand ze bevat.De opslag draait op schemaversie 2: taken en toetsen staan naast de bestaande activiteiten en instellingen, die onder hun eigen sleutels blijven staan. Bestaande data gaat dus niet verloren en ontbrekende taken/toetsen worden een lege lijst.
Dit formaat wordt stabiel gehouden: de planner (de andere Claude) baseert zijn bestanden
hierop. app blijft "vertrektijd-agenda" en de structuur blijft
{ app, version, exportedAt, settings, activities, tasks, exams }. version wordt alleen
verhoogd bij een echte wijziging, en oudere versies blijven inleesbaar. Onbekende velden worden
genegeerd; ontbrekende velden krijgen een veilige standaardwaarde.
Een volledig, geldig voorbeeld staat in
examples/planner-voorbeeld.json, inclusief een leerblok dat
aan een toets is gekoppeld en een werkblok dat aan een opdracht is gekoppeld.
tasks[] (opdrachten/huiswerk):
| Veld | Type | Verplicht | Toelichting |
|---|---|---|---|
id | string | ja | Stabiele id; gebruikt voor upsert bij samenvoegen. |
subject | string | ja | Vak, bv. "Wiskunde". |
title | string | ja | Wat er moet gebeuren. |
description | string | nee | Extra toelichting. |
deadline | string | ja | YYYY-MM-DD. |
estimatedMinutes | number | ja | Geschatte totale werktijd. |
priority | "high"|"medium"|"low"|"later" | ja | 🔴 / 🟠 / 🟡 / 🟢. |
status | "todo"|"doing"|"done" | ja | Voortgang. |
steps[] | { id, title, estimatedMinutes?, done } | nee | Afvinkbare deelstappen. |
createdAt / updatedAt | string (ISO) | ja | Tijdstempels. |
exams[] (toetsen): zelfde priority/status/tijdstempels, met id, subject,
title?, date (YYYY-MM-DD), topics? (string[]) en prepMinutes? (number).
activities[] (agenda): het bestaande activiteitsmodel. category is een van de vijf
ingebouwde types — "school", "werk", "gym", "koken", "hobby" — of de id van een
eigen type uit settings.customCategories. Iets anders blijft staan zoals je het schrijft,
maar komt er neutraal uit (grijs, met de tekst zelf als naam): de app doet niet alsof
"sport" hetzelfde is als "gym". Voor leer-/werkblokken uit het leerplan: zet
category: "school" en source: "leerplan". Koppel een blok aan schoolwerk met
linkedTaskId of linkedExamId (de id van een taak of toets); de app toont dan bij die taak/
toets hoeveel leertijd is ingepland en labelt het blok in de agenda.
De app werkt standaard lokaal (localStorage). Optioneel kun je login met e-mail + wachtwoord en
cloud-synchronisatie tussen apparaten activeren met een gratis Supabase-project. Eenmalige
instelstappen (project, database-schema, sleutels in Vercel) staan in
SUPABASE-SETUP.md. Zonder de env-variabelen
NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY blijft alles lokaal en verandert er
niets. Elke gebruiker heeft alleen toegang tot zijn eigen data (Row Level Security).
Naast import/export kan Claude ook rechtstreeks bij de agenda. Dat scheelt het heen en weer slepen van een bestand: je vraagt in een gesprek om een planning, Claude leest wat er staat en zet de blokken er zelf in.
Hoe het werkt. De app biedt één adres aan, /api/mcp, dat het
Model Context Protocol spreekt over HTTP. Je maakt in
Instellingen → Claude-connector een sleutel aan en plakt adres en sleutel in Claude onder
Connectors → Aangepaste connector toevoegen. Vanaf dan heeft Claude drie gereedschappen:
| Gereedschap | Wat het doet |
|---|---|
read_agenda | De agenda over een periode, herhalingen al uitgerekend tot losse dagen, met vertrektijd en reisduur per activiteit. Plus het open huiswerk en de komende toetsen. |
save_activities | Blokken toevoegen of wijzigen. Zonder id komt er een blok bij; met een bestaand id vervangt het dat blok — zo verplaats je iets. |
delete_activities | Blokken weghalen op hun id, met een grafsteen zodat ze niet terugkomen bij de volgende sync. |
Waar de grenzen liggen. Bewust klein gehouden:
user_data). De app haalt die op bij het
openen, dus een planning die Claude nu schrijft zie je op je telefoon zodra je hem weer opent.Beveiliging. Van de sleutel staat alleen een SHA-256 in de database, nooit de sleutel zelf —
raakt die tabel op straat, dan ligt daarmee niemands agenda open. De sleutel wijst naar precies
één gebruiker en de route kan bij niets anders. Een sleutel intrekken kan op elk moment in de
instellingen. Zie SUPABASE-SETUP.md stap 10 voor het aanzetten.
Deze lijst stond een tijd lang stil terwijl de app doorliep: vier van de zes punten waren allang gebouwd. Dat is vervelender dan het lijkt — je gaat iets bouwen dat er al is, of je denkt dat de app minder kan dan hij kan. Daarom hieronder eerst wat er wél staat.
Inmiddels gebouwd:
| waar | |
|---|---|
| Vervoermiddel per activiteit | keuze in het activiteitenformulier |
| Tweewekelijks en maandelijks herhalen | RecurrenceFreq, met keuze in het formulier |
Marge per activiteit (Activity.bufferMinutes) | veld in het formulier; leeg volgt de algemene marge |
| Leerblok gekoppeld aan opdracht of toets | linkedTaskId / linkedExamId, gezet door "Leertijd inplannen" |
| Synchronisatie tussen je eigen apparaten | Supabase, met realtime en versiecontrole |
| Roosters van buiten inlezen | .ics-abonnementen via /api/rooster |
Nog niet gebouwd:
.ics-link), schrijven niet.
Dat is een andere orde: daarvoor moet je bij het account van de gebruiker kunnen.src/lib/server/ houdt daar ruimte voor: die kan erbij zonder de rest aan te raken.Bewust niet:
walk zit nog in het model zodat oude activiteiten
die erop stonden hun naam en icoon houden, maar hij staat niet in de keuze: voor de
afstanden waar deze app over gaat is het OV of de fiets, en een looproute van tien
kilometer helpt niemand. Binnen een OV-reis wordt lopen wél getoond.npm run dev
npm run build
npm run typecheck
Cannot find module './586.js' of andere ontbrekende chunks
De build-cache is stuk. Dat gebeurt vooral wanneer npm run build draait terwijl
npm run dev nog openstaat: de productiebuild overschrijft dezelfde .next/-map.
Stop de dev-server, gooi de cache weg en start opnieuw:
rm -rf .next && npm run dev
Op Windows PowerShell:
Remove-Item -Recurse -Force .next; npm run dev
Draai npm run build dus niet tegelijk met npm run dev. Voor een snelle controle op
typefouten kun je wel gerust npm run typecheck naast de dev-server gebruiken.
| Variabele | Nodig voor | Opmerking |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | accounts & synchronisatie | mag publiek zijn |
NEXT_PUBLIC_SUPABASE_ANON_KEY | accounts & synchronisatie | mag publiek zijn, beschermd door RLS |
SUPABASE_SERVICE_ROLE_KEY | "Account verwijderen" | geheim, uitsluitend server-side |
NEXT_PUBLIC_SENTRY_DSN | foutmonitoring (optioneel) | zonder deze waarde gaat er niets naar buiten |
NOMINATIM_USER_AGENT | nette identificatie bij de gratis kaartdiensten | bv. Vertrektijd/1.0 (https://jouw-app.vercel.app) |
ADMIN_EMAILS | het beheerdersoverzicht op /beheer (optioneel) | komma-lijst; leeg = niemand kan erbij |
NEXT_PUBLIC_SITE_URL | sitemap en de Play Store-verpakking (optioneel) | zonder deze waarde blijft de sitemap leeg |
ANDROID_PACKAGE_NAME | de Android-app (optioneel) | standaard nl.vertrektijd.agenda |
ANDROID_SHA256_FINGERPRINT | de Android-app (optioneel) | uit Play Console, niet uit je eigen keystore; zie PLAYSTORE.md |
Zonder SUPABASE_SERVICE_ROLE_KEY blijft de app werken; de verwijderknop meldt dan
dat het handmatig moet. Zet die sleutel nooit in een NEXT_PUBLIC_-variabele:
hij omzeilt Row Level Security en geeft toegang tot de gegevens van álle gebruikers.
Elk apparaat haalt op bij het openen en bij terugkomen in de app (hoogstens twee keer per
minuut). Staat realtime aan voor user_data — één regel SQL, zie SUPABASE-SETUP.md — dan duwt
de database een wijziging er bovendien meteen heen: wat je op je laptop plant staat binnen een
seconde op je telefoon.
Een apparaat negeert daarbij zijn eigen echo: syncOnce geeft terug welke versie het zojuist
schreef, en een melding met precies die versie wordt overgeslagen. Anders haalt een apparaat na
elke eigen wijziging alles nog een keer op.
Schrijven gaat altijd via syncOnce: ophalen, samenvoegen, en alleen wegschrijven als de rij nog
is zoals hij hem las. Was er iemand voor je (je andere apparaat, of Claude via de connector), dan
wordt het samenvoegen overgedaan op de verse rij. Zie src/lib/optimistic.ts.
/beheer)Voor wie de app draait, niet voor wie hem gebruikt: hoeveel accounts er zijn, hoeveel mensen de app per dag openden over de laatste dertig dagen, wat ze deden, en of de diensten waar de app van afhangt het op dit moment doen (database, OV-planner, adressen zoeken).
Bewust niet in het menu — je komt er via de URL. De afscherming zit in
/api/admin/overview en niet in het scherm: een pagina die zichzelf verbergt houdt niemand
tegen die de route zelf aanroept. Die route controleert je Supabase-token en vergelijkt je
e-mailadres met ADMIN_EMAILS; mag je niet, dan krijg je een 404 en niet "verboden" — dat
laatste vertelt een vreemde dat de pagina bestaat.
Staat ADMIN_EMAILS leeg, dan kan niemand erbij, ook de eigenaar niet. Dat is expres.
De cijfers komen uit app_events (een dag, een naam, een aantal — zie SUPABASE-SETUP.md) en uit
een telling van de rijen in user_data. Er staat geen bezoeker in: "hoeveel mensen openden de
app" klopt doordat de app die gebeurtenis hooguit één keer per dag stuurt, bijgehouden in de
browser zelf.
npm test
Vitest draait de tests over tijdrekenen, herhalingen, vertrek-/thuiskomsttijd en de sync-samenvoeging. Draai ze voordat je iets pusht.
npm run e2e
Playwright doet wat een gebruiker doet: een formulier openen, een eigen type maken, leertijd inplannen, huiswerk afvinken. Dat vangt precies de fouten die een rekentest niet kan zien — een verplicht veld dat niemand kan invullen, een knop die te klein is om te raken, een scherm dat wel de goede getallen heeft en toch niet werkt. Het draait op een telefoonmaat, want daar wordt de app gebruikt.
De tests bouwen de app eerst (next build) en starten hem op poort 3210; ze
testen dus de echte build en niet next dev. Staat Chromium al ergens op je
machine, zet dan PLAYWRIGHT_CHROMIUM_PATH naar dat pad — anders haalt
Playwright er zelf een op.
Bewust een handjevol tests, geen dekking-om-de-dekking. Wat erin staat zijn de dingen die eerder kapot zijn gegaan zonder dat een unittest piepte.
Geen echte adressen in tests, commentaar of documentatie. Het is verleidelijk
om je eigen huis en werk als voorbeeld te nemen — ze zijn bij de hand en ze
kloppen. Maar een testbestand blijft staan, en een repo wordt ooit gedeeld of
verkocht; dan staat er in de geschiedenis waar iemand woont, werkt en sport.
Gebruik Voorbeeldstraat, Voorbeeldweg, Voorbeeldlaan met de plaatsnaam en
de coördinaten die je nodig hebt: de reistijden rekenen op de coördinaten, dus de
test betekent precies hetzelfde. De scripts in scripts/ geocoderen wel echt en
staan daarom standaard op een stationsplein — een openbare plek is van niemand.